From 5bfc17bd64e5f4ec2f085bdd2abf5ee5ddfb45e9 Mon Sep 17 00:00:00 2001 From: catbref Date: Tue, 10 Mar 2020 13:32:10 +0000 Subject: [PATCH] Remove node UI and have tray icon open local/remove UI server No more "node UI". UI provided by 3rd party. "Open UI" tray icon menu item now attempts to open UI at various local servers (see Settings.uilocalServers) or some random remote server (Settings.uiRemoteServers). Default UI port now 12388 (Settings.uiPort). --- .../org/qortal/controller/Controller.java | 14 --- src/main/java/org/qortal/gui/SysTray.java | 63 +++++++++-- .../java/org/qortal/settings/Settings.java | 33 +++--- .../qortal/ui/DownloadResourceService.java | 46 -------- src/main/java/org/qortal/ui/UiService.java | 101 ------------------ .../java/org/qortal/utils/RandomizeList.java | 29 +++++ src/main/resources/i18n/SysTray_en.properties | 2 +- src/main/resources/i18n/SysTray_zh.properties | 2 +- .../resources/node-management-ui/index.html | 1 - .../ntpcfg.bat | 0 .../qortal/test/apps/CheckTranslations.java | 2 +- 11 files changed, 99 insertions(+), 194 deletions(-) delete mode 100644 src/main/java/org/qortal/ui/DownloadResourceService.java delete mode 100644 src/main/java/org/qortal/ui/UiService.java create mode 100644 src/main/java/org/qortal/utils/RandomizeList.java delete mode 100644 src/main/resources/node-management-ui/index.html rename src/main/resources/{node-ui-downloads => node-management}/ntpcfg.bat (100%) 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;