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).
This commit is contained in:
catbref 2020-03-10 13:32:10 +00:00
parent a3c44428d3
commit 5bfc17bd64
11 changed files with 99 additions and 194 deletions

View File

@ -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();

View File

@ -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<Void, Void> {
static class OpenUiWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() {
List<String> 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<Void, Void> {
@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<Void, Void> {
static class ClosingWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() {
Controller.getInstance().shutdown();

View File

@ -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() {

View File

@ -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.
* <p>
* Sets <tt>Content-Type</tt> header to <tt>application/octet-stream</tt><br>
* Sets <tt>Content-Disposition</tt> header to <tt>attachment; filename="<i>basename</i>"</tt><br>
* where <i>basename</i> is that last component of requested URI path.
* <p>
* Example usage:<br>
* <br>
* <tt>... = new ServletHolder("servlet-name", new DefaultServlet(new DownloadResourceService()));</tt>
*/
public class DownloadResourceService extends ResourceService {
@Override
protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, final HttpContent content, Enumeration<String> 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);
}
}

View File

@ -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;
}
}

View File

@ -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 <T> List<T> randomize(List<T> inputList) {
List<T> outputList = new ArrayList<T>();
Iterator<T> 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;
}
}

View File

@ -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

View File

@ -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

View File

@ -1 +0,0 @@
Node UI goes here!

View File

@ -14,7 +14,7 @@ public class CheckTranslations {
private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" };
private static final Set<String> 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;