From 01b1741e830bf98acd92b17113f9a2f333314722 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 10 May 2022 14:25:21 +0100 Subject: [PATCH] Factor out Tor wrapper from plugin --- .../plugin/tor/AndroidTorPluginFactory.java | 31 +- .../AndroidTorWrapper.java} | 103 ++- .../bramble/plugin/PluginManagerImpl.java | 42 +- .../bramble/plugin/tor/TorPlugin.java | 664 ++--------------- .../bramble/plugin/tor/TorPluginFactory.java | 9 +- .../tor/wrapper/AbstractTorWrapper.java | 673 ++++++++++++++++++ .../bramble/plugin/tor/wrapper/TorUtils.java | 66 ++ .../plugin/tor/wrapper/TorWrapper.java | 153 ++++ .../bramble/plugin/tor/JavaTorPlugin.java | 62 -- .../bramble/plugin/tor/UnixTorPlugin.java | 60 -- .../plugin/tor/UnixTorPluginFactory.java | 23 +- .../plugin/tor/WindowsTorPluginFactory.java | 22 +- .../plugin/tor/wrapper/JavaTorWrapper.java | 47 ++ .../plugin/tor/wrapper/UnixTorWrapper.java | 35 + .../WindowsTorWrapper.java} | 43 +- .../bramble/plugin/tor/BridgeTest.java | 12 +- 16 files changed, 1177 insertions(+), 868 deletions(-) rename bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/{AndroidTorPlugin.java => wrapper/AndroidTorWrapper.java} (72%) create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/AbstractTorWrapper.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorUtils.java create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorWrapper.java delete mode 100644 bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java delete mode 100644 bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java create mode 100644 bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/JavaTorWrapper.java create mode 100644 bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/UnixTorWrapper.java rename bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/{WindowsTorPlugin.java => wrapper/WindowsTorWrapper.java} (58%) diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java index 59d0ff03e..34621a1af 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPluginFactory.java @@ -5,6 +5,7 @@ import android.app.Application; import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.plugin.Backoff; @@ -16,8 +17,9 @@ import org.briarproject.bramble.api.plugin.TorSocksPort; import org.briarproject.bramble.api.system.AndroidWakeLockManager; import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; import org.briarproject.bramble.api.system.WakefulIoExecutor; +import org.briarproject.bramble.plugin.tor.wrapper.AndroidTorWrapper; +import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper; import org.briarproject.nullsafety.NotNullByDefault; import java.io.File; @@ -28,6 +30,7 @@ import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import javax.net.SocketFactory; +import static android.os.Build.VERSION.SDK_INT; import static org.briarproject.bramble.util.AndroidUtils.getSupportedArchitectures; @Immutable @@ -39,13 +42,13 @@ public class AndroidTorPluginFactory extends TorPluginFactory { @Inject AndroidTorPluginFactory(@IoExecutor Executor ioExecutor, + @EventExecutor Executor eventExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, NetworkManager networkManager, LocationUtils locationUtils, EventBus eventBus, SocketFactory torSocketFactory, BackoffFactory backoffFactory, - ResourceProvider resourceProvider, CircumventionProvider circumventionProvider, BatteryManager batteryManager, Clock clock, @@ -55,8 +58,8 @@ public class AndroidTorPluginFactory extends TorPluginFactory { @TorControlPort int torControlPort, Application app, AndroidWakeLockManager wakeLockManager) { - super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, - eventBus, torSocketFactory, backoffFactory, resourceProvider, + super(ioExecutor, eventExecutor, wakefulIoExecutor, networkManager, + locationUtils, eventBus, torSocketFactory, backoffFactory, circumventionProvider, batteryManager, clock, crypto, torDirectory, torSocksPort, torControlPort); this.app = app; @@ -79,12 +82,18 @@ public class AndroidTorPluginFactory extends TorPluginFactory { TorPlugin createPluginInstance(Backoff backoff, TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, String architecture) { - return new AndroidTorPlugin(ioExecutor, - wakefulIoExecutor, app, networkManager, locationUtils, - torSocketFactory, clock, resourceProvider, - circumventionProvider, batteryManager, wakeLockManager, - backoff, torRendezvousCrypto, callback, architecture, - MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, - torControlPort); + TorWrapper tor = new AndroidTorWrapper(app, wakeLockManager, + ioExecutor, eventExecutor, architecture, torDirectory, + torSocksPort, torControlPort); + // Android versions 7.1 and newer can verify Let's Encrypt TLS certs + // signed with the IdentTrust DST Root X3 certificate. Older versions + // of Android consider the certificate to have expired at the end of + // September 2021. + boolean canVerifyLetsEncryptCerts = SDK_INT >= 25; + return new TorPlugin(ioExecutor, wakefulIoExecutor, + networkManager, locationUtils, torSocketFactory, + circumventionProvider, batteryManager, backoff, + torRendezvousCrypto, tor, callback, MAX_LATENCY, + MAX_IDLE_TIME, canVerifyLetsEncryptCerts); } } diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/AndroidTorWrapper.java similarity index 72% rename from bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java rename to bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/AndroidTorWrapper.java index 1789ffcb0..36b8ced24 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/AndroidTorWrapper.java @@ -1,46 +1,36 @@ -package org.briarproject.bramble.plugin.tor; +package org.briarproject.bramble.plugin.tor.wrapper; import android.app.Application; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.os.Build; -import org.briarproject.bramble.api.battery.BatteryManager; -import org.briarproject.bramble.api.network.NetworkManager; -import org.briarproject.bramble.api.plugin.Backoff; -import org.briarproject.bramble.api.plugin.PluginCallback; import org.briarproject.bramble.api.system.AndroidWakeLock; import org.briarproject.bramble.api.system.AndroidWakeLockManager; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; -import org.briarproject.bramble.util.AndroidUtils; -import org.briarproject.nullsafety.MethodsNotNullByDefault; -import org.briarproject.nullsafety.ParametersNotNullByDefault; +import org.briarproject.nullsafety.NotNullByDefault; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Logger; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import javax.net.SocketFactory; - -import androidx.annotation.ChecksSdkIntAtLeast; - import static android.os.Build.VERSION.SDK_INT; import static java.util.Arrays.asList; import static java.util.logging.Level.INFO; import static java.util.logging.Logger.getLogger; -@MethodsNotNullByDefault -@ParametersNotNullByDefault -class AndroidTorPlugin extends TorPlugin { +@NotNullByDefault +public class AndroidTorWrapper extends AbstractTorWrapper { private static final List LIBRARY_ARCHITECTURES = asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64"); @@ -50,37 +40,22 @@ class AndroidTorPlugin extends TorPlugin { private static final String SNOWFLAKE_LIB_NAME = "libsnowflake.so"; private static final Logger LOG = - getLogger(AndroidTorPlugin.class.getName()); + getLogger(AndroidTorWrapper.class.getName()); private final Application app; private final AndroidWakeLock wakeLock; private final File torLib, obfs4Lib, snowflakeLib; - AndroidTorPlugin(Executor ioExecutor, - Executor wakefulIoExecutor, - Application app, - NetworkManager networkManager, - LocationUtils locationUtils, - SocketFactory torSocketFactory, - Clock clock, - ResourceProvider resourceProvider, - CircumventionProvider circumventionProvider, - BatteryManager batteryManager, + public AndroidTorWrapper(Application app, AndroidWakeLockManager wakeLockManager, - Backoff backoff, - TorRendezvousCrypto torRendezvousCrypto, - PluginCallback callback, + Executor ioExecutor, + Executor eventExecutor, String architecture, - long maxLatency, - int maxIdleTime, File torDirectory, int torSocksPort, int torControlPort) { - super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, - torSocketFactory, clock, resourceProvider, - circumventionProvider, batteryManager, backoff, - torRendezvousCrypto, callback, architecture, maxLatency, - maxIdleTime, torDirectory, torSocksPort, torControlPort); + super(ioExecutor, eventExecutor, architecture, torDirectory, + torSocksPort, torControlPort); this.app = app; wakeLock = wakeLockManager.createWakeLock("TorPlugin"); String nativeLibDir = app.getApplicationInfo().nativeLibraryDir; @@ -106,22 +81,30 @@ class AndroidTorPlugin extends TorPlugin { } @Override - protected void enableNetwork(boolean enable) throws IOException { + public InputStream getResourceInputStream(String name, String extension) { + Resources res = app.getResources(); + // Extension is ignored on Android, resources are retrieved without it + int resId = res.getIdentifier(name, "raw", app.getPackageName()); + return res.openRawResource(resId); + } + + @Override + public void enableNetwork(boolean enable) throws IOException { if (enable) wakeLock.acquire(); - super.enableNetwork(enable); - if (!enable) wakeLock.release(); + try { + super.enableNetwork(enable); + } finally { + if (!enable) wakeLock.release(); + } } @Override - @ChecksSdkIntAtLeast(api = 25) - protected boolean canVerifyLetsEncryptCerts() { - return SDK_INT >= 25; - } - - @Override - public void stop() { - super.stop(); - wakeLock.release(); + public void stop() throws IOException { + try { + super.stop(); + } finally { + wakeLock.release(); + } } @Override @@ -136,8 +119,8 @@ class AndroidTorPlugin extends TorPlugin { @Override protected File getSnowflakeExecutableFile() { - return snowflakeLib.exists() - ? snowflakeLib : super.getSnowflakeExecutableFile(); + return snowflakeLib.exists() ? snowflakeLib + : super.getSnowflakeExecutableFile(); } @Override @@ -184,6 +167,7 @@ class AndroidTorPlugin extends TorPlugin { } List libPaths = getSupportedLibraryPaths(libName); for (File apk : findApkFiles(sourceDir)) { + @SuppressWarnings("IOStreamConstructor") ZipInputStream zin = new ZipInputStream(new FileInputStream(apk)); for (ZipEntry e = zin.getNextEntry(); e != null; e = zin.getNextEntry()) { @@ -228,11 +212,22 @@ class AndroidTorPlugin extends TorPlugin { */ private List getSupportedLibraryPaths(String libName) { List architectures = new ArrayList<>(); - for (String abi : AndroidUtils.getSupportedArchitectures()) { + for (String abi : getSupportedArchitectures()) { if (LIBRARY_ARCHITECTURES.contains(abi)) { architectures.add("lib/" + abi + "/" + libName); } } return architectures; } + + private Collection getSupportedArchitectures() { + List abis = new ArrayList<>(); + if (SDK_INT >= 21) { + abis.addAll(asList(Build.SUPPORTED_ABIS)); + } else { + abis.add(Build.CPU_ABI); + if (Build.CPU_ABI2 != null) abis.add(Build.CPU_ABI2); + } + return abis; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java index 64b53e003..6467d185c 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java @@ -40,9 +40,9 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; +import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; @@ -288,8 +288,10 @@ class PluginManagerImpl implements PluginManager, Service { private class Callback implements PluginCallback { private final TransportId id; - private final AtomicReference state = - new AtomicReference<>(STARTING_STOPPING); + private final Object stateLock = new Object(); + + @GuardedBy("lock") + private State state = STARTING_STOPPING; private Callback(TransportId id) { this.id = id; @@ -343,22 +345,26 @@ class PluginManagerImpl implements PluginManager, Service { @Override public void pluginStateChanged(State newState) { - State oldState = state.getAndSet(newState); - if (newState != oldState) { - if (LOG.isLoggable(INFO)) { - LOG.info(id + " changed from state " + oldState - + " to " + newState); + synchronized (stateLock) { + if (newState != state) { + State oldState = state; + state = newState; + if (LOG.isLoggable(INFO)) { + LOG.info(id + " changed from state " + oldState + + " to " + newState); + } + eventBus.broadcast(new TransportStateEvent(id, newState)); + if (newState == ACTIVE) { + eventBus.broadcast(new TransportActiveEvent(id)); + } else if (oldState == ACTIVE) { + eventBus.broadcast(new TransportInactiveEvent(id)); + } + } else if (newState == DISABLED) { + // Broadcast an event even though the state hasn't changed, + // as the reasons for the plugin being disabled may have + // changed + eventBus.broadcast(new TransportStateEvent(id, newState)); } - eventBus.broadcast(new TransportStateEvent(id, newState)); - if (newState == ACTIVE) { - eventBus.broadcast(new TransportActiveEvent(id)); - } else if (oldState == ACTIVE) { - eventBus.broadcast(new TransportInactiveEvent(id)); - } - } else if (newState == DISABLED) { - // Broadcast an event even though the state hasn't changed, as - // the reasons for the plugin being disabled may have changed - eventBus.broadcast(new TransportStateEvent(id, newState)); } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index 75579e4cb..d95d24dc4 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -1,8 +1,5 @@ package org.briarproject.bramble.plugin.tor; -import net.freehaven.tor.control.EventHandler; -import net.freehaven.tor.control.TorControlConnection; - import org.briarproject.bramble.PoliteExecutor; import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.battery.BatteryManager; @@ -27,30 +24,21 @@ import org.briarproject.bramble.api.rendezvous.KeyMaterialSource; import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint; import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent; -import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; import org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType; -import org.briarproject.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper; +import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.HiddenServiceProperties; +import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState; +import org.briarproject.nullsafety.InterfaceNotNullByDefault; import org.briarproject.nullsafety.NotNullByDefault; -import org.briarproject.nullsafety.ParametersNotNullByDefault; -import java.io.ByteArrayInputStream; -import java.io.EOFException; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Scanner; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; @@ -63,13 +51,9 @@ import javax.net.SocketFactory; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; -import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS; -import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY; import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED; import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING; @@ -94,33 +78,16 @@ import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DAT import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.SNOWFLAKE; import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES; -import static org.briarproject.bramble.util.IoUtils.copyAndClose; import static org.briarproject.bramble.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.PrivacyUtils.scrubOnion; -import static org.briarproject.bramble.util.StringUtils.UTF_8; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; -import static org.briarproject.nullsafety.NullSafety.requireNonNull; -@MethodsNotNullByDefault -@ParametersNotNullByDefault -abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { +@InterfaceNotNullByDefault +class TorPlugin implements DuplexPlugin, EventListener { protected static final Logger LOG = getLogger(TorPlugin.class.getName()); - private static final String[] EVENTS = { - "CIRC", - "ORCONN", - "STATUS_GENERAL", - "STATUS_CLIENT", - "HS_DESC", - "NOTICE", - "WARN", - "ERR" - }; - private static final String OWNER = "__OwningControllerProcess"; - private static final int COOKIE_TIMEOUT_MS = 3000; - private static final int COOKIE_POLLING_INTERVAL_MS = 200; private static final Pattern ONION_V3 = Pattern.compile("[a-z2-7]{56}"); protected final Executor ioExecutor; @@ -129,91 +96,63 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private final NetworkManager networkManager; private final LocationUtils locationUtils; private final SocketFactory torSocketFactory; - private final Clock clock; + private final CircumventionProvider circumventionProvider; private final BatteryManager batteryManager; private final Backoff backoff; private final TorRendezvousCrypto torRendezvousCrypto; + private final TorWrapper tor; private final PluginCallback callback; - private final String architecture; - private final CircumventionProvider circumventionProvider; - private final ResourceProvider resourceProvider; private final long maxLatency; private final int maxIdleTime; + private final boolean canVerifyLetsEncryptCerts; private final int socketTimeout; - private final File torDirectory; - private final File configFile; - private final int torSocksPort; - private final int torControlPort; - private final File doneFile, cookieFile; private final AtomicBoolean used = new AtomicBoolean(false); protected final PluginState state = new PluginState(); - private volatile Socket controlSocket = null; - private volatile TorControlConnection controlConnection = null; private volatile Settings settings = null; - protected abstract int getProcessId(); - - protected abstract long getLastUpdateTime(); - TorPlugin(Executor ioExecutor, Executor wakefulIoExecutor, NetworkManager networkManager, LocationUtils locationUtils, SocketFactory torSocketFactory, - Clock clock, - ResourceProvider resourceProvider, CircumventionProvider circumventionProvider, BatteryManager batteryManager, Backoff backoff, TorRendezvousCrypto torRendezvousCrypto, + TorWrapper tor, PluginCallback callback, - String architecture, long maxLatency, int maxIdleTime, - File torDirectory, - int torSocksPort, - int torControlPort) { + boolean canVerifyLetsEncryptCerts) { this.ioExecutor = ioExecutor; this.wakefulIoExecutor = wakefulIoExecutor; this.networkManager = networkManager; this.locationUtils = locationUtils; this.torSocketFactory = torSocketFactory; - this.clock = clock; - this.resourceProvider = resourceProvider; this.circumventionProvider = circumventionProvider; this.batteryManager = batteryManager; this.backoff = backoff; this.torRendezvousCrypto = torRendezvousCrypto; + this.tor = tor; this.callback = callback; - this.architecture = architecture; this.maxLatency = maxLatency; this.maxIdleTime = maxIdleTime; - if (maxIdleTime > Integer.MAX_VALUE / 2) + this.canVerifyLetsEncryptCerts = canVerifyLetsEncryptCerts; + if (maxIdleTime > Integer.MAX_VALUE / 2) { socketTimeout = Integer.MAX_VALUE; - else socketTimeout = maxIdleTime * 2; - this.torDirectory = torDirectory; - this.torSocksPort = torSocksPort; - this.torControlPort = torControlPort; - configFile = new File(torDirectory, "torrc"); - doneFile = new File(torDirectory, "done"); - cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); + } else { + socketTimeout = maxIdleTime * 2; + } // Don't execute more than one connection status check at a time connectionStatusExecutor = new PoliteExecutor("TorPlugin", ioExecutor, 1); - } - - protected File getTorExecutableFile() { - return new File(torDirectory, "tor"); - } - - protected File getObfs4ExecutableFile() { - return new File(torDirectory, "obfs4proxy"); - } - - protected File getSnowflakeExecutableFile() { - return new File(torDirectory, "snowflake"); + tor.setStateObserver(torState -> { + State s = state.getState(torState); + if (s == ACTIVE) backoff.reset(); + callback.pluginStateChanged(s); + }); } @Override @@ -234,89 +173,18 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public void start() throws PluginException { if (used.getAndSet(true)) throw new IllegalStateException(); - if (!torDirectory.exists()) { - if (!torDirectory.mkdirs()) { - LOG.warning("Could not create Tor directory."); - throw new PluginException(); - } - } // Load the settings settings = callback.getSettings(); + // Start Tor try { - // Install or update the assets if necessary - if (!assetsAreUpToDate()) installAssets(); - // Start from the default config every time - extract(getConfigInputStream(), configFile); - } catch (IOException e) { - throw new PluginException(e); - } - if (cookieFile.exists() && !cookieFile.delete()) - LOG.warning("Old auth cookie not deleted"); - // Start a new Tor process - LOG.info("Starting Tor"); - File torFile = getTorExecutableFile(); - String torPath = torFile.getAbsolutePath(); - String configPath = configFile.getAbsolutePath(); - String pid = String.valueOf(getProcessId()); - Process torProcess; - ProcessBuilder pb = - new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); - Map env = pb.environment(); - env.put("HOME", torDirectory.getAbsolutePath()); - pb.directory(torDirectory); - pb.redirectErrorStream(true); - try { - torProcess = pb.start(); - } catch (SecurityException | IOException e) { - throw new PluginException(e); - } - try { - // Wait for the Tor process to start - waitForTorToStart(torProcess); - // Wait for the auth cookie file to be created/updated - long start = clock.currentTimeMillis(); - while (cookieFile.length() < 32) { - if (clock.currentTimeMillis() - start > COOKIE_TIMEOUT_MS) { - LOG.warning("Auth cookie not created"); - if (LOG.isLoggable(INFO)) listFiles(torDirectory); - throw new PluginException(); - } - //noinspection BusyWait - Thread.sleep(COOKIE_POLLING_INTERVAL_MS); - } - LOG.info("Auth cookie created"); + tor.start(); } catch (InterruptedException e) { LOG.warning("Interrupted while starting Tor"); Thread.currentThread().interrupt(); throw new PluginException(); - } - try { - // Open a control connection and authenticate using the cookie file - controlSocket = new Socket("127.0.0.1", torControlPort); - controlConnection = new TorControlConnection(controlSocket); - controlConnection.authenticate(read(cookieFile)); - // Tell Tor to exit when the control connection is closed - controlConnection.takeOwnership(); - controlConnection.resetConf(singletonList(OWNER)); - // Register to receive events from the Tor process - controlConnection.setEventHandler(this); - controlConnection.setEvents(asList(EVENTS)); - // Check whether Tor has already bootstrapped - String info = controlConnection.getInfo("status/bootstrap-phase"); - if (info != null && info.contains("PROGRESS=100")) { - LOG.info("Tor has already bootstrapped"); - state.setBootstrapped(); - } - // Check whether Tor has already built a circuit - info = controlConnection.getInfo("status/circuit-established"); - if ("1".equals(info)) { - LOG.info("Tor has already built a circuit"); - state.setCircuitBuilt(true); - } } catch (IOException e) { throw new PluginException(e); } - state.setStarted(); // Check whether we're online updateConnectionStatus(networkManager.getNetworkStatus(), batteryManager.isCharging()); @@ -324,130 +192,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { bind(); } - private boolean assetsAreUpToDate() { - return doneFile.lastModified() > getLastUpdateTime(); - } - - private void installAssets() throws IOException { - // The done file may already exist from a previous installation - //noinspection ResultOfMethodCallIgnored - doneFile.delete(); - installTorExecutable(); - installObfs4Executable(); - installSnowflakeExecutable(); - if (!doneFile.createNewFile()) - LOG.warning("Failed to create done file"); - } - - protected void extract(InputStream in, File dest) throws IOException { - OutputStream out = new FileOutputStream(dest); - copyAndClose(in, out); - } - - protected void installTorExecutable() throws IOException { - if (LOG.isLoggable(INFO)) - LOG.info("Installing Tor binary for " + architecture); - File torFile = getTorExecutableFile(); - extract(getExecutableInputStream("tor"), torFile); - if (!torFile.setExecutable(true, true)) throw new IOException(); - } - - protected void installObfs4Executable() throws IOException { - if (LOG.isLoggable(INFO)) - LOG.info("Installing obfs4proxy binary for " + architecture); - File obfs4File = getObfs4ExecutableFile(); - extract(getExecutableInputStream("obfs4proxy"), obfs4File); - if (!obfs4File.setExecutable(true, true)) throw new IOException(); - } - - protected void installSnowflakeExecutable() throws IOException { - if (LOG.isLoggable(INFO)) - LOG.info("Installing snowflake binary for " + architecture); - File snowflakeFile = getSnowflakeExecutableFile(); - extract(getExecutableInputStream("snowflake"), snowflakeFile); - if (!snowflakeFile.setExecutable(true, true)) throw new IOException(); - } - - private InputStream getExecutableInputStream(String basename) { - String ext = getExecutableExtension(); - return requireNonNull(resourceProvider - .getResourceInputStream(architecture + "/" + basename, ext)); - } - - protected String getExecutableExtension() { - return ""; - } - - private static void append(StringBuilder strb, String name, Object value) { - strb.append(name); - strb.append(" "); - strb.append(value); - strb.append("\n"); - } - - private InputStream getConfigInputStream() { - File dataDirectory = new File(torDirectory, ".tor"); - StringBuilder strb = new StringBuilder(); - append(strb, "ControlPort", torControlPort); - append(strb, "CookieAuthentication", 1); - append(strb, "DataDirectory", dataDirectory.getAbsolutePath()); - append(strb, "DisableNetwork", 1); - append(strb, "RunAsDaemon", 1); - append(strb, "SafeSocks", 1); - append(strb, "SocksPort", torSocksPort); - strb.append("GeoIPFile\n"); - strb.append("GeoIPv6File\n"); - append(strb, "ConnectionPadding", 0); - String obfs4Path = getObfs4ExecutableFile().getAbsolutePath(); - append(strb, "ClientTransportPlugin obfs4 exec", obfs4Path); - append(strb, "ClientTransportPlugin meek_lite exec", obfs4Path); - String snowflakePath = getSnowflakeExecutableFile().getAbsolutePath(); - append(strb, "ClientTransportPlugin snowflake exec", snowflakePath); - return new ByteArrayInputStream(strb.toString().getBytes(UTF_8)); - } - - private void listFiles(File f) { - if (f.isDirectory()) { - File[] children = f.listFiles(); - if (children != null) for (File child : children) listFiles(child); - } else { - LOG.info(f.getAbsolutePath() + " " + f.length()); - } - } - - private byte[] read(File f) throws IOException { - byte[] b = new byte[(int) f.length()]; - FileInputStream in = new FileInputStream(f); - try { - int offset = 0; - while (offset < b.length) { - int read = in.read(b, offset, b.length - offset); - if (read == -1) throw new EOFException(); - offset += read; - } - return b; - } finally { - tryToClose(in, LOG, WARNING); - } - } - - protected void waitForTorToStart(Process torProcess) - throws InterruptedException, PluginException { - Scanner stdout = new Scanner(torProcess.getInputStream()); - // Log the first line of stdout (contains Tor and library versions) - if (stdout.hasNextLine()) LOG.info(stdout.nextLine()); - // Read the process's stdout (and redirected stderr) until it detaches - while (stdout.hasNextLine()) stdout.nextLine(); - stdout.close(); - // Wait for the process to detach or exit - int exit = torProcess.waitFor(); - if (exit != 0) { - if (LOG.isLoggable(WARNING)) - LOG.warning("Tor exited with value " + exit); - throw new PluginException(); - } - } - private void bind() { ioExecutor.execute(() -> { // If there's already a port number stored in config, reuse it @@ -471,9 +215,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { return; } // Store the port number - String localPort = String.valueOf(ss.getLocalPort()); + int localPort = ss.getLocalPort(); Settings s = new Settings(); - s.put(PREF_TOR_PORT, localPort); + s.put(PREF_TOR_PORT, String.valueOf(localPort)); callback.mergeSettings(s); // Create a hidden service if necessary ioExecutor.execute(() -> publishHiddenService(localPort)); @@ -483,48 +227,28 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { }); } - private void publishHiddenService(String port) { - if (!state.isTorRunning()) return; - String privKey3 = settings.get(HS_PRIVATE_KEY_V3); - publishV3HiddenService(port, privKey3); - } - - private void publishV3HiddenService(String port, @Nullable String privKey) { + private void publishHiddenService(int localPort) { + if (!tor.isTorRunning()) return; + String privKey = settings.get(HS_PRIVATE_KEY_V3); LOG.info("Creating v3 hidden service"); - Map portLines = singletonMap(80, "127.0.0.1:" + port); - Map response; + HiddenServiceProperties hsProps; try { - // Use the control connection to set up the hidden service - if (privKey == null) { - response = controlConnection.addOnion("NEW:ED25519-V3", - portLines, null); - } else { - response = controlConnection.addOnion(privKey, portLines); - } + hsProps = tor.publishHiddenService(localPort, 80, privKey); } catch (IOException e) { logException(LOG, WARNING, e); return; } - if (!response.containsKey(HS_ADDRESS)) { - LOG.warning("Tor did not return a hidden service address"); - return; - } - if (privKey == null && !response.containsKey(HS_PRIVKEY)) { - LOG.warning("Tor did not return a private key"); - return; - } - String onion3 = response.get(HS_ADDRESS); if (LOG.isLoggable(INFO)) { - LOG.info("V3 hidden service " + scrubOnion(onion3)); + LOG.info("V3 hidden service " + scrubOnion(hsProps.onion)); } if (privKey == null) { // Publish the hidden service's onion hostname in transport props TransportProperties p = new TransportProperties(); - p.put(PROP_ONION_V3, onion3); + p.put(PROP_ONION_V3, hsProps.onion); callback.mergeLocalProperties(p); // Save the hidden service's private key for next time Settings s = new Settings(); - s.put(HS_PRIVATE_KEY_V3, response.get(HS_PRIVKEY)); + s.put(HS_PRIVATE_KEY_V3, hsProps.privKey); callback.mergeSettings(s); } } @@ -547,50 +271,28 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } } - protected void enableNetwork(boolean enable) throws IOException { - if (!state.enableNetwork(enable)) return; // Unchanged - controlConnection.setConf("DisableNetwork", enable ? "0" : "1"); - } - private void enableBridges(List bridgeTypes, String countryCode) throws IOException { - if (!state.setBridgeTypes(bridgeTypes)) return; // Unchanged if (bridgeTypes.isEmpty()) { - controlConnection.setConf("UseBridges", "0"); - controlConnection.resetConf(singletonList("Bridge")); + tor.disableBridges(); } else { - Collection conf = new ArrayList<>(); - conf.add("UseBridges 1"); - boolean letsEncrypt = canVerifyLetsEncryptCerts(); + List bridges = new ArrayList<>(); for (BridgeType bridgeType : bridgeTypes) { - conf.addAll(circumventionProvider - .getBridges(bridgeType, countryCode, letsEncrypt)); + bridges.addAll(circumventionProvider.getBridges(bridgeType, + countryCode, canVerifyLetsEncryptCerts)); } - controlConnection.setConf(conf); + tor.enableBridges(bridges); } } - /** - * Returns true if this device can verify Let's Encrypt certificates signed - * with the IdentTrust DST Root X3 certificate, which expired at the end of - * September 2021. - */ - protected boolean canVerifyLetsEncryptCerts() { - return true; - } - @Override public void stop() { ServerSocket ss = state.setStopped(); tryToClose(ss, LOG, WARNING); - if (controlSocket != null && controlConnection != null) { - try { - LOG.info("Stopping Tor"); - controlConnection.shutdownTor("TERM"); - controlSocket.close(); - } catch (IOException e) { - logException(LOG, WARNING, e); - } + try { + tor.stop(); + } catch (IOException e) { + logException(LOG, WARNING, e); } } @@ -701,6 +403,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { TransportProperties remoteProperties = new TransportProperties(); remoteProperties.put(PROP_ONION_V3, remoteOnion); try { + @SuppressWarnings("resource") ServerSocket ss = new ServerSocket(); ss.bind(new InetSocketAddress("127.0.0.1", 0)); int port = ss.getLocalPort(); @@ -717,9 +420,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { LOG.info("Rendezvous server socket closed"); } }); - Map portLines = - singletonMap(80, "127.0.0.1:" + port); - controlConnection.addOnion(blob, portLines); + tor.publishHiddenService(port, 80, blob); return new RendezvousEndpoint() { @Override @@ -729,8 +430,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public void close() throws IOException { - controlConnection.delOnion(localOnion); - tryToClose(ss, LOG, WARNING); + try { + tor.removeHiddenService(localOnion); + } finally { + tryToClose(ss, LOG, WARNING); + } } }; } catch (IOException e) { @@ -739,121 +443,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } } - @Override - public void circuitStatus(String status, String id, String path) { - // In case of races between receiving CIRCUIT_ESTABLISHED and setting - // DisableNetwork, set our circuitBuilt flag if not already set - if (status.equals("BUILT") && state.setCircuitBuilt(true)) { - LOG.info("Circuit built"); - backoff.reset(); - } - } - - @Override - public void streamStatus(String status, String id, String target) { - } - - @Override - public void orConnStatus(String status, String orName) { - if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status); - - if (status.equals("CONNECTED")) state.onOrConnectionConnected(); - else if (status.equals("CLOSED")) state.onOrConnectionClosed(); - } - - @Override - public void bandwidthUsed(long read, long written) { - } - - @Override - public void newDescriptors(List orList) { - } - - @Override - public void message(String severity, String msg) { - if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg); - } - - @Override - public void unrecognized(String type, String msg) { - if (type.equals("STATUS_CLIENT")) { - handleClientStatus(removeSeverity(msg)); - } else if (type.equals("STATUS_GENERAL")) { - handleGeneralStatus(removeSeverity(msg)); - } else if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) { - String[] parts = msg.split(" "); - if (parts.length < 2) { - LOG.warning("Failed to parse HS_DESC UPLOADED event"); - } else if (LOG.isLoggable(INFO)) { - LOG.info("V3 descriptor uploaded for " + scrubOnion(parts[1])); - } - } - } - - @Override - public void controlConnectionClosed() { - if (state.isTorRunning()) { - // TODO: Restart the Tor process - LOG.warning("Control connection closed"); - } - } - - private String removeSeverity(String msg) { - return msg.replaceFirst("[^ ]+ ", ""); - } - - private void handleClientStatus(String msg) { - if (msg.startsWith("BOOTSTRAP PROGRESS=100")) { - LOG.info("Bootstrapped"); - state.setBootstrapped(); - backoff.reset(); - } else if (msg.startsWith("CIRCUIT_ESTABLISHED")) { - if (state.setCircuitBuilt(true)) { - LOG.info("Circuit built"); - backoff.reset(); - } - } else if (msg.startsWith("CIRCUIT_NOT_ESTABLISHED")) { - if (state.setCircuitBuilt(false)) { - LOG.info("Circuit not built"); - // TODO: Disable and re-enable network to prompt Tor to rebuild - // its guard/bridge connections? This will also close any - // established circuits, which might still be functioning - } - } - } - - private void handleGeneralStatus(String msg) { - if (msg.startsWith("CLOCK_JUMPED")) { - Long time = parseLongArgument(msg, "TIME"); - if (time != null && LOG.isLoggable(WARNING)) { - LOG.warning("Clock jumped " + time + " seconds"); - } - } else if (msg.startsWith("CLOCK_SKEW")) { - Long skew = parseLongArgument(msg, "SKEW"); - if (skew != null && LOG.isLoggable(WARNING)) { - LOG.warning("Clock is skewed by " + skew + " seconds"); - } - } - } - - @Nullable - private Long parseLongArgument(String msg, String argName) { - String[] args = msg.split(" "); - for (String arg : args) { - if (arg.startsWith(argName + "=")) { - try { - return Long.parseLong(arg.substring(argName.length() + 1)); - } catch (NumberFormatException e) { - break; - } - } - } - if (LOG.isLoggable(WARNING)) { - LOG.warning("Failed to parse " + argName + " from '" + msg + "'"); - } - return null; - } - @Override public void eventOccurred(Event e) { if (e instanceof SettingsUpdatedEvent) { @@ -876,7 +465,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private void updateConnectionStatus(NetworkStatus status, boolean charging) { connectionStatusExecutor.execute(() -> { - if (!state.isTorRunning()) return; + if (!tor.isTorRunning()) return; boolean online = status.isConnected(); boolean wifi = status.isWifi(); boolean ipv6Only = status.isIpv6Only(); @@ -960,41 +549,22 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { try { if (enableNetwork) { enableBridges(bridgeTypes, country); - enableConnectionPadding(enableConnectionPadding); - enableIpv6(ipv6Only); + tor.enableConnectionPadding(enableConnectionPadding); + tor.enableIpv6(ipv6Only); } - enableNetwork(enableNetwork); + tor.enableNetwork(enableNetwork); } catch (IOException e) { logException(LOG, WARNING, e); } }); } - private void enableConnectionPadding(boolean enable) throws IOException { - if (!state.enableConnectionPadding(enable)) return; // Unchanged - controlConnection.setConf("ConnectionPadding", enable ? "1" : "0"); - } - - private void enableIpv6(boolean enable) throws IOException { - if (!state.enableIpv6(enable)) return; // Unchanged - controlConnection.setConf("ClientUseIPv4", enable ? "0" : "1"); - controlConnection.setConf("ClientUseIPv6", enable ? "1" : "0"); - } - @ThreadSafe @NotNullByDefault - protected class PluginState { + private class PluginState { @GuardedBy("this") - private boolean started = false, - stopped = false, - networkInitialised = false, - networkEnabled = false, - paddingEnabled = false, - ipv6Enabled = false, - bootstrapped = false, - circuitBuilt = false, - settingsChecked = false; + private boolean settingsChecked = false; @GuardedBy("this") private int reasonsDisabled = 0; @@ -1003,84 +573,13 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Nullable private ServerSocket serverSocket = null; - @GuardedBy("this") - private int orConnectionsConnected = 0; - - @GuardedBy("this") - private List bridgeTypes = emptyList(); - - private synchronized void setStarted() { - started = true; - callback.pluginStateChanged(getState()); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private synchronized boolean isTorRunning() { - return started && !stopped; - } - @Nullable private synchronized ServerSocket setStopped() { - stopped = true; ServerSocket ss = serverSocket; serverSocket = null; - callback.pluginStateChanged(getState()); return ss; } - private synchronized void setBootstrapped() { - boolean wasBootstrapped = bootstrapped; - bootstrapped = true; - if (!wasBootstrapped) callback.pluginStateChanged(getState()); - } - - /** - * Sets the `circuitBuilt` flag and returns true if the flag has - * changed. - */ - private synchronized boolean setCircuitBuilt(boolean built) { - if (built == circuitBuilt) return false; // Unchanged - circuitBuilt = built; - callback.pluginStateChanged(getState()); - return true; // Changed - } - - /** - * Sets the `networkEnabled` flag and returns true if the flag has - * changed. - */ - private synchronized boolean enableNetwork(boolean enable) { - boolean wasInitialised = networkInitialised; - boolean wasEnabled = networkEnabled; - networkInitialised = true; - networkEnabled = enable; - if (!enable) circuitBuilt = false; - if (!wasInitialised || enable != wasEnabled) { - callback.pluginStateChanged(getState()); - } - return enable != wasEnabled; - } - - /** - * Sets the `paddingEnabled` flag and returns true if the flag has - * changed. Doesn't affect getState(). - */ - private synchronized boolean enableConnectionPadding(boolean enable) { - if (enable == paddingEnabled) return false; // Unchanged - paddingEnabled = enable; - return true; // Changed - } - - /** - * Sets the `ipv6Enabled` flag and returns true if the flag has - * changed. Doesn't affect getState(). - */ - private synchronized boolean enableIpv6(boolean enable) { - if (enable == ipv6Enabled) return false; // Unchanged - ipv6Enabled = enable; - return true; // Changed - } - private synchronized void setReasonsDisabled(int reasons) { boolean wasChecked = settingsChecked; settingsChecked = true; @@ -1093,7 +592,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { // Doesn't affect getState() private synchronized boolean setServerSocket(ServerSocket ss) { - if (stopped || serverSocket != null) return false; + if (serverSocket != null || !tor.isTorRunning()) return false; serverSocket = ss; return true; } @@ -1103,57 +602,22 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { if (serverSocket == ss) serverSocket = null; } - /** - * Sets the list of bridge types being used and returns true if the - * list has changed. The list is empty if bridges are disabled. - * Doesn't affect getState(). - */ - private synchronized boolean setBridgeTypes(List types) { - if (types.equals(bridgeTypes)) return false; // Unchanged - bridgeTypes = types; - return true; // Changed + private synchronized State getState() { + return getState(tor.getTorState()); } - private synchronized State getState() { - if (!started || stopped || !settingsChecked) { + private synchronized State getState(TorState torState) { + if (torState == TorState.STARTING_STOPPING || !settingsChecked) { return STARTING_STOPPING; } if (reasonsDisabled != 0) return DISABLED; - if (!networkInitialised) return ENABLING; - if (!networkEnabled) return INACTIVE; - return bootstrapped && circuitBuilt && orConnectionsConnected > 0 - ? ACTIVE : ENABLING; + if (torState == TorState.CONNECTING) return ENABLING; + if (torState == TorState.CONNECTED) return ACTIVE; + return INACTIVE; } private synchronized int getReasonsDisabled() { return getState() == DISABLED ? reasonsDisabled : 0; } - - private synchronized void onOrConnectionConnected() { - int oldConnected = orConnectionsConnected; - orConnectionsConnected++; - logOrConnections(); - if (oldConnected == 0) callback.pluginStateChanged(getState()); - } - - private synchronized void onOrConnectionClosed() { - int oldConnected = orConnectionsConnected; - orConnectionsConnected--; - if (orConnectionsConnected < 0) { - LOG.warning("Count was zero before connection closed"); - orConnectionsConnected = 0; - } - logOrConnections(); - if (orConnectionsConnected == 0 && oldConnected != 0) { - callback.pluginStateChanged(getState()); - } - } - - @GuardedBy("this") - private void logOrConnections() { - if (LOG.isLoggable(INFO)) { - LOG.info(orConnectionsConnected + " OR connections connected"); - } - } } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java index f1efd8825..53419f04b 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor; import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.plugin.Backoff; @@ -17,7 +18,6 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; import org.briarproject.bramble.api.system.WakefulIoExecutor; import org.briarproject.nullsafety.NotNullByDefault; @@ -45,13 +45,12 @@ abstract class TorPluginFactory implements DuplexPluginFactory { private static final int MAX_POLLING_INTERVAL = 10 * 60 * 1000; // 10 mins private static final double BACKOFF_BASE = 1.2; - protected final Executor ioExecutor, wakefulIoExecutor; + protected final Executor ioExecutor, eventExecutor, wakefulIoExecutor; protected final NetworkManager networkManager; protected final LocationUtils locationUtils; protected final EventBus eventBus; protected final SocketFactory torSocketFactory; protected final BackoffFactory backoffFactory; - protected final ResourceProvider resourceProvider; protected final CircumventionProvider circumventionProvider; protected final BatteryManager batteryManager; protected final Clock clock; @@ -61,13 +60,13 @@ abstract class TorPluginFactory implements DuplexPluginFactory { protected final int torControlPort; TorPluginFactory(@IoExecutor Executor ioExecutor, + @EventExecutor Executor eventExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, NetworkManager networkManager, LocationUtils locationUtils, EventBus eventBus, SocketFactory torSocketFactory, BackoffFactory backoffFactory, - ResourceProvider resourceProvider, CircumventionProvider circumventionProvider, BatteryManager batteryManager, Clock clock, @@ -76,13 +75,13 @@ abstract class TorPluginFactory implements DuplexPluginFactory { @TorSocksPort int torSocksPort, @TorControlPort int torControlPort) { this.ioExecutor = ioExecutor; + this.eventExecutor = eventExecutor; this.wakefulIoExecutor = wakefulIoExecutor; this.networkManager = networkManager; this.locationUtils = locationUtils; this.eventBus = eventBus; this.torSocketFactory = torSocketFactory; this.backoffFactory = backoffFactory; - this.resourceProvider = resourceProvider; this.circumventionProvider = circumventionProvider; this.batteryManager = batteryManager; this.clock = clock; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/AbstractTorWrapper.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/AbstractTorWrapper.java new file mode 100644 index 000000000..95c52dbd0 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/AbstractTorWrapper.java @@ -0,0 +1,673 @@ +package org.briarproject.bramble.plugin.tor.wrapper; + +import net.freehaven.tor.control.EventHandler; +import net.freehaven.tor.control.TorControlConnection; + +import org.briarproject.nullsafety.InterfaceNotNullByDefault; +import org.briarproject.nullsafety.NotNullByDefault; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS; +import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY; +import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.UTF_8; +import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.copyAndClose; +import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.scrubOnion; +import static org.briarproject.bramble.plugin.tor.wrapper.TorUtils.tryToClose; +import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.CONNECTED; +import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.CONNECTING; +import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.DISABLED; +import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.TorState.STARTING_STOPPING; +import static org.briarproject.nullsafety.NullSafety.requireNonNull; + +@InterfaceNotNullByDefault +abstract class AbstractTorWrapper implements EventHandler, TorWrapper { + + private static final String[] EVENTS = { + "CIRC", + "ORCONN", + "STATUS_GENERAL", + "STATUS_CLIENT", + "HS_DESC", + "NOTICE", + "WARN", + "ERR" + }; + + private static final String OWNER = "__OwningControllerProcess"; + private static final int COOKIE_TIMEOUT_MS = 3000; + private static final int COOKIE_POLLING_INTERVAL_MS = 200; + + protected final Executor ioExecutor; + protected final Executor eventExecutor; + private final String architecture; + private final File torDirectory, configFile, doneFile, cookieFile; + private final int torSocksPort; + private final int torControlPort; + private final AtomicBoolean used = new AtomicBoolean(false); + + protected final NetworkState state = new NetworkState(); + + private volatile Socket controlSocket = null; + private volatile TorControlConnection controlConnection = null; + + protected abstract int getProcessId(); + + protected abstract long getLastUpdateTime(); + + protected abstract InputStream getResourceInputStream(String name, + String extension); + + AbstractTorWrapper(Executor ioExecutor, + Executor eventExecutor, + String architecture, + File torDirectory, + int torSocksPort, + int torControlPort) { + this.ioExecutor = ioExecutor; + this.eventExecutor = eventExecutor; + this.architecture = architecture; + this.torDirectory = torDirectory; + this.torSocksPort = torSocksPort; + this.torControlPort = torControlPort; + configFile = new File(torDirectory, "torrc"); + doneFile = new File(torDirectory, "done"); + cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); + } + + protected File getTorExecutableFile() { + return new File(torDirectory, "tor"); + } + + protected File getObfs4ExecutableFile() { + return new File(torDirectory, "obfs4proxy"); + } + + protected File getSnowflakeExecutableFile() { + return new File(torDirectory, "snowflake"); + } + + @Override + public void setStateObserver(@Nullable StateObserver stateObserver) { + state.setObserver(stateObserver); + } + + @Override + public void start() throws IOException, InterruptedException { + if (used.getAndSet(true)) throw new IllegalStateException(); + if (!torDirectory.exists()) { + if (!torDirectory.mkdirs()) { + throw new IOException("Could not create Tor directory"); + } + } + // Install or update the assets if necessary + if (!assetsAreUpToDate()) installAssets(); + // Start from the default config every time + extract(getConfigInputStream(), configFile); + if (cookieFile.exists() && !cookieFile.delete()) + LOG.warning("Old auth cookie not deleted"); + // Start a new Tor process + LOG.info("Starting Tor"); + File torFile = getTorExecutableFile(); + String torPath = torFile.getAbsolutePath(); + String configPath = configFile.getAbsolutePath(); + String pid = String.valueOf(getProcessId()); + Process torProcess; + ProcessBuilder pb = + new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); + Map env = pb.environment(); + env.put("HOME", torDirectory.getAbsolutePath()); + pb.directory(torDirectory); + pb.redirectErrorStream(true); + try { + torProcess = pb.start(); + } catch (SecurityException e) { + throw new IOException(e); + } + // Wait for the Tor process to start + waitForTorToStart(torProcess); + // Wait for the auth cookie file to be created/updated + long start = System.currentTimeMillis(); + while (cookieFile.length() < 32) { + if (System.currentTimeMillis() - start > COOKIE_TIMEOUT_MS) { + throw new IOException("Auth cookie not created"); + } + //noinspection BusyWait + Thread.sleep(COOKIE_POLLING_INTERVAL_MS); + } + LOG.info("Auth cookie created"); + // Open a control connection and authenticate using the cookie file + controlSocket = new Socket("127.0.0.1", torControlPort); + controlConnection = new TorControlConnection(controlSocket); + controlConnection.authenticate(read(cookieFile)); + // Tell Tor to exit when the control connection is closed + controlConnection.takeOwnership(); + controlConnection.resetConf(singletonList(OWNER)); + // Register to receive events from the Tor process + controlConnection.setEventHandler(this); + controlConnection.setEvents(asList(EVENTS)); + // Check whether Tor has already bootstrapped + String info = controlConnection.getInfo("status/bootstrap-phase"); + if (info != null && info.contains("PROGRESS=100")) { + LOG.info("Tor has already bootstrapped"); + state.setBootstrapped(); + } + // Check whether Tor has already built a circuit + info = controlConnection.getInfo("status/circuit-established"); + if ("1".equals(info)) { + LOG.info("Tor has already built a circuit"); + state.setCircuitBuilt(true); + } + state.setStarted(); + } + + private boolean assetsAreUpToDate() { + return doneFile.lastModified() > getLastUpdateTime(); + } + + private void installAssets() throws IOException { + // The done file may already exist from a previous installation + //noinspection ResultOfMethodCallIgnored + doneFile.delete(); + installTorExecutable(); + installObfs4Executable(); + installSnowflakeExecutable(); + extract(getConfigInputStream(), configFile); + if (!doneFile.createNewFile()) { + LOG.warning("Failed to create done file"); + } + } + + protected void extract(InputStream in, File dest) throws IOException { + @SuppressWarnings("IOStreamConstructor") + OutputStream out = new FileOutputStream(dest); + copyAndClose(in, out); + } + + protected void installTorExecutable() throws IOException { + if (LOG.isLoggable(INFO)) { + LOG.info("Installing Tor binary for " + architecture); + } + File torFile = getTorExecutableFile(); + extract(getExecutableInputStream("tor"), torFile); + if (!torFile.setExecutable(true, true)) throw new IOException(); + } + + protected void installObfs4Executable() throws IOException { + if (LOG.isLoggable(INFO)) { + LOG.info("Installing obfs4proxy binary for " + architecture); + } + File obfs4File = getObfs4ExecutableFile(); + extract(getExecutableInputStream("obfs4proxy"), obfs4File); + if (!obfs4File.setExecutable(true, true)) throw new IOException(); + } + + protected void installSnowflakeExecutable() throws IOException { + if (LOG.isLoggable(INFO)) { + LOG.info("Installing snowflake binary for " + architecture); + } + File snowflakeFile = getSnowflakeExecutableFile(); + extract(getExecutableInputStream("snowflake"), snowflakeFile); + if (!snowflakeFile.setExecutable(true, true)) throw new IOException(); + } + + private InputStream getExecutableInputStream(String basename) { + String ext = getExecutableExtension(); + return requireNonNull( + getResourceInputStream(architecture + "/" + basename, ext)); + } + + protected String getExecutableExtension() { + return ""; + } + + private static void append(StringBuilder strb, String name, Object value) { + strb.append(name); + strb.append(" "); + strb.append(value); + strb.append("\n"); + } + + private InputStream getConfigInputStream() { + File dataDirectory = new File(torDirectory, ".tor"); + StringBuilder strb = new StringBuilder(); + append(strb, "ControlPort", torControlPort); + append(strb, "CookieAuthentication", 1); + append(strb, "DataDirectory", dataDirectory.getAbsolutePath()); + append(strb, "DisableNetwork", 1); + append(strb, "RunAsDaemon", 1); + append(strb, "SafeSocks", 1); + append(strb, "SocksPort", torSocksPort); + strb.append("GeoIPFile\n"); + strb.append("GeoIPv6File\n"); + append(strb, "ConnectionPadding", 0); + String obfs4Path = getObfs4ExecutableFile().getAbsolutePath(); + append(strb, "ClientTransportPlugin obfs4 exec", obfs4Path); + append(strb, "ClientTransportPlugin meek_lite exec", obfs4Path); + String snowflakePath = getSnowflakeExecutableFile().getAbsolutePath(); + append(strb, "ClientTransportPlugin snowflake exec", snowflakePath); + return new ByteArrayInputStream(strb.toString().getBytes(UTF_8)); + } + + private byte[] read(File f) throws IOException { + byte[] b = new byte[(int) f.length()]; + FileInputStream in = new FileInputStream(f); + try { + int offset = 0; + while (offset < b.length) { + int read = in.read(b, offset, b.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + return b; + } finally { + tryToClose(in, LOG, WARNING); + } + } + + protected void waitForTorToStart(Process torProcess) + throws InterruptedException, IOException { + Scanner stdout = new Scanner(torProcess.getInputStream()); + // Log the first line of stdout (contains Tor and library versions) + if (stdout.hasNextLine()) LOG.info(stdout.nextLine()); + // Read the process's stdout (and redirected stderr) until it detaches + while (stdout.hasNextLine()) stdout.nextLine(); + stdout.close(); + // Wait for the process to detach or exit + int exit = torProcess.waitFor(); + if (exit != 0) throw new IOException("Tor exited with value " + exit); + } + + @Override + public HiddenServiceProperties publishHiddenService(int localPort, + int remotePort, @Nullable String privKey) throws IOException { + Map portLines = + singletonMap(remotePort, "127.0.0.1:" + localPort); + // Use the control connection to set up the hidden service + Map response; + if (privKey == null) { + response = getControlConnection().addOnion("NEW:ED25519-V3", + portLines, null); + } else { + response = getControlConnection().addOnion(privKey, portLines); + } + if (!response.containsKey(HS_ADDRESS)) { + throw new IOException("Missing hidden service address"); + } + if (privKey == null && !response.containsKey(HS_PRIVKEY)) { + throw new IOException("Missing private key"); + } + String onion = response.get(HS_ADDRESS); + if (privKey == null) privKey = response.get(HS_PRIVKEY); + return new HiddenServiceProperties(onion, privKey); + } + + @Override + public void removeHiddenService(String onion) throws IOException { + getControlConnection().delOnion(onion); + } + + @Override + public void enableNetwork(boolean enable) throws IOException { + if (!state.enableNetwork(enable)) return; // Unchanged + getControlConnection().setConf("DisableNetwork", enable ? "0" : "1"); + } + + @Override + public void enableBridges(List bridges) throws IOException { + if (!state.setBridges(bridges)) return; // Unchanged + List conf = new ArrayList<>(bridges.size() + 1); + conf.add("UseBridges 1"); + conf.addAll(bridges); + getControlConnection().setConf(conf); + } + + @Override + public void disableBridges() throws IOException { + if (!state.setBridges(emptyList())) return; // Unchanged + getControlConnection().setConf("UseBridges", "0"); + } + + @Override + public void stop() throws IOException { + state.setStopped(); + if (controlSocket != null && controlConnection != null) { + LOG.info("Stopping Tor"); + try { + controlConnection.shutdownTor("TERM"); + } finally { + tryToClose(controlSocket, LOG, WARNING); + } + } + } + + @Override + public void circuitStatus(String status, String id, String path) { + // In case of races between receiving CIRCUIT_ESTABLISHED and setting + // DisableNetwork, set our circuitBuilt flag if not already set + if (status.equals("BUILT") && state.setCircuitBuilt(true)) { + LOG.info("Circuit built"); + } + } + + @Override + public void streamStatus(String status, String id, String target) { + } + + @Override + public void orConnStatus(String status, String orName) { + if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status); + + if (status.equals("CONNECTED")) state.onOrConnectionConnected(); + else if (status.equals("CLOSED")) state.onOrConnectionClosed(); + } + + @Override + public void bandwidthUsed(long read, long written) { + } + + @Override + public void newDescriptors(List orList) { + } + + @Override + public void message(String severity, String msg) { + if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg); + } + + @Override + public void unrecognized(String type, String msg) { + if (type.equals("STATUS_CLIENT")) { + handleClientStatus(removeSeverity(msg)); + } else if (type.equals("STATUS_GENERAL")) { + handleGeneralStatus(removeSeverity(msg)); + } else if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) { + String[] parts = msg.split(" "); + if (parts.length < 2) { + LOG.warning("Failed to parse HS_DESC UPLOADED event"); + } else if (LOG.isLoggable(INFO)) { + LOG.info("V3 descriptor uploaded for " + scrubOnion(parts[1])); + } + } + } + + private String removeSeverity(String msg) { + return msg.replaceFirst("[^ ]+ ", ""); + } + + private void handleClientStatus(String msg) { + if (msg.startsWith("BOOTSTRAP PROGRESS=100")) { + LOG.info("Bootstrapped"); + state.setBootstrapped(); + } else if (msg.startsWith("CIRCUIT_ESTABLISHED")) { + if (state.setCircuitBuilt(true)) LOG.info("Circuit built"); + } else if (msg.startsWith("CIRCUIT_NOT_ESTABLISHED")) { + if (state.setCircuitBuilt(false)) { + LOG.info("Circuit not built"); + // TODO: Disable and re-enable network to prompt Tor to rebuild + // its guard/bridge connections? This will also close any + // established circuits, which might still be functioning + } + } + } + + private void handleGeneralStatus(String msg) { + if (msg.startsWith("CLOCK_JUMPED")) { + Long time = parseLongArgument(msg, "TIME"); + if (time != null && LOG.isLoggable(WARNING)) { + LOG.warning("Clock jumped " + time + " seconds"); + } + } else if (msg.startsWith("CLOCK_SKEW")) { + Long skew = parseLongArgument(msg, "SKEW"); + if (skew != null && LOG.isLoggable(WARNING)) { + LOG.warning("Clock is skewed by " + skew + " seconds"); + } + } + } + + @Nullable + private Long parseLongArgument(String msg, String argName) { + String[] args = msg.split(" "); + for (String arg : args) { + if (arg.startsWith(argName + "=")) { + try { + return Long.parseLong(arg.substring(argName.length() + 1)); + } catch (NumberFormatException e) { + break; + } + } + } + if (LOG.isLoggable(WARNING)) { + LOG.warning("Failed to parse " + argName + " from '" + msg + "'"); + } + return null; + } + + @Override + public void controlConnectionClosed() { + if (state.isTorRunning()) { + // TODO: Restart the Tor process + LOG.warning("Control connection closed"); + } + } + + @Override + public void enableConnectionPadding(boolean enable) throws IOException { + if (!state.enableConnectionPadding(enable)) return; // Unchanged + getControlConnection().setConf("ConnectionPadding", enable ? "1" : "0"); + } + + @Override + public void enableIpv6(boolean enable) throws IOException { + if (!state.enableIpv6(enable)) return; // Unchanged + getControlConnection().setConf("ClientUseIPv4", enable ? "0" : "1"); + getControlConnection().setConf("ClientUseIPv6", enable ? "1" : "0"); + } + + @Override + public TorState getTorState() { + return state.getState(); + } + + @Override + public boolean isTorRunning() { + return state.isTorRunning(); + } + + private TorControlConnection getControlConnection() throws IOException { + TorControlConnection controlConnection = this.controlConnection; + if (controlConnection == null) { + throw new IOException("Control connection not opened"); + } + return controlConnection; + } + + @ThreadSafe + @NotNullByDefault + private class NetworkState { + + @GuardedBy("this") + @Nullable + private StateObserver observer = null; + + @GuardedBy("this") + private boolean started = false, + stopped = false, + networkInitialised = false, + networkEnabled = false, + paddingEnabled = false, + ipv6Enabled = false, + bootstrapped = false, + circuitBuilt = false; + + @GuardedBy("this") + private int orConnectionsConnected = 0; + + @GuardedBy("this") + @Nullable + private TorState state = null; + + private synchronized void setObserver( + @Nullable StateObserver observer) { + this.observer = observer; + } + + @GuardedBy("this") + private void updateObserver() { + TorState newState = getState(); + if (newState != state) { + state = newState; + // Post the new state to the event executor. The contract of + // this executor is to execute tasks in the order they're + // submitted, so state changes will be observed in the correct + // order but outside the lock + if (observer != null) { + eventExecutor.execute(() -> + observer.observeState(newState)); + } + } + } + + @GuardedBy("this") + private List bridges = emptyList(); + + private synchronized void setStarted() { + started = true; + updateObserver(); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private synchronized boolean isTorRunning() { + return started && !stopped; + } + + private synchronized void setStopped() { + stopped = true; + updateObserver(); + } + + private synchronized void setBootstrapped() { + if (bootstrapped) return; + bootstrapped = true; + updateObserver(); + } + + /** + * Sets the `circuitBuilt` flag and returns true if the flag has + * changed. + */ + private synchronized boolean setCircuitBuilt(boolean built) { + if (built == circuitBuilt) return false; // Unchanged + circuitBuilt = built; + updateObserver(); + return true; // Changed + } + + /** + * Sets the `networkEnabled` flag and returns true if the flag has + * changed. + */ + private synchronized boolean enableNetwork(boolean enable) { + boolean wasInitialised = networkInitialised; + boolean wasEnabled = networkEnabled; + networkInitialised = true; + networkEnabled = enable; + if (!enable) circuitBuilt = false; + if (!wasInitialised || enable != wasEnabled) { + updateObserver(); + } + return enable != wasEnabled; + } + + /** + * Sets the `paddingEnabled` flag and returns true if the flag has + * changed. Doesn't affect getState(). + */ + private synchronized boolean enableConnectionPadding(boolean enable) { + if (enable == paddingEnabled) return false; // Unchanged + paddingEnabled = enable; + return true; // Changed + } + + /** + * Sets the `ipv6Enabled` flag and returns true if the flag has + * changed. Doesn't affect getState(). + */ + private synchronized boolean enableIpv6(boolean enable) { + if (enable == ipv6Enabled) return false; // Unchanged + ipv6Enabled = enable; + return true; // Changed + } + + /** + * Sets the list of bridges being used and returns true if the + * list has changed. The list is empty if bridges are disabled. + * Doesn't affect getState(). + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private synchronized boolean setBridges(List bridges) { + if (this.bridges.equals(bridges)) return false; // Unchanged + this.bridges = bridges; + return true; // Changed + } + + private synchronized TorState getState() { + if (!started || stopped) return STARTING_STOPPING; + if (!networkInitialised) return CONNECTING; + if (!networkEnabled) return DISABLED; + return bootstrapped && circuitBuilt && orConnectionsConnected > 0 + ? CONNECTED : CONNECTING; + } + + private synchronized void onOrConnectionConnected() { + int oldConnected = orConnectionsConnected; + orConnectionsConnected++; + logOrConnections(); + if (oldConnected == 0) updateObserver(); + } + + private synchronized void onOrConnectionClosed() { + int oldConnected = orConnectionsConnected; + orConnectionsConnected--; + if (orConnectionsConnected < 0) { + LOG.warning("Count was zero before connection closed"); + orConnectionsConnected = 0; + } + logOrConnections(); + if (orConnectionsConnected == 0 && oldConnected != 0) { + updateObserver(); + } + } + + @GuardedBy("this") + private void logOrConnections() { + if (LOG.isLoggable(INFO)) { + LOG.info(orConnectionsConnected + " OR connections connected"); + } + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorUtils.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorUtils.java new file mode 100644 index 000000000..d83d3feb9 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorUtils.java @@ -0,0 +1,66 @@ +package org.briarproject.bramble.plugin.tor.wrapper; + +import org.briarproject.nullsafety.NotNullByDefault; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.bramble.plugin.tor.wrapper.TorWrapper.LOG; + +@NotNullByDefault +class TorUtils { + + @SuppressWarnings("CharsetObjectCanBeUsed") + static final Charset UTF_8 = Charset.forName("UTF-8"); + + static String scrubOnion(String onion) { + // Keep first three characters of onion address + return onion.substring(0, 3) + "[scrubbed]"; + } + + static void copyAndClose(InputStream in, OutputStream out) { + byte[] buf = new byte[4096]; + try { + while (true) { + int read = in.read(buf); + if (read == -1) break; + out.write(buf, 0, read); + } + in.close(); + out.flush(); + out.close(); + } catch (IOException e) { + tryToClose(in, LOG, WARNING); + tryToClose(out, LOG, WARNING); + } + } + + static void tryToClose(@Nullable Closeable c, Logger logger, Level level) { + try { + if (c != null) c.close(); + } catch (IOException e) { + logException(logger, level, e); + } + } + + static void tryToClose(@Nullable Socket s, Logger logger, Level level) { + try { + if (s != null) s.close(); + } catch (IOException e) { + logException(logger, level, e); + } + } + + private static void logException(Logger logger, Level level, Throwable t) { + if (logger.isLoggable(level)) logger.log(level, t.toString(), t); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorWrapper.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorWrapper.java new file mode 100644 index 000000000..8618c8424 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/TorWrapper.java @@ -0,0 +1,153 @@ +package org.briarproject.bramble.plugin.tor.wrapper; + +import org.briarproject.nullsafety.NotNullByDefault; + +import java.io.IOException; +import java.util.List; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import static java.util.logging.Logger.getLogger; + +@NotNullByDefault +public interface TorWrapper { + + Logger LOG = getLogger(TorWrapper.class.getName()); + + /** + * Starts the Tor process. + *

+ * This method must only be called once. To restart the Tor process, stop + * this wrapper instance and then create a new instance. + */ + void start() throws IOException, InterruptedException; + + /** + * Tell the Tor process to stop and returns without waiting for the + * process to exit. + */ + void stop() throws IOException; + + /** + * Sets an observer for observing the state of the wrapper, replacing any + * existing observer, or removes any existing observer if the argument is + * null. + */ + void setStateObserver(@Nullable StateObserver stateObserver); + + /** + * Returns the current state of the wrapper. + */ + TorState getTorState(); + + /** + * Returns true if the wrapper has been {@link #start() started} and not + * yet {@link #stop()} stopped. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + boolean isTorRunning(); + + /** + * Publishes an ephemeral hidden service. + * + * @param localPort The local port on which the service is listening. + * @param remotePort The port number that clients of the service will see. + * @param privateKey The private key of the hidden service, in the form + * returned by a previous call to this method, or null if a new service + * should be created. + */ + HiddenServiceProperties publishHiddenService(int localPort, + int remotePort, @Nullable String privateKey) throws IOException; + + /** + * Removes (unpublishes) an ephemeral hidden service that was created by + * calling {@link #publishHiddenService(int, int, String)}. + */ + void removeHiddenService(String onion) throws IOException; + + /** + * Enables or disables the Tor process's network connection. The network + * connection is disabled by default. + */ + void enableNetwork(boolean enable) throws IOException; + + /** + * Configures Tor to use the given list of bridges for connecting to the + * Tor network. Bridges are not used by default. + *

+ * Each item in the list should be a bridge line in the same + * format that would be used in a torrc file (including the Bridge keyword). + */ + void enableBridges(List bridges) throws IOException; + + /** + * Configures Tor not to use bridges for connecting to the Tor network. + * Bridges are not used by default. + */ + void disableBridges() throws IOException; + + /** + * Enables or disables connection padding. Padding is disabled by default. + */ + void enableConnectionPadding(boolean enable) throws IOException; + + /** + * Configures Tor to use IPv6 or IPv4 for connecting to the Tor network. + * IPv4 is used by default. + */ + void enableIpv6(boolean ipv6Only) throws IOException; + + /** + * The state of the Tor wrapper. + */ + enum TorState { + + /** + * The Tor process is either starting or stopping. + */ + STARTING_STOPPING, + + /** + * The Tor process has started, its network connection is enabled, and + * it is connecting (or reconnecting) to the Tor network. + */ + CONNECTING, + + /** + * The Tor process has started, its network connection is enabled, and + * it has connected to the Tor network. In this state it should be + * possible to make connections via the SOCKS port. + */ + CONNECTED, + + /** + * The Tor process has started but its network connection is disabled. + */ + DISABLED + } + + /** + * An interface for observing changes to the {@link TorState state} of the + * Tor process. + */ + interface StateObserver { + + /** + * This method is called whenever the state of the Tor process changes. + * The call happens on the event executor supplied to the wrapper's + * constructor. + */ + void observeState(TorState s); + } + + class HiddenServiceProperties { + + public final String onion, privKey; + + HiddenServiceProperties(String onion, String privKey) { + this.onion = onion; + this.privKey = privKey; + } + } +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java deleted file mode 100644 index 5dd647ead..000000000 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/JavaTorPlugin.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.briarproject.bramble.plugin.tor; - -import org.briarproject.bramble.api.battery.BatteryManager; -import org.briarproject.bramble.api.network.NetworkManager; -import org.briarproject.bramble.api.plugin.Backoff; -import org.briarproject.bramble.api.plugin.PluginCallback; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; -import org.briarproject.nullsafety.NotNullByDefault; - -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.CodeSource; -import java.util.concurrent.Executor; - -import javax.net.SocketFactory; - -@NotNullByDefault -abstract class JavaTorPlugin extends TorPlugin { - - JavaTorPlugin(Executor ioExecutor, - Executor wakefulIoExecutor, - NetworkManager networkManager, - LocationUtils locationUtils, - SocketFactory torSocketFactory, - Clock clock, - ResourceProvider resourceProvider, - CircumventionProvider circumventionProvider, - BatteryManager batteryManager, - Backoff backoff, - TorRendezvousCrypto torRendezvousCrypto, - PluginCallback callback, - String architecture, - long maxLatency, - int maxIdleTime, - File torDirectory, - int torSocksPort, - int torControlPort) { - super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, - torSocketFactory, clock, resourceProvider, - circumventionProvider, batteryManager, backoff, - torRendezvousCrypto, callback, architecture, - maxLatency, maxIdleTime, torDirectory, torSocksPort, - torControlPort); - } - - @Override - protected long getLastUpdateTime() { - CodeSource codeSource = - getClass().getProtectionDomain().getCodeSource(); - if (codeSource == null) throw new AssertionError("CodeSource null"); - try { - URI path = codeSource.getLocation().toURI(); - File file = new File(path); - return file.lastModified(); - } catch (URISyntaxException e) { - throw new AssertionError(e); - } - } -} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java deleted file mode 100644 index 90de04434..000000000 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPlugin.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.briarproject.bramble.plugin.tor; - -import com.sun.jna.Library; -import com.sun.jna.Native; - -import org.briarproject.bramble.api.battery.BatteryManager; -import org.briarproject.bramble.api.network.NetworkManager; -import org.briarproject.bramble.api.plugin.Backoff; -import org.briarproject.bramble.api.plugin.PluginCallback; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; -import org.briarproject.nullsafety.NotNullByDefault; - -import java.io.File; -import java.util.concurrent.Executor; - -import javax.net.SocketFactory; - -@NotNullByDefault -class UnixTorPlugin extends JavaTorPlugin { - - UnixTorPlugin(Executor ioExecutor, - Executor wakefulIoExecutor, - NetworkManager networkManager, - LocationUtils locationUtils, - SocketFactory torSocketFactory, - Clock clock, - ResourceProvider resourceProvider, - CircumventionProvider circumventionProvider, - BatteryManager batteryManager, - Backoff backoff, - TorRendezvousCrypto torRendezvousCrypto, - PluginCallback callback, - String architecture, - long maxLatency, - int maxIdleTime, - File torDirectory, - int torSocksPort, - int torControlPort) { - super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, - torSocketFactory, clock, resourceProvider, - circumventionProvider, batteryManager, backoff, - torRendezvousCrypto, callback, architecture, - maxLatency, maxIdleTime, torDirectory, torSocksPort, - torControlPort); - } - - @Override - protected int getProcessId() { - return CLibrary.INSTANCE.getpid(); - } - - private interface CLibrary extends Library { - - CLibrary INSTANCE = Native.loadLibrary("c", CLibrary.class); - - int getpid(); - } -} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java index bfc86cdbf..441259ea4 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor; import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.plugin.Backoff; @@ -13,8 +14,9 @@ import org.briarproject.bramble.api.plugin.TorDirectory; import org.briarproject.bramble.api.plugin.TorSocksPort; import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; import org.briarproject.bramble.api.system.WakefulIoExecutor; +import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper; +import org.briarproject.bramble.plugin.tor.wrapper.UnixTorWrapper; import org.briarproject.nullsafety.NotNullByDefault; import java.io.File; @@ -34,13 +36,13 @@ public class UnixTorPluginFactory extends TorPluginFactory { @Inject UnixTorPluginFactory(@IoExecutor Executor ioExecutor, + @EventExecutor Executor eventExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, NetworkManager networkManager, LocationUtils locationUtils, EventBus eventBus, SocketFactory torSocketFactory, BackoffFactory backoffFactory, - ResourceProvider resourceProvider, CircumventionProvider circumventionProvider, BatteryManager batteryManager, Clock clock, @@ -48,8 +50,8 @@ public class UnixTorPluginFactory extends TorPluginFactory { @TorDirectory File torDirectory, @TorSocksPort int torSocksPort, @TorControlPort int torControlPort) { - super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, - eventBus, torSocketFactory, backoffFactory, resourceProvider, + super(ioExecutor, eventExecutor, wakefulIoExecutor, networkManager, + locationUtils, eventBus, torSocketFactory, backoffFactory, circumventionProvider, batteryManager, clock, crypto, torDirectory, torSocksPort, torControlPort); } @@ -62,6 +64,7 @@ public class UnixTorPluginFactory extends TorPluginFactory { if (LOG.isLoggable(INFO)) { LOG.info("System's os.arch is " + arch); } + //noinspection IfCanBeSwitch if (arch.equals("amd64")) return "x86_64"; else if (arch.equals("aarch64")) return "aarch64"; else if (arch.equals("arm")) return "armhf"; @@ -72,11 +75,11 @@ public class UnixTorPluginFactory extends TorPluginFactory { TorPlugin createPluginInstance(Backoff backoff, TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, String architecture) { - return new UnixTorPlugin(ioExecutor, wakefulIoExecutor, - networkManager, locationUtils, torSocketFactory, clock, - resourceProvider, circumventionProvider, batteryManager, - backoff, torRendezvousCrypto, callback, architecture, - MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, - torControlPort); + TorWrapper tor = new UnixTorWrapper(ioExecutor, eventExecutor, + architecture, torDirectory, torSocksPort, torControlPort); + return new TorPlugin(ioExecutor, wakefulIoExecutor, networkManager, + locationUtils, torSocketFactory, circumventionProvider, + batteryManager, backoff, torRendezvousCrypto, tor, callback, + MAX_LATENCY, MAX_IDLE_TIME, true); } } diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPluginFactory.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPluginFactory.java index cf4f5a3c4..c113af80a 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPluginFactory.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPluginFactory.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.tor; import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.plugin.Backoff; @@ -13,8 +14,9 @@ import org.briarproject.bramble.api.plugin.TorDirectory; import org.briarproject.bramble.api.plugin.TorSocksPort; import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; import org.briarproject.bramble.api.system.WakefulIoExecutor; +import org.briarproject.bramble.plugin.tor.wrapper.TorWrapper; +import org.briarproject.bramble.plugin.tor.wrapper.WindowsTorWrapper; import org.briarproject.nullsafety.NotNullByDefault; import java.io.File; @@ -34,13 +36,13 @@ public class WindowsTorPluginFactory extends TorPluginFactory { @Inject WindowsTorPluginFactory(@IoExecutor Executor ioExecutor, + @EventExecutor Executor eventExecutor, @WakefulIoExecutor Executor wakefulIoExecutor, NetworkManager networkManager, LocationUtils locationUtils, EventBus eventBus, SocketFactory torSocketFactory, BackoffFactory backoffFactory, - ResourceProvider resourceProvider, CircumventionProvider circumventionProvider, BatteryManager batteryManager, Clock clock, @@ -48,8 +50,8 @@ public class WindowsTorPluginFactory extends TorPluginFactory { @TorDirectory File torDirectory, @TorSocksPort int torSocksPort, @TorControlPort int torControlPort) { - super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, - eventBus, torSocketFactory, backoffFactory, resourceProvider, + super(ioExecutor, eventExecutor, wakefulIoExecutor, networkManager, + locationUtils, eventBus, torSocketFactory, backoffFactory, circumventionProvider, batteryManager, clock, crypto, torDirectory, torSocksPort, torControlPort); } @@ -70,11 +72,11 @@ public class WindowsTorPluginFactory extends TorPluginFactory { TorPlugin createPluginInstance(Backoff backoff, TorRendezvousCrypto torRendezvousCrypto, PluginCallback callback, String architecture) { - return new WindowsTorPlugin(ioExecutor, wakefulIoExecutor, - networkManager, locationUtils, torSocketFactory, clock, - resourceProvider, circumventionProvider, batteryManager, - backoff, torRendezvousCrypto, callback, architecture, - MAX_LATENCY, MAX_IDLE_TIME, torDirectory, torSocksPort, - torControlPort); + TorWrapper tor = new WindowsTorWrapper(ioExecutor, eventExecutor, + architecture, torDirectory, torSocksPort, torControlPort); + return new TorPlugin(ioExecutor, wakefulIoExecutor, networkManager, + locationUtils, torSocketFactory, circumventionProvider, + batteryManager, backoff, torRendezvousCrypto, tor, callback, + MAX_LATENCY, MAX_IDLE_TIME, true); } } diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/JavaTorWrapper.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/JavaTorWrapper.java new file mode 100644 index 000000000..9d8ff3e9b --- /dev/null +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/JavaTorWrapper.java @@ -0,0 +1,47 @@ +package org.briarproject.bramble.plugin.tor.wrapper; + +import org.briarproject.nullsafety.NotNullByDefault; + +import java.io.File; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.CodeSource; +import java.util.concurrent.Executor; + +import static org.briarproject.nullsafety.NullSafety.requireNonNull; + +@NotNullByDefault +abstract class JavaTorWrapper extends AbstractTorWrapper { + + JavaTorWrapper(Executor ioExecutor, + Executor eventExecutor, + String architecture, + File torDirectory, + int torSocksPort, + int torControlPort) { + super(ioExecutor, eventExecutor, architecture, torDirectory, + torSocksPort, torControlPort); + } + + @Override + protected long getLastUpdateTime() { + CodeSource codeSource = + getClass().getProtectionDomain().getCodeSource(); + if (codeSource == null) throw new AssertionError("CodeSource null"); + try { + URI path = codeSource.getLocation().toURI(); + File file = new File(path); + return file.lastModified(); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Override + protected InputStream getResourceInputStream(String name, + String extension) { + ClassLoader cl = getClass().getClassLoader(); + return requireNonNull(cl.getResourceAsStream(name + extension)); + } +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/UnixTorWrapper.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/UnixTorWrapper.java new file mode 100644 index 000000000..14c3c5773 --- /dev/null +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/UnixTorWrapper.java @@ -0,0 +1,35 @@ +package org.briarproject.bramble.plugin.tor.wrapper; + +import com.sun.jna.Library; +import com.sun.jna.Native; + +import org.briarproject.nullsafety.NotNullByDefault; + +import java.io.File; +import java.util.concurrent.Executor; + +@NotNullByDefault +public class UnixTorWrapper extends JavaTorWrapper { + + public UnixTorWrapper(Executor ioExecutor, + Executor eventExecutor, + String architecture, + File torDirectory, + int torSocksPort, + int torControlPort) { + super(ioExecutor, eventExecutor, architecture, torDirectory, + torSocksPort, torControlPort); + } + + @Override + protected int getProcessId() { + return CLibrary.INSTANCE.getpid(); + } + + private interface CLibrary extends Library { + + CLibrary INSTANCE = Native.loadLibrary("c", CLibrary.class); + + int getpid(); + } +} diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/WindowsTorWrapper.java similarity index 58% rename from bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java rename to bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/WindowsTorWrapper.java index c5fa0013d..8c31a1b5c 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/WindowsTorPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/wrapper/WindowsTorWrapper.java @@ -1,54 +1,29 @@ -package org.briarproject.bramble.plugin.tor; +package org.briarproject.bramble.plugin.tor.wrapper; import com.sun.jna.platform.win32.Kernel32; -import org.briarproject.bramble.api.battery.BatteryManager; -import org.briarproject.bramble.api.network.NetworkManager; -import org.briarproject.bramble.api.plugin.Backoff; -import org.briarproject.bramble.api.plugin.PluginCallback; -import org.briarproject.bramble.api.plugin.PluginException; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.bramble.api.system.LocationUtils; -import org.briarproject.bramble.api.system.ResourceProvider; import org.briarproject.nullsafety.NotNullByDefault; import java.io.File; +import java.io.IOException; import java.util.Scanner; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; -import javax.net.SocketFactory; - import static java.util.logging.Level.INFO; @NotNullByDefault -class WindowsTorPlugin extends JavaTorPlugin { +public class WindowsTorWrapper extends JavaTorWrapper { - WindowsTorPlugin(Executor ioExecutor, - Executor wakefulIoExecutor, - NetworkManager networkManager, - LocationUtils locationUtils, - SocketFactory torSocketFactory, - Clock clock, - ResourceProvider resourceProvider, - CircumventionProvider circumventionProvider, - BatteryManager batteryManager, - Backoff backoff, - TorRendezvousCrypto torRendezvousCrypto, - PluginCallback callback, + public WindowsTorWrapper(Executor ioExecutor, + Executor eventExecutor, String architecture, - long maxLatency, - int maxIdleTime, File torDirectory, int torSocksPort, int torControlPort) { - super(ioExecutor, wakefulIoExecutor, networkManager, locationUtils, - torSocketFactory, clock, resourceProvider, - circumventionProvider, batteryManager, backoff, - torRendezvousCrypto, callback, architecture, - maxLatency, maxIdleTime, torDirectory, torSocksPort, - torControlPort); + super(ioExecutor, eventExecutor, architecture, torDirectory, + torSocksPort, torControlPort); } @Override @@ -58,7 +33,7 @@ class WindowsTorPlugin extends JavaTorPlugin { @Override protected void waitForTorToStart(Process torProcess) - throws InterruptedException, PluginException { + throws InterruptedException, IOException { // On Windows the RunAsDaemon option has no effect, so Tor won't detach. // Wait for the control port to be opened, then continue to read its // stdout and stderr in a background thread until it exits. @@ -91,7 +66,7 @@ class WindowsTorPlugin extends JavaTorPlugin { } }); // Wait for the startup result - if (!success.take()) throw new PluginException(); + if (!success.take()) throw new IOException(); } @Override diff --git a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java index 52ffb400f..dd584d2a5 100644 --- a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java +++ b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java @@ -5,6 +5,7 @@ import org.briarproject.bramble.api.Multiset; import org.briarproject.bramble.api.battery.BatteryManager; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.plugin.BackoffFactory; @@ -113,6 +114,9 @@ public class BridgeTest extends BrambleTestCase { @IoExecutor Executor ioExecutor; @Inject + @EventExecutor + Executor eventExecutor; + @Inject @WakefulIoExecutor Executor wakefulIoExecutor; @Inject @@ -185,9 +189,9 @@ public class BridgeTest extends BrambleTestCase { return singletonList(params.bridge); } }; - factory = new UnixTorPluginFactory(ioExecutor, wakefulIoExecutor, - networkManager, locationUtils, eventBus, torSocketFactory, - backoffFactory, resourceProvider, bridgeProvider, + factory = new UnixTorPluginFactory(ioExecutor, eventExecutor, + wakefulIoExecutor, networkManager, locationUtils, eventBus, + torSocketFactory, backoffFactory, bridgeProvider, batteryManager, clock, crypto, torDir, torSocksPort, torControlPort); } @@ -207,7 +211,7 @@ public class BridgeTest extends BrambleTestCase { DuplexPlugin duplexPlugin = factory.createPlugin(new TestPluginCallback()); assertNotNull(duplexPlugin); - UnixTorPlugin plugin = (UnixTorPlugin) duplexPlugin; + TorPlugin plugin = (TorPlugin) duplexPlugin; LOG.warning("Testing " + params.bridge); try {