package io.bitsquare.p2p.network; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext; import com.msopentech.thali.java.toronionproxy.JavaOnionProxyManager; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import io.bitsquare.app.Log; import io.bitsquare.common.Timer; import io.bitsquare.common.UserThread; import io.bitsquare.common.util.Utilities; import io.bitsquare.p2p.NodeAddress; import io.bitsquare.p2p.Utils; import io.nucleo.net.HiddenServiceDescriptor; import io.nucleo.net.JavaTorNode; import io.nucleo.net.TorNode; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.net.Socket; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static com.google.common.base.Preconditions.checkArgument; // Run in UserThread public class TorNetworkNode extends NetworkNode { private static final Logger log = LoggerFactory.getLogger(TorNetworkNode.class); private static final int MAX_RESTART_ATTEMPTS = 5; private static final long SHUT_DOWN_TIMEOUT_SEC = 5; private final File torDir; private TorNode torNetworkNode; private HiddenServiceDescriptor hiddenServiceDescriptor; private Timer shutDownTimeoutTimer; private int restartCounter; private MonadicBinding<Boolean> allShutDown; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public TorNetworkNode(int servicePort, File torDir) { super(servicePort); this.torDir = torDir; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @Override public void start(@Nullable SetupListener setupListener) { if (setupListener != null) addSetupListener(setupListener); createExecutorService(); // Create the tor node (takes about 6 sec.) createTorNode(torDir, torNode -> { Log.traceCall("torNode created"); TorNetworkNode.this.torNetworkNode = torNode; setupListeners.stream().forEach(SetupListener::onTorNodeReady); // Create Hidden Service (takes about 40 sec.) createHiddenService(torNode, Utils.findFreeSystemPort(), servicePort, hiddenServiceDescriptor -> { Log.traceCall("hiddenService created"); TorNetworkNode.this.hiddenServiceDescriptor = hiddenServiceDescriptor; nodeAddressProperty.set(new NodeAddress(hiddenServiceDescriptor.getFullAddress())); startServer(hiddenServiceDescriptor.getServerSocket()); setupListeners.stream().forEach(SetupListener::onHiddenServicePublished); }); }); } @Override protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException { checkArgument(peerNodeAddress.hostName.endsWith(".onion"), "PeerAddress is not an onion address"); return torNetworkNode.connectToHiddenService(peerNodeAddress.hostName, peerNodeAddress.port); } public Socks5Proxy getSocksProxy() { return torNetworkNode != null ? torNetworkNode.getSocksProxy() : null; } public void shutDown(@Nullable Runnable shutDownCompleteHandler) { Log.traceCall(); BooleanProperty torNetworkNodeShutDown = torNetworkNodeShutDown(); BooleanProperty networkNodeShutDown = networkNodeShutDown(); BooleanProperty shutDownTimerTriggered = shutDownTimerTriggered(); // Need to store allShutDown to not get garbage collected allShutDown = EasyBind.combine(torNetworkNodeShutDown, networkNodeShutDown, shutDownTimerTriggered, (a, b, c) -> (a && b) || c); allShutDown.subscribe((observable, oldValue, newValue) -> { if (newValue) { shutDownTimeoutTimer.stop(); long ts = System.currentTimeMillis(); log.debug("Shutdown executorService"); try { MoreExecutors.shutdownAndAwaitTermination(executorService, 500, TimeUnit.MILLISECONDS); log.debug("Shutdown executorService done after " + (System.currentTimeMillis() - ts) + " ms."); log.debug("Shutdown completed"); } catch (Throwable t) { log.error("Shutdown executorService failed with exception: " + t.getMessage()); t.printStackTrace(); } finally { if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); } } }); } private BooleanProperty torNetworkNodeShutDown() { final BooleanProperty done = new SimpleBooleanProperty(); executorService.submit(() -> { Utilities.setThreadName("torNetworkNodeShutDown"); long ts = System.currentTimeMillis(); log.debug("Shutdown torNetworkNode"); try { if (torNetworkNode != null) torNetworkNode.shutdown(); log.debug("Shutdown torNetworkNode done after " + (System.currentTimeMillis() - ts) + " ms."); } catch (Throwable e) { log.error("Shutdown torNetworkNode failed with exception: " + e.getMessage()); e.printStackTrace(); } finally { UserThread.execute(() -> done.set(true)); } }); return done; } private BooleanProperty networkNodeShutDown() { final BooleanProperty done = new SimpleBooleanProperty(); super.shutDown(() -> done.set(true)); return done; } private BooleanProperty shutDownTimerTriggered() { final BooleanProperty done = new SimpleBooleanProperty(); shutDownTimeoutTimer = UserThread.runAfter(() -> { log.error("A timeout occurred at shutDown"); done.set(true); }, SHUT_DOWN_TIMEOUT_SEC); return done; } /////////////////////////////////////////////////////////////////////////////////////////// // shutdown, restart /////////////////////////////////////////////////////////////////////////////////////////// private void restartTor(String errorMessage) { Log.traceCall(); restartCounter++; if (restartCounter > MAX_RESTART_ATTEMPTS) { String msg = "We tried to restart Tor " + restartCounter + " times, but it continued to fail with error message:\n" + errorMessage + "\n\n" + "Please check your internet connection and firewall and try to start again."; log.error(msg); throw new RuntimeException(msg); } } /////////////////////////////////////////////////////////////////////////////////////////// // create tor /////////////////////////////////////////////////////////////////////////////////////////// private void createTorNode(final File torDir, final Consumer<TorNode> resultHandler) { Log.traceCall(); ListenableFuture<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>> future = executorService.submit(() -> { Utilities.setThreadName("TorNetworkNode:CreateTorNode"); long ts = System.currentTimeMillis(); if (torDir.mkdirs()) log.trace("Created directory for tor at {}", torDir.getAbsolutePath()); TorNode<JavaOnionProxyManager, JavaOnionProxyContext> torNode = new JavaTorNode(torDir); log.debug("\n\n############################################################\n" + "TorNode created:" + "\nTook " + (System.currentTimeMillis() - ts) + " ms" + "\n############################################################\n"); return torNode; }); Futures.addCallback(future, new FutureCallback<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>>() { public void onSuccess(TorNode<JavaOnionProxyManager, JavaOnionProxyContext> torNode) { UserThread.execute(() -> resultHandler.accept(torNode)); } public void onFailure(@NotNull Throwable throwable) { UserThread.execute(() -> { log.error("TorNode creation failed with exception: " + throwable.getMessage()); restartTor(throwable.getMessage()); }); } }); } private void createHiddenService(TorNode torNode, int localPort, int servicePort, Consumer<HiddenServiceDescriptor> resultHandler) { Log.traceCall(); ListenableFuture<Object> future = executorService.submit(() -> { Utilities.setThreadName("TorNetworkNode:CreateHiddenService"); { long ts = System.currentTimeMillis(); HiddenServiceDescriptor hiddenServiceDescriptor = torNode.createHiddenService(localPort, servicePort); torNode.addHiddenServiceReadyListener(hiddenServiceDescriptor, descriptor -> { log.debug("\n\n############################################################\n" + "Hidden service published:" + "\nAddress=" + descriptor.getFullAddress() + "\nTook " + (System.currentTimeMillis() - ts) + " ms" + "\n############################################################\n"); UserThread.execute(() -> resultHandler.accept(hiddenServiceDescriptor)); }); return null; } }); Futures.addCallback(future, new FutureCallback<Object>() { public void onSuccess(Object hiddenServiceDescriptor) { log.debug("HiddenServiceDescriptor created. Wait for publishing."); } public void onFailure(@NotNull Throwable throwable) { UserThread.execute(() -> { log.error("Hidden service creation failed"); restartTor(throwable.getMessage()); }); } }); } }