From 7042dd819f03067e1f05aa525591a016f95bc4f5 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 19 Jul 2019 15:05:58 +0100 Subject: [PATCH] Include NTP checking/reconfigure tools + bump version to 1.3.1 SysTray pop-up menu now includes entry for launching https://time.is so node owners can check their system clocks against internet time. Windows installs also have additional systray menu entry which runs ntpcfg.bat script, included in resources. Also available as download via node-UI servlet, e.g. http://localhost:9880/downloads/ntpcfg.bat ntpcfg.bat reconfigures Windows Time Service with many NTP servers, restarts the service, and also makes sure it auto-starts on boot. Added DEBUG-level logging when rejecting nodes due to excessive time difference (during PROOF handshake stage). Bumped default settings values for minOutboundPeers from 10 to 20. Bumped default settings values for maxPeers from 30 to 50. --- src/main/java/org/qora/gui/SysTray.java | 67 +++++++++++++++++++ src/main/java/org/qora/network/Handshake.java | 4 +- src/main/java/org/qora/settings/Settings.java | 4 +- .../org/qora/ui/DownloadResourceService.java | 46 +++++++++++++ src/main/java/org/qora/ui/UiService.java | 12 +++- src/main/resources/i18n/SysTray_en.properties | 16 +++-- src/main/resources/i18n/SysTray_zh.properties | 16 +++-- .../resources/node-ui-downloads/ntpcfg.bat | 33 +++++++++ 8 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/qora/ui/DownloadResourceService.java create mode 100755 src/main/resources/node-ui-downloads/ntpcfg.bat diff --git a/src/main/java/org/qora/gui/SysTray.java b/src/main/java/org/qora/gui/SysTray.java index b53be3b3..0b10aa92 100644 --- a/src/main/java/org/qora/gui/SysTray.java +++ b/src/main/java/org/qora/gui/SysTray.java @@ -9,7 +9,15 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; +import java.io.IOException; +import java.io.InputStream; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; import javax.swing.JDialog; import javax.swing.JMenuItem; @@ -23,11 +31,13 @@ import org.apache.logging.log4j.Logger; import org.qora.controller.Controller; import org.qora.globalization.Translator; import org.qora.settings.Settings; +import org.qora.ui.UiService; import org.qora.utils.URLViewer; public class SysTray { protected static final Logger LOGGER = LogManager.getLogger(SplashFrame.class); + private static final String NTP_SCRIPT = "ntpcfg.bat"; private static SysTray instance; private TrayIcon trayIcon = null; @@ -145,6 +155,35 @@ public class SysTray { }); menu.add(openUi); + JMenuItem openTimeCheck = new JMenuItem(Translator.INSTANCE.translate("SysTray", "CHECK_TIME_ACCURACY")); + openTimeCheck.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + destroyHiddenDialog(); + + try { + URLViewer.openWebpage(new URL("https://time.is")); + } catch (Exception e1) { + LOGGER.error("Unable to open time-check website in browser"); + } + } + }); + menu.add(openTimeCheck); + + // Only for Windows users + if (System.getProperty("os.name").toLowerCase().contains("win")) { + JMenuItem syncTime = new JMenuItem(Translator.INSTANCE.translate("SysTray", "SYNCHRONIZE_CLOCK")); + syncTime.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + destroyHiddenDialog(); + + new SynchronizeWorker().execute(); + } + }); + menu.add(syncTime); + } + JMenuItem exit = new JMenuItem(Translator.INSTANCE.translate("SysTray", "EXIT")); exit.addActionListener(new ActionListener() { @Override @@ -159,6 +198,34 @@ public class SysTray { return menu; } + class SynchronizeWorker extends SwingWorker { + @Override + protected Void doInBackground() { + // Extract reconfiguration script from resources + String resourceName = "/" + UiService.DOWNLOADS_RESOURCE_PATH + "/" + NTP_SCRIPT; + Path scriptPath = Paths.get(NTP_SCRIPT); + + try (InputStream in = SysTray.class.getResourceAsStream(resourceName)) { + Files.copy(in, scriptPath, StandardCopyOption.REPLACE_EXISTING); + } catch (IllegalArgumentException | IOException e) { + LOGGER.warn(String.format("Couldn't locate NTP configuration resource: %s", resourceName)); + return null; + } + + // Now execute extracted script + List scriptCmd = Arrays.asList(NTP_SCRIPT); + LOGGER.info(String.format("Running NTP configuration script: %s", String.join(" ", scriptCmd))); + try { + new ProcessBuilder(scriptCmd).start(); + } catch (IOException e) { + LOGGER.warn(String.format("Failed to execute NTP configuration script: %s", e.getMessage())); + return null; + } + + return null; + } + } + class ClosingWorker extends SwingWorker { @Override protected Void doInBackground() { diff --git a/src/main/java/org/qora/network/Handshake.java b/src/main/java/org/qora/network/Handshake.java index 17dbc6c6..cafa6d4f 100644 --- a/src/main/java/org/qora/network/Handshake.java +++ b/src/main/java/org/qora/network/Handshake.java @@ -101,8 +101,10 @@ public enum Handshake { ProofMessage proofMessage = (ProofMessage) message; // Check peer's timestamp is within acceptable bounds - if (Math.abs(proofMessage.getTimestamp() - peer.getConnectionTimestamp()) > MAX_TIMESTAMP_DELTA) + if (Math.abs(proofMessage.getTimestamp() - peer.getConnectionTimestamp()) > MAX_TIMESTAMP_DELTA) { + LOGGER.debug(String.format("Rejecting PROOF from %s as timestamp delta %d greater than max %d", peer, Math.abs(proofMessage.getTimestamp() - peer.getConnectionTimestamp()), MAX_TIMESTAMP_DELTA)); return null; + } // If we connected outbound to peer, then this is a faked confirmation response, so we're good if (peer.isOutbound()) diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 8659f461..9ceb1840 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -75,9 +75,9 @@ public class Settings { /** Minimum number of peers to allow block generation / synchronization. */ private int minBlockchainPeers = 3; /** Target number of outbound connections to peers we should make. */ - private int minOutboundPeers = 10; + private int minOutboundPeers = 20; /** Maximum number of peer connections we allow. */ - private int maxPeers = 30; + private int maxPeers = 50; // Which blockchains this node is running private String blockchainConfig = null; // use default from resources diff --git a/src/main/java/org/qora/ui/DownloadResourceService.java b/src/main/java/org/qora/ui/DownloadResourceService.java new file mode 100644 index 00000000..57613560 --- /dev/null +++ b/src/main/java/org/qora/ui/DownloadResourceService.java @@ -0,0 +1,46 @@ +package org.qora.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/qora/ui/UiService.java b/src/main/java/org/qora/ui/UiService.java index 3e515a78..647b8f9b 100644 --- a/src/main/java/org/qora/ui/UiService.java +++ b/src/main/java/org/qora/ui/UiService.java @@ -13,6 +13,8 @@ import org.qora.settings.Settings; public class UiService { + public static final String DOWNLOADS_RESOURCE_PATH = "node-ui-downloads"; + private final Server server; public UiService() { @@ -42,9 +44,17 @@ public class UiService { 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); - ClassLoader loader = this.getClass().getClassLoader(); uiServlet.setInitParameter("resourceBase", loader.getResource("node-management-ui/").toString()); uiServlet.setInitParameter("dirAllowed", "true"); uiServlet.setInitParameter("pathInfoOnly", "true"); diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties index ba6bd5cf..d161acd0 100644 --- a/src/main/resources/i18n/SysTray_en.properties +++ b/src/main/resources/i18n/SysTray_en.properties @@ -1,7 +1,15 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # SysTray pop-up menu -OPEN_NODE_UI=Open Node UI -EXIT=Exit + +CHECK_TIME_ACCURACY = Check time accuracy + +EXIT = Exit # Nagging about lack of NTP time sync -NTP_NAG_CAPTION=No connections? -NTP_NAG_TEXT=Please enable Windows automatic time synchronization +NTP_NAG_CAPTION = No connections? + +NTP_NAG_TEXT = Please enable Windows automatic time synchronization + +OPEN_NODE_UI = Open Node UI + +SYNCHRONIZE_CLOCK = Synchronize clock diff --git a/src/main/resources/i18n/SysTray_zh.properties b/src/main/resources/i18n/SysTray_zh.properties index 1e56cba7..ff161d3d 100644 --- a/src/main/resources/i18n/SysTray_zh.properties +++ b/src/main/resources/i18n/SysTray_zh.properties @@ -1,7 +1,15 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # SysTray pop-up menu -OPEN_NODE_UI=\u5F00\u542F\u754C\u9762 -EXIT=\u9000\u51FA\u8F6F\u4EF6 + +CHECK_TIME_ACCURACY = \u68C0\u67E5\u65F6\u95F4\u51C6\u786E\u6027 + +EXIT = \u9000\u51FA\u8F6F\u4EF6 # Nagging about lack of NTP time sync -NTP_NAG_CAPTION=\u6CA1\u6709\u8FDE\u63A5\u4E0A\u8282\u70B9\uFF1F -NTP_NAG_TEXT=\u8BF7\u542F\u7528Windows\u81EA\u52A8\u65F6\u95F4\u540C\u6B65\u3002 +NTP_NAG_CAPTION = \u6CA1\u6709\u8FDE\u63A5\u4E0A\u8282\u70B9\uFF1F + +NTP_NAG_TEXT = \u8BF7\u542F\u7528Windows\u81EA\u52A8\u65F6\u95F4\u540C\u6B65\u3002 + +OPEN_NODE_UI = \u5F00\u542F\u754C\u9762 + +SYNCHRONIZE_CLOCK = \u540C\u6B65\u65F6\u949F diff --git a/src/main/resources/node-ui-downloads/ntpcfg.bat b/src/main/resources/node-ui-downloads/ntpcfg.bat new file mode 100755 index 00000000..e9725808 --- /dev/null +++ b/src/main/resources/node-ui-downloads/ntpcfg.bat @@ -0,0 +1,33 @@ +@echo off + +:: BatchGotAdmin +:------------------------------------- +REM --> Check for permissions +>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system" + +REM --> If error flag set, we do not have admin. +if '%errorlevel%' NEQ '0' ( + echo Requesting administrative privileges... + goto UACPrompt +) else ( goto gotAdmin ) + +:UACPrompt + echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs" + echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs" + + "%temp%\getadmin.vbs" + exit /B + +:gotAdmin + if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" ) + pushd "%CD%" + CD /D "%~dp0" +:-------------------------------------- + +net stop "Windows Time" + +w32tm /config "/manualpeerlist:pool.ntp.org 0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org cn.pool.ntp.org 0.cn.pool.ntp.org 1.cn.pool.ntp.org 2.cn.pool.ntp.org 3.cn.pool.ntp.org" + +net start "Windows Time" + +sc config w32time start= auto