move internal tor to a top level app service, support non-ssl proxying

This commit is contained in:
Craig Raw 2021-02-04 12:00:49 +02:00
parent 095518e858
commit e3d7bb57ee
18 changed files with 422 additions and 163 deletions

View file

@ -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 @Subscribe
public void bwtBootStatus(BwtBootStatusEvent event) { public void bwtBootStatus(BwtBootStatusEvent event) {
serverToggle.setDisable(true); serverToggle.setDisable(true);
statusUpdated(new StatusEvent(event.getStatus(), 60)); if(AppServices.isConnecting()) {
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { statusUpdated(new StatusEvent(event.getStatus(), 60));
statusBar.setProgress(0.01); if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0.01);
}
} }
} }
@ -1313,14 +1344,21 @@ public class AppController implements Initializable {
} }
@Subscribe @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); serverToggle.setDisable(false);
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) { statusUpdated(new StatusEvent(event.getStatus()));
statusUpdated(new StatusEvent("Disconnected")); }
}
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { @Subscribe
statusBar.setProgress(0); public void torReadyStatus(TorReadyStatusEvent event) {
} serverToggle.setDisable(false);
statusUpdated(new StatusEvent(event.getStatus()));
} }
@Subscribe @Subscribe

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow; package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
@ -31,11 +32,14 @@ import javafx.scene.text.Font;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.Window; import javafx.stage.Window;
import javafx.util.Duration; import javafx.util.Duration;
import org.berndpruenster.netlayer.tor.Tor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
@ -45,9 +49,9 @@ import java.util.stream.Collectors;
public class AppServices { public class AppServices {
private static final Logger log = LoggerFactory.getLogger(AppServices.class); private static final Logger log = LoggerFactory.getLogger(AppServices.class);
private static final int SERVER_PING_PERIOD = 1 * 60 * 1000; private static final int SERVER_PING_PERIOD_SECS = 60;
private static final int ENUMERATE_HW_PERIOD = 30 * 1000; private static final int ENUMERATE_HW_PERIOD_SECS = 30;
private static final int RATES_PERIOD = 5 * 60 * 1000; private static final int RATES_PERIOD_SECS = 5 * 60;
private static final int VERSION_CHECK_PERIOD_HOURS = 24; private static final int VERSION_CHECK_PERIOD_HOURS = 24;
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO; private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD"); private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
@ -68,6 +72,8 @@ public class AppServices {
private VersionCheckService versionCheckService; private VersionCheckService versionCheckService;
private TorService torService;
private static Integer currentBlockHeight; private static Integer currentBlockHeight;
private static Map<Integer, Double> targetBlockFeeRates; private static Map<Integer, Double> targetBlockFeeRates;
@ -86,14 +92,10 @@ public class AppServices {
@Override @Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) { public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
if(online) { if(online) {
restartService(connectionService); if(Config.get().requiresTor() && !isTorRunning()) {
torService.start();
if(ratesService.getExchangeSource() != ExchangeSource.NONE) { } else {
restartService(ratesService); restartServices();
}
if(Config.get().isCheckNewVersions()) {
restartService(versionCheckService);
} }
} else { } else {
connectionService.cancel(); 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) { private void restartService(ScheduledService<?> service) {
if(service.isRunning()) { if(service.isRunning()) {
service.cancel(); 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() { public void stop() {
if(connectionService != null) { if(connectionService != null) {
connectionService.cancel(); connectionService.cancel();
@ -156,20 +169,23 @@ public class AppServices {
if(versionCheckService != null) { if(versionCheckService != null) {
versionCheckService.cancel(); versionCheckService.cancel();
} }
if(Tor.getDefault() != null) {
Tor.getDefault().shutdown();
}
} }
private ElectrumServer.ConnectionService createConnectionService() { private ElectrumServer.ConnectionService createConnectionService() {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(); ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
connectionService.setPeriod(new Duration(SERVER_PING_PERIOD)); connectionService.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS));
connectionService.setRestartOnFailure(true); connectionService.setRestartOnFailure(true);
EventManager.get().register(connectionService); EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
if(connectionService.isRunning()) { connectionService.setOnRunning(workerStateEvent -> {
EventManager.get().post(new StatusEvent(newValue)); if(!ElectrumServer.isConnected()) {
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
} }
}); });
connectionService.setOnSucceeded(successEvent -> { connectionService.setOnSucceeded(successEvent -> {
connectionService.setRestartOnFailure(true); connectionService.setRestartOnFailure(true);
@ -201,8 +217,10 @@ public class AppServices {
} }
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) { private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(exchangeSource, currency); ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
ratesService.setPeriod(new Duration(RATES_PERIOD)); exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
ratesService.setRestartOnFailure(true); ratesService.setRestartOnFailure(true);
ratesService.setOnSucceeded(successEvent -> { ratesService.setOnSucceeded(successEvent -> {
@ -230,7 +248,7 @@ public class AppServices {
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() { private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null); 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 -> { enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue(); List<Device> devices = enumerateService.getValue();
@ -247,6 +265,45 @@ public class AppServices {
return enumerateService; 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) { static void initialize(MainApp application) {
INSTANCE = new AppServices(application); INSTANCE = new AppServices(application);
} }
@ -404,16 +461,6 @@ public class AppServices {
targetBlockFeeRates = event.getTargetBlockFeeRates(); targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes()); addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate(); 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 @Subscribe

View file

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

View file

@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.event;
public class TorBootStatusEvent extends TorStatusEvent {
public TorBootStatusEvent() {
super("Starting Tor...");
}
}

View file

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

View file

@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.event;
public class TorReadyStatusEvent extends TorStatusEvent {
public TorReadyStatusEvent() {
super("Tor started");
}
}

View file

@ -4,10 +4,7 @@ import com.google.gson.*;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.net.CoreAuthType; import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection; import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -143,6 +140,10 @@ public class Config {
flush(); flush();
} }
public boolean isFetchRates() {
return getExchangeSource() != ExchangeSource.NONE;
}
public ExchangeSource getExchangeSource() { public ExchangeSource getExchangeSource() {
return exchangeSource; return exchangeSource;
} }
@ -269,10 +270,27 @@ public class Config {
flush(); flush();
} }
public boolean hasServerAddress() {
return getServerAddress() != null && !getServerAddress().isEmpty();
}
public String getServerAddress() { public String getServerAddress() {
return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : getElectrumServer(); 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() { public String getCoreServer() {
return coreServer; return coreServer;
} }

View file

@ -143,7 +143,6 @@ public class Bwt {
this.terminating = false; this.terminating = false;
this.ready = false; this.ready = false;
this.shutdownPtr = null; this.shutdownPtr = null;
Platform.runLater(() -> EventManager.get().post(new BwtShutdownEvent()));
} }
public boolean isRunning() { public boolean isRunning() {

View file

@ -780,7 +780,6 @@ public class ElectrumServer {
private final ReentrantLock bwtStartLock = new ReentrantLock(); private final ReentrantLock bwtStartLock = new ReentrantLock();
private final Condition bwtStartCondition = bwtStartLock.newCondition(); private final Condition bwtStartCondition = bwtStartLock.newCondition();
private Throwable bwtStartException; private Throwable bwtStartException;
private final StringProperty statusProperty = new SimpleStringProperty();
public ConnectionService() { public ConnectionService() {
this(true); this(true);
@ -930,13 +929,16 @@ public class ElectrumServer {
Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService(); Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService();
disconnectionService.setOnSucceeded(workerStateEvent -> { disconnectionService.setOnSucceeded(workerStateEvent -> {
ElectrumServer.bwtElectrumServer = null; ElectrumServer.bwtElectrumServer = null;
if(subscribe) {
EventManager.get().post(new BwtShutdownEvent());
}
}); });
disconnectionService.setOnFailed(workerStateEvent -> { disconnectionService.setOnFailed(workerStateEvent -> {
log.error("Failed to stop BWT", workerStateEvent.getSource().getException()); log.error("Failed to stop BWT", workerStateEvent.getSource().getException());
}); });
Platform.runLater(disconnectionService::start); disconnectionService.start();
} else { } else if(subscribe) {
Platform.runLater(() -> EventManager.get().post(new DisconnectionEvent())); EventManager.get().post(new DisconnectionEvent());
} }
} }
@ -951,11 +953,6 @@ public class ElectrumServer {
log.error("Uncaught error in ConnectionService", e); log.error("Uncaught error in ConnectionService", e);
} }
@Subscribe
public void torStatus(TorStatusEvent event) {
statusProperty.set(event.getStatus());
}
@Subscribe @Subscribe
public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) { public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) {
if(this.isRunning()) { if(this.isRunning()) {
@ -984,10 +981,6 @@ public class ElectrumServer {
bwtStartLock.unlock(); bwtStartLock.unlock();
} }
} }
public StringProperty statusProperty() {
return statusProperty;
}
} }
public static class ReadRunnable implements Runnable { public static class ReadRunnable implements Runnable {

View file

@ -1,9 +1,8 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent; import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.io.Config;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service; import javafx.concurrent.Service;
import javafx.concurrent.Task; import javafx.concurrent.Task;
@ -13,7 +12,6 @@ import org.slf4j.LoggerFactory;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.net.InetSocketAddress;
import java.net.Proxy; import java.net.Proxy;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -52,7 +50,7 @@ public enum ExchangeSource {
private CoinbaseRates getRates() { private CoinbaseRates getRates() {
String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC"; 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)) { 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(); Gson gson = new Gson();
@ -83,7 +81,7 @@ public enum ExchangeSource {
private CoinGeckoRates getRates() { private CoinGeckoRates getRates() {
String url = "https://api.coingecko.com/api/v3/exchange_rates"; 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)) { 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(); 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 @Override
public String toString() { public String toString() {
return name; return name;

View file

@ -28,12 +28,13 @@ public enum Protocol {
@Override @Override
public Transport getTransport(HostAndPort server, HostAndPort proxy) { 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 @Override
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) { public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
throw new UnsupportedOperationException("TCP protocol does not support proxying"); return getTransport(server, proxy);
} }
}, },
SSL { SSL {
@ -116,7 +117,7 @@ public enum Protocol {
} }
public boolean isOnionAddress(HostAndPort server) { 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) { public static Protocol getProtocol(String url) {

View file

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

View file

@ -47,8 +47,12 @@ public class TcpTransport implements Transport, Closeable {
private final Gson gson = new Gson(); private final Gson gson = new Gson();
public TcpTransport(HostAndPort server) { public TcpTransport(HostAndPort server) {
this(server, null);
}
public TcpTransport(HostAndPort server, HostAndPort proxy) {
this.server = server; this.server = server;
this.socketFactory = SocketFactory.getDefault(); this.socketFactory = (proxy == null ? SocketFactory.getDefault() : new ProxySocketFactory(proxy));
} }
@Override @Override

View file

@ -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<NativeTor> {
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<NativeTor> 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<String, String> 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;
}
};
}
}

View file

@ -1,18 +1,11 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort; import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.StatusEvent;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import javafx.application.Platform;
import org.berndpruenster.netlayer.tor.*; import org.berndpruenster.netlayer.tor.*;
import java.io.*; import java.io.*;
import java.net.Socket; 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 class TorTcpTransport extends TcpTransport {
public static final String TOR_DIR_PREFIX = "tor"; public static final String TOR_DIR_PREFIX = "tor";
@ -21,35 +14,16 @@ public class TorTcpTransport extends TcpTransport {
super(server); super(server);
} }
public TorTcpTransport(HostAndPort server, HostAndPort proxy) {
super(server, proxy);
}
@Override @Override
protected Socket createSocket() throws IOException { protected Socket createSocket() throws IOException {
if(Tor.getDefault() == null) { if(!AppServices.isTorRunning()) {
Platform.runLater(() -> { throw new IllegalStateException("Can't create Tor socket, Tor is not running");
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<String, String> torrcOptionsMap = new LinkedHashMap<>();
torrcOptionsMap.put("DisableNetwork", "0");
Torrc override = new Torrc(torrcOptionsMap);
NativeTor nativeTor = new NativeTor(torInstallDir, Collections.emptyList(), override);
Tor.setDefault(nativeTor);
} catch(TorCtlException e) {
e.printStackTrace();
throw new IOException(e);
}
} }
Platform.runLater(() -> {
String status = "Tor running, connecting to " + server.toString() + "...";
EventManager.get().post(new TorStatusEvent(status));
});
return new TorSocket(server.getHost(), server.getPort(), "sparrow"); return new TorSocket(server.getHost(), server.getPort(), "sparrow");
} }
} }

View file

@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.MainApp; import com.sparrowwallet.sparrow.MainApp;
import com.sparrowwallet.sparrow.event.VersionUpdatedEvent; import com.sparrowwallet.sparrow.event.VersionUpdatedEvent;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
@ -15,6 +16,7 @@ import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.Proxy;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.SignatureException; import java.security.SignatureException;
@ -44,7 +46,8 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
private VersionCheck getVersionCheck() throws IOException { private VersionCheck getVersionCheck() throws IOException {
URL url = new URL(VERSION_CHECK_URL); 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)) { try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
Gson gson = new Gson(); Gson gson = new Gson();

View file

@ -14,7 +14,6 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.*; import com.sparrowwallet.sparrow.net.*;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.concurrent.WorkerStateEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.text.Font; import javafx.scene.text.Font;
@ -22,6 +21,8 @@ import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration; import javafx.util.Duration;
import net.freehaven.tor.control.TorControlError;
import org.berndpruenster.netlayer.tor.Tor;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.ValidationSupport;
@ -121,6 +122,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
private final ValidationSupport validationSupport = new ValidationSupport(); private final ValidationSupport validationSupport = new ValidationSupport();
private TorService torService;
private ElectrumServer.ConnectionService connectionService; private ElectrumServer.ConnectionService connectionService;
@Override @Override
@ -242,13 +245,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
proxyHost.setText(proxyHost.getText().trim()); proxyHost.setText(proxyHost.getText().trim());
proxyHost.setDisable(!newValue); proxyHost.setDisable(!newValue);
proxyPort.setDisable(!newValue); proxyPort.setDisable(!newValue);
if(newValue) {
electrumUseSsl.setSelected(true);
electrumUseSsl.setDisable(true);
} else {
electrumUseSsl.setDisable(false);
}
}); });
boolean isConnected = AppServices.isConnecting() || AppServices.isConnected(); boolean isConnected = AppServices.isConnecting() || AppServices.isConnected();
@ -263,7 +259,12 @@ public class ServerPreferencesController extends PreferencesDetailController {
testConnection.setOnAction(event -> { testConnection.setOnAction(event -> {
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
testResults.setText("Connecting to " + config.getServerAddress() + "..."); testResults.setText("Connecting to " + config.getServerAddress() + "...");
startElectrumConnection();
if(Config.get().requiresTor() && Tor.getDefault() == null) {
startTor();
} else {
startElectrumConnection();
}
}); });
editConnection.managedProperty().bind(editConnection.visibleProperty()); editConnection.managedProperty().bind(editConnection.visibleProperty());
@ -340,11 +341,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
proxyHost.setDisable(!config.isUseProxy()); proxyHost.setDisable(!config.isUseProxy());
proxyPort.setDisable(!config.isUseProxy()); proxyPort.setDisable(!config.isUseProxy());
if(config.isUseProxy()) {
electrumUseSsl.setSelected(true);
electrumUseSsl.setDisable(true);
}
String proxyServer = config.getProxyServer(); String proxyServer = config.getProxyServer();
if(proxyServer != null) { if(proxyServer != null) {
HostAndPort server = HostAndPort.fromString(proxyServer); HostAndPort server = HostAndPort.fromString(proxyServer);
@ -357,6 +353,32 @@ public class ServerPreferencesController extends PreferencesDetailController {
setFieldsEditable(!isConnected); 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() { private void startElectrumConnection() {
if(connectionService != null && connectionService.isRunning()) { if(connectionService != null && connectionService.isRunning()) {
connectionService.cancel(); connectionService.cancel();
@ -364,10 +386,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
connectionService = new ElectrumServer.ConnectionService(false); connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.hours(1)); connectionService.setPeriod(Duration.hours(1));
connectionService.setRestartOnFailure(false);
EventManager.get().register(connectionService); EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
testResults.setText(testResults.getText() + "\n" + newValue);
});
connectionService.setOnSucceeded(successEvent -> { connectionService.setOnSucceeded(successEvent -> {
EventManager.get().unregister(connectionService); EventManager.get().unregister(connectionService);
@ -379,8 +399,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
}); });
connectionService.setOnFailed(workerStateEvent -> { connectionService.setOnFailed(workerStateEvent -> {
EventManager.get().unregister(connectionService); EventManager.get().unregister(connectionService);
showConnectionFailure(workerStateEvent); showConnectionFailure(workerStateEvent.getSource().getException());
connectionService.cancel();
}); });
connectionService.start(); connectionService.start();
} }
@ -421,13 +440,15 @@ public class ServerPreferencesController extends PreferencesDetailController {
} }
} }
private void showConnectionFailure(WorkerStateEvent failEvent) { private void showConnectionFailure(Throwable exception) {
Throwable e = failEvent.getSource().getException(); log.error("Connection error", exception);
log.error("Connection error", e); String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage();
String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); if(exception.getCause() != null && exception.getCause() instanceof SSLHandshakeException) {
if(e.getCause() != null && e.getCause() instanceof SSLHandshakeException) {
reason = "SSL Handshake Error\n" + reason; 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); testResults.setText("Could not connect:\n\n" + reason);
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.EXCLAMATION_CIRCLE, "failure")); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.EXCLAMATION_CIRCLE, "failure"));

View file

@ -26,4 +26,5 @@ open module com.sparrowwallet.sparrow {
requires jcommander; requires jcommander;
requires slf4j.api; requires slf4j.api;
requires bwt.jni; requires bwt.jni;
requires jtorctl;
} }