From e3d7bb57eec5aa14af3cb0ba20449f6ceba8479c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 4 Feb 2021 12:00:49 +0200 Subject: [PATCH] move internal tor to a top level app service, support non-ssl proxying --- .../sparrowwallet/sparrow/AppController.java | 58 +++++-- .../sparrowwallet/sparrow/AppServices.java | 161 +++++++++++------- .../sparrow/event/ConnectionStartEvent.java | 15 ++ .../sparrow/event/TorBootStatusEvent.java | 7 + .../sparrow/event/TorFailedStatusEvent.java | 16 ++ .../sparrow/event/TorReadyStatusEvent.java | 7 + .../com/sparrowwallet/sparrow/io/Config.java | 26 ++- .../com/sparrowwallet/sparrow/net/Bwt.java | 1 - .../sparrow/net/ElectrumServer.java | 19 +-- .../sparrow/net/ExchangeSource.java | 19 +-- .../sparrowwallet/sparrow/net/Protocol.java | 7 +- .../sparrow/net/ProxySocketFactory.java | 67 ++++++++ .../sparrow/net/TcpTransport.java | 6 +- .../sparrowwallet/sparrow/net/TorService.java | 61 +++++++ .../sparrow/net/TorTcpTransport.java | 40 +---- .../sparrow/net/VersionCheckService.java | 5 +- .../ServerPreferencesController.java | 69 +++++--- src/main/java/module-info.java | 1 + 18 files changed, 422 insertions(+), 163 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/ConnectionStartEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/TorBootStatusEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/TorReadyStatusEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/ProxySocketFactory.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/TorService.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index ac44b4a6..e3cc73e9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1273,12 +1273,43 @@ public class AppController implements Initializable { } } + @Subscribe + public void connectionStart(ConnectionStartEvent event) { + statusUpdated(new StatusEvent(event.getStatus(), 120)); + } + + @Subscribe + public void connectionFailed(ConnectionFailedEvent event) { + String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage(); + String status = "Connection error: " + reason; + statusUpdated(new StatusEvent(status)); + } + + @Subscribe + public void connection(ConnectionEvent event) { + String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight(); + statusUpdated(new StatusEvent(status)); + } + + @Subscribe + public void disconnection(DisconnectionEvent event) { + serverToggle.setDisable(false); + if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) { + statusUpdated(new StatusEvent("Disconnected")); + } + if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { + statusBar.setProgress(0); + } + } + @Subscribe public void bwtBootStatus(BwtBootStatusEvent event) { serverToggle.setDisable(true); - statusUpdated(new StatusEvent(event.getStatus(), 60)); - if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { - statusBar.setProgress(0.01); + if(AppServices.isConnecting()) { + statusUpdated(new StatusEvent(event.getStatus(), 60)); + if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { + statusBar.setProgress(0.01); + } } } @@ -1313,14 +1344,21 @@ public class AppController implements Initializable { } @Subscribe - public void disconnection(DisconnectionEvent event) { + public void torBootStatus(TorBootStatusEvent event) { + serverToggle.setDisable(true); + statusUpdated(new StatusEvent(event.getStatus(), 120)); + } + + @Subscribe + public void torFailedStatus(TorFailedStatusEvent event) { serverToggle.setDisable(false); - if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) { - statusUpdated(new StatusEvent("Disconnected")); - } - if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { - statusBar.setProgress(0); - } + statusUpdated(new StatusEvent(event.getStatus())); + } + + @Subscribe + public void torReadyStatus(TorReadyStatusEvent event) { + serverToggle.setDisable(false); + statusUpdated(new StatusEvent(event.getStatus())); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 05ae62f3..f5a020b3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow; import com.google.common.eventbus.Subscribe; +import com.google.common.net.HostAndPort; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; @@ -31,11 +32,14 @@ import javafx.scene.text.Font; import javafx.stage.Stage; import javafx.stage.Window; import javafx.util.Duration; +import org.berndpruenster.netlayer.tor.Tor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; @@ -45,9 +49,9 @@ import java.util.stream.Collectors; public class AppServices { private static final Logger log = LoggerFactory.getLogger(AppServices.class); - private static final int SERVER_PING_PERIOD = 1 * 60 * 1000; - private static final int ENUMERATE_HW_PERIOD = 30 * 1000; - private static final int RATES_PERIOD = 5 * 60 * 1000; + private static final int SERVER_PING_PERIOD_SECS = 60; + private static final int ENUMERATE_HW_PERIOD_SECS = 30; + private static final int RATES_PERIOD_SECS = 5 * 60; private static final int VERSION_CHECK_PERIOD_HOURS = 24; private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO; private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD"); @@ -68,6 +72,8 @@ public class AppServices { private VersionCheckService versionCheckService; + private TorService torService; + private static Integer currentBlockHeight; private static Map targetBlockFeeRates; @@ -86,14 +92,10 @@ public class AppServices { @Override public void changed(ObservableValue observable, Boolean oldValue, Boolean online) { if(online) { - restartService(connectionService); - - if(ratesService.getExchangeSource() != ExchangeSource.NONE) { - restartService(ratesService); - } - - if(Config.get().isCheckNewVersions()) { - restartService(versionCheckService); + if(Config.get().requiresTor() && !isTorRunning()) { + torService.start(); + } else { + restartServices(); } } else { connectionService.cancel(); @@ -103,6 +105,44 @@ public class AppServices { } }; + public AppServices(MainApp application) { + this.application = application; + EventManager.get().register(this); + } + + public void start() { + Config config = Config.get(); + connectionService = createConnectionService(); + ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency()); + versionCheckService = createVersionCheckService(); + torService = createTorService(); + + onlineProperty.addListener(onlineServicesListener); + + if(config.getMode() == Mode.ONLINE) { + if(config.requiresTor()) { + torService.start(); + } else { + restartServices(); + } + } + } + + private void restartServices() { + Config config = Config.get(); + if(config.hasServerAddress()) { + restartService(connectionService); + } + + if(config.isFetchRates()) { + restartService(ratesService); + } + + if(config.isCheckNewVersions()) { + restartService(versionCheckService); + } + } + private void restartService(ScheduledService service) { if(service.isRunning()) { service.cancel(); @@ -117,33 +157,6 @@ public class AppServices { } } - public AppServices(MainApp application) { - this.application = application; - EventManager.get().register(this); - } - - public void start() { - Config config = Config.get(); - connectionService = createConnectionService(); - if(config.getMode() == Mode.ONLINE && config.getServerAddress() != null && !config.getServerAddress().isEmpty()) { - connectionService.start(); - } - - ExchangeSource source = config.getExchangeSource() != null ? config.getExchangeSource() : DEFAULT_EXCHANGE_SOURCE; - Currency currency = config.getFiatCurrency() != null ? config.getFiatCurrency() : DEFAULT_FIAT_CURRENCY; - ratesService = createRatesService(source, currency); - if(config.getMode() == Mode.ONLINE && source != ExchangeSource.NONE) { - ratesService.start(); - } - - versionCheckService = createVersionCheckService(); - if(config.getMode() == Mode.ONLINE && config.isCheckNewVersions()) { - versionCheckService.start(); - } - - onlineProperty.addListener(onlineServicesListener); - } - public void stop() { if(connectionService != null) { connectionService.cancel(); @@ -156,20 +169,23 @@ public class AppServices { if(versionCheckService != null) { versionCheckService.cancel(); } + + if(Tor.getDefault() != null) { + Tor.getDefault().shutdown(); + } } private ElectrumServer.ConnectionService createConnectionService() { ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(); - connectionService.setPeriod(new Duration(SERVER_PING_PERIOD)); + connectionService.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS)); connectionService.setRestartOnFailure(true); - EventManager.get().register(connectionService); - connectionService.statusProperty().addListener((observable, oldValue, newValue) -> { - if(connectionService.isRunning()) { - EventManager.get().post(new StatusEvent(newValue)); + + connectionService.setOnRunning(workerStateEvent -> { + if(!ElectrumServer.isConnected()) { + EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress())); } }); - connectionService.setOnSucceeded(successEvent -> { connectionService.setRestartOnFailure(true); @@ -201,8 +217,10 @@ public class AppServices { } private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) { - ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(exchangeSource, currency); - ratesService.setPeriod(new Duration(RATES_PERIOD)); + ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService( + exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource, + currency == null ? DEFAULT_FIAT_CURRENCY : currency); + ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS)); ratesService.setRestartOnFailure(true); ratesService.setOnSucceeded(successEvent -> { @@ -230,7 +248,7 @@ public class AppServices { private Hwi.ScheduledEnumerateService createDeviceEnumerateService() { Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null); - enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD)); + enumerateService.setPeriod(Duration.seconds(ENUMERATE_HW_PERIOD_SECS)); enumerateService.setOnSucceeded(workerStateEvent -> { List devices = enumerateService.getValue(); @@ -247,6 +265,45 @@ public class AppServices { return enumerateService; } + private TorService createTorService() { + TorService torService = new TorService(); + torService.setPeriod(Duration.hours(1000)); + torService.setRestartOnFailure(true); + + torService.setOnRunning(workerStateEvent -> { + EventManager.get().post(new TorBootStatusEvent()); + }); + torService.setOnSucceeded(workerStateEvent -> { + Tor.setDefault(torService.getValue()); + torService.cancel(); + restartServices(); + EventManager.get().post(new TorReadyStatusEvent()); + }); + torService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new TorFailedStatusEvent(workerStateEvent.getSource().getException())); + }); + + return torService; + } + + public static boolean isTorRunning() { + return Tor.getDefault() != null; + } + + public static Proxy getProxy() { + Config config = Config.get(); + if(config.isUseProxy()) { + HostAndPort proxy = HostAndPort.fromString(config.getProxyServer()); + InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT)); + return new Proxy(Proxy.Type.SOCKS, proxyAddress); + } else if(AppServices.isTorRunning()) { + InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT); + return new Proxy(Proxy.Type.SOCKS, proxyAddress); + } + + return null; + } + static void initialize(MainApp application) { INSTANCE = new AppServices(application); } @@ -404,16 +461,6 @@ public class AppServices { targetBlockFeeRates = event.getTargetBlockFeeRates(); addMempoolRateSizes(event.getMempoolRateSizes()); minimumRelayFeeRate = event.getMinimumRelayFeeRate(); - String banner = event.getServerBanner(); - String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight(); - EventManager.get().post(new StatusEvent(status)); - } - - @Subscribe - public void connectionFailed(ConnectionFailedEvent event) { - String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage(); - String status = "Connection error: " + reason; - EventManager.get().post(new StatusEvent(status)); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ConnectionStartEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionStartEvent.java new file mode 100644 index 00000000..91643b1a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionStartEvent.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.sparrow.AppServices; + +public class ConnectionStartEvent { + private final String status; + + public ConnectionStartEvent(String serverAddress) { + this.status = AppServices.isTorRunning() ? "Tor running, connecting to " + serverAddress + "..." : "Connecting to " + serverAddress + "..."; + } + + public String getStatus() { + return status; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TorBootStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TorBootStatusEvent.java new file mode 100644 index 00000000..4eff367e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/TorBootStatusEvent.java @@ -0,0 +1,7 @@ +package com.sparrowwallet.sparrow.event; + +public class TorBootStatusEvent extends TorStatusEvent { + public TorBootStatusEvent() { + super("Starting Tor..."); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java new file mode 100644 index 00000000..e8192804 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/TorFailedStatusEvent.java @@ -0,0 +1,16 @@ +package com.sparrowwallet.sparrow.event; + +public class TorFailedStatusEvent extends TorStatusEvent { + private final Throwable exception; + + public TorFailedStatusEvent(Throwable exception) { + super("Tor failed to start: " + (exception.getCause() != null ? + (exception.getCause().getMessage().contains("Failed to bind") ? exception.getCause().getMessage() + " Is a Tor proxy already running?" : exception.getCause().getMessage() ) : + exception.getMessage())); + this.exception = exception; + } + + public Throwable getException() { + return exception; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TorReadyStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TorReadyStatusEvent.java new file mode 100644 index 00000000..89abffc0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/TorReadyStatusEvent.java @@ -0,0 +1,7 @@ +package com.sparrowwallet.sparrow.event; + +public class TorReadyStatusEvent extends TorStatusEvent { + public TorReadyStatusEvent() { + super("Tor started"); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 745a726c..998c7f89 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -4,10 +4,7 @@ import com.google.gson.*; import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Theme; -import com.sparrowwallet.sparrow.net.CoreAuthType; -import com.sparrowwallet.sparrow.net.ExchangeSource; -import com.sparrowwallet.sparrow.net.FeeRatesSource; -import com.sparrowwallet.sparrow.net.ServerType; +import com.sparrowwallet.sparrow.net.*; import com.sparrowwallet.sparrow.wallet.FeeRatesSelection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -143,6 +140,10 @@ public class Config { flush(); } + public boolean isFetchRates() { + return getExchangeSource() != ExchangeSource.NONE; + } + public ExchangeSource getExchangeSource() { return exchangeSource; } @@ -269,10 +270,27 @@ public class Config { flush(); } + public boolean hasServerAddress() { + return getServerAddress() != null && !getServerAddress().isEmpty(); + } + public String getServerAddress() { return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : getElectrumServer(); } + public boolean requiresTor() { + if(isUseProxy() || !hasServerAddress()) { + return false; + } + + Protocol protocol = Protocol.getProtocol(getServerAddress()); + if(protocol == null) { + return false; + } + + return protocol.isOnionAddress(protocol.getServerHostAndPort(getServerAddress())); + } + public String getCoreServer() { return coreServer; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java index cdfe07d2..b679bd41 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java @@ -143,7 +143,6 @@ public class Bwt { this.terminating = false; this.ready = false; this.shutdownPtr = null; - Platform.runLater(() -> EventManager.get().post(new BwtShutdownEvent())); } public boolean isRunning() { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 6a30436f..b1fe278d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -780,7 +780,6 @@ public class ElectrumServer { private final ReentrantLock bwtStartLock = new ReentrantLock(); private final Condition bwtStartCondition = bwtStartLock.newCondition(); private Throwable bwtStartException; - private final StringProperty statusProperty = new SimpleStringProperty(); public ConnectionService() { this(true); @@ -930,13 +929,16 @@ public class ElectrumServer { Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService(); disconnectionService.setOnSucceeded(workerStateEvent -> { ElectrumServer.bwtElectrumServer = null; + if(subscribe) { + EventManager.get().post(new BwtShutdownEvent()); + } }); disconnectionService.setOnFailed(workerStateEvent -> { log.error("Failed to stop BWT", workerStateEvent.getSource().getException()); }); - Platform.runLater(disconnectionService::start); - } else { - Platform.runLater(() -> EventManager.get().post(new DisconnectionEvent())); + disconnectionService.start(); + } else if(subscribe) { + EventManager.get().post(new DisconnectionEvent()); } } @@ -951,11 +953,6 @@ public class ElectrumServer { log.error("Uncaught error in ConnectionService", e); } - @Subscribe - public void torStatus(TorStatusEvent event) { - statusProperty.set(event.getStatus()); - } - @Subscribe public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) { if(this.isRunning()) { @@ -984,10 +981,6 @@ public class ElectrumServer { bwtStartLock.unlock(); } } - - public StringProperty statusProperty() { - return statusProperty; - } } public static class ReadRunnable implements Runnable { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java index f9d4eec6..e6d25180 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java @@ -1,9 +1,8 @@ package com.sparrowwallet.sparrow.net; -import com.google.common.net.HostAndPort; import com.google.gson.Gson; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent; -import com.sparrowwallet.sparrow.io.Config; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; @@ -13,7 +12,6 @@ import org.slf4j.LoggerFactory; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -52,7 +50,7 @@ public enum ExchangeSource { private CoinbaseRates getRates() { String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC"; - Proxy proxy = getProxy(); + Proxy proxy = AppServices.getProxy(); try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { Gson gson = new Gson(); @@ -83,7 +81,7 @@ public enum ExchangeSource { private CoinGeckoRates getRates() { String url = "https://api.coingecko.com/api/v3/exchange_rates"; - Proxy proxy = getProxy(); + Proxy proxy = AppServices.getProxy(); try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { Gson gson = new Gson(); @@ -116,17 +114,6 @@ public enum ExchangeSource { } } - private static Proxy getProxy() { - Config config = Config.get(); - if(config.isUseProxy()) { - HostAndPort proxy = HostAndPort.fromString(config.getProxyServer()); - InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT)); - return new Proxy(Proxy.Type.SOCKS, proxyAddress); - } - - return null; - } - @Override public String toString() { return name; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java index bfe24dcb..8a332649 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Protocol.java @@ -28,12 +28,13 @@ public enum Protocol { @Override public Transport getTransport(HostAndPort server, HostAndPort proxy) { - throw new UnsupportedOperationException("TCP protocol does not support proxying"); + //Avoid using a TorSocket if a proxy is specified, even if a .onion address + return new TcpTransport(server, proxy); } @Override public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) { - throw new UnsupportedOperationException("TCP protocol does not support proxying"); + return getTransport(server, proxy); } }, SSL { @@ -116,7 +117,7 @@ public enum Protocol { } public boolean isOnionAddress(HostAndPort server) { - return server.getHost().toLowerCase().endsWith(".onion"); + return server.getHost().toLowerCase().endsWith(TorService.TOR_ADDRESS_SUFFIX); } public static Protocol getProtocol(String url) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ProxySocketFactory.java b/src/main/java/com/sparrowwallet/sparrow/net/ProxySocketFactory.java new file mode 100644 index 00000000..5172e2e1 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/ProxySocketFactory.java @@ -0,0 +1,67 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; + +import javax.net.SocketFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + +import static com.sparrowwallet.sparrow.net.ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT; + +public class ProxySocketFactory extends SocketFactory { + private final Proxy proxy; + + public ProxySocketFactory() { + this(Proxy.NO_PROXY); + } + + public ProxySocketFactory(HostAndPort proxyHostAndPort) { + this(getSocksProxy(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(DEFAULT_PROXY_PORT))); + } + + public ProxySocketFactory(Proxy proxy) { + this.proxy = proxy; + } + + @Override + public Socket createSocket() { + return new Socket(proxy); + } + + @Override + public Socket createSocket(String address, int port) throws IOException { + return createSocket(new InetSocketAddress(address, port), null); + } + + @Override + public Socket createSocket(String address, int port, InetAddress localAddress, int localPort) throws IOException { + return createSocket(new InetSocketAddress(address, port), new InetSocketAddress(localAddress, localPort)); + } + + @Override + public Socket createSocket(InetAddress address, int port) throws IOException { + return createSocket(new InetSocketAddress(address, port), null); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return createSocket(new InetSocketAddress(address, port), new InetSocketAddress(localAddress, localPort)); + } + + private Socket createSocket(InetSocketAddress address, InetSocketAddress bindAddress) throws IOException { + Socket socket = new Socket(proxy); + if(bindAddress != null) { + socket.bind(bindAddress); + } + + socket.connect(address); + return socket; + } + + private static Proxy getSocksProxy(String proxyAddress, int proxyPort) { + return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyAddress, proxyPort)); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java index b9e1d040..62cd1e35 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TcpTransport.java @@ -47,8 +47,12 @@ public class TcpTransport implements Transport, Closeable { private final Gson gson = new Gson(); public TcpTransport(HostAndPort server) { + this(server, null); + } + + public TcpTransport(HostAndPort server, HostAndPort proxy) { this.server = server; - this.socketFactory = SocketFactory.getDefault(); + this.socketFactory = (proxy == null ? SocketFactory.getDefault() : new ProxySocketFactory(proxy)); } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorService.java b/src/main/java/com/sparrowwallet/sparrow/net/TorService.java new file mode 100644 index 00000000..c59cedb6 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorService.java @@ -0,0 +1,61 @@ +package com.sparrowwallet.sparrow.net; + +import javafx.concurrent.ScheduledService; +import javafx.concurrent.Task; +import net.freehaven.tor.control.TorControlError; +import org.berndpruenster.netlayer.tor.NativeTor; +import org.berndpruenster.netlayer.tor.Tor; +import org.berndpruenster.netlayer.tor.TorCtlException; +import org.berndpruenster.netlayer.tor.Torrc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; + +/** + * Service to start internal Tor (including a Tor proxy running on localhost:9050) + * + * This is a ScheduledService to take advantage of the retry on failure behaviour + */ +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"; + + @Override + protected Task createTask() { + return new Task<>() { + protected NativeTor call() throws IOException { + 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("DisableNetwork", "0"); + Torrc override = new Torrc(torrcOptionsMap); + + return new NativeTor(torInstallDir, Collections.emptyList(), override); + } catch(TorCtlException e) { + log.error("Failed to start Tor", e); + if(e.getCause() instanceof TorControlError) { + throw new IOException("Failed to start Tor", e.getCause()); + } else { + throw new IOException("Failed to start Tor", e); + } + } + } + + return null; + } + }; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java index 946c0baa..3669864f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java @@ -1,18 +1,11 @@ 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 com.sparrowwallet.sparrow.AppServices; 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"; @@ -21,35 +14,16 @@ public class TorTcpTransport extends TcpTransport { super(server); } + public TorTcpTransport(HostAndPort server, HostAndPort proxy) { + super(server, proxy); + } + @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); - } + if(!AppServices.isTorRunning()) { + throw new IllegalStateException("Can't create Tor socket, Tor is not running"); } - 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/net/VersionCheckService.java b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java index 39640221..92cef779 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java @@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.MainApp; import com.sparrowwallet.sparrow.event.VersionUpdatedEvent; import javafx.concurrent.ScheduledService; @@ -15,6 +16,7 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.HttpsURLConnection; import java.io.IOException; import java.io.InputStreamReader; +import java.net.Proxy; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.SignatureException; @@ -44,7 +46,8 @@ public class VersionCheckService extends ScheduledService { private VersionCheck getVersionCheck() throws IOException { URL url = new URL(VERSION_CHECK_URL); - HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); + Proxy proxy = AppServices.getProxy(); + HttpsURLConnection conn = (HttpsURLConnection)(proxy == null ? url.openConnection() : url.openConnection(proxy)); try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) { Gson gson = new Gson(); diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java index 3104f664..bacce0ab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerPreferencesController.java @@ -14,7 +14,6 @@ import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.net.*; import javafx.application.Platform; import javafx.beans.value.ChangeListener; -import javafx.concurrent.WorkerStateEvent; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.text.Font; @@ -22,6 +21,8 @@ import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.util.Duration; +import net.freehaven.tor.control.TorControlError; +import org.berndpruenster.netlayer.tor.Tor; import org.controlsfx.glyphfont.Glyph; import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationSupport; @@ -121,6 +122,8 @@ public class ServerPreferencesController extends PreferencesDetailController { private final ValidationSupport validationSupport = new ValidationSupport(); + private TorService torService; + private ElectrumServer.ConnectionService connectionService; @Override @@ -242,13 +245,6 @@ public class ServerPreferencesController extends PreferencesDetailController { proxyHost.setText(proxyHost.getText().trim()); proxyHost.setDisable(!newValue); proxyPort.setDisable(!newValue); - - if(newValue) { - electrumUseSsl.setSelected(true); - electrumUseSsl.setDisable(true); - } else { - electrumUseSsl.setDisable(false); - } }); boolean isConnected = AppServices.isConnecting() || AppServices.isConnected(); @@ -263,7 +259,12 @@ public class ServerPreferencesController extends PreferencesDetailController { testConnection.setOnAction(event -> { testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); testResults.setText("Connecting to " + config.getServerAddress() + "..."); - startElectrumConnection(); + + if(Config.get().requiresTor() && Tor.getDefault() == null) { + startTor(); + } else { + startElectrumConnection(); + } }); editConnection.managedProperty().bind(editConnection.visibleProperty()); @@ -340,11 +341,6 @@ public class ServerPreferencesController extends PreferencesDetailController { proxyHost.setDisable(!config.isUseProxy()); proxyPort.setDisable(!config.isUseProxy()); - if(config.isUseProxy()) { - electrumUseSsl.setSelected(true); - electrumUseSsl.setDisable(true); - } - String proxyServer = config.getProxyServer(); if(proxyServer != null) { HostAndPort server = HostAndPort.fromString(proxyServer); @@ -357,6 +353,32 @@ public class ServerPreferencesController extends PreferencesDetailController { setFieldsEditable(!isConnected); } + private void startTor() { + if(torService != null && torService.isRunning()) { + return; + } + + torService = new TorService(); + torService.setPeriod(Duration.hours(1000)); + torService.setRestartOnFailure(false); + + torService.setOnRunning(workerStateEvent -> { + testResults.setText(testResults.getText() + "\nStarting Tor..."); + }); + torService.setOnSucceeded(workerStateEvent -> { + Tor.setDefault(torService.getValue()); + torService.cancel(); + testResults.setText(testResults.getText() + "\nTor started"); + startElectrumConnection(); + }); + torService.setOnFailed(workerStateEvent -> { + testResults.setText(testResults.getText() + "\nTor failed to start"); + showConnectionFailure(workerStateEvent.getSource().getException()); + }); + + torService.start(); + } + private void startElectrumConnection() { if(connectionService != null && connectionService.isRunning()) { connectionService.cancel(); @@ -364,10 +386,8 @@ public class ServerPreferencesController extends PreferencesDetailController { connectionService = new ElectrumServer.ConnectionService(false); connectionService.setPeriod(Duration.hours(1)); + connectionService.setRestartOnFailure(false); EventManager.get().register(connectionService); - connectionService.statusProperty().addListener((observable, oldValue, newValue) -> { - testResults.setText(testResults.getText() + "\n" + newValue); - }); connectionService.setOnSucceeded(successEvent -> { EventManager.get().unregister(connectionService); @@ -379,8 +399,7 @@ public class ServerPreferencesController extends PreferencesDetailController { }); connectionService.setOnFailed(workerStateEvent -> { EventManager.get().unregister(connectionService); - showConnectionFailure(workerStateEvent); - connectionService.cancel(); + showConnectionFailure(workerStateEvent.getSource().getException()); }); connectionService.start(); } @@ -421,13 +440,15 @@ 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) { + private void showConnectionFailure(Throwable exception) { + log.error("Connection error", exception); + String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage(); + if(exception.getCause() != null && exception.getCause() instanceof SSLHandshakeException) { reason = "SSL Handshake Error\n" + reason; } + if(exception.getCause() != null && exception.getCause() instanceof TorControlError && exception.getCause().getMessage().contains("Failed to bind")) { + reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?"; + } testResults.setText("Could not connect:\n\n" + reason); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.EXCLAMATION_CIRCLE, "failure")); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index a6a4839d..2d772351 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -26,4 +26,5 @@ open module com.sparrowwallet.sparrow { requires jcommander; requires slf4j.api; requires bwt.jni; + requires jtorctl; } \ No newline at end of file