diff --git a/src/main/java/org/qora/api/ApiService.java b/src/main/java/org/qora/api/ApiService.java index 917aac4f..709039ad 100644 --- a/src/main/java/org/qora/api/ApiService.java +++ b/src/main/java/org/qora/api/ApiService.java @@ -2,6 +2,9 @@ package org.qora.api; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; +import java.net.InetAddress; +import java.net.InetSocketAddress; + import org.eclipse.jetty.rewrite.handler.RedirectPatternRule; import org.eclipse.jetty.rewrite.handler.RewriteHandler; import org.eclipse.jetty.server.CustomRequestLog; @@ -23,93 +26,93 @@ import org.qora.settings.Settings; public class ApiService { - private final Server server; - private final ResourceConfig config; - - public ApiService() { - config = new ResourceConfig(); - config.packages("org.qora.api.resource"); - config.register(OpenApiResource.class); - config.register(ApiDefinition.class); - config.register(AnnotationPostProcessor.class); - - // Create RPC server - this.server = new Server(Settings.getInstance().getApiPort()); - - // Error handler - ErrorHandler errorHandler = new ApiErrorHandler(); - this.server.setErrorHandler(errorHandler); - - // Request logging - if (Settings.getInstance().isApiLoggingEnabled()) { - RequestLogWriter logWriter = new RequestLogWriter("API-requests.log"); - logWriter.setAppend(true); - logWriter.setTimeZone("UTC"); - RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT); - server.setRequestLog(requestLog); - } - - // IP address based access control - InetAccessHandler accessHandler = new InetAccessHandler(); - for (String pattern : Settings.getInstance().getApiWhitelist()) { - accessHandler.include(pattern); - } - this.server.setHandler(accessHandler); - - // URL rewriting - RewriteHandler rewriteHandler = new RewriteHandler(); - accessHandler.setHandler(rewriteHandler); - - // Context - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); - context.setContextPath("/"); - rewriteHandler.setHandler(context); - - // Cross-origin resource sharing - FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class); - corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); - corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE"); - corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false"); - context.addFilter(corsFilterHolder, "/*", null); - - // API servlet - ServletContainer container = new ServletContainer(config); - ServletHolder apiServlet = new ServletHolder(container); - apiServlet.setInitOrder(1); - context.addServlet(apiServlet, "/*"); - - // Swagger-UI static content - ClassLoader loader = this.getClass().getClassLoader(); - ServletHolder swaggerUIServlet = new ServletHolder("static-swagger-ui", DefaultServlet.class); - swaggerUIServlet.setInitParameter("resourceBase", loader.getResource("resources/swagger-ui/").toString()); - swaggerUIServlet.setInitParameter("dirAllowed", "true"); - swaggerUIServlet.setInitParameter("pathInfoOnly", "true"); - context.addServlet(swaggerUIServlet, "/api-documentation/*"); - - rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect to Swagger UI start page - rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to Swagger UI start page - } - - // XXX: replace singleton pattern by dependency injection? private static ApiService instance; + private final ResourceConfig config; + private Server server; + + private ApiService() { + this.config = new ResourceConfig(); + this.config.packages("org.qora.api.resource"); + this.config.register(OpenApiResource.class); + this.config.register(ApiDefinition.class); + this.config.register(AnnotationPostProcessor.class); + } + public static ApiService getInstance() { - if (instance == null) { + if (instance == null) instance = new ApiService(); - } return instance; } public Iterable> getResources() { // return resources; - return config.getClasses(); + return this.config.getClasses(); } public void start() { try { + // Create API server + InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress()); + InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort()); + this.server = new Server(endpoint); + + // Error handler + ErrorHandler errorHandler = new ApiErrorHandler(); + this.server.setErrorHandler(errorHandler); + + // Request logging + if (Settings.getInstance().isApiLoggingEnabled()) { + RequestLogWriter logWriter = new RequestLogWriter("API-requests.log"); + logWriter.setAppend(true); + logWriter.setTimeZone("UTC"); + RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT); + this.server.setRequestLog(requestLog); + } + + // IP address based access control + InetAccessHandler accessHandler = new InetAccessHandler(); + for (String pattern : Settings.getInstance().getApiWhitelist()) { + accessHandler.include(pattern); + } + this.server.setHandler(accessHandler); + + // URL rewriting + RewriteHandler rewriteHandler = new RewriteHandler(); + accessHandler.setHandler(rewriteHandler); + + // Context + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + context.setContextPath("/"); + rewriteHandler.setHandler(context); + + // Cross-origin resource sharing + FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class); + corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); + corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE"); + corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false"); + context.addFilter(corsFilterHolder, "/*", null); + + // API servlet + ServletContainer container = new ServletContainer(this.config); + ServletHolder apiServlet = new ServletHolder(container); + apiServlet.setInitOrder(1); + context.addServlet(apiServlet, "/*"); + + // Swagger-UI static content + ClassLoader loader = this.getClass().getClassLoader(); + ServletHolder swaggerUIServlet = new ServletHolder("static-swagger-ui", DefaultServlet.class); + swaggerUIServlet.setInitParameter("resourceBase", loader.getResource("resources/swagger-ui/").toString()); + swaggerUIServlet.setInitParameter("dirAllowed", "true"); + swaggerUIServlet.setInitParameter("pathInfoOnly", "true"); + context.addServlet(swaggerUIServlet, "/api-documentation/*"); + + rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect to Swagger UI start page + rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to Swagger UI start page + // Start server - server.start(); + this.server.start(); } catch (Exception e) { // Failed to start throw new RuntimeException("Failed to start API", e); @@ -119,9 +122,12 @@ public class ApiService { public void stop() { try { // Stop server - server.stop(); + this.server.stop(); } catch (Exception e) { // Failed to stop } + + this.server = null; } + } diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 14bea809..96e482e9 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -43,6 +43,9 @@ public class Settings { // Settings, and other config files private String userPath; + // Common to all networking (UI/API/P2P) + private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses + // Node management UI private boolean uiEnabled = true; private Integer uiPort; @@ -71,7 +74,6 @@ public class Settings { // Peer-to-peer related private boolean isTestNet = false; private Integer listenPort; - private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses /** Minimum number of peers to allow block generation / synchronization. */ private int minBlockchainPeers = 3; /** Target number of outbound connections to peers we should make. */ diff --git a/src/main/java/org/qora/ui/UiService.java b/src/main/java/org/qora/ui/UiService.java index 647b8f9b..f8756e36 100644 --- a/src/main/java/org/qora/ui/UiService.java +++ b/src/main/java/org/qora/ui/UiService.java @@ -1,5 +1,8 @@ package org.qora.ui; +import java.net.InetAddress; +import java.net.InetSocketAddress; + import org.eclipse.jetty.rewrite.handler.RedirectPatternRule; import org.eclipse.jetty.rewrite.handler.RewriteHandler; import org.eclipse.jetty.server.Server; @@ -14,69 +17,70 @@ import org.qora.settings.Settings; public class UiService { public static final String DOWNLOADS_RESOURCE_PATH = "node-ui-downloads"; - - private final Server server; - - public UiService() { - // Create node management UI server - this.server = new Server(Settings.getInstance().getUiPort()); - - // IP address based access control - InetAccessHandler accessHandler = new InetAccessHandler(); - for (String pattern : Settings.getInstance().getUiWhitelist()) { - accessHandler.include(pattern); - } - this.server.setHandler(accessHandler); - - // URL rewriting - RewriteHandler rewriteHandler = new RewriteHandler(); - accessHandler.setHandler(rewriteHandler); - - // Context - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); - context.setContextPath("/"); - rewriteHandler.setHandler(context); - - // Cross-origin resource sharing - FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class); - corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); - corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE"); - corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false"); - context.addFilter(corsFilterHolder, "/*", null); - - ClassLoader loader = this.getClass().getClassLoader(); - - // Node management UI download servlet - ServletHolder uiDownloadServlet = new ServletHolder("node-ui-download", new DefaultServlet(new DownloadResourceService())); - uiDownloadServlet.setInitParameter("resourceBase", loader.getResource(DOWNLOADS_RESOURCE_PATH + "/").toString()); - uiDownloadServlet.setInitParameter("dirAllowed", "true"); - uiDownloadServlet.setInitParameter("pathInfoOnly", "true"); - context.addServlet(uiDownloadServlet, "/downloads/*"); - - // Node management UI static content servlet - ServletHolder uiServlet = new ServletHolder("node-management-ui", DefaultServlet.class); - uiServlet.setInitParameter("resourceBase", loader.getResource("node-management-ui/").toString()); - uiServlet.setInitParameter("dirAllowed", "true"); - uiServlet.setInitParameter("pathInfoOnly", "true"); - context.addServlet(uiServlet, "/*"); - - rewriteHandler.addRule(new RedirectPatternRule("", "/index.html")); // node management UI start page - } - private static UiService instance; + private Server server; + + private UiService() { + } + public static UiService getInstance() { - if (instance == null) { + if (instance == null) instance = new UiService(); - } return instance; } public void start() { try { + // Create node management UI server + InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress()); + InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getUiPort()); + this.server = new Server(endpoint); + + // IP address based access control + InetAccessHandler accessHandler = new InetAccessHandler(); + for (String pattern : Settings.getInstance().getUiWhitelist()) { + accessHandler.include(pattern); + } + this.server.setHandler(accessHandler); + + // URL rewriting + RewriteHandler rewriteHandler = new RewriteHandler(); + accessHandler.setHandler(rewriteHandler); + + // Context + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + context.setContextPath("/"); + rewriteHandler.setHandler(context); + + // Cross-origin resource sharing + FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class); + corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); + corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE"); + corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false"); + context.addFilter(corsFilterHolder, "/*", null); + + ClassLoader loader = this.getClass().getClassLoader(); + + // Node management UI download servlet + ServletHolder uiDownloadServlet = new ServletHolder("node-ui-download", new DefaultServlet(new DownloadResourceService())); + uiDownloadServlet.setInitParameter("resourceBase", loader.getResource(DOWNLOADS_RESOURCE_PATH + "/").toString()); + uiDownloadServlet.setInitParameter("dirAllowed", "true"); + uiDownloadServlet.setInitParameter("pathInfoOnly", "true"); + context.addServlet(uiDownloadServlet, "/downloads/*"); + + // Node management UI static content servlet + ServletHolder uiServlet = new ServletHolder("node-management-ui", DefaultServlet.class); + uiServlet.setInitParameter("resourceBase", loader.getResource("node-management-ui/").toString()); + uiServlet.setInitParameter("dirAllowed", "true"); + uiServlet.setInitParameter("pathInfoOnly", "true"); + context.addServlet(uiServlet, "/*"); + + rewriteHandler.addRule(new RedirectPatternRule("", "/index.html")); // node management UI start page + // Start server - server.start(); + this.server.start(); } catch (Exception e) { // Failed to start throw new RuntimeException("Failed to start node management UI", e); @@ -86,10 +90,12 @@ public class UiService { public void stop() { try { // Stop server - server.stop(); + this.server.stop(); } catch (Exception e) { // Failed to stop } + + this.server = null; } }