diff --git a/build.gradle b/build.gradle index 6907be64..8cb8051b 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,19 @@ if(System.getProperty("os.arch") == "aarch64") { } def headless = "true".equals(System.getProperty("java.awt.headless")) +def vTor = '4.7.13-4' +def vKmpTor = '1.4.2' +def kmpOs = osName +if(os.macOsX) { + kmpOs = "macos" +} else if(os.windows) { + kmpOs = "mingw" +} +def kmpArch = "x64" +if(System.getProperty("os.arch") == "aarch64") { + kmpArch = "arm64" +} + group "com.sparrowwallet" version "${sparrowVersion}" @@ -84,10 +97,15 @@ dependencies { implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") { exclude group: 'com.nativelibs4java', module: 'bridj' } - implementation("com.sparrowwallet:netlayer-jpms-${osName}${targetName}:0.6.8") { - exclude group: 'org.jetbrains.kotlin' + implementation "io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}" + if(kmpOs == "linux" && kmpArch == "arm64") { + implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}") + } else { + implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}") } - implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20') + implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}") + implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}") + implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.6.4') implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7') implementation('org.controlsfx:controlsfx:11.1.0' ) { exclude group: 'org.openjfx', module: 'javafx-base' @@ -163,7 +181,8 @@ run { "--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow", "--add-opens=java.base/java.net=com.sparrowwallet.sparrow", "--add-opens=java.base/java.io=com.google.gson", - "--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow"] + "--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow", + "--add-reads=kotlin.stdlib=kotlinx.coroutines.core.jvm"] if(os.macOsX) { applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png", @@ -221,7 +240,8 @@ jlink { "--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind", "--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation", "--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core", - "--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"] + "--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor", + "--add-reads=kotlin.stdlib=kotlinx.coroutines.core.jvm"] if(os.windows) { jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"] @@ -403,11 +423,6 @@ extraJavaModuleInfo { requires('java.xml') requires('java.logging') } - module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') { - exports('mu') - requires('kotlin.stdlib') - requires('org.slf4j') - } module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') { exports('com.google.common.util.concurrent.internal') } @@ -483,13 +498,6 @@ extraJavaModuleInfo { requires('javafx.graphics') } module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0') - module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20') - module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20') - module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') { - exports('kotlin') - exports('kotlin.jvm') - exports('kotlin.collections') - } module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') { exports('com.sparrowwallet.hummingbird') exports('com.sparrowwallet.hummingbird.registry') @@ -597,29 +605,131 @@ extraJavaModuleInfo { requires('javafx.graphics') requires('java.xml') } - module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') { - exports('org.berndpruenster.netlayer.tor') - requires('com.github.ravn.jsocks') - requires('com.github.JesusMcCloud.jtorctl') + module('kotlinx-coroutines-core-jvm-1.6.4.jar', 'kotlinx.coroutines.core.jvm', '1.6.4') { + exports('kotlinx.coroutines') requires('kotlin.stdlib') - requires('commons.compress') - requires('org.tukaani.xz') + requires('java.instrument') + uses('kotlinx.coroutines.CoroutineExceptionHandler') + uses('kotlinx.coroutines.internal.MainDispatcherFactory') + } + module('kotlinx-coroutines-javafx-1.6.4.jar', 'kotlinx.coroutines.javafx', '1.6.4') { + exports('kotlinx.coroutines.javafx') + requires('kotlinx.coroutines.core.jvm') + requires('kotlin.stdlib') + requires('javafx.graphics') + } + module("kmp-tor-jvm-${vKmpTor}.jar", 'kmp.tor.jvm', "${vTor}-${vKmpTor}") { + exports('io.matthewnelson.kmp.tor') + requires('kmp.tor.binary.extract.jvm') + requires('kmp.tor.manager.jvm') + requires('kmp.tor.manager.common.jvm') + requires('kmp.tor.controller.common.jvm') + requires('kotlin.stdlib') + requires('kotlinx.coroutines.core.jvm') requires('java.management') - requires('io.github.microutils.kotlin.logging') } - module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') { - exports('net.freehaven.tor.control') + if(kmpOs == "linux" && kmpArch == "arm64") { + module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") { + exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}") + exports("kmptor.${kmpOs}.${kmpArch}") + } + } else { + module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") { + exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}") + exports("kmptor.${kmpOs}.${kmpArch}") + } } - module('commons-compress-1.18.jar', 'commons.compress', '1.18') { - exports('org.apache.commons.compress') - requires('org.tukaani.xz') + module("kmp-tor-binary-extract-jvm-${vTor}.jar", 'kmp.tor.binary.extract.jvm', "${vTor}") { + exports('io.matthewnelson.kmp.tor.binary.extract') + exports('io.matthewnelson.kmp.tor.binary.extract.internal') + requires('kotlin.stdlib') + requires("kmp.tor.binary.${kmpOs}${kmpArch}") + requires('kmp.tor.binary.geoip.jvm') } - module('xz-1.6.jar', 'org.tukaani.xz', '1.6') { - exports('org.tukaani.xz') + module("kmp-tor-manager-jvm-${vKmpTor}.jar", 'kmp.tor.manager.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.manager') + exports('io.matthewnelson.kmp.tor.manager.util') + requires('kmp.tor.controller.common.jvm') + requires('kmp.tor.manager.common.jvm') + requires('kotlin.stdlib') + requires('kotlinx.coroutines.core.jvm') + requires('kotlinx.atomicfu') + requires('kmp.tor.controller.jvm') + requires('kmp.tor.common.jvm') } - module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') { - exports('com.runjva.sourceforge.jsocks.protocol') - requires('org.slf4j') + module("kmp-tor-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.manager.common.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.manager.common') + exports('io.matthewnelson.kmp.tor.manager.common.event') + exports('io.matthewnelson.kmp.tor.manager.common.state') + requires('kmp.tor.controller.common.jvm') + requires('kmp.tor.common.jvm') + requires('kotlin.stdlib') + } + module("kmp-tor-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.controller.common.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.controller.common.config') + exports('io.matthewnelson.kmp.tor.controller.common.file') + exports('io.matthewnelson.kmp.tor.controller.common.control') + exports('io.matthewnelson.kmp.tor.controller.common.control.usecase') + exports('io.matthewnelson.kmp.tor.controller.common.events') + exports('io.matthewnelson.kmp.tor.controller.common.exceptions') + requires('kmp.tor.common.jvm') + requires('kotlin.stdlib') + requires('kotlinx.atomicfu') + } + module("kmp-tor-common-jvm-${vKmpTor}.jar", 'kmp.tor.common.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.common.address') + requires('parcelize.jvm') + requires('kotlin.stdlib') + } + module("kmp-tor-controller-jvm-${vKmpTor}.jar", 'kmp.tor.controller.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.controller.internal.controller') + requires('kmp.tor.common.jvm') + requires('kmp.tor.controller.common.jvm') + requires('kotlinx.coroutines.core.jvm') + requires('kotlin.stdlib') + requires('kotlinx.atomicfu') + requires('encoding.core.jvm') + requires('encoding.base16.jvm') + } + module("kmp-tor-ext-callback-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.common.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.ext.callback.common') + } + module("kmp-tor-ext-callback-manager-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.ext.callback.manager') + requires('kmp.tor.manager.jvm') + requires('kmp.tor.ext.callback.common.jvm') + requires('kmp.tor.ext.callback.manager.common.jvm') + requires('kmp.tor.ext.callback.controller.common.jvm') + requires('kmp.tor.manager.common.jvm') + requires('kmp.tor.controller.common.jvm') + requires('kotlin.stdlib') + requires('kotlinx.coroutines.core.jvm') + } + module("kmp-tor-ext-callback-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.common.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.ext.callback.manager.common') + requires('kmp.tor.ext.callback.controller.common.jvm') + } + module("kmp-tor-ext-callback-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.controller.common.jvm', "${vKmpTor}") { + exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control') + exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase') + } + module("kmp-tor-binary-geoip-jvm-${vTor}.jar", 'kmp.tor.binary.geoip.jvm', "${vTor}") { + exports('io.matthewnelson.kmp.tor.binary.geoip') + exports('kmptor') + } + module("encoding-base16-jvm-1.2.1.jar", 'encoding.base16.jvm', "1.2.1") { + exports('io.matthewnelson.encoding.base16') + requires('encoding.core.jvm') + requires('kotlin.stdlib') + } + module("encoding-base32-jvm-1.2.1.jar", 'encoding.base32.jvm', "1.2.1") + module("encoding-base64-jvm-1.2.1.jar", 'encoding.base64.jvm', "1.2.1") + module("encoding-core-jvm-1.2.1.jar", 'encoding.core.jvm', "1.2.1") { + exports('io.matthewnelson.encoding.core') + requires('kotlin.stdlib') + } + module("parcelize-jvm-0.1.2.jar", 'parcelize.jvm', "0.1.2") { + exports('io.matthewnelson.component.parcelize') } module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0') module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') { diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index ae48f7c2..b6579f49 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -50,7 +50,6 @@ import javafx.stage.Screen; import javafx.stage.Stage; import javafx.stage.Window; import javafx.util.Duration; -import org.berndpruenster.netlayer.tor.Tor; import org.controlsfx.glyphfont.Glyph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -252,7 +251,7 @@ public class AppServices { } if(Tor.getDefault() != null) { - Tor.getDefault().shutdown(); + Tor.getDefault().getTorManager().destroy(true, success -> {}); } } @@ -414,23 +413,6 @@ public class AppServices { EventManager.get().post(new TorReadyStatusEvent()); }); torService.setOnFailed(workerStateEvent -> { - Throwable exception = workerStateEvent.getSource().getException(); - if(exception instanceof TorServerAlreadyBoundException) { - String proxyServer = Config.get().getProxyServer(); - if(proxyServer == null || proxyServer.equals("")) { - proxyServer = "localhost:9050"; - Config.get().setProxyServer(proxyServer); - } - - if(proxyServer.equals("localhost:9050") || proxyServer.equals("127.0.0.1:9050")) { - Config.get().setUseProxy(true); - torService.cancel(); - restartServices(); - EventManager.get().post(new TorExternalStatusEvent()); - return; - } - } - EventManager.get().post(new TorFailedStatusEvent(workerStateEvent.getSource().getException())); }); @@ -490,8 +472,7 @@ public class AppServices { InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT)); proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress); } else if(AppServices.isTorRunning()) { - InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT); - proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress); + proxy = Tor.getDefault().getProxy(); } //Setting new proxy authentication credentials will force a new Tor circuit to be created @@ -546,7 +527,7 @@ public class AppServices { public static HostAndPort getTorProxy() { return AppServices.isTorRunning() ? - HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : + Tor.getDefault().getProxyHostAndPort() : (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer())); } diff --git a/src/main/java/com/sparrowwallet/sparrow/TorLogHandler.java b/src/main/java/com/sparrowwallet/sparrow/TorLogHandler.java deleted file mode 100644 index 53adafa6..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/TorLogHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sparrowwallet.sparrow; - -import com.sparrowwallet.drongo.LogHandler; -import com.sparrowwallet.sparrow.event.TorStatusEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.event.Level; - -public class TorLogHandler implements LogHandler { - private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class); - - @Override - public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) { - log.debug(message); - EventManager.get().post(new TorStatusEvent(message)); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TorStatusLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/TorStatusLabel.java index 81549b03..b91bc84c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TorStatusLabel.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TorStatusLabel.java @@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.Config; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; +import javafx.concurrent.Worker; import javafx.geometry.Insets; import javafx.scene.Group; import javafx.scene.Node; @@ -33,15 +34,15 @@ public class TorStatusLabel extends Label { } public void update() { - boolean proxyInUse = AppServices.isUsingProxy(); - boolean internal = AppServices.isTorRunning(); - - if(!proxyInUse || internal) { + if(!Config.get().isUseProxy()) { torConnectionTest.cancel(); - if(internal) { + if(AppServices.isTorRunning()) { setTooltip(new Tooltip("Internal Tor proxy enabled")); } } else if(!torConnectionTest.isRunning()) { + if(torConnectionTest.getState() == Worker.State.CANCELLED || torConnectionTest.getState() == Worker.State.FAILED) { + torConnectionTest.reset(); + } torConnectionTest.setPeriod(Duration.seconds(20.0)); torConnectionTest.setBackoffStrategy(null); torConnectionTest.setOnSucceeded(workerStateEvent -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java index 1ba1c899..0badf9b6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java @@ -1,13 +1,10 @@ package com.sparrowwallet.sparrow.event; -import com.sparrowwallet.sparrow.net.TorServerAlreadyBoundException; - public class TorFailedStatusEvent extends TorStatusEvent { private final Throwable exception; public TorFailedStatusEvent(Throwable exception) { - super("Tor failed to start: " + (exception instanceof TorServerAlreadyBoundException ? exception.getCause().getMessage() + " Is a Tor proxy already running?" : - (exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage()))); + super("Tor failed to start: " + (exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage())); this.exception = exception; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Auth47.java b/src/main/java/com/sparrowwallet/sparrow/net/Auth47.java index aae2c647..afb920a9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Auth47.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Auth47.java @@ -128,7 +128,7 @@ public class Auth47 { } Proxy proxy = AppServices.getProxy(); - if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX)) { + if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(Tor.TOR_ADDRESS_SUFFIX)) { throw new Auth47Exception("A Tor proxy must be configured to authenticate this resource."); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java index 4d5a9921..2c5a5424 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java @@ -184,7 +184,7 @@ public class Bwt { private HostAndPort getTorProxy() { return AppServices.isTorRunning() ? - HostAndPort.fromParts("127.0.0.1", TorService.PROXY_PORT) : + Tor.getDefault().getProxyHostAndPort() : (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer().replace("localhost", "127.0.0.1"))); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/LnurlAuth.java b/src/main/java/com/sparrowwallet/sparrow/net/LnurlAuth.java index faa08f6c..19dd6759 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/LnurlAuth.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/LnurlAuth.java @@ -108,7 +108,7 @@ public class LnurlAuth { } Proxy proxy = AppServices.getProxy(); - if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX)) { + if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(Tor.TOR_ADDRESS_SUFFIX)) { throw new LnurlAuthException("A Tor proxy must be configured to authenticate this resource."); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java index 81c62c1f..66bc58a3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java @@ -151,7 +151,7 @@ public enum Protocol { } public static boolean isOnionHost(String host) { - return host != null && host.toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX); + return host != null && host.toLowerCase(Locale.ROOT).endsWith(Tor.TOR_ADDRESS_SUFFIX); } public static boolean isOnionAddress(Server server) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java index 923df8b4..325a08f9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ProxyTcpOverTlsTransport.java @@ -32,7 +32,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport { protected void createSocket() throws IOException { InetSocketAddress proxyAddr = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT)); socket = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr)); - socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(Protocol.SSL.getDefaultPort()))); + socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(getDefaultPort()))); socket = sslSocketFactory.createSocket(socket, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true); startHandshake((SSLSocket)socket); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java index 38504747..df34b435 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpOverTlsTransport.java @@ -125,4 +125,9 @@ public class TcpOverTlsTransport extends TcpTransport { return Storage.getCertificateFile(server.getHost()) == null; } + + @Override + protected int getDefaultPort() { + return Protocol.SSL.getDefaultPort(); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java index 66dda50c..cbf17515 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java @@ -254,7 +254,11 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter { protected void createSocket() throws IOException { socket = socketFactory.createSocket(); - socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(Protocol.TCP.getDefaultPort()))); + socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(getDefaultPort()))); + } + + protected int getDefaultPort() { + return Protocol.TCP.getDefaultPort(); } public boolean isClosed() { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Tor.java b/src/main/java/com/sparrowwallet/sparrow/net/Tor.java new file mode 100644 index 00000000..03d39678 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/Tor.java @@ -0,0 +1,158 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.TorStatusEvent; +import com.sparrowwallet.sparrow.io.Storage; +import io.matthewnelson.kmp.tor.KmpTorLoaderJvm; +import io.matthewnelson.kmp.tor.PlatformInstaller; +import io.matthewnelson.kmp.tor.TorConfigProviderJvm; +import io.matthewnelson.kmp.tor.binary.extract.TorBinaryResource; +import io.matthewnelson.kmp.tor.common.address.Port; +import io.matthewnelson.kmp.tor.common.address.ProxyAddress; +import io.matthewnelson.kmp.tor.controller.common.config.TorConfig; +import io.matthewnelson.kmp.tor.controller.common.file.Path; +import io.matthewnelson.kmp.tor.ext.callback.manager.CallbackTorManager; +import io.matthewnelson.kmp.tor.manager.TorManager; +import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent; +import io.matthewnelson.kmp.tor.manager.util.PortUtil; +import org.controlsfx.tools.Platform; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; + +public class Tor { + private static final Logger log = LoggerFactory.getLogger(Tor.class); + + public static final String TOR_DIR = "tor"; + public static final String TOR_ADDRESS_SUFFIX = ".onion"; + + private static Tor tor; + + private final Path path = Path.invoke(Storage.getSparrowHome().getAbsolutePath()).builder().addSegment(TOR_DIR).build(); + private final CallbackTorManager instance; + private ProxyAddress socksAddress; + + public Tor() { + Platform platform = Platform.getCurrent(); + String arch = System.getProperty("os.arch"); + PlatformInstaller installer; + PlatformInstaller.InstallOption installOption = PlatformInstaller.InstallOption.CleanInstallIfMissing; + + if(platform == Platform.OSX) { + if(arch.equals("aarch64")) { + installer = PlatformInstaller.macosArm64(installOption); + } else { + installer = PlatformInstaller.macosX64(installOption); + } + } else if(platform == Platform.WINDOWS) { + installer = PlatformInstaller.mingwX64(installOption); + } else if(platform == Platform.UNIX) { + if(arch.equals("aarch64")) { + TorBinaryResource linuxArm64 = TorBinaryResource.from(TorBinaryResource.OS.Linux, "arm64", + "588496f3164d52b91f17e4db3372d8dfefa6366a8df265eebd4a28d4128992aa", + List.of("libevent-2.1.so.7.gz", "libstdc++.so.6.gz", "libcrypto.so.1.1.gz", "tor.gz", "libssl.so.1.1.gz")); + installer = PlatformInstaller.custom(installOption, linuxArm64); + } else { + installer = PlatformInstaller.linuxX64(installOption); + } + } else { + throw new UnsupportedOperationException("Sparrow's bundled Tor is not supported on " + platform + " " + arch); + } + + TorConfigProviderJvm torConfigProviderJvm = new TorConfigProviderJvm() { + @NotNull + @Override + public Path getWorkDir() { + return path.builder().addSegment("work").build(); + } + + @NotNull + @Override + public Path getCacheDir() { + return path.builder().addSegment("cache").build(); + } + + @NotNull + @Override + protected TorConfig provide() { + TorConfig.Builder builder = new TorConfig.Builder(); + + TorConfig.Setting.DormantCanceledByStartup dormantCanceledByStartup = new TorConfig.Setting.DormantCanceledByStartup(); + dormantCanceledByStartup.set(TorConfig.Option.AorTorF.getTrue()); + builder.put(dormantCanceledByStartup); + + return builder.build(); + } + }; + + KmpTorLoaderJvm jvmLoader = new KmpTorLoaderJvm(installer, torConfigProviderJvm); + TorManager torManager = TorManager.newInstance(jvmLoader); + + torManager.debug(true); + torManager.addListener(new TorManagerEvent.Listener() { + @Override + public void managerEventWarn(@NotNull String message) { + log.warn(message); + EventManager.get().post(new TorStatusEvent(message)); + } + + @Override + public void managerEventInfo(@NotNull String message) { + log.info(message); + EventManager.get().post(new TorStatusEvent(message)); + } + + @Override + public void managerEventDebug(@NotNull String message) { + log.debug(message); + EventManager.get().post(new TorStatusEvent(message)); + } + + @Override + public void managerEventAddressInfo(@NotNull TorManagerEvent.AddressInfo info) { + if(info.isNull) { + socksAddress = null; + } else { + socksAddress = info.socksInfoToProxyAddress().iterator().next(); + } + } + }); + + this.instance = new CallbackTorManager(torManager, uncaughtException -> { + log.error("Uncaught exception from CallbackTorManager", uncaughtException); + }); + } + + public static Tor getDefault() { + return Tor.tor; + } + + public static void setDefault(Tor tor) { + Tor.tor = tor; + } + + public CallbackTorManager getTorManager() { + return instance; + } + + public Proxy getProxy() { + if(socksAddress == null) { + throw new IllegalStateException("Tor socks proxy is not yet instantiated"); + } + + return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(socksAddress.address.getValue(), socksAddress.port.getValue())); + } + + public HostAndPort getProxyHostAndPort() { + return HostAndPort.fromParts(socksAddress.address.getValue(), socksAddress.port.getValue()); + } + + public static boolean isRunningExternally() { + return !PortUtil.isTcpPortAvailable(Port.invoke(9050)); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorServerAlreadyBoundException.java b/src/main/java/com/sparrowwallet/sparrow/net/TorServerAlreadyBoundException.java deleted file mode 100644 index 2ee06fab..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorServerAlreadyBoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sparrowwallet.sparrow.net; - -public class TorServerAlreadyBoundException extends TorServerException { - public TorServerAlreadyBoundException(Throwable cause) { - super(cause); - } - - public TorServerAlreadyBoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorServerException.java b/src/main/java/com/sparrowwallet/sparrow/net/TorServerException.java index 4919b217..afe40ab2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorServerException.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorServerException.java @@ -5,6 +5,10 @@ public class TorServerException extends ServerException { super(cause); } + public TorServerException(String message) { + super(message); + } + public TorServerException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorService.java b/src/main/java/com/sparrowwallet/sparrow/net/TorService.java index 4d497b07..ab4251ed 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorService.java @@ -1,86 +1,85 @@ package com.sparrowwallet.sparrow.net; +import io.matthewnelson.kmp.tor.ext.callback.manager.CallbackTorManager; +import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; -import net.freehaven.tor.control.TorControlError; -import org.berndpruenster.netlayer.tor.*; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.Socket; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; /** - * Service to start internal Tor (including a Tor proxy running on localhost:9050) + * Service to start internal Tor (including a Tor proxy running on localhost:9050, or another port if unavailable) * * This is a ScheduledService to take advantage of the retry on failure behaviour */ -public class TorService extends ScheduledService { +public class TorService extends ScheduledService { private static final Logger log = LoggerFactory.getLogger(TorService.class); - public static final int PROXY_PORT = 9050; - public static final String TOR_DIR_PREFIX = "tor"; - public static final String TOR_ADDRESS_SUFFIX = ".onion"; + private final ReentrantLock startupLock = new ReentrantLock(); + private final Condition startupCondition = startupLock.newCondition(); @Override - protected Task createTask() { + protected Task createTask() { return new Task<>() { - protected NativeTor call() throws IOException, TorServerException { - if(Tor.getDefault() == null) { - Path path = Files.createTempDirectory(TOR_DIR_PREFIX); - File torInstallDir = path.toFile(); - torInstallDir.deleteOnExit(); - try { - LinkedHashMap torrcOptionsMap = new LinkedHashMap<>(); - torrcOptionsMap.put("SocksPort", Integer.toString(PROXY_PORT)); - torrcOptionsMap.put("HashedControlPassword", "16:D780432418F09B06609940000924317D3B9DF522A3191F8F4E597E9329"); - torrcOptionsMap.put("DisableNetwork", "0"); - Torrc override = new Torrc(torrcOptionsMap); + private Exception startupException; - return new NativeTor(torInstallDir, Collections.emptyList(), override); - } catch(TorCtlException e) { - if(e.getCause() instanceof TorControlError) { - if(e.getCause().getMessage().contains("Failed to bind")) { - throw new TorServerAlreadyBoundException("Tor server already bound", e.getCause()); + @Override + protected Tor call() throws Exception { + Tor tor = Tor.getDefault(); + if(tor == null) { + tor = new Tor(); + CallbackTorManager callbackTorManager = tor.getTorManager(); + callbackTorManager.addListener(new TorManagerEvent.Listener() { + @Override + public void managerEventAddressInfo(@NotNull TorManagerEvent.AddressInfo info) { + if(!info.isNull) { + try { + startupLock.lock(); + startupCondition.signalAll(); + } finally { + startupLock.unlock(); + } } - log.error("Failed to start Tor", e); - throw new TorServerException("Failed to start Tor", e.getCause()); - } else { - log.error("Failed to start Tor", e); - throw new TorServerException("Failed to start Tor", e); } + }); + callbackTorManager.start(throwable -> { + if(throwable instanceof Exception exception) { + startupException = exception; + } else { + startupException = new Exception(throwable); + } + log.error("Error", throwable); + try { + startupLock.lock(); + startupCondition.signalAll(); + } finally { + startupLock.unlock(); + } + }, success -> { + log.info("Tor daemon started successfully"); + }); + + try { + startupLock.lock(); + if(!startupCondition.await(5, TimeUnit.MINUTES)) { + throw new TorStartupException("Tor failed to start after 5 minutes, giving up"); + } + + if(startupException != null) { + throw startupException; + } + } finally { + startupLock.unlock(); } } - return null; + return tor; } }; } - - public static Socket getControlSocket() { - Tor tor = Tor.getDefault(); - if(tor != null) { - try { - Class torClass = Class.forName("org.berndpruenster.netlayer.tor.Tor"); - Field torControllerField = torClass.getDeclaredField("torController"); - torControllerField.setAccessible(true); - TorController torController = (TorController)torControllerField.get(tor); - - Class torControllerClass = Class.forName("org.berndpruenster.netlayer.tor.TorController"); - Field socketField = torControllerClass.getDeclaredField("socket"); - socketField.setAccessible(true); - return (Socket)socketField.get(torController); - } catch(Exception e) { - log.error("Error retrieving Tor control socket", e); - } - } - - return null; - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorStartupException.java b/src/main/java/com/sparrowwallet/sparrow/net/TorStartupException.java new file mode 100644 index 00000000..2d8303a2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorStartupException.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.net; + +public class TorStartupException extends TorServerException { + public TorStartupException(Throwable cause) { + super(cause); + } + + public TorStartupException(String message) { + super(message); + } + + public TorStartupException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java index ad7f374d..46667f96 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpOverTlsTransport.java @@ -27,21 +27,15 @@ public class TorTcpOverTlsTransport extends TcpOverTlsTransport { @Override protected void createSocket() throws IOException { - TorTcpTransport torTcpTransport = new TorTcpTransport(server); + TorTcpTransport torTcpTransport = new TorTcpTransport(server) { + @Override + protected int getDefaultPort() { + return Protocol.SSL.getDefaultPort(); + } + }; torTcpTransport.createSocket(); socket = torTcpTransport.socket; - 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); - } - socket = sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(Protocol.SSL.getDefaultPort()), true); startHandshake((SSLSocket)socket); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java index 627d7603..e6929340 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java @@ -2,14 +2,12 @@ package com.sparrowwallet.sparrow.net; import com.google.common.net.HostAndPort; import com.sparrowwallet.sparrow.AppServices; -import org.berndpruenster.netlayer.tor.*; import java.io.*; +import java.net.InetSocketAddress; import java.net.Socket; public class TorTcpTransport extends TcpTransport { - public static final String TOR_DIR_PREFIX = "tor"; - public TorTcpTransport(HostAndPort server) { super(server); } @@ -24,6 +22,7 @@ public class TorTcpTransport extends TcpTransport { throw new IllegalStateException("Can't create Tor socket, Tor is not running"); } - socket = new TorSocket(server.getHost(), server.getPortOrDefault(Protocol.TCP.getDefaultPort()), "sparrow"); + socket = new Socket(Tor.getDefault().getProxy()); + socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(getDefaultPort()))); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index eb447580..85248689 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -28,8 +28,6 @@ import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.util.Duration; -import javafx.util.StringConverter; -import org.berndpruenster.netlayer.tor.Tor; import org.controlsfx.control.SegmentedButton; import org.controlsfx.glyphfont.Glyph; import org.controlsfx.validation.ValidationResult; @@ -494,20 +492,6 @@ public class ServerPreferencesController extends PreferencesDetailController { torService.cancel(); testResults.appendText("\nTor failed to start"); showConnectionFailure(workerStateEvent.getSource().getException()); - - Throwable exception = workerStateEvent.getSource().getException(); - if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER && - exception instanceof TorServerAlreadyBoundException && - useProxyOriginal == null && !useProxy.isSelected() && - (proxyHost.getText().isEmpty() || proxyHost.getText().equals("localhost") || proxyHost.getText().equals("127.0.0.1")) && - (proxyPort.getText().isEmpty() || proxyPort.getText().equals("9050"))) { - useProxy.setSelected(true); - proxyHost.setText("localhost"); - proxyPort.setText("9050"); - useProxyOriginal = false; - testResults.appendText("\n\nAssuming Tor proxy is running on port 9050 and trying again..."); - startElectrumConnection(); - } }); torService.start(); @@ -658,8 +642,6 @@ public class ServerPreferencesController extends PreferencesDetailController { reason = tlsServerException.getMessage() + "\n\n" + reason; } else if(exception instanceof ProxyServerException) { reason += ". Check if the proxy server is running."; - } else if(exception instanceof TorServerAlreadyBoundException) { - reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?"; } else if(reason != null && (reason.contains("Check if Bitcoin Core is running") || reason.contains("Could not connect to Bitcoin Core RPC"))) { reason += "\n\nSee https://sparrowwallet.com/docs/connect-node.html"; } else if(reason != null && (reason.startsWith("Cannot connect to hidden service"))) { diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/ServerTestDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/ServerTestDialog.java index c7e2fabf..39908090 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/ServerTestDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/ServerTestDialog.java @@ -15,7 +15,6 @@ import com.sparrowwallet.sparrow.terminal.SparrowTerminal; import javafx.application.Platform; import javafx.scene.control.ButtonType; import javafx.util.Duration; -import org.berndpruenster.netlayer.tor.Tor; import java.io.File; import java.text.DateFormat; @@ -203,8 +202,6 @@ public class ServerTestDialog extends DialogWindow { reason = tlsServerException.getMessage() + "\n\n" + reason; } else if(exception instanceof ProxyServerException) { reason += ". Check if the proxy server is running."; - } else if(exception instanceof TorServerAlreadyBoundException) { - reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?"; } else if(reason != null && reason.contains("Check if Bitcoin Core is running")) { reason += "\n\nSee https://sparrowwallet.com/docs/connect-node.html"; } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java index 829327a7..17a6284b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java @@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.whirlpool.tor; import com.google.common.net.HostAndPort; import com.samourai.tor.client.TorClientService; -import com.sparrowwallet.sparrow.net.TorService; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.net.Tor; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; +import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,13 +25,12 @@ public class SparrowTorClientService extends TorClientService { public void changeIdentity() { HostAndPort proxy = whirlpool.getTorProxy(); if(proxy != null) { - Socket controlSocket = TorService.getControlSocket(); - if(controlSocket != null) { - try { - writeNewNym(controlSocket); - } catch(Exception e) { - log.warn("Error sending NEWNYM to " + controlSocket, e); - } + if(AppServices.isTorRunning()) { + Tor.getDefault().getTorManager().signal(TorControlSignal.Signal.NewNym, throwable -> { + log.warn("Failed to signal newnym"); + }, successEvent -> { + log.info("Signalled newnym for new Tor circuit"); + }); } else { HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1); try(Socket socket = new Socket(control.getHost(), control.getPort())) { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c8be4c62..5670603e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -19,7 +19,19 @@ open module com.sparrowwallet.sparrow { requires org.jetbrains.annotations; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.annotation; - requires netlayer.jpms; + requires kotlin.stdlib; + requires kmp.tor.jvm; + requires kmp.tor.binary.extract.jvm; + requires kmp.tor.common.jvm; + requires kmp.tor.controller.common.jvm; + requires kmp.tor.manager.jvm; + requires kmp.tor.manager.common.jvm; + requires kmp.tor.ext.callback.manager.jvm; + requires kmp.tor.ext.callback.common.jvm; + requires kmp.tor.ext.callback.manager.common.jvm; + requires kmp.tor.ext.callback.controller.common.jvm; + requires parcelize.jvm; + requires kotlinx.coroutines.javafx; requires org.slf4j; requires com.google.gson; requires org.jdbi.v3.core; @@ -31,7 +43,6 @@ open module com.sparrowwallet.sparrow { requires org.fxmisc.flowless; requires com.github.sarxos.webcam.capture; requires centerdevice.nsmenufx; - requires com.github.JesusMcCloud.jtorctl; requires com.beust.jcommander; requires org.slf4j.jul.to.slf4j; requires net.sourceforge.javacsv; diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 36d8eb26..93746fb8 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -60,14 +60,6 @@ - - com.sparrowwallet.sparrow.TorLogHandler - - - - - -