diff --git a/build.gradle b/build.gradle index 78cf8bb4..9a5720e5 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ def sparrowVersion = '0.9.3' def os = org.gradle.internal.os.OperatingSystem.current() def osName = os.getFamilyName() if(os.macOsX) { - osName = "mac" + osName = "osx" } group "com.sparrowwallet" @@ -19,6 +19,7 @@ repositories { mavenCentral() maven { url 'https://oss.sonatype.org/content/groups/public' } maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' } + maven { url 'https://jitpack.io' } } tasks.withType(AbstractArchiveTask) { @@ -54,6 +55,7 @@ dependencies { implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') { exclude group: 'com.nativelibs4java', module: 'bridj' } + implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7') implementation('org.controlsfx:controlsfx:11.0.2' ) { exclude group: 'org.openjfx', module: 'javafx-base' @@ -80,8 +82,8 @@ compileJava { processResources { doLast { - delete fileTree("$buildDir/resources/main/external").matching { - exclude "$osName/**" + delete fileTree("$buildDir/resources/main/native").matching { + exclude "${osName}/**" } } } @@ -101,7 +103,8 @@ run { "--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx", "--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx", "--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow", - "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow"] + "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", + "--add-opens=java.base/java.net=com.sparrowwallet.sparrow"] if(os.macOsX) { applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", @@ -118,6 +121,7 @@ jlink { requires 'javafx.base' requires 'com.fasterxml.jackson.databind' requires 'jdk.crypto.cryptoki' + requires 'java.management' } options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png'] @@ -139,6 +143,7 @@ jlink { "--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", + "--add-opens=java.base/java.net=com.sparrowwallet.sparrow", "--add-reads=com.sparrowwallet.merged.module=java.desktop"] } addExtraDependencies("javafx") @@ -160,7 +165,7 @@ jlink { } if(os.macOsX) { installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)'] - imageOptions += ['--icon', 'src/main/deploy/package/mac/sparrow.icns', '--resource-dir', 'src/main/deploy/package/mac/'] + imageOptions += ['--icon', 'src/main/deploy/package/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/'] installerType = "dmg" } } diff --git a/src/main/deploy/package/mac/Info.plist b/src/main/deploy/package/osx/Info.plist similarity index 100% rename from src/main/deploy/package/mac/Info.plist rename to src/main/deploy/package/osx/Info.plist diff --git a/src/main/deploy/package/mac/Sparrow.entitlements b/src/main/deploy/package/osx/Sparrow.entitlements similarity index 100% rename from src/main/deploy/package/mac/Sparrow.entitlements rename to src/main/deploy/package/osx/Sparrow.entitlements diff --git a/src/main/deploy/package/mac/sparrow.icns b/src/main/deploy/package/osx/sparrow.icns similarity index 100% rename from src/main/deploy/package/mac/sparrow.icns rename to src/main/deploy/package/osx/sparrow.icns diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index efb6089b..edfe311d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -280,6 +280,13 @@ public class AppController implements Initializable { connectionService.setPeriod(new Duration(SERVER_PING_PERIOD)); connectionService.setRestartOnFailure(true); + EventManager.get().register(connectionService); + connectionService.statusProperty().addListener((observable, oldValue, newValue) -> { + if(connectionService.isRunning()) { + EventManager.get().post(new StatusEvent(newValue)); + } + }); + connectionService.setOnSucceeded(successEvent -> { changeMode = false; onlineProperty.setValue(true); @@ -350,8 +357,8 @@ public class AppController implements Initializable { tk.createQuitMenuItem(MainApp.APP_NAME)); tk.setApplicationMenu(defaultApplicationMenu); - fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide")); - helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide")); + fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide")); + helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide")); } } @@ -1155,7 +1162,7 @@ public class AppController implements Initializable { public void statusUpdated(StatusEvent event) { statusBar.setText(event.getStatus()); - PauseTransition wait = new PauseTransition(Duration.seconds(10)); + PauseTransition wait = new PauseTransition(Duration.seconds(20)); wait.setOnFinished((e) -> { if(statusBar.getText().equals(event.getStatus())) { statusBar.setText(""); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TorStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TorStatusEvent.java new file mode 100644 index 00000000..242ae026 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/TorStatusEvent.java @@ -0,0 +1,13 @@ +package com.sparrowwallet.sparrow.event; + +public class TorStatusEvent { + private final String status; + + public TorStatusEvent(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index 74a57609..afc19291 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -23,7 +23,6 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.*; import java.util.stream.Collectors; -import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -200,7 +199,7 @@ public class Hwi { File hwiExecutable = Config.get().getHwi(); if(hwiExecutable != null && hwiExecutable.exists()) { if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(TEMP_FILE_PREFIX) || !testHwi(hwiExecutable))) { - if(Platform.getCurrent().getPlatformId().toLowerCase().equals("mac")) { + if(Platform.getCurrent() == Platform.OSX) { deleteDirectory(hwiExecutable.getParentFile()); } else { hwiExecutable.delete(); @@ -218,7 +217,7 @@ public class Hwi { //The check will still happen on first invocation, but will not thereafter //See https://github.com/bitcoin-core/HWI/issues/327 for details if(platform == Platform.OSX) { - InputStream inputStream = Hwi.class.getResourceAsStream("/external/mac/hwi-1.1.2-mac-amd64-signed.zip"); + InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/hwi-1.1.2-mac-amd64-signed.zip"); Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable)); File tempHwiDir = tempHwiDirPath.toFile(); //tempHwiDir.deleteOnExit(); @@ -249,10 +248,10 @@ public class Hwi { InputStream inputStream; Path tempExecPath; if(platform == Platform.WINDOWS) { - inputStream = Hwi.class.getResourceAsStream("/external/windows/hwi.exe"); + inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe"); tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null); } else { - inputStream = Hwi.class.getResourceAsStream("/external/linux/hwi"); + inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi"); tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 5208c5b8..7b3bf12a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.net; import com.github.arteam.simplejsonrpc.client.Transport; +import com.google.common.eventbus.Subscribe; import com.google.common.net.HostAndPort; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.Utils; @@ -8,8 +9,11 @@ import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; +import com.sparrowwallet.sparrow.event.TorStatusEvent; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.wallet.SendController; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; @@ -578,6 +582,7 @@ public class ElectrumServer { private boolean firstCall = true; private Thread reader; private long feeRatesRetrievedAt; + private StringProperty statusProperty = new SimpleStringProperty(); public ConnectionService() { this(true); @@ -675,6 +680,15 @@ public class ElectrumServer { public void uncaughtException(Thread t, Throwable e) { log.error("Uncaught error in ConnectionService", e); } + + @Subscribe + public void torStatus(TorStatusEvent event) { + statusProperty.set(event.getStatus()); + } + + public StringProperty statusProperty() { + return statusProperty; + } } public static class ReadRunnable implements Runnable { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java index 87b3f791..1cbe3baf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java @@ -14,12 +14,16 @@ public enum Protocol { TCP { @Override public Transport getTransport(HostAndPort server) { + if(isOnionAddress(server)) { + return new TorTcpTransport(server); + } + return new TcpTransport(server); } @Override public Transport getTransport(HostAndPort server, File serverCert) { - return new TcpTransport(server); + return getTransport(server); } @Override @@ -35,11 +39,19 @@ public enum Protocol { SSL { @Override public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { + if(isOnionAddress(server)) { + return new TorTcpOverTlsTransport(server); + } + return new TcpOverTlsTransport(server); } @Override public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + if(isOnionAddress(server)) { + return new TorTcpOverTlsTransport(server, serverCert); + } + return new TcpOverTlsTransport(server, serverCert); } @@ -82,6 +94,10 @@ public enum Protocol { return toUrlString() + hostAndPort.toString(); } + public boolean isOnionAddress(HostAndPort server) { + return server.getHost().toLowerCase().endsWith(".onion"); + } + public static Protocol getProtocol(String url) { if(url.startsWith("tcp://")) { return TCP; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java new file mode 100644 index 00000000..5fc8d638 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java @@ -0,0 +1,49 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLSocket; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class TorTcpOverTlsTransport extends TcpOverTlsTransport { + private static final Logger log = LoggerFactory.getLogger(TorTcpOverTlsTransport.class); + + public TorTcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException { + super(server); + } + + public TorTcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + super(server, crtFile); + } + + @Override + protected Socket createSocket() throws IOException { + TorTcpTransport torTcpTransport = new TorTcpTransport(server); + Socket socket = torTcpTransport.createSocket(); + + try { + Field socketField = socket.getClass().getDeclaredField("socket"); + socketField.setAccessible(true); + Socket innerSocket = (Socket)socketField.get(socket); + Field connectedField = innerSocket.getClass().getSuperclass().getDeclaredField("connected"); + connectedField.setAccessible(true); + connectedField.set(innerSocket, true); + } catch(Exception e) { + log.error("Could not set socket connected status", e); + } + + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(DEFAULT_PORT), true); + sslSocket.startHandshake(); + + return sslSocket; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java new file mode 100644 index 00000000..946c0baa --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java @@ -0,0 +1,55 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.StatusEvent; +import com.sparrowwallet.sparrow.event.TorStatusEvent; +import javafx.application.Platform; +import org.berndpruenster.netlayer.tor.*; + +import java.io.*; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; + +public class TorTcpTransport extends TcpTransport { + public static final String TOR_DIR_PREFIX = "tor"; + + public TorTcpTransport(HostAndPort server) { + super(server); + } + + @Override + protected Socket createSocket() throws IOException { + if(Tor.getDefault() == null) { + Platform.runLater(() -> { + String status = "Starting Tor..."; + EventManager.get().post(new TorStatusEvent(status)); + }); + + Path path = Files.createTempDirectory(TOR_DIR_PREFIX); + File torInstallDir = path.toFile(); + torInstallDir.deleteOnExit(); + try { + LinkedHashMap torrcOptionsMap = new LinkedHashMap<>(); + torrcOptionsMap.put("DisableNetwork", "0"); + Torrc override = new Torrc(torrcOptionsMap); + + NativeTor nativeTor = new NativeTor(torInstallDir, Collections.emptyList(), override); + Tor.setDefault(nativeTor); + } catch(TorCtlException e) { + e.printStackTrace(); + throw new IOException(e); + } + } + + Platform.runLater(() -> { + String status = "Tor running, connecting to " + server.toString() + "..."; + EventManager.get().post(new TorStatusEvent(status)); + }); + + return new TorSocket(server.getHost(), server.getPort(), "sparrow"); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index 1fc1af52..fe8e557c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -28,6 +28,8 @@ import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.Validator; import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.net.ssl.SSLHandshakeException; import java.io.File; @@ -36,6 +38,8 @@ import java.security.cert.CertificateFactory; import java.util.List; public class ServerPreferencesController extends PreferencesDetailController { + private static final Logger log = LoggerFactory.getLogger(ServerPreferencesController.class); + @FXML private TextField host; @@ -140,13 +144,20 @@ public class ServerPreferencesController extends PreferencesDetailController { testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false); - connectionService.setPeriod(Duration.minutes(1)); + connectionService.setPeriod(Duration.ZERO); + EventManager.get().register(connectionService); + connectionService.statusProperty().addListener((observable, oldValue, newValue) -> { + testResults.setText(testResults.getText() + "\n" + newValue); + }); + connectionService.setOnSucceeded(successEvent -> { + EventManager.get().unregister(connectionService); ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue(); showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner()); connectionService.cancel(); }); connectionService.setOnFailed(workerStateEvent -> { + EventManager.get().unregister(connectionService); showConnectionFailure(workerStateEvent); connectionService.cancel(); }); @@ -230,6 +241,7 @@ public class ServerPreferencesController extends PreferencesDetailController { private void showConnectionFailure(WorkerStateEvent failEvent) { Throwable e = failEvent.getSource().getException(); + log.error("Connection error", e); String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); if(e.getCause() != null && e.getCause() instanceof SSLHandshakeException) { reason = "SSL Handshake Error\n" + reason; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 5134badb..ed170d7f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -20,6 +20,7 @@ open module com.sparrowwallet.sparrow { requires com.fasterxml.jackson.databind; requires cbor; requires webcam.capture; + requires netlayer.jpms; requires centerdevice.nsmenufx; requires slf4j.api; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index d43a487d..32ae8522 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -29,11 +29,11 @@ - - + + - + @@ -67,7 +67,7 @@ - + diff --git a/src/main/resources/external/linux/hwi b/src/main/resources/native/linux/x64/hwi similarity index 100% rename from src/main/resources/external/linux/hwi rename to src/main/resources/native/linux/x64/hwi diff --git a/src/main/resources/external/mac/hwi-1.1.2-mac-amd64-signed.zip b/src/main/resources/native/osx/x64/hwi-1.1.2-mac-amd64-signed.zip similarity index 100% rename from src/main/resources/external/mac/hwi-1.1.2-mac-amd64-signed.zip rename to src/main/resources/native/osx/x64/hwi-1.1.2-mac-amd64-signed.zip diff --git a/src/main/resources/external/windows/hwi.exe b/src/main/resources/native/windows/x64/hwi.exe similarity index 100% rename from src/main/resources/external/windows/hwi.exe rename to src/main/resources/native/windows/x64/hwi.exe