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