diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java index 3916c221a73ffbbe79bfc5f34ff0690a5b07b639..7f150c58fdd3c8b63327fae497dc138e03772231 100644 --- a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java +++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/AbstractTorWrapper.java @@ -127,69 +127,85 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper { @Override public void start() throws IOException, InterruptedException { - state.setStarting(); - 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()); - ProcessBuilder pb = new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); - Map<String, String> env = pb.environment(); - env.put("HOME", torDirectory.getAbsolutePath()); - pb.directory(torDirectory); - pb.redirectErrorStream(true); + if (!state.setStarting()) return; // Not in the appropriate state try { - torProcess = pb.start(); - } catch (SecurityException e) { - throw new IOException(e); - } - // Wait for the Tor process to start - waitForTorToStart(requireNonNull(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"); + if (!torDirectory.exists()) { + if (!torDirectory.mkdirs()) { + throw new IOException("Could not create Tor directory"); + } } - //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=")) { - int percentage = parseBootstrapPercentage(info); - if (percentage == 100) LOG.info("Tor has already bootstrapped"); - state.setBootstrapPercentage(percentage); - } - // 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); + // 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()); + ProcessBuilder pb = new ProcessBuilder(torPath, "-f", configPath, OWNER, pid); + Map<String, String> 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(requireNonNull(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=")) { + int percentage = parseBootstrapPercentage(info); + if (percentage == 100) LOG.info("Tor has already bootstrapped"); + state.setBootstrapPercentage(percentage); + } + // 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) { + // Clean up + if (controlSocket != null) { + tryToClose(controlSocket, LOG, WARNING); + controlSocket = null; + controlConnection = null; + } + if (torProcess != null) { + torProcess.destroy(); + torProcess.waitFor(); + torProcess = null; + } + state.setStartupFailed(); + throw e; } state.setStarted(); } @@ -383,7 +399,7 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper { @Override public void stop() throws IOException, InterruptedException { - state.setStopping(); + if (!state.setStopping()) return; // Not in the appropriate state try { if (controlConnection != null) { controlConnection.shutdownTor("TERM"); @@ -616,39 +632,69 @@ abstract class AbstractTorWrapper implements EventHandler, TorWrapper { } } - private synchronized void setStarting() { - // It's legal to call start() if the wrapper has never been started, or has been - // started and then stopped + /** + * If the current process state is {@link ProcessState#NOT_STARTED NOT_STARTED} or + * {@link ProcessState#STOPPED STOPPED}, sets the process state to + * {@link ProcessState#STARTING STARTING} and returns true. Otherwise returns false. + */ + private synchronized boolean setStarting() { + // It's appropriate to call start() if the wrapper has never been started, + // or has been started and then stopped if (processState != ProcessState.NOT_STARTED && processState != ProcessState.STOPPED) { - throw new IllegalStateException(); + return false; } processState = ProcessState.STARTING; updateState(); + return true; } private synchronized void setStarted() { - // It's illegal to call start() and stop() concurrently + // We should always be in the STARTING state when this is called if (processState != ProcessState.STARTING) throw new IllegalStateException(); processState = ProcessState.STARTED; updateState(); } + private synchronized void setStartupFailed() { + // We should always be in the STARTING state when this is called + if (processState != ProcessState.STARTING) throw new IllegalStateException(); + processState = ProcessState.STOPPED; + // Reset all state related to the failed attempt + networkInitialised = false; + networkEnabled = false; + paddingEnabled = false; + ipv6Enabled = false; + circuitBuilt = false; + bootstrapPercentage = 0; + bridges = emptyList(); + orConnectionsConnected = 0; + updateState(); + } + + /** + * Returns true if the current process state is {@link ProcessState#STARTED STARTED}. + */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") private synchronized boolean isTorRunning() { return processState == ProcessState.STARTED; } - private synchronized void setStopping() { - // It's legal to call stop() if start() has returned or thrown an exception - if (processState != ProcessState.STARTING && processState != ProcessState.STARTED) { - throw new IllegalStateException(); - } + /** + * If the current process state is {@link ProcessState#STARTING STARTING} or + * {@link ProcessState#STARTED STARTED}, changes the process state to + * {@link ProcessState#STOPPING STOPPING} and returns true. Otherwise returns false. + */ + private synchronized boolean setStopping() { + // It's appropriate to call stop() if start() has returned without throwing + // an exception + if (processState != ProcessState.STARTED) return false; processState = ProcessState.STOPPING; updateState(); + return true; } private synchronized void setStopped() { - // It's illegal to call start() and stop() concurrently + // We should always be in the STOPPING state when this is called if (processState != ProcessState.STOPPING) throw new IllegalStateException(); processState = ProcessState.STOPPED; // Reset all state related to the process that has stopped diff --git a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java index 961ba8f158c7c77f31693ab1f6b77b82df8b95c4..bb0e0f35b1d4bb5daf9bb0e0b1a767dee9e644e1 100644 --- a/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java +++ b/onionwrapper-core/src/main/java/org/briarproject/onionwrapper/TorWrapper.java @@ -28,9 +28,6 @@ public interface TorWrapper { * {@link #enableIpv6(boolean)}) should be called after this method returns. * <p> * Do not call this method concurrently with {@link #stop()}. - * <p> - * If this method throws an exception, call {@link #stop()} before trying - * to call this method again. */ void start() throws IOException, InterruptedException;