diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaCliNetworkManager.java b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaCliNetworkManager.java index 133055c3ec926b5941ca639d7214e3d4687c1c25..26d619d5c73679f494802f19ac6a127cd2d6a8af 100644 --- a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaCliNetworkManager.java +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaCliNetworkManager.java @@ -16,31 +16,31 @@ import static org.slf4j.LoggerFactory.getLogger; class JavaCliNetworkManager implements NetworkManager { - private static final Logger LOG = getLogger(JavaCliNetworkManager.class); - - @Inject - JavaCliNetworkManager() { - } - - @Override - public NetworkStatus getNetworkStatus() { - boolean connected = false, hasIpv4 = false, hasIpv6Unicast = false; - try { - for (NetworkInterface i : getNetworkInterfaces()) { - if (i.isLoopback() || !i.isUp()) continue; - for (InetAddress addr : list(i.getInetAddresses())) { - connected = true; - if (addr instanceof Inet4Address) { - hasIpv4 = true; - } else if (!addr.isMulticastAddress()) { - hasIpv6Unicast = true; - } - } - } - } catch (SocketException e) { - logException(LOG, e); - } - return new NetworkStatus(connected, false, !hasIpv4 && hasIpv6Unicast); - } + private static final Logger LOG = getLogger(JavaCliNetworkManager.class); + + @Inject + JavaCliNetworkManager() { + } + + @Override + public NetworkStatus getNetworkStatus() { + boolean connected = false, hasIpv4 = false, hasIpv6Unicast = false; + try { + for (NetworkInterface i : getNetworkInterfaces()) { + if (i.isLoopback() || !i.isUp()) continue; + for (InetAddress addr : list(i.getInetAddresses())) { + connected = true; + if (addr instanceof Inet4Address) { + hasIpv4 = true; + } else if (!addr.isMulticastAddress()) { + hasIpv6Unicast = true; + } + } + } + } catch (SocketException e) { + logException(LOG, e); + } + return new NetworkStatus(connected, false, !hasIpv4 && hasIpv6Unicast); + } } diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java index e097d26e7060728b0d692c4d6ea3de8001d4b7e0..6056de6a0c9796fdd81dc82c5345ab175d755afd 100644 --- a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorPlugin.java @@ -18,43 +18,44 @@ import javax.annotation.Nullable; public class JavaTorPlugin extends TorPlugin { - JavaTorPlugin(Executor ioExecutor, - SettingsManager settingsManager, - NetworkManager networkManager, - LocationUtils locationUtils, - Clock clock, - ResourceProvider resourceProvider, - CircumventionProvider circumventionProvider, - Backoff backoff, - @Nullable String architecture, - File torDirectory) { - super(ioExecutor, settingsManager, networkManager, locationUtils, clock, resourceProvider, - circumventionProvider, backoff, architecture, torDirectory); - } - - @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 int getProcessId() { - return CLibrary.INSTANCE.getpid(); - } - - private interface CLibrary extends Library { - - CLibrary INSTANCE = Native.load("c", CLibrary.class); - - int getpid(); - } + JavaTorPlugin(Executor ioExecutor, + SettingsManager settingsManager, + NetworkManager networkManager, + LocationUtils locationUtils, + Clock clock, + ResourceProvider resourceProvider, + CircumventionProvider circumventionProvider, + Backoff backoff, + @Nullable String architecture, + File torDirectory) { + super(ioExecutor, settingsManager, networkManager, locationUtils, clock, + resourceProvider, + circumventionProvider, backoff, architecture, torDirectory); + } + + @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 int getProcessId() { + return CLibrary.INSTANCE.getpid(); + } + + private interface CLibrary extends Library { + + CLibrary INSTANCE = Native.load("c", CLibrary.class); + + int getpid(); + } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/PoliteExecutor.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/PoliteExecutor.java index 4149077b7c878a6c494b8e561db5f0421909b2c6..fc547da66694700999b4ffda1a0133b99b123982 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/PoliteExecutor.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/PoliteExecutor.java @@ -36,7 +36,7 @@ public class PoliteExecutor implements Executor { * concurrently */ public PoliteExecutor(String tag, Executor delegate, - int maxConcurrentTasks) { + int maxConcurrentTasks) { this.delegate = delegate; this.maxConcurrentTasks = maxConcurrentTasks; log = Logger.getLogger(tag); diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/MigrationListener.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/MigrationListener.java index d9283393f2dcbf5cc09db159546b19c47bf76fbb..05bb1d011a48e990b0454edbcec51ced37a50174 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/MigrationListener.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/MigrationListener.java @@ -2,15 +2,15 @@ package org.briarproject.mailbox.core.db; public interface MigrationListener { - /** - * This is called when a migration is started while opening the database. - * It will be called once for each migration being applied. - */ - void onDatabaseMigration(); + /** + * This is called when a migration is started while opening the database. + * It will be called once for each migration being applied. + */ + void onDatabaseMigration(); - /** - * This is called when compaction is started while opening the database. - */ - void onDatabaseCompaction(); + /** + * This is called when compaction is started while opening the database. + */ + void onDatabaseCompaction(); } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java index 2e8aa3ee71e0895b2cc056f5935972850c7482d0..03567871b02ea5362a2947bdf5ffd55f95e527f2 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java @@ -14,99 +14,99 @@ import kotlinx.coroutines.flow.StateFlow; */ public interface LifecycleManager { - /** - * The result of calling {@link #startServices()}. - */ - enum StartResult { - ALREADY_RUNNING, - SERVICE_ERROR, - SUCCESS - } - - /** - * The state the lifecycle can be in. - * Returned by {@link #getLifecycleState()} - */ - enum LifecycleState { - - STOPPED, - STARTING, - MIGRATING_DATABASE, - COMPACTING_DATABASE, - STARTING_SERVICES, - RUNNING, - STOPPING; - - public boolean isAfter(LifecycleState state) { - return ordinal() > state.ordinal(); - } - } - - /** - * Registers a hook to be called after the database is opened and before - * {@link Service services} are started. This method should be called - * before {@link #startServices()}. - */ - void registerOpenDatabaseHook(OpenDatabaseHook hook); - - /** - * Registers a {@link Service} to be started and stopped. This method - * should be called before {@link #startServices()}. - */ - void registerService(Service s); - - /** - * Registers an {@link ExecutorService} to be shut down. This method - * should be called before {@link #startServices()}. - */ - void registerForShutdown(ExecutorService e); - - /** - * Opens the {@link Database} using the given key and starts any - * registered {@link Service Services}. - */ - @Wakeful - StartResult startServices(); - - /** - * Stops any registered {@link Service Services}, shuts down any - * registered {@link ExecutorService ExecutorServices}, and closes the - * {@link Database}. - */ - @Wakeful - void stopServices(); - - /** - * Waits for the {@link Database} to be opened before returning. - */ - void waitForDatabase() throws InterruptedException; - - /** - * Waits for the {@link Database} to be opened and all registered - * {@link Service Services} to start before returning. - */ - void waitForStartup() throws InterruptedException; - - /** - * Waits for all registered {@link Service Services} to stop, all - * registered {@link ExecutorService ExecutorServices} to shut down, and - * the {@link Database} to be closed before returning. - */ - void waitForShutdown() throws InterruptedException; - - /** - * Returns the current state of the lifecycle. - */ - LifecycleState getLifecycleState(); - - StateFlow<LifecycleState> getLifecycleStateFlow(); - - interface OpenDatabaseHook { - /** - * Called when the database is being opened, before - * {@link #waitForDatabase()} returns. - */ - @Wakeful - void onDatabaseOpened(); - } + /** + * The result of calling {@link #startServices()}. + */ + enum StartResult { + ALREADY_RUNNING, + SERVICE_ERROR, + SUCCESS + } + + /** + * The state the lifecycle can be in. + * Returned by {@link #getLifecycleState()} + */ + enum LifecycleState { + + STOPPED, + STARTING, + MIGRATING_DATABASE, + COMPACTING_DATABASE, + STARTING_SERVICES, + RUNNING, + STOPPING; + + public boolean isAfter(LifecycleState state) { + return ordinal() > state.ordinal(); + } + } + + /** + * Registers a hook to be called after the database is opened and before + * {@link Service services} are started. This method should be called + * before {@link #startServices()}. + */ + void registerOpenDatabaseHook(OpenDatabaseHook hook); + + /** + * Registers a {@link Service} to be started and stopped. This method + * should be called before {@link #startServices()}. + */ + void registerService(Service s); + + /** + * Registers an {@link ExecutorService} to be shut down. This method + * should be called before {@link #startServices()}. + */ + void registerForShutdown(ExecutorService e); + + /** + * Opens the {@link Database} using the given key and starts any + * registered {@link Service Services}. + */ + @Wakeful + StartResult startServices(); + + /** + * Stops any registered {@link Service Services}, shuts down any + * registered {@link ExecutorService ExecutorServices}, and closes the + * {@link Database}. + */ + @Wakeful + void stopServices(); + + /** + * Waits for the {@link Database} to be opened before returning. + */ + void waitForDatabase() throws InterruptedException; + + /** + * Waits for the {@link Database} to be opened and all registered + * {@link Service Services} to start before returning. + */ + void waitForStartup() throws InterruptedException; + + /** + * Waits for all registered {@link Service Services} to stop, all + * registered {@link ExecutorService ExecutorServices} to shut down, and + * the {@link Database} to be closed before returning. + */ + void waitForShutdown() throws InterruptedException; + + /** + * Returns the current state of the lifecycle. + */ + LifecycleState getLifecycleState(); + + StateFlow<LifecycleState> getLifecycleStateFlow(); + + interface OpenDatabaseHook { + /** + * Called when the database is being opened, before + * {@link #waitForDatabase()} returns. + */ + @Wakeful + void onDatabaseOpened(); + } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/ServiceException.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/ServiceException.java index fbd05bb6a4323e9c7cbc5f5057b465b0601e2dc3..fae8cd2ccfb34d28dc831b7d736d47f16b3952d4 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/ServiceException.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/ServiceException.java @@ -5,15 +5,15 @@ package org.briarproject.mailbox.core.lifecycle; */ public class ServiceException extends Exception { - public ServiceException() { - super(); - } + public ServiceException() { + super(); + } - public ServiceException(String msg) { - super(msg); - } + public ServiceException(String msg) { + super(msg); + } - public ServiceException(Throwable cause) { - super(cause); - } + public ServiceException(Throwable cause) { + super(cause); + } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Clock.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Clock.java index 2ed21d9e891f0526ddc496e3514e37c767bc09d8..c3d8f0e3959ec6a496f8ae6a8d30c3f72bbec94b 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Clock.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/Clock.java @@ -1,5 +1,5 @@ package org.briarproject.mailbox.core.system; public interface Clock { - long currentTimeMillis(); + long currentTimeMillis(); } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/ResourceProvider.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/ResourceProvider.java index 70ec10cd411deeb6dad0c511e7b96cc834502f44..8a3d0387460799adba97776bad4523ba490702ad 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/ResourceProvider.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/ResourceProvider.java @@ -4,5 +4,5 @@ import java.io.InputStream; public interface ResourceProvider { - InputStream getResourceInputStream(String name, String extension); + InputStream getResourceInputStream(String name, String extension); } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/CircumventionProvider.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/CircumventionProvider.java index f1d33614cc082a19f409921d57e196130f74af68..ff6a32100449e9e054cad315828f9c61dc6f6968 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/CircumventionProvider.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/CircumventionProvider.java @@ -6,7 +6,7 @@ public interface CircumventionProvider { /** * Countries where Tor is blocked, i.e. vanilla Tor connection won't work. - * + * <p> * See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 * and https://trac.torproject.org/projects/tor/wiki/doc/OONI/censorshipwiki */ @@ -16,7 +16,7 @@ public interface CircumventionProvider { * Countries where obfs4 or meek bridge connections are likely to work. * Should be a subset of {@link #BLOCKED}. */ - String[] BRIDGES = { "CN", "IR", "EG", "BY", "TR", "SY", "VE" }; + String[] BRIDGES = {"CN", "IR", "EG", "BY", "TR", "SY", "VE"}; /** * Countries where obfs4 bridges won't work and meek is needed. diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java index 276a592e30e1569c757afc60e696b5c14128ca83..59b63c9e8c92e1c089b1aa25453225591f7683ef 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java @@ -64,605 +64,607 @@ import static org.briarproject.mailbox.core.util.PrivacyUtils.scrubOnion; import static org.slf4j.LoggerFactory.getLogger; public abstract class TorPlugin - implements Service, EventHandler, EventListener { - - private static final Logger LOG = getLogger(TorPlugin.class); - - private static final String[] EVENTS = { - "CIRC", "ORCONN", "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 final Executor ioExecutor; - private final Executor connectionStatusExecutor; - private final SettingsManager settingsManager; - private final NetworkManager networkManager; - private final LocationUtils locationUtils; - private final Clock clock; - private final Backoff backoff; - @Nullable - private final String architecture; - private final CircumventionProvider circumventionProvider; - private final ResourceProvider resourceProvider; - private final File torDirectory, geoIpFile, configFile; - 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; - - protected abstract int getProcessId(); - - protected abstract long getLastUpdateTime(); - - TorPlugin(Executor ioExecutor, - SettingsManager settingsManager, - NetworkManager networkManager, - LocationUtils locationUtils, - Clock clock, - ResourceProvider resourceProvider, - CircumventionProvider circumventionProvider, - Backoff backoff, - @Nullable String architecture, - File torDirectory) { - this.ioExecutor = ioExecutor; - this.settingsManager = settingsManager; - this.networkManager = networkManager; - this.locationUtils = locationUtils; - this.clock = clock; - this.resourceProvider = resourceProvider; - this.circumventionProvider = circumventionProvider; - this.backoff = backoff; - this.architecture = architecture; - this.torDirectory = torDirectory; - geoIpFile = new File(torDirectory, "geoip"); - configFile = new File(torDirectory, "torrc"); - doneFile = new File(torDirectory, "done"); - cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); - // 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"); - } - - @Override - public void startService() throws ServiceException { - if (used.getAndSet(true)) throw new IllegalStateException(); - if (!torDirectory.exists()) { - if (!torDirectory.mkdirs()) { - LOG.warn("Could not create Tor directory."); - throw new ServiceException(); - } - } - // Install or update the assets if necessary - if (!assetsAreUpToDate()) installAssets(); - if (cookieFile.exists() && !cookieFile.delete()) - LOG.warn("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<String, String> env = pb.environment(); - env.put("HOME", torDirectory.getAbsolutePath()); - pb.directory(torDirectory); - try { - torProcess = pb.start(); - } catch (SecurityException | IOException e) { - throw new ServiceException(e); - } - // Log the process's standard output until it detaches - if (LOG.isInfoEnabled()) { - Scanner stdout = new Scanner(torProcess.getInputStream()); - Scanner stderr = new Scanner(torProcess.getErrorStream()); - while (stdout.hasNextLine() || stderr.hasNextLine()) { - if (stdout.hasNextLine()) { - LOG.info(stdout.nextLine()); - } - if (stderr.hasNextLine()) { - LOG.info(stderr.nextLine()); - } - } - stdout.close(); - stderr.close(); - } - try { - // Wait for the process to detach or exit - int exit = torProcess.waitFor(); - if (exit != 0) { - warn(LOG, () -> "Tor exited with value " + exit); - throw new ServiceException(); - } - // 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.warn("Auth cookie not created"); - if (LOG.isInfoEnabled()) listFiles(torDirectory); - throw new ServiceException(); - } - //noinspection BusyWait - Thread.sleep(COOKIE_POLLING_INTERVAL_MS); - } - LOG.info("Auth cookie created"); - } catch (InterruptedException e) { - LOG.warn("Interrupted while starting Tor"); - Thread.currentThread().interrupt(); - throw new ServiceException(); - } - try { - // Open a control connection and authenticate using the cookie file - controlSocket = new Socket("127.0.0.1", CONTROL_PORT); - 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 phase = controlConnection.getInfo("status/bootstrap-phase"); - if (phase != null && phase.contains("PROGRESS=100")) { - LOG.info("Tor has already bootstrapped"); - state.setBootstrapped(); - } - } catch (IOException e) { - throw new ServiceException(e); - } - state.setStarted(); - // Check whether we're online - updateConnectionStatus(networkManager.getNetworkStatus()); - // Create a hidden service if necessary - ioExecutor.execute(() -> publishHiddenService(String.valueOf(WebServerManager.PORT))); - } - - private boolean assetsAreUpToDate() { - return doneFile.lastModified() > getLastUpdateTime(); - } - - private void installAssets() throws ServiceException { - if (architecture == null) - throw new ServiceException("Tor not supported on this architecture"); - try { - // The done file may already exist from a previous installation - //noinspection ResultOfMethodCallIgnored - doneFile.delete(); - installTorExecutable(); - installObfs4Executable(); - extract(getGeoIpInputStream(), geoIpFile); - extract(getConfigInputStream(), configFile); - if (!doneFile.createNewFile()) - LOG.warn("Failed to create done file"); - } catch (IOException e) { - throw new ServiceException(e); - } - } - - protected void extract(InputStream in, File dest) throws IOException { - OutputStream out = new FileOutputStream(dest); - copyAndClose(in, out); - } - - protected void installTorExecutable() throws IOException { - info(LOG, () -> "Installing Tor binary for " + architecture); - File torFile = getTorExecutableFile(); - extract(getTorInputStream(), torFile); - if (!torFile.setExecutable(true, true)) throw new IOException(); - } - - protected void installObfs4Executable() throws IOException { - info(LOG, () -> "Installing obfs4proxy binary for " + architecture); - File obfs4File = getObfs4ExecutableFile(); - extract(getObfs4InputStream(), obfs4File); - if (!obfs4File.setExecutable(true, true)) throw new IOException(); - } - - private InputStream getTorInputStream() throws IOException { - InputStream in = resourceProvider - .getResourceInputStream("tor_" + architecture, ".zip"); - ZipInputStream zin = new ZipInputStream(in); - if (zin.getNextEntry() == null) throw new IOException(); - return zin; - } - - private InputStream getGeoIpInputStream() throws IOException { - InputStream in = resourceProvider.getResourceInputStream("geoip", - ".zip"); - ZipInputStream zin = new ZipInputStream(in); - if (zin.getNextEntry() == null) throw new IOException(); - return zin; - } - - private InputStream getObfs4InputStream() throws IOException { - InputStream in = resourceProvider - .getResourceInputStream("obfs4proxy_" + architecture, ".zip"); - ZipInputStream zin = new ZipInputStream(in); - if (zin.getNextEntry() == null) throw new IOException(); - return zin; - } - - private InputStream getConfigInputStream() { - ClassLoader cl = getClass().getClassLoader(); - return requireNonNull(cl.getResourceAsStream("torrc")); - } - - 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); - } - } - - @IoExecutor - private void publishHiddenService(String port) { - if (!state.isTorRunning()) return; - - Settings s; - try { - s = settingsManager.getSettings(SETTINGS_NAMESPACE); - } catch (DbException e) { - logException(LOG, e); - s = new Settings(); - } - String privateKey3 = s.get(HS_PRIVATE_KEY_V3); - publishV3HiddenService(port, privateKey3); - } - - @IoExecutor - private void publishV3HiddenService(String port, @Nullable String privKey) { - LOG.info("Creating v3 hidden service"); - Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port); - Map<String, String> response; - 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); - } - } catch (IOException e) { - logException(LOG, e); - return; - } - if (!response.containsKey(HS_ADDRESS)) { - LOG.warn("Tor did not return a hidden service address"); - return; - } - if (privKey == null && !response.containsKey(HS_PRIVKEY)) { - LOG.warn("Tor did not return a private key"); - return; - } - Settings s = new Settings(); - String onion3 = response.get(HS_ADDRESS); - s.put(HS_ADDRESS_V3, onion3); - info(LOG, () -> "V3 hidden service " + scrubOnion(onion3)); - - // TODO remove before release - LOG.warn("V3 hidden service: http://" + onion3 + ".onion"); - - if (privKey == null) { - s.put(HS_PRIVATE_KEY_V3, response.get(HS_PRIVKEY)); - try { - settingsManager.mergeSettings(s, SETTINGS_NAMESPACE); - } catch (DbException e) { - logException(LOG, e); - } - } - } - - @Nullable - public String getHiddenServiceAddress() throws DbException { - Settings s = settingsManager.getSettings(SETTINGS_NAMESPACE); - return s.get(HS_ADDRESS_V3); - } - - protected void enableNetwork(boolean enable) throws IOException { - state.enableNetwork(enable); - controlConnection.setConf("DisableNetwork", enable ? "0" : "1"); - } - - private void enableBridges(boolean enable, boolean needsMeek) - throws IOException { - if (enable) { - Collection<String> conf = new ArrayList<>(); - conf.add("UseBridges 1"); - File obfs4File = getObfs4ExecutableFile(); - if (needsMeek) { - conf.add("ClientTransportPlugin meek_lite exec " + - obfs4File.getAbsolutePath()); - } else { - conf.add("ClientTransportPlugin obfs4 exec " + - obfs4File.getAbsolutePath()); - } - conf.addAll(circumventionProvider.getBridges(needsMeek)); - controlConnection.setConf(conf); - } else { - controlConnection.setConf("UseBridges", "0"); - } - } - - @Override - public void stopService() { - ServerSocket ss = state.setStopped(); - tryToClose(ss, LOG); - if (controlSocket != null && controlConnection != null) { - try { - LOG.info("Stopping Tor"); - controlConnection.setConf("DisableNetwork", "1"); - controlConnection.shutdownTor("TERM"); - controlSocket.close(); - } catch (IOException e) { - logException(LOG, e); - } - } - } - - @Override - public void circuitStatus(String status, String id, String path) { - if (status.equals("BUILT") && - state.getAndSetCircuitBuilt()) { - LOG.info("First circuit built"); - backoff.reset(); - } - } - - @Override - public void streamStatus(String status, String id, String target) { - } - - @Override - public void orConnStatus(String status, String orName) { - info(LOG, () -> "OR connection " + status + " " + orName); - if (status.equals("CLOSED") || status.equals("FAILED")) { - // Check whether we've lost connectivity - updateConnectionStatus(networkManager.getNetworkStatus()); - } - } - - @Override - public void bandwidthUsed(long read, long written) { - } - - @Override - public void newDescriptors(List<String> orList) { - } - - @Override - public void message(String severity, String msg) { - info(LOG, () -> severity + " " + msg); - if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) { - state.setBootstrapped(); - backoff.reset(); - } - } - - @Override - public void unrecognized(String type, String msg) { - if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) { - LOG.info("V3 descriptor uploaded"); - } - } - - @Override - public void eventOccurred(Event e) { - if (e instanceof NetworkStatusEvent) { - updateConnectionStatus(((NetworkStatusEvent) e).getStatus()); - } - } - - private void disableNetwork() { - connectionStatusExecutor.execute(() -> { - try { - if (state.isTorRunning()) enableNetwork(false); - } catch (IOException ex) { - logException(LOG, ex); - } - }); - } - - private void updateConnectionStatus(NetworkStatus status) { - connectionStatusExecutor.execute(() -> { - if (!state.isTorRunning()) return; - boolean online = status.isConnected(); - boolean wifi = status.isWifi(); - boolean ipv6Only = status.isIpv6Only(); - String country = locationUtils.getCurrentCountry(); - boolean blocked = - circumventionProvider.isTorProbablyBlocked(country); - boolean bridgesWork = circumventionProvider.doBridgesWork(country); - - if (LOG.isInfoEnabled()) { - LOG.info("Online: " + online + ", wifi: " + wifi - + ", IPv6 only: " + ipv6Only); - if (country.isEmpty()) LOG.info("Country code unknown"); - else LOG.info("Country code: " + country); - } - - int reasonsDisabled = 0; - boolean enableNetwork = false; - boolean enableBridges = false; - boolean useMeek = false; - - if (!online) { - LOG.info("Disabling network, device is offline"); - } else { - LOG.info("Enabling network"); - enableNetwork = true; - if (blocked && bridgesWork) { - if (ipv6Only || circumventionProvider.needsMeek(country)) { - LOG.info("Using meek bridges"); - enableBridges = true; - useMeek = true; - } else { - LOG.info("Using obfs4 bridges"); - enableBridges = true; - } - } else { - LOG.info("Not using bridges"); - } - } - state.setReasonsDisabled(reasonsDisabled); - try { - if (enableNetwork) { - enableBridges(enableBridges, useMeek); - enableConnectionPadding(true); - useIpv6(ipv6Only); - } - enableNetwork(enableNetwork); - } catch (IOException e) { - logException(LOG, e); - } - }); - } - - private void enableConnectionPadding(boolean enable) throws IOException { - controlConnection.setConf("ConnectionPadding", enable ? "1" : "0"); - } - - private void useIpv6(boolean ipv6Only) throws IOException { - controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1"); - controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0"); - } - - @ThreadSafe - protected class PluginState { - - @GuardedBy("this") - private boolean started = false, - stopped = false, - networkInitialised = false, - networkEnabled = false, - bootstrapped = false, - circuitBuilt = false, - settingsChecked = false; - - @GuardedBy("this") - private int reasonsDisabled = 0; - - @GuardedBy("this") - @Nullable - private ServerSocket serverSocket = null; - - synchronized void setStarted() { - started = true; + implements Service, EventHandler, EventListener { + + private static final Logger LOG = getLogger(TorPlugin.class); + + private static final String[] EVENTS = { + "CIRC", "ORCONN", "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 final Executor ioExecutor; + private final Executor connectionStatusExecutor; + private final SettingsManager settingsManager; + private final NetworkManager networkManager; + private final LocationUtils locationUtils; + private final Clock clock; + private final Backoff backoff; + @Nullable + private final String architecture; + private final CircumventionProvider circumventionProvider; + private final ResourceProvider resourceProvider; + private final File torDirectory, geoIpFile, configFile; + 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; + + protected abstract int getProcessId(); + + protected abstract long getLastUpdateTime(); + + TorPlugin(Executor ioExecutor, + SettingsManager settingsManager, + NetworkManager networkManager, + LocationUtils locationUtils, + Clock clock, + ResourceProvider resourceProvider, + CircumventionProvider circumventionProvider, + Backoff backoff, + @Nullable String architecture, + File torDirectory) { + this.ioExecutor = ioExecutor; + this.settingsManager = settingsManager; + this.networkManager = networkManager; + this.locationUtils = locationUtils; + this.clock = clock; + this.resourceProvider = resourceProvider; + this.circumventionProvider = circumventionProvider; + this.backoff = backoff; + this.architecture = architecture; + this.torDirectory = torDirectory; + geoIpFile = new File(torDirectory, "geoip"); + configFile = new File(torDirectory, "torrc"); + doneFile = new File(torDirectory, "done"); + cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); + // 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"); + } + + @Override + public void startService() throws ServiceException { + if (used.getAndSet(true)) throw new IllegalStateException(); + if (!torDirectory.exists()) { + if (!torDirectory.mkdirs()) { + LOG.warn("Could not create Tor directory."); + throw new ServiceException(); + } + } + // Install or update the assets if necessary + if (!assetsAreUpToDate()) installAssets(); + if (cookieFile.exists() && !cookieFile.delete()) + LOG.warn("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<String, String> env = pb.environment(); + env.put("HOME", torDirectory.getAbsolutePath()); + pb.directory(torDirectory); + try { + torProcess = pb.start(); + } catch (SecurityException | IOException e) { + throw new ServiceException(e); + } + // Log the process's standard output until it detaches + if (LOG.isInfoEnabled()) { + Scanner stdout = new Scanner(torProcess.getInputStream()); + Scanner stderr = new Scanner(torProcess.getErrorStream()); + while (stdout.hasNextLine() || stderr.hasNextLine()) { + if (stdout.hasNextLine()) { + LOG.info(stdout.nextLine()); + } + if (stderr.hasNextLine()) { + LOG.info(stderr.nextLine()); + } + } + stdout.close(); + stderr.close(); + } + try { + // Wait for the process to detach or exit + int exit = torProcess.waitFor(); + if (exit != 0) { + warn(LOG, () -> "Tor exited with value " + exit); + throw new ServiceException(); + } + // 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.warn("Auth cookie not created"); + if (LOG.isInfoEnabled()) listFiles(torDirectory); + throw new ServiceException(); + } + //noinspection BusyWait + Thread.sleep(COOKIE_POLLING_INTERVAL_MS); + } + LOG.info("Auth cookie created"); + } catch (InterruptedException e) { + LOG.warn("Interrupted while starting Tor"); + Thread.currentThread().interrupt(); + throw new ServiceException(); + } + try { + // Open a control connection and authenticate using the cookie file + controlSocket = new Socket("127.0.0.1", CONTROL_PORT); + 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 phase = controlConnection.getInfo("status/bootstrap-phase"); + if (phase != null && phase.contains("PROGRESS=100")) { + LOG.info("Tor has already bootstrapped"); + state.setBootstrapped(); + } + } catch (IOException e) { + throw new ServiceException(e); + } + state.setStarted(); + // Check whether we're online + updateConnectionStatus(networkManager.getNetworkStatus()); + // Create a hidden service if necessary + ioExecutor.execute(() -> publishHiddenService( + String.valueOf(WebServerManager.PORT))); + } + + private boolean assetsAreUpToDate() { + return doneFile.lastModified() > getLastUpdateTime(); + } + + private void installAssets() throws ServiceException { + if (architecture == null) + throw new ServiceException( + "Tor not supported on this architecture"); + try { + // The done file may already exist from a previous installation + //noinspection ResultOfMethodCallIgnored + doneFile.delete(); + installTorExecutable(); + installObfs4Executable(); + extract(getGeoIpInputStream(), geoIpFile); + extract(getConfigInputStream(), configFile); + if (!doneFile.createNewFile()) + LOG.warn("Failed to create done file"); + } catch (IOException e) { + throw new ServiceException(e); + } + } + + protected void extract(InputStream in, File dest) throws IOException { + OutputStream out = new FileOutputStream(dest); + copyAndClose(in, out); + } + + protected void installTorExecutable() throws IOException { + info(LOG, () -> "Installing Tor binary for " + architecture); + File torFile = getTorExecutableFile(); + extract(getTorInputStream(), torFile); + if (!torFile.setExecutable(true, true)) throw new IOException(); + } + + protected void installObfs4Executable() throws IOException { + info(LOG, () -> "Installing obfs4proxy binary for " + architecture); + File obfs4File = getObfs4ExecutableFile(); + extract(getObfs4InputStream(), obfs4File); + if (!obfs4File.setExecutable(true, true)) throw new IOException(); + } + + private InputStream getTorInputStream() throws IOException { + InputStream in = resourceProvider + .getResourceInputStream("tor_" + architecture, ".zip"); + ZipInputStream zin = new ZipInputStream(in); + if (zin.getNextEntry() == null) throw new IOException(); + return zin; + } + + private InputStream getGeoIpInputStream() throws IOException { + InputStream in = resourceProvider.getResourceInputStream("geoip", + ".zip"); + ZipInputStream zin = new ZipInputStream(in); + if (zin.getNextEntry() == null) throw new IOException(); + return zin; + } + + private InputStream getObfs4InputStream() throws IOException { + InputStream in = resourceProvider + .getResourceInputStream("obfs4proxy_" + architecture, ".zip"); + ZipInputStream zin = new ZipInputStream(in); + if (zin.getNextEntry() == null) throw new IOException(); + return zin; + } + + private InputStream getConfigInputStream() { + ClassLoader cl = getClass().getClassLoader(); + return requireNonNull(cl.getResourceAsStream("torrc")); + } + + 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); + } + } + + @IoExecutor + private void publishHiddenService(String port) { + if (!state.isTorRunning()) return; + + Settings s; + try { + s = settingsManager.getSettings(SETTINGS_NAMESPACE); + } catch (DbException e) { + logException(LOG, e); + s = new Settings(); + } + String privateKey3 = s.get(HS_PRIVATE_KEY_V3); + publishV3HiddenService(port, privateKey3); + } + + @IoExecutor + private void publishV3HiddenService(String port, @Nullable String privKey) { + LOG.info("Creating v3 hidden service"); + Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port); + Map<String, String> response; + 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); + } + } catch (IOException e) { + logException(LOG, e); + return; + } + if (!response.containsKey(HS_ADDRESS)) { + LOG.warn("Tor did not return a hidden service address"); + return; + } + if (privKey == null && !response.containsKey(HS_PRIVKEY)) { + LOG.warn("Tor did not return a private key"); + return; + } + Settings s = new Settings(); + String onion3 = response.get(HS_ADDRESS); + s.put(HS_ADDRESS_V3, onion3); + info(LOG, () -> "V3 hidden service " + scrubOnion(onion3)); + + // TODO remove before release + LOG.warn("V3 hidden service: http://" + onion3 + ".onion"); + + if (privKey == null) { + s.put(HS_PRIVATE_KEY_V3, response.get(HS_PRIVKEY)); + try { + settingsManager.mergeSettings(s, SETTINGS_NAMESPACE); + } catch (DbException e) { + logException(LOG, e); + } + } + } + + @Nullable + public String getHiddenServiceAddress() throws DbException { + Settings s = settingsManager.getSettings(SETTINGS_NAMESPACE); + return s.get(HS_ADDRESS_V3); + } + + protected void enableNetwork(boolean enable) throws IOException { + state.enableNetwork(enable); + controlConnection.setConf("DisableNetwork", enable ? "0" : "1"); + } + + private void enableBridges(boolean enable, boolean needsMeek) + throws IOException { + if (enable) { + Collection<String> conf = new ArrayList<>(); + conf.add("UseBridges 1"); + File obfs4File = getObfs4ExecutableFile(); + if (needsMeek) { + conf.add("ClientTransportPlugin meek_lite exec " + + obfs4File.getAbsolutePath()); + } else { + conf.add("ClientTransportPlugin obfs4 exec " + + obfs4File.getAbsolutePath()); + } + conf.addAll(circumventionProvider.getBridges(needsMeek)); + controlConnection.setConf(conf); + } else { + controlConnection.setConf("UseBridges", "0"); + } + } + + @Override + public void stopService() { + ServerSocket ss = state.setStopped(); + tryToClose(ss, LOG); + if (controlSocket != null && controlConnection != null) { + try { + LOG.info("Stopping Tor"); + controlConnection.setConf("DisableNetwork", "1"); + controlConnection.shutdownTor("TERM"); + controlSocket.close(); + } catch (IOException e) { + logException(LOG, e); + } + } + } + + @Override + public void circuitStatus(String status, String id, String path) { + if (status.equals("BUILT") && + state.getAndSetCircuitBuilt()) { + LOG.info("First circuit built"); + backoff.reset(); + } + } + + @Override + public void streamStatus(String status, String id, String target) { + } + + @Override + public void orConnStatus(String status, String orName) { + info(LOG, () -> "OR connection " + status + " " + orName); + if (status.equals("CLOSED") || status.equals("FAILED")) { + // Check whether we've lost connectivity + updateConnectionStatus(networkManager.getNetworkStatus()); + } + } + + @Override + public void bandwidthUsed(long read, long written) { + } + + @Override + public void newDescriptors(List<String> orList) { + } + + @Override + public void message(String severity, String msg) { + info(LOG, () -> severity + " " + msg); + if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) { + state.setBootstrapped(); + backoff.reset(); + } + } + + @Override + public void unrecognized(String type, String msg) { + if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) { + LOG.info("V3 descriptor uploaded"); + } + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof NetworkStatusEvent) { + updateConnectionStatus(((NetworkStatusEvent) e).getStatus()); + } + } + + private void disableNetwork() { + connectionStatusExecutor.execute(() -> { + try { + if (state.isTorRunning()) enableNetwork(false); + } catch (IOException ex) { + logException(LOG, ex); + } + }); + } + + private void updateConnectionStatus(NetworkStatus status) { + connectionStatusExecutor.execute(() -> { + if (!state.isTorRunning()) return; + boolean online = status.isConnected(); + boolean wifi = status.isWifi(); + boolean ipv6Only = status.isIpv6Only(); + String country = locationUtils.getCurrentCountry(); + boolean blocked = + circumventionProvider.isTorProbablyBlocked(country); + boolean bridgesWork = circumventionProvider.doBridgesWork(country); + + if (LOG.isInfoEnabled()) { + LOG.info("Online: " + online + ", wifi: " + wifi + + ", IPv6 only: " + ipv6Only); + if (country.isEmpty()) LOG.info("Country code unknown"); + else LOG.info("Country code: " + country); + } + + int reasonsDisabled = 0; + boolean enableNetwork = false; + boolean enableBridges = false; + boolean useMeek = false; + + if (!online) { + LOG.info("Disabling network, device is offline"); + } else { + LOG.info("Enabling network"); + enableNetwork = true; + if (blocked && bridgesWork) { + if (ipv6Only || circumventionProvider.needsMeek(country)) { + LOG.info("Using meek bridges"); + enableBridges = true; + useMeek = true; + } else { + LOG.info("Using obfs4 bridges"); + enableBridges = true; + } + } else { + LOG.info("Not using bridges"); + } + } + state.setReasonsDisabled(reasonsDisabled); + try { + if (enableNetwork) { + enableBridges(enableBridges, useMeek); + enableConnectionPadding(true); + useIpv6(ipv6Only); + } + enableNetwork(enableNetwork); + } catch (IOException e) { + logException(LOG, e); + } + }); + } + + private void enableConnectionPadding(boolean enable) throws IOException { + controlConnection.setConf("ConnectionPadding", enable ? "1" : "0"); + } + + private void useIpv6(boolean ipv6Only) throws IOException { + controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1"); + controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0"); + } + + @ThreadSafe + protected class PluginState { + + @GuardedBy("this") + private boolean started = false, + stopped = false, + networkInitialised = false, + networkEnabled = false, + bootstrapped = false, + circuitBuilt = false, + settingsChecked = false; + + @GuardedBy("this") + private int reasonsDisabled = 0; + + @GuardedBy("this") + @Nullable + private ServerSocket serverSocket = null; + + synchronized void setStarted() { + started = true; // callback.pluginStateChanged(getState()); - } + } - synchronized boolean isTorRunning() { - return started && !stopped; - } + synchronized boolean isTorRunning() { + return started && !stopped; + } - @Nullable - synchronized ServerSocket setStopped() { - stopped = true; - ServerSocket ss = serverSocket; - serverSocket = null; + @Nullable + synchronized ServerSocket setStopped() { + stopped = true; + ServerSocket ss = serverSocket; + serverSocket = null; // callback.pluginStateChanged(getState()); - return ss; - } + return ss; + } - synchronized void setBootstrapped() { - bootstrapped = true; + synchronized void setBootstrapped() { + bootstrapped = true; // callback.pluginStateChanged(getState()); - } + } - synchronized boolean getAndSetCircuitBuilt() { - boolean firstCircuit = !circuitBuilt; - circuitBuilt = true; + synchronized boolean getAndSetCircuitBuilt() { + boolean firstCircuit = !circuitBuilt; + circuitBuilt = true; // callback.pluginStateChanged(getState()); - return firstCircuit; - } + return firstCircuit; + } - synchronized void enableNetwork(boolean enable) { - networkInitialised = true; - networkEnabled = enable; - if (!enable) circuitBuilt = false; + synchronized void enableNetwork(boolean enable) { + networkInitialised = true; + networkEnabled = enable; + if (!enable) circuitBuilt = false; // callback.pluginStateChanged(getState()); - } + } - synchronized void setReasonsDisabled(int reasonsDisabled) { - settingsChecked = true; - this.reasonsDisabled = reasonsDisabled; + synchronized void setReasonsDisabled(int reasonsDisabled) { + settingsChecked = true; + this.reasonsDisabled = reasonsDisabled; // callback.pluginStateChanged(getState()); - } - - synchronized State getState() { - if (!started || stopped || !settingsChecked) { - return STARTING_STOPPING; - } - if (reasonsDisabled != 0) return DISABLED; - if (!networkInitialised) return ENABLING; - if (!networkEnabled) return INACTIVE; - return bootstrapped && circuitBuilt ? ACTIVE : ENABLING; - } - - synchronized int getReasonsDisabled() { - return getState() == DISABLED ? reasonsDisabled : 0; - } - - } - - enum State { - - /** - * The plugin has not finished starting or has been stopped. - */ - STARTING_STOPPING, - - /** - * The plugin is disabled by settings. - */ - DISABLED, - - /** - * The plugin is being enabled and can't yet make or receive - * connections. - */ - ENABLING, - - /** - * The plugin is enabled and can make or receive connections. - */ - ACTIVE, - - /** - * The plugin is enabled but can't make or receive connections - */ - INACTIVE - } + } + + synchronized State getState() { + if (!started || stopped || !settingsChecked) { + return STARTING_STOPPING; + } + if (reasonsDisabled != 0) return DISABLED; + if (!networkInitialised) return ENABLING; + if (!networkEnabled) return INACTIVE; + return bootstrapped && circuitBuilt ? ACTIVE : ENABLING; + } + + synchronized int getReasonsDisabled() { + return getState() == DISABLED ? reasonsDisabled : 0; + } + + } + + enum State { + + /** + * The plugin has not finished starting or has been stopped. + */ + STARTING_STOPPING, + + /** + * The plugin is disabled by settings. + */ + DISABLED, + + /** + * The plugin is being enabled and can't yet make or receive + * connections. + */ + ENABLING, + + /** + * The plugin is enabled and can make or receive connections. + */ + ACTIVE, + + /** + * The plugin is enabled but can't make or receive connections + */ + INACTIVE + } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java index f0f70a70ecb71355a14a69833ded9f33c6e395af..b6cd2117c141babd94ef53fa5ca789a05c486284 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java @@ -19,99 +19,101 @@ import static org.slf4j.LoggerFactory.getLogger; public class IoUtils { - private static final Logger LOG = getLogger(IoUtils.class); - - public static void deleteFileOrDir(File f) { - if (f.isFile()) { - delete(f); - } else if (f.isDirectory()) { - File[] children = f.listFiles(); - if (children == null) { - warn(LOG, () -> "Could not list files in " + f.getAbsolutePath()); - } else { - for (File child : children) deleteFileOrDir(child); - } - delete(f); - } - } - - private static void delete(File f) { - if (!f.delete()) warn(LOG, () -> "Could not delete " + f.getAbsolutePath()); - } - - public 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); - tryToClose(out, LOG); - } - } - - public static void tryToClose(@Nullable Closeable c, Logger logger) { - try { - if (c != null) c.close(); - } catch (IOException e) { - logException(logger, e); - } - } - - public static void tryToClose(@Nullable Socket s, Logger logger) { - try { - if (s != null) s.close(); - } catch (IOException e) { - logException(logger, e); - } - } - - public static void tryToClose(@Nullable ServerSocket ss, Logger logger) { - try { - if (ss != null) ss.close(); - } catch (IOException e) { - logException(logger, e); - } - } - - public static void read(InputStream in, byte[] b) throws IOException { - int offset = 0; - while (offset < b.length) { - int read = in.read(b, offset, b.length - offset); - if (read == -1) throw new EOFException(); - offset += read; - } - } - - // Workaround for a bug in Android 7, see - // https://android-review.googlesource.com/#/c/271775/ - public static InputStream getInputStream(Socket s) throws IOException { - try { - return s.getInputStream(); - } catch (NullPointerException e) { - throw new IOException(e); - } - } - - // Workaround for a bug in Android 7, see - // https://android-review.googlesource.com/#/c/271775/ - public static OutputStream getOutputStream(Socket s) throws IOException { - try { - return s.getOutputStream(); - } catch (NullPointerException e) { - throw new IOException(e); - } - } - - public static boolean isNonEmptyDirectory(File f) { - if (!f.isDirectory()) return false; - File[] children = f.listFiles(); - return children != null && children.length > 0; - } + private static final Logger LOG = getLogger(IoUtils.class); + + public static void deleteFileOrDir(File f) { + if (f.isFile()) { + delete(f); + } else if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children == null) { + warn(LOG, + () -> "Could not list files in " + f.getAbsolutePath()); + } else { + for (File child : children) deleteFileOrDir(child); + } + delete(f); + } + } + + private static void delete(File f) { + if (!f.delete()) + warn(LOG, () -> "Could not delete " + f.getAbsolutePath()); + } + + public 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); + tryToClose(out, LOG); + } + } + + public static void tryToClose(@Nullable Closeable c, Logger logger) { + try { + if (c != null) c.close(); + } catch (IOException e) { + logException(logger, e); + } + } + + public static void tryToClose(@Nullable Socket s, Logger logger) { + try { + if (s != null) s.close(); + } catch (IOException e) { + logException(logger, e); + } + } + + public static void tryToClose(@Nullable ServerSocket ss, Logger logger) { + try { + if (ss != null) ss.close(); + } catch (IOException e) { + logException(logger, e); + } + } + + public static void read(InputStream in, byte[] b) throws IOException { + int offset = 0; + while (offset < b.length) { + int read = in.read(b, offset, b.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + } + + // Workaround for a bug in Android 7, see + // https://android-review.googlesource.com/#/c/271775/ + public static InputStream getInputStream(Socket s) throws IOException { + try { + return s.getInputStream(); + } catch (NullPointerException e) { + throw new IOException(e); + } + } + + // Workaround for a bug in Android 7, see + // https://android-review.googlesource.com/#/c/271775/ + public static OutputStream getOutputStream(Socket s) throws IOException { + try { + return s.getOutputStream(); + } catch (NullPointerException e) { + throw new IOException(e); + } + } + + public static boolean isNonEmptyDirectory(File f) { + if (!f.isDirectory()) return false; + File[] children = f.listFiles(); + return children != null && children.length > 0; + } }