diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 01b51a5e..829f0b00 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -77,7 +77,6 @@ import org.qortal.transaction.ArbitraryTransaction; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.ValidationResult; -import org.qortal.ui.UiService; import org.qortal.utils.Base58; import org.qortal.utils.ByteArray; import org.qortal.utils.NTP; @@ -348,16 +347,6 @@ public class Controller extends Thread { return; // Not System.exit() so that GUI can display error } - LOGGER.info(String.format("Starting node management UI on port %d", Settings.getInstance().getUiPort())); - try { - UiService uiService = UiService.getInstance(); - uiService.start(); - } catch (Exception e) { - LOGGER.error("Unable to start node management UI", e); - Gui.getInstance().fatalError("Node management UI failure", e); - return; // Not System.exit() so that GUI can display error - } - // If GUI is enabled, we're no longer starting up but actually running now Gui.getInstance().notifyRunning(); } @@ -638,9 +627,6 @@ public class Controller extends Thread { if (!isStopping) { isStopping = true; - LOGGER.info("Shutting down node management UI"); - UiService.getInstance().stop(); - LOGGER.info("Shutting down API"); ApiService.getInstance().stop(); diff --git a/src/main/java/org/qortal/gui/SysTray.java b/src/main/java/org/qortal/gui/SysTray.java index 70b7d1f0..c456d6fe 100644 --- a/src/main/java/org/qortal/gui/SysTray.java +++ b/src/main/java/org/qortal/gui/SysTray.java @@ -10,11 +10,14 @@ import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.io.IOException; import java.io.InputStream; +import java.net.InetSocketAddress; import java.net.URL; +import java.nio.channels.SocketChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,7 +33,7 @@ import org.apache.logging.log4j.Logger; import org.qortal.controller.Controller; import org.qortal.globalization.Translator; import org.qortal.settings.Settings; -import org.qortal.ui.UiService; +import org.qortal.utils.RandomizeList; import org.qortal.utils.URLViewer; public class SysTray { @@ -144,15 +147,11 @@ public class SysTray { } }); - JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_NODE_UI")); + JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_UI")); openUi.addActionListener(actionEvent -> { destroyHiddenDialog(); - try { - URLViewer.openWebpage(new URL("http://localhost:" + Settings.getInstance().getUiPort())); - } catch (Exception e) { - LOGGER.error("Unable to open node UI in browser"); - } + new OpenUiWorker().execute(); }); menu.add(openUi); @@ -174,7 +173,7 @@ public class SysTray { syncTime.addActionListener(actionEvent -> { destroyHiddenDialog(); - new SynchronizeWorker().execute(); + new SynchronizeClockWorker().execute(); }); menu.add(syncTime); } @@ -190,11 +189,53 @@ public class SysTray { return menu; } - class SynchronizeWorker extends SwingWorker { + static class OpenUiWorker extends SwingWorker { + @Override + protected Void doInBackground() { + List uiServers = new ArrayList<>(); + + String[] remoteUiServers = Settings.getInstance().getRemoteUiServers(); + uiServers.addAll(Arrays.asList(remoteUiServers)); + // Randomize remote servers + uiServers = RandomizeList.randomize(uiServers); + + // Prepend local servers + String[] localUiServers = Settings.getInstance().getLocalUiServers(); + uiServers.addAll(0, Arrays.asList(localUiServers)); + + // Check each server in turn before opening browser tab + int uiPort = Settings.getInstance().getUiServerPort(); + for (String uiServer : uiServers) { + InetSocketAddress socketAddress = new InetSocketAddress(uiServer, uiPort); + + // If we couldn't resolve try next + if (socketAddress.isUnresolved()) + continue; + + try (SocketChannel socketChannel = SocketChannel.open()) { + socketChannel.socket().connect(socketAddress, 100); + + // If we reach here, then socket connected to UI server! + URLViewer.openWebpage(new URL(String.format("http://%s:%d", uiServer, uiPort))); + + return null; + } catch (IOException e) { + // try next server + } catch (Exception e) { + LOGGER.error("Unable to open UI website in browser"); + return null; + } + } + + return null; + } + } + + static class SynchronizeClockWorker extends SwingWorker { @Override protected Void doInBackground() { // Extract reconfiguration script from resources - String resourceName = "/" + UiService.DOWNLOADS_RESOURCE_PATH + "/" + NTP_SCRIPT; + String resourceName = "/node-management/" + NTP_SCRIPT; Path scriptPath = Paths.get(NTP_SCRIPT); try (InputStream in = SysTray.class.getResourceAsStream(resourceName)) { @@ -218,7 +259,7 @@ public class SysTray { } } - class ClosingWorker extends SwingWorker { + static class ClosingWorker extends SwingWorker { @Override protected Void doInBackground() { Controller.getInstance().shutdown(); diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index cee8c018..a6de4052 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -31,9 +31,6 @@ public class Settings { private static final int MAINNET_API_PORT = 12391; private static final int TESTNET_API_PORT = 62391; - private static final int MAINNET_UI_PORT = 12390; - private static final int TESTNET_UI_PORT = 62390; - private static final Logger LOGGER = LogManager.getLogger(Settings.class); private static final String SETTINGS_FILENAME = "settings.json"; @@ -43,14 +40,17 @@ public class Settings { // Settings, and other config files private String userPath; - // Common to all networking (UI/API/P2P) + // Common to all networking (API/P2P) private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses - // Node management UI - private boolean uiEnabled = true; - private Integer uiPort; - private String[] uiWhitelist = new String[] { - "::1", "127.0.0.1" + // UI servers + private int uiPort = 12388; + private String[] uiLocalServers = new String[] { + "localhost", "172.24.1.1", "qor.tal" + }; + private String[] uiRemoteServers = new String[] { + "node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org", + "node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org" }; // API-related @@ -244,19 +244,16 @@ public class Settings { return this.userPath; } - public boolean isUiEnabled() { - return this.uiEnabled; + public int getUiServerPort() { + return this.uiPort; } - public int getUiPort() { - if (this.uiPort != null) - return this.uiPort; - - return this.isTestNet ? TESTNET_UI_PORT : MAINNET_UI_PORT; + public String[] getLocalUiServers() { + return this.uiLocalServers; } - public String[] getUiWhitelist() { - return this.uiWhitelist; + public String[] getRemoteUiServers() { + return this.uiRemoteServers; } public boolean isApiEnabled() { diff --git a/src/main/java/org/qortal/ui/DownloadResourceService.java b/src/main/java/org/qortal/ui/DownloadResourceService.java deleted file mode 100644 index 0345bdf0..00000000 --- a/src/main/java/org/qortal/ui/DownloadResourceService.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.qortal.ui; - -import java.io.IOException; -import java.util.Enumeration; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.HttpContent; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.server.ResourceService; -import org.eclipse.jetty.util.URIUtil; - -/** - * Replace ResourceService that delivers content as "attachments", typically forcing download instead of rendering. - *

- * Sets Content-Type header to application/octet-stream
- * Sets Content-Disposition header to attachment; filename="basename"
- * where basename is that last component of requested URI path. - *

- * Example usage:
- *
- * ... = new ServletHolder("servlet-name", new DefaultServlet(new DownloadResourceService())); - */ -public class DownloadResourceService extends ResourceService { - - @Override - protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, final HttpContent content, Enumeration reqRanges) throws IOException { - final boolean _pathInfoOnly = super.isPathInfoOnly(); - String servletPath = _pathInfoOnly ? "/" : request.getServletPath(); - String pathInfo = request.getPathInfo(); - String pathInContext = URIUtil.addPaths(servletPath,pathInfo); - - // Find basename of requested content - final int slashIndex = pathInContext.lastIndexOf(URIUtil.SLASH); - if (slashIndex != -1) - pathInContext = pathInContext.substring(slashIndex + 1); - - // Add appropriate headers - response.setHeader(HttpHeader.CONTENT_TYPE.asString(), "application/octet-stream"); - response.setHeader("Content-Disposition", "attachment; filename=\"" + pathInContext + "\""); - - return super.sendData(request, response, include, content, reqRanges); - } - -} diff --git a/src/main/java/org/qortal/ui/UiService.java b/src/main/java/org/qortal/ui/UiService.java deleted file mode 100644 index 83a09b8f..00000000 --- a/src/main/java/org/qortal/ui/UiService.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.qortal.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; -import org.eclipse.jetty.server.handler.InetAccessHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlets.CrossOriginFilter; -import org.qortal.settings.Settings; - -public class UiService { - - public static final String DOWNLOADS_RESOURCE_PATH = "node-ui-downloads"; - private static UiService instance; - - private Server server; - - private UiService() { - } - - public static UiService getInstance() { - 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 - this.server.start(); - } catch (Exception e) { - // Failed to start - throw new RuntimeException("Failed to start node management UI", e); - } - } - - public void stop() { - try { - // Stop server - this.server.stop(); - } catch (Exception e) { - // Failed to stop - } - - this.server = null; - } - -} diff --git a/src/main/java/org/qortal/utils/RandomizeList.java b/src/main/java/org/qortal/utils/RandomizeList.java new file mode 100644 index 00000000..ef557e57 --- /dev/null +++ b/src/main/java/org/qortal/utils/RandomizeList.java @@ -0,0 +1,29 @@ +package org.qortal.utils; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +public class RandomizeList { + private static final Random random = new Random(); + + public static List randomize(List inputList) { + List outputList = new ArrayList(); + + Iterator inputIterator = inputList.iterator(); + while (inputIterator.hasNext()) { + T element = inputIterator.next(); + + if (outputList.isEmpty()) { + outputList.add(element); + } else { + int outputIndex = random.nextInt(outputList.size() + 1); + outputList.add(outputIndex, element); + } + } + + return outputList; + } + +} diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties index e087a08e..9dd826e9 100644 --- a/src/main/resources/i18n/SysTray_en.properties +++ b/src/main/resources/i18n/SysTray_en.properties @@ -22,7 +22,7 @@ NTP_NAG_TEXT_UNIX = Install NTP service to get an accurate clock. NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix. -OPEN_NODE_UI = Open Node UI +OPEN_UI = Open UI SYNCHRONIZE_CLOCK = Synchronize clock diff --git a/src/main/resources/i18n/SysTray_zh.properties b/src/main/resources/i18n/SysTray_zh.properties index 587b4302..2f1a8abc 100644 --- a/src/main/resources/i18n/SysTray_zh.properties +++ b/src/main/resources/i18n/SysTray_zh.properties @@ -22,7 +22,7 @@ NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7 NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002 -OPEN_NODE_UI = \u5F00\u542F\u754C\u9762 +OPEN_UI = \u5F00\u542F\u754C\u9762 SYNCHRONIZE_CLOCK = \u540C\u6B65\u65F6\u949F diff --git a/src/main/resources/node-management-ui/index.html b/src/main/resources/node-management-ui/index.html deleted file mode 100644 index 5dffd921..00000000 --- a/src/main/resources/node-management-ui/index.html +++ /dev/null @@ -1 +0,0 @@ -Node UI goes here! diff --git a/src/main/resources/node-ui-downloads/ntpcfg.bat b/src/main/resources/node-management/ntpcfg.bat similarity index 100% rename from src/main/resources/node-ui-downloads/ntpcfg.bat rename to src/main/resources/node-management/ntpcfg.bat diff --git a/src/test/java/org/qortal/test/apps/CheckTranslations.java b/src/test/java/org/qortal/test/apps/CheckTranslations.java index df15b67e..ad6824ad 100644 --- a/src/test/java/org/qortal/test/apps/CheckTranslations.java +++ b/src/test/java/org/qortal/test/apps/CheckTranslations.java @@ -14,7 +14,7 @@ public class CheckTranslations { private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" }; private static final Set SYSTRAY_KEYS = Set.of("BLOCK_HEIGHT", "CHECK_TIME_ACCURACY", "CONNECTION", "CONNECTIONS", - "EXIT", "MINTING_DISABLED", "MINTING_ENABLED", "OPEN_NODE_UI", "SYNCHRONIZE_CLOCK", "SYNCHRONIZING_CLOCK"); + "EXIT", "MINTING_DISABLED", "MINTING_ENABLED", "OPEN_UI", "SYNCHRONIZE_CLOCK", "SYNCHRONIZING_CLOCK"); private static String failurePrefix;