update tor to 0.4.7.13 using kmp-tor library

This commit is contained in:
Craig Raw 2023-03-28 14:38:20 +02:00
parent acab50cdcd
commit faa5a11c94
24 changed files with 435 additions and 213 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.");
}

View file

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

View file

@ -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.");
}

View file

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

View file

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

View file

@ -125,4 +125,9 @@ public class TcpOverTlsTransport extends TcpTransport {
return Storage.getCertificateFile(server.getHost()) == null;
}
@Override
protected int getDefaultPort() {
return Protocol.SSL.getDefaultPort();
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<NativeTor> {
public class TorService extends ScheduledService<Tor> {
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<NativeTor> createTask() {
protected Task<Tor> 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<String, String> 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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -60,14 +60,6 @@
</encoder>
</appender>
<appender name="APPLICATION_TOR" class="com.sparrowwallet.drongo.ApplicationAppender">
<callback>com.sparrowwallet.sparrow.TorLogHandler</callback>
</appender>
<logger name="org.berndpruenster.netlayer.tor" level="debug" additivity="false">
<appender-ref ref="APPLICATION_TOR" />
</logger>
<root level="warn">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />