Commit 9ce463be authored by zeroleak's avatar zeroleak
Browse files

enable Tor per usage: coordinator, backend

parent daac8a80
......@@ -76,10 +76,17 @@ Custom config can be appended to Torrc with:
cli.torConfig.customTorrc = /path/to/torrc
```
Tor can be enabled with:
```
cli.tor = true # global toggle
cli.torConfig.coordinator.enabled = true # whirlpool server
cli.torConfig.backend.enabled = true # wallet backend
```
Tor mode can be customized with:
```
cli.torConfig.onionServer = true # whirlpool server
cli.torConfig.onionBackend = true # wallet backend
cli.torConfig.coordinator.onion = true # whirlpool server
cli.torConfig.backend.onion = true # wallet backend
```
- `true`: Tor hidden services
- `false`: clearnet over Tor
......
package com.samourai.http.client;
import com.samourai.whirlpool.cli.config.CliConfig;
import com.samourai.whirlpool.cli.services.CliTorClientService;
import com.samourai.whirlpool.cli.utils.CliUtils;
import java.lang.invoke.MethodHandles;
import org.eclipse.jetty.client.HttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CliHttpClient extends JavaHttpClient {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private CliTorClientService torClientService;
private CliConfig cliConfig;
public CliHttpClient(CliTorClientService torClientService, CliConfig cliConfig) {
super(cliConfig.getRequestTimeout());
this.torClientService = torClientService;
this.cliConfig = cliConfig;
}
@Override
protected HttpClient computeHttpClient(boolean isRegisterOutput) throws Exception {
HttpClient httpClient =
CliUtils.computeHttpClient(isRegisterOutput, torClientService, cliConfig.getCliProxy());
return httpClient;
}
}
package com.samourai.http.client;
import com.samourai.wallet.api.backend.beans.HttpException;
import com.samourai.whirlpool.client.utils.ClientUtils;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.Map;
......@@ -17,58 +18,52 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
public abstract class JavaHttpClient extends JacksonHttpClient {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public class JavaHttpClient extends JacksonHttpClient {
private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private HttpClient httpClientShared;
private HttpClient httpClientRegOut;
private HttpUsage httpUsage;
private HttpClient httpClient;
private long requestTimeout;
public JavaHttpClient(long requestTimeout) {
public JavaHttpClient(HttpUsage httpUsage, HttpClient httpClient, long requestTimeout) {
super();
httpClientShared = null;
httpClientRegOut = null;
log = ClientUtils.prefixLogger(log, httpUsage.name());
this.httpUsage = httpUsage;
this.httpClient = httpClient;
this.requestTimeout = requestTimeout;
}
public void changeIdentity() {
// restart httpClientRegOut
@Override
public void connect() throws Exception {
if (!httpClient.isRunning()) {
httpClient.start();
}
}
public void restart() {
try {
httpClientRegOut.stop();
httpClientRegOut.start();
if (log.isDebugEnabled()) {
log.debug("restart");
}
if (httpClient.isRunning()) {
httpClient.stop();
}
httpClient.start();
} catch (Exception e) {
log.error("", e);
try {
httpClientRegOut.stop();
} catch (Exception ee) {
}
httpClientRegOut = null;
}
// httpClientShared will renew on next websocket connexion, don't break opened connexions
}
protected abstract HttpClient computeHttpClient(boolean isRegisterOutput) throws Exception;
@Override
protected String requestJsonGet(String urlStr, Map<String, String> headers) throws Exception {
Request req = computeHttpRequest(false, urlStr, HttpMethod.GET, headers);
Request req = computeHttpRequest(urlStr, HttpMethod.GET, headers);
return requestJson(req);
}
@Override
protected String requestJsonPost(String urlStr, Map<String, String> headers, String jsonBody)
throws Exception {
Request req = computeHttpRequest(false, urlStr, HttpMethod.POST, headers);
req.content(
new StringContentProvider(
MediaType.APPLICATION_JSON_VALUE, jsonBody, StandardCharsets.UTF_8));
return requestJson(req);
}
@Override
protected String requestJsonPostOverTor(
String urlStr, Map<String, String> headers, String jsonBody) throws Exception {
Request req = computeHttpRequest(true, urlStr, HttpMethod.POST, headers);
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
req.content(
new StringContentProvider(
MediaType.APPLICATION_JSON_VALUE, jsonBody, StandardCharsets.UTF_8));
......@@ -78,7 +73,7 @@ public abstract class JavaHttpClient extends JacksonHttpClient {
@Override
protected String requestJsonPostUrlEncoded(
String urlStr, Map<String, String> headers, Map<String, String> body) throws Exception {
Request req = computeHttpRequest(false, urlStr, HttpMethod.POST, headers);
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
req.content(new FormContentProvider(computeBodyFields(body)));
return requestJson(req);
}
......@@ -104,35 +99,18 @@ public abstract class JavaHttpClient extends JacksonHttpClient {
return responseContent;
}
public HttpClient getHttpClient(boolean isRegisterOutput) throws Exception {
if (!isRegisterOutput) {
if (httpClientShared == null) {
if (log.isDebugEnabled()) {
log.debug("+httpClientShared");
}
httpClientShared = computeHttpClient(isRegisterOutput);
}
return httpClientShared;
} else {
if (httpClientRegOut == null) {
if (log.isDebugEnabled()) {
log.debug("+httpClientRegOut");
}
httpClientRegOut = computeHttpClient(isRegisterOutput);
}
return httpClientRegOut;
}
public HttpClient getJettyHttpClient() throws Exception {
connect();
return httpClient;
}
private Request computeHttpRequest(
boolean isRegOut, String url, HttpMethod method, Map<String, String> headers)
private Request computeHttpRequest(String url, HttpMethod method, Map<String, String> headers)
throws Exception {
if (log.isDebugEnabled()) {
String headersStr = headers != null ? " (" + headers.keySet() + ")" : "";
log.debug("+" + method + ": " + url + headersStr);
}
HttpClient httpClient = getHttpClient(isRegOut);
Request req = httpClient.newRequest(url);
Request req = getJettyHttpClient().newRequest(url);
req.method(method);
if (headers != null) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
......@@ -144,30 +122,12 @@ public abstract class JavaHttpClient extends JacksonHttpClient {
}
@Override
protected void onRequestError(Exception e, boolean isRegisterOutput) {
super.onRequestError(e, isRegisterOutput);
if (!isRegisterOutput) {
if (httpClientShared != null) {
try {
httpClientShared.stop();
} catch (Exception ee) {
}
if (log.isDebugEnabled()) {
log.debug("--httpClientShared");
}
httpClientShared = null;
}
} else {
if (httpClientRegOut != null) {
try {
httpClientRegOut.stop();
} catch (Exception ee) {
}
if (log.isDebugEnabled()) {
log.debug("--httpClientRegOut");
}
httpClientRegOut = null;
}
}
protected void onRequestError(Exception e) {
super.onRequestError(e);
restart();
}
public HttpUsage getHttpUsage() {
return httpUsage;
}
}
package com.samourai.stomp.client;
import com.samourai.http.client.CliHttpClient;
import com.samourai.whirlpool.cli.config.CliConfig;
import com.samourai.whirlpool.cli.services.CliTorClientService;
import com.samourai.whirlpool.cli.services.JavaHttpClientService;
import com.samourai.http.client.JavaHttpClient;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.utils.MessageErrorListener;
import java.util.Arrays;
......@@ -32,22 +29,14 @@ public class JavaStompClient implements IStompClient {
private static final Logger log = LoggerFactory.getLogger(JavaStompClient.class);
private static final int HEARTBEAT_DELAY = 20000;
private CliTorClientService torClientService;
private CliConfig cliConfig;
private CliHttpClient httpClientService;
private JavaHttpClient httpClient;
private TaskScheduler taskScheduler;
private WebSocketStompClient stompClient;
private StompSession stompSession;
public JavaStompClient(
CliTorClientService torClientService,
CliConfig cliConfig,
JavaHttpClientService httpClientService,
ThreadPoolTaskScheduler taskScheduler) {
this.torClientService = torClientService;
this.cliConfig = cliConfig;
this.httpClientService = httpClientService;
public JavaStompClient(JavaHttpClient httpClient, ThreadPoolTaskScheduler taskScheduler) {
this.httpClient = httpClient;
this.taskScheduler = taskScheduler;
}
......@@ -164,16 +153,16 @@ public class JavaStompClient implements IStompClient {
}
private SockJsClient computeWebSocketClient() throws Exception {
HttpClient httpClient = httpClientService.getHttpClient(false);
HttpClient jettyHttpClient = httpClient.getJettyHttpClient();
if (log.isDebugEnabled()) {
log.debug("Using websocket transports: Websocket, XHR");
}
JettyWebSocketClient jettyWebSocketClient =
new JettyWebSocketClient(new WebSocketClient(httpClient));
new JettyWebSocketClient(new WebSocketClient(jettyHttpClient));
List<Transport> webSocketTransports =
Arrays.asList(
new WebSocketTransport(jettyWebSocketClient), new JettyXhrTransport(httpClient));
new WebSocketTransport(jettyWebSocketClient), new JettyXhrTransport(jettyHttpClient));
SockJsClient sockJsClient = new SockJsClient(webSocketTransports);
jettyWebSocketClient.start();
......
......@@ -2,7 +2,9 @@ package com.samourai.tor.client;
import com.msopentech.thali.toronionproxy.OsData;
import com.msopentech.thali.toronionproxy.TorSettings;
import com.samourai.http.client.HttpUsage;
import com.samourai.tor.client.utils.WhirlpoolTorInstaller;
import com.samourai.whirlpool.cli.beans.CliProxy;
import com.samourai.whirlpool.cli.beans.CliTorExecutableMode;
import com.samourai.whirlpool.cli.config.CliConfig;
import com.samourai.whirlpool.cli.utils.CliUtils;
......@@ -11,6 +13,7 @@ import java.io.File;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
......@@ -21,20 +24,21 @@ public class JavaTorClient {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private CliConfig cliConfig;
private Collection<HttpUsage> torHttpUsages;
private TorOnionProxyInstance torInstance;
private boolean started = false;
public JavaTorClient(CliConfig cliConfig) {
public JavaTorClient(CliConfig cliConfig, Collection<HttpUsage> torHttpUsages) {
this.cliConfig = cliConfig;
this.torHttpUsages = torHttpUsages;
}
public void setup() throws Exception {
TorSettings torSettings = computeTorSettings();
Optional<File> torExecutable = computeTorExecutableAndVerify();
this.torInstance =
new TorOnionProxyInstance(
new WhirlpoolTorInstaller("whirlpoolTor", torExecutable), torSettings);
WhirlpoolTorInstaller torInstaller = new WhirlpoolTorInstaller("whirlpoolTor", torExecutable);
this.torInstance = new TorOnionProxyInstance(torInstaller, torSettings, torHttpUsages);
}
private Optional<File> computeTorExecutableAndVerify() throws Exception {
......@@ -213,15 +217,6 @@ public class JavaTorClient {
}
}
public void disconnect() {
if (log.isDebugEnabled()) {
log.debug("Disconnecting");
}
started = false;
torInstance.stop();
}
public void shutdown() {
started = false;
torInstance.clear();
......@@ -235,8 +230,8 @@ public class JavaTorClient {
return torInstance.getProgress();
}
public JavaTorConnexion getConnexion() {
return torInstance;
public Optional<CliProxy> getTorProxy(HttpUsage httpUsage) {
return torInstance.getTorProxy(httpUsage);
}
private TorSettings computeTorSettings() throws Exception {
......
package com.samourai.tor.client;
import com.samourai.whirlpool.cli.beans.CliProxy;
import com.samourai.whirlpool.client.exception.NotifiableException;
public interface JavaTorConnexion {
CliProxy getTorProxy(boolean isRegisterOutput) throws NotifiableException;
}
package com.samourai.tor.client;
import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext;
import com.msopentech.thali.toronionproxy.*;
import com.msopentech.thali.toronionproxy.OnionProxyManager;
import com.msopentech.thali.toronionproxy.TorConfig;
import com.msopentech.thali.toronionproxy.TorConfigBuilder;
import com.msopentech.thali.toronionproxy.TorSettings;
import com.samourai.http.client.HttpUsage;
import com.samourai.tor.client.utils.WhirlpoolTorInstaller;
import com.samourai.whirlpool.cli.Application;
import com.samourai.whirlpool.cli.beans.CliProxy;
import com.samourai.whirlpool.cli.beans.CliProxyProtocol;
import com.samourai.whirlpool.client.exception.NotifiableException;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.SocketUtils;
public class TorOnionProxyInstance implements JavaTorConnexion {
public class TorOnionProxyInstance {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final int PROGRESS_CONNECTING = 50;
private OnionProxyManager onionProxyManager;
private Thread startThread;
private boolean torSocksReady = false;
private CliProxy torSocksShared = null;
private CliProxy torSocksRegOut = null;
private Map<HttpUsage, CliProxy> torProxies = null;
private int progress;
public TorOnionProxyInstance(WhirlpoolTorInstaller torInstaller, TorSettings torSettings)
public TorOnionProxyInstance(
WhirlpoolTorInstaller torInstaller, TorSettings torSettings, Collection<HttpUsage> httpUsages)
throws Exception {
TorConfig torConfig = torInstaller.getConfig();
if (log.isDebugEnabled()) {
......@@ -35,13 +43,13 @@ public class TorOnionProxyInstance implements JavaTorConnexion {
TorConfigBuilder builder = onionProxyManager.getContext().newConfigBuilder().updateTorConfig();
int socksPortShared = SocketUtils.findAvailableTcpPort();
builder.socksPort(Integer.toString(socksPortShared), null);
torSocksShared = new CliProxy(CliProxyProtocol.SOCKS, "127.0.0.1", socksPortShared);
int socksPortRegOut = SocketUtils.findAvailableTcpPort();
builder.socksPort(Integer.toString(socksPortRegOut), null);
torSocksRegOut = new CliProxy(CliProxyProtocol.SOCKS, "127.0.0.1", socksPortRegOut);
torProxies = new ConcurrentHashMap<>();
for (HttpUsage httpUsage : httpUsages) {
int socksPort = SocketUtils.findAvailableTcpPort();
builder.socksPort(Integer.toString(socksPort), null);
CliProxy torProxy = new CliProxy(CliProxyProtocol.SOCKS, "127.0.0.1", socksPort);
torProxies.put(httpUsage, torProxy);
}
onionProxyManager.getContext().getInstaller().updateTorConfigCustom(builder.asString());
onionProxyManager.setup();
......@@ -105,7 +113,7 @@ public class TorOnionProxyInstance implements JavaTorConnexion {
boolean ready = onionProxyManager.isRunning();
if (ready && progress != 100) {
if (log.isDebugEnabled()) {
log.debug("Tor connected! " + torSocksShared + " | " + torSocksRegOut);
log.debug("Tor connected! torProxies=" + torProxies);
}
progress = 100;
}
......@@ -184,10 +192,13 @@ public class TorOnionProxyInstance implements JavaTorConnexion {
return progress;
}
@Override
public CliProxy getTorProxy(boolean isRegisterOutput) throws NotifiableException {
public Optional<CliProxy> getTorProxy(HttpUsage httpUsage) {
waitTorSocks();
return isRegisterOutput ? torSocksRegOut : torSocksShared;
CliProxy torProxy = torProxies.get(httpUsage);
if (torProxy == null) {
return Optional.empty();
}
return Optional.of(torProxy);
}
private void waitTorSocks() {
......
package com.samourai.whirlpool.cli.config;
import com.samourai.http.client.IHttpClient;
import com.samourai.http.client.HttpUsage;
import com.samourai.http.client.IHttpClientService;
import com.samourai.stomp.client.IStompClientService;
import com.samourai.wallet.api.backend.BackendApi;
import com.samourai.wallet.api.backend.BackendServer;
......@@ -8,6 +9,9 @@ import com.samourai.wallet.util.FormatsUtilGeneric;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
import com.samourai.whirlpool.client.wallet.persist.WhirlpoolWalletPersistHandler;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
......@@ -23,7 +27,7 @@ public class CliConfig extends CliConfigFile {
@Override
public WhirlpoolWalletConfig computeWhirlpoolWalletConfig(
IHttpClient httpClient,
IHttpClientService httpClientService,
IStompClientService stompClientService,
WhirlpoolWalletPersistHandler persistHandler,
BackendApi backendApi) {
......@@ -35,7 +39,7 @@ public class CliConfig extends CliConfigFile {
WhirlpoolWalletConfig config =
super.computeWhirlpoolWalletConfig(
httpClient, stompClientService, persistHandler, backendApi);
httpClientService, stompClientService, persistHandler, backendApi);
config.setAutoTx0PoolId(autoTx0PoolId);
return config;
}
......@@ -103,8 +107,32 @@ public class CliConfig extends CliConfigFile {
private String computeBackendUrlSamourai() {
boolean isTestnet = FormatsUtilGeneric.getInstance().isTestNet(getServer().getParams());
BackendServer backendServer = BackendServer.get(isTestnet);
boolean useOnion = getTor() && getTorConfig().isOnionBackend();
boolean useOnion =
getTor()
&& getTorConfig().getBackend().isEnabled()
&& getTorConfig().getBackend().isOnion();
String backendUrl = backendServer.getBackendUrl(useOnion);
return backendUrl;
}
public Collection<HttpUsage> computeTorHttpUsages() {
List<HttpUsage> httpUsages = new LinkedList<>();
if (!getTor()) {
// tor is disabled
return httpUsages;
}
// backend
if (getTorConfig().getBackend().isEnabled()) {
httpUsages.add(HttpUsage.BACKEND);
}
// coordinator
if (getTorConfig().getCoordinator().isEnabled()) {
httpUsages.add(HttpUsage.COORDINATOR_WEBSOCKET);
httpUsages.add(HttpUsage.COORDINATOR_REST);
httpUsages.add(HttpUsage.COORDINATOR_REGISTER_OUTPUT);
}
return httpUsages;
}
}
package com.samourai.whirlpool.cli.config;
import com.samourai.http.client.IHttpClient;
import com.samourai.http.client.IHttpClientService;
import com.samourai.stomp.client.IStompClientService;
import com.samourai.wallet.api.backend.BackendApi;
import com.samourai.whirlpool.cli.beans.CliProxy;
......@@ -234,9 +234,9 @@ public abstract class CliConfigFile {
return clients;
}
// public void setClients(int clients) { // TODO constraint temporary disabled
// public void setClients(int clients) {
public void setClients(Integer clients) {
this.clients = clients != null ? clients : 0;
this.clients = clients;
}
public int getClientsPerPool() {
......@@ -361,16 +361,16 @@ public abstract class CliConfigFile {
public static class TorConfig {
@NotEmpty private String executable;
private CliTorExecutableMode executableMode;
@NotEmpty private boolean onionServer;
@NotEmpty private boolean onionBackend;
@NotEmpty private TorConfigItem coordinator;
@NotEmpty private TorConfigItem backend;
private String customTorrc;
public TorConfig() {}
public TorConfig(TorConfig copy) {
this.executable = copy.executable;
this.onionServer = copy.onionServer;
this.onionBackend = copy.onionBackend;
this.coordinator = copy.coordinator;
this.backend = copy.backend;
this.customTorrc = copy.customTorrc;
}
......@@ -391,20 +391,20 @@ public abstract class CliConfigFile {
return executableMode;
}
public boolean isOnionServer() {
return onionServer;
public TorConfigItem getCoordinator() {
return coordinator;
}
public void setOnionServer(boolean onionServer) {
this.onionServer = onionServer;
public void setCoordinator(TorConfigItem coordinator) {
this.coordinator = coordinator;
}
public boolean isOnionBackend() {
return onionBackend;
public TorConfigItem getBackend() {
return backend;
}