/** * Publishes a hidden service * * @param hiddenServicePort The port that the hidden service will accept connections on * @param localPort The local port that the hidden service will relay connections to * @return The hidden service's onion address in the form X.onion. * @throws java.io.IOException - File errors */ public synchronized String publishHiddenService(int hiddenServicePort, int localPort) throws IOException { if (controlConnection == null) { throw new RuntimeException("Service is not running."); } List<ConfigEntry> currentHiddenServices = controlConnection.getConf("HiddenServiceOptions"); if ((currentHiddenServices.size() == 1 && currentHiddenServices.get(0).key.compareTo("HiddenServiceOptions") == 0 && currentHiddenServices.get(0).value.compareTo("") == 0) == false) { throw new RuntimeException( "Sorry, only one hidden service to a customer and we already have one. Please send complaints to https://github.com/thaliproject/Tor_Onion_Proxy_Library/issues/5 with your scenario so we can justify fixing this."); } LOG.info("Creating hidden service"); File hostnameFile = onionProxyContext.getHostNameFile(); if (hostnameFile.getParentFile().exists() == false && hostnameFile.getParentFile().mkdirs() == false) { throw new RuntimeException("Could not create hostnameFile parent directory"); } if (hostnameFile.exists() == false && hostnameFile.createNewFile() == false) { throw new RuntimeException("Could not create hostnameFile"); } // Watch for the hostname file being created/updated WriteObserver hostNameFileObserver = onionProxyContext.generateWriteObserver(hostnameFile); // Use the control connection to update the Tor config List<String> config = Arrays.asList( "HiddenServiceDir " + hostnameFile.getParentFile().getAbsolutePath(), "HiddenServicePort " + hiddenServicePort + " 127.0.0.1:" + localPort); controlConnection.setConf(config); controlConnection.saveConf(); // Wait for the hostname file to be created/updated if (!hostNameFileObserver.poll(HOSTNAME_TIMEOUT, MILLISECONDS)) { FileUtilities.listFilesToLog(hostnameFile.getParentFile()); throw new RuntimeException("Wait for hidden service hostname file to be created expired."); } // Publish the hidden service's onion hostname in transport properties String hostname = new String(FileUtilities.read(hostnameFile), "UTF-8").trim(); LOG.info("Hidden service config has completed."); return hostname; }
protected synchronized void installAndConfigureFiles() throws IOException, InterruptedException { onionProxyContext.installFiles(); if (!setExecutable(onionProxyContext.getTorExecutableFile())) { throw new RuntimeException("could not make Tor executable."); } // We need to edit the config file to specify exactly where the cookie/geoip files should be // stored, on // Android this is always a fixed location relative to the configFiles which is why this extra // step // wasn't needed in Briar's Android code. But in Windows it ends up in the user's // AppData/Roaming. Rather // than track it down we just tell Tor where to put it. PrintWriter printWriter = null; try { printWriter = new PrintWriter( new BufferedWriter(new FileWriter(onionProxyContext.getTorrcFile(), true))); printWriter.println("CookieAuthFile " + onionProxyContext.getCookieFile().getAbsolutePath()); // For some reason the GeoIP's location can only be given as a file name, not a path and it // has // to be in the data directory so we need to set both printWriter.println( "DataDirectory " + onionProxyContext.getWorkingDirectory().getAbsolutePath()); printWriter.println("GeoIPFile " + onionProxyContext.getGeoIpFile().getName()); printWriter.println("GeoIPv6File " + onionProxyContext.getGeoIpv6File().getName()); } finally { if (printWriter != null) { printWriter.close(); } } }
/** * This is a blocking call that will try to start the Tor OP, connect it to the network and get it * to be fully bootstrapped. Sometimes the bootstrap process just hangs for no apparent reason so * the method will wait for the given time for bootstrap to finish and if it doesn't then will * restart the bootstrap process the given number of repeats. * * @param secondsBeforeTimeOut Seconds to wait for boot strapping to finish * @param numberOfRetries Number of times to try recycling the Tor OP before giving up on * bootstrapping working * @return True if bootstrap succeeded, false if there is a problem or the bootstrap couldn't * complete in the given time. * @throws java.lang.InterruptedException - You know, if we are interrupted * @throws java.io.IOException - IO Exceptions */ public synchronized boolean startWithRepeat(int secondsBeforeTimeOut, int numberOfRetries) throws InterruptedException, IOException { if (secondsBeforeTimeOut <= 0 || numberOfRetries < 0) { throw new IllegalArgumentException("secondsBeforeTimeOut >= 0 & numberOfRetries > 0"); } try { for (int retryCount = 0; retryCount < numberOfRetries; ++retryCount) { if (installAndStartTorOp() == false) { return false; } enableNetwork(true); // We will check every second to see if boot strapping has finally finished for (int secondsWaited = 0; secondsWaited < secondsBeforeTimeOut; ++secondsWaited) { if (isBootstrapped() == false) { Thread.sleep(1000, 0); } else { return true; } } // Bootstrapping isn't over so we need to restart and try again stop(); // Experimentally we have found that if a Tor OP has run before and thus has cached // descriptors // and that when we try to start it again it won't start then deleting the cached data can // fix this. // But, if there is cached data and things do work then the Tor OP will start faster than it // would // if we delete everything. // So our compromise is that we try to start the Tor OP 'as is' on the first round and after // that // we delete all the files. onionProxyContext.deleteAllFilesButHiddenServices(); } return false; } finally { // Make sure we return the Tor OP in some kind of consistent state, even if it's 'off'. if (isRunning() == false) { stop(); } } }
/** * Returns the root directory in which the Tor Onion Proxy keeps its files. This is mostly * intended for debugging purposes. * * @return Working directory for Tor Onion Proxy files */ public File getWorkingDirectory() { return onionProxyContext.getWorkingDirectory(); }
/** * Installs all necessary files and starts the Tor OP in offline mode (e.g. * networkEnabled(false)). This would only be used if you wanted to start the Tor OP so that the * install and related is all done but aren't ready to actually connect it to the network. * * @return True if all files installed and Tor OP successfully started * @throws java.io.IOException - IO Exceptions * @throws java.lang.InterruptedException - If we are, well, interrupted */ public synchronized boolean installAndStartTorOp() throws IOException, InterruptedException { // The Tor OP will die if it looses the connection to its socket so if there is no controlSocket // defined // then Tor is dead. This assumes, of course, that takeOwnership works and we can't end up with // Zombies. if (controlConnection != null) { LOG.info("Tor is already running"); return true; } // The code below is why this method is synchronized, we don't want two instances of it running // at once // as the result would be a mess of screwed up files and connections. LOG.info("Tor is not running"); installAndConfigureFiles(); LOG.info("Starting Tor"); File cookieFile = onionProxyContext.getCookieFile(); if (cookieFile.getParentFile().exists() == false && cookieFile.getParentFile().mkdirs() == false) { throw new RuntimeException("Could not create cookieFile parent directory"); } // The original code from Briar watches individual files, not a directory and Android's file // observer // won't work on files that don't exist. Rather than take 5 seconds to rewrite Briar's code I // instead // just make sure the file exists if (cookieFile.exists() == false && cookieFile.createNewFile() == false) { throw new RuntimeException("Could not create cookieFile"); } File workingDirectory = onionProxyContext.getWorkingDirectory(); // Watch for the auth cookie file being created/updated WriteObserver cookieObserver = onionProxyContext.generateWriteObserver(cookieFile); // Start a new Tor process String torPath = onionProxyContext.getTorExecutableFile().getAbsolutePath(); String configPath = onionProxyContext.getTorrcFile().getAbsolutePath(); String pid = onionProxyContext.getProcessId(); String[] cmd = {torPath, "-f", configPath, OWNER, pid}; String[] env = onionProxyContext.getEnvironmentArgsForExec(); ProcessBuilder processBuilder = new ProcessBuilder(cmd); onionProxyContext.setEnvironmentArgsAndWorkingDirectoryForStart(processBuilder); Process torProcess = null; try { // torProcess = Runtime.getRuntime().exec(cmd, env, workingDirectory); torProcess = processBuilder.start(); CountDownLatch controlPortCountDownLatch = new CountDownLatch(1); eatStream(torProcess.getInputStream(), false, controlPortCountDownLatch); eatStream(torProcess.getErrorStream(), true, null); // On platforms other than Windows we run as a daemon and so we need to wait for the process // to detach // or exit. In the case of Windows the equivalent is running as a service and unfortunately // that requires // managing the service, such as turning it off or uninstalling it when it's time to move on. // Any number // of errors can prevent us from doing the cleanup and so we would leave the process running // around. Rather // than do that on Windows we just let the process run on the exec and hence don't look for an // exit code. // This does create a condition where the process has exited due to a problem but we should // hopefully // detect that when we try to use the control connection. if (OsData.getOsType() != OsData.OsType.Windows) { int exit = torProcess.waitFor(); torProcess = null; if (exit != 0) { LOG.warn("Tor exited with value " + exit); return false; } } // Wait for the auth cookie file to be created/updated if (!cookieObserver.poll(COOKIE_TIMEOUT, MILLISECONDS)) { LOG.warn("Auth cookie not created"); FileUtilities.listFilesToLog(workingDirectory); return false; } // Now we should be able to connect to the new process controlPortCountDownLatch.await(); controlSocket = new Socket("127.0.0.1", control_port); // Open a control connection and authenticate using the cookie file TorControlConnection controlConnection = new TorControlConnection(controlSocket); controlConnection.authenticate(FileUtilities.read(cookieFile)); // Tell Tor to exit when the control connection is closed controlConnection.takeOwnership(); controlConnection.resetConf(Collections.singletonList(OWNER)); // Register to receive events from the Tor process controlConnection.setEventHandler(new OnionProxyManagerEventHandler()); controlConnection.setEvents(Arrays.asList(EVENTS)); // We only set the class property once the connection is in a known good state this.controlConnection = controlConnection; return true; } catch (SecurityException e) { LOG.warn(e.toString(), e); return false; } catch (InterruptedException e) { LOG.warn("Interrupted while starting Tor", e); Thread.currentThread().interrupt(); return false; } finally { if (controlConnection == null && torProcess != null) { // It's possible that something 'bad' could happen after we executed exec but before we // takeOwnership() // in which case the Tor OP will hang out as a zombie until this process is killed. This is // problematic // when we want to do things like torProcess.destroy(); } } }