From 5751958eaf2da2caa4b2c16ef79448747b889e87 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Tue, 2 Apr 2019 16:52:28 +0100 Subject: [PATCH] Add an absurd amount of logging. --- .../tor/control/TorControlConnection.java | 1724 +++++++++-------- 1 file changed, 930 insertions(+), 794 deletions(-) diff --git a/bramble-core/src/main/java/net/freehaven/tor/control/TorControlConnection.java b/bramble-core/src/main/java/net/freehaven/tor/control/TorControlConnection.java index 9c5d8f08d..607edcbc7 100644 --- a/bramble-core/src/main/java/net/freehaven/tor/control/TorControlConnection.java +++ b/bramble-core/src/main/java/net/freehaven/tor/control/TorControlConnection.java @@ -13,7 +13,6 @@ import java.io.PrintWriter; import java.io.Reader; import java.io.Writer; import java.net.Socket; -import java.net.SocketException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -22,841 +21,978 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; -import java.util.concurrent.CancellationException; +import java.util.logging.Logger; -/** A connection to a running Tor process as specified in control-spec.txt. */ +import static java.util.logging.Logger.getLogger; + +/** + * A connection to a running Tor process as specified in control-spec.txt. + */ public class TorControlConnection implements TorControlCommands { - private final LinkedList waiters; - private final BufferedReader input; - private final Writer output; + private static final Logger LOG = + getLogger(TorControlConnection.class.getName()); - private ControlParseThread thread; // Locking: this + private final LinkedList waiters; + private final BufferedReader input; + private final Writer output; - private volatile EventHandler handler; - private volatile PrintWriter debugOutput; - private volatile IOException parseThreadException; + private ControlParseThread thread; // Locking: this - static class Waiter { + private volatile EventHandler handler; + private volatile PrintWriter debugOutput; + private volatile IOException parseThreadException; - List response; // Locking: this - boolean interrupted; + static class Waiter { - synchronized List getResponse() throws InterruptedException { - while (response == null) { - wait(); - if (interrupted) { - throw new InterruptedException(); - } - } - return response; - } + List response; // Locking: this + boolean interrupted; - synchronized void setResponse(List response) { - this.response = response; - notifyAll(); - } - - synchronized void interrupt() { - interrupted = true; - notifyAll(); - } - } - - static class ReplyLine { - - final String status; - final String msg; - final String rest; - - ReplyLine(String status, String msg, String rest) { - this.status = status; this.msg = msg; this.rest = rest; - } - } - - /** Create a new TorControlConnection to communicate with Tor over - * a given socket. After calling this constructor, it is typical to - * call launchThread and authenticate. */ - public TorControlConnection(Socket connection) throws IOException { - this(connection.getInputStream(), connection.getOutputStream()); - } - - /** Create a new TorControlConnection to communicate with Tor over - * an arbitrary pair of data streams. - */ - public TorControlConnection(InputStream i, OutputStream o) { - this(new InputStreamReader(i), new OutputStreamWriter(o)); - } - - public TorControlConnection(Reader i, Writer o) { - this.output = o; - if (i instanceof BufferedReader) - this.input = (BufferedReader) i; - else - this.input = new BufferedReader(i); - this.waiters = new LinkedList(); - } - - protected final void writeEscaped(String s) throws IOException { - StringTokenizer st = new StringTokenizer(s, "\n"); - while (st.hasMoreTokens()) { - String line = st.nextToken(); - if (line.startsWith(".")) - line = "."+line; - if (line.endsWith("\r")) - line += "\n"; - else - line += "\r\n"; - if (debugOutput != null) - debugOutput.print(">> "+line); - output.write(line); - } - output.write(".\r\n"); - if (debugOutput != null) - debugOutput.print(">> .\n"); - } - - protected static final String quote(String s) { - StringBuffer sb = new StringBuffer("\""); - for (int i = 0; i < s.length(); ++i) { - char c = s.charAt(i); - switch (c) - { - case '\r': - case '\n': - case '\\': - case '\"': - sb.append('\\'); - } - sb.append(c); - } - sb.append('\"'); - return sb.toString(); - } - - protected final ArrayList readReply() throws IOException { - ArrayList reply = new ArrayList(); - char c; - do { - String line = input.readLine(); - if (line == null) { - // if line is null, the end of the stream has been reached, i.e. - // the connection to Tor has been closed! - if (reply.isEmpty()) { - // nothing received so far, can exit cleanly - return reply; - } - // received half of a reply before the connection broke down - throw new TorControlSyntaxError("Connection to Tor " + - " broke down while receiving reply!"); - } - if (debugOutput != null) - debugOutput.println("<< "+line); - if (line.length() < 4) - throw new TorControlSyntaxError("Line (\""+line+"\") too short"); - String status = line.substring(0,3); - c = line.charAt(3); - String msg = line.substring(4); - String rest = null; - if (c == '+') { - StringBuffer data = new StringBuffer(); - while (true) { - line = input.readLine(); - if (debugOutput != null) - debugOutput.print("<< "+line); - if (line.equals(".")) - break; - else if (line.startsWith(".")) - line = line.substring(1); - data.append(line).append('\n'); - } - rest = data.toString(); - } - reply.add(new ReplyLine(status, msg, rest)); - } while (c != ' '); - - return reply; - } - - protected synchronized List sendAndWaitForResponse(String s, - String rest) throws IOException { - if (parseThreadException != null) throw parseThreadException; - checkThread(); - Waiter w = new Waiter(); - if (debugOutput != null) - debugOutput.print(">> "+s); - synchronized (waiters) { - output.write(s); - if (rest != null) - writeEscaped(rest); - output.flush(); - waiters.addLast(w); - } - List lst; - try { - lst = w.getResponse(); - } catch (InterruptedException ex) { - throw new IOException("Interrupted"); - } - for (Iterator i = lst.iterator(); i.hasNext(); ) { - ReplyLine c = i.next(); - if (! c.status.startsWith("2")) - throw new TorControlError("Error reply: "+c.msg); - } - return lst; - } - - /** Helper: decode a CMD_EVENT command and dispatch it to our - * EventHandler (if any). */ - protected void handleEvent(ArrayList events) { - if (handler == null) - return; - - for (Iterator i = events.iterator(); i.hasNext(); ) { - ReplyLine line = i.next(); - int idx = line.msg.indexOf(' '); - String tp = line.msg.substring(0, idx).toUpperCase(); - String rest = line.msg.substring(idx+1); - if (tp.equals("CIRC")) { - List lst = Bytes.splitStr(null, rest); - handler.circuitStatus(lst.get(1), - lst.get(0), - lst.get(1).equals("LAUNCHED") - || lst.size() < 3 ? "" - : lst.get(2)); - } else if (tp.equals("STREAM")) { - List lst = Bytes.splitStr(null, rest); - handler.streamStatus(lst.get(1), - lst.get(0), - lst.get(3)); - // XXXX circID. - } else if (tp.equals("ORCONN")) { - List lst = Bytes.splitStr(null, rest); - handler.orConnStatus(lst.get(1), lst.get(0)); - } else if (tp.equals("BW")) { - List lst = Bytes.splitStr(null, rest); - handler.bandwidthUsed(Integer.parseInt(lst.get(0)), - Integer.parseInt(lst.get(1))); - } else if (tp.equals("NEWDESC")) { - List lst = Bytes.splitStr(null, rest); - handler.newDescriptors(lst); - } else if (tp.equals("DEBUG") || - tp.equals("INFO") || - tp.equals("NOTICE") || - tp.equals("WARN") || - tp.equals("ERR")) { - handler.message(tp, rest); - } else { - handler.unrecognized(tp, rest); - } - } - } - - - /** Sets w as the PrintWriter for debugging output, - * which writes out all messages passed between Tor and the controller. - * Outgoing messages are preceded by "\>\>" and incoming messages are preceded - * by "\<\<" - */ - public void setDebugging(PrintWriter w) { - debugOutput = w; - } - - /** Sets s as the PrintStream for debugging output, - * which writes out all messages passed between Tor and the controller. - * Outgoing messages are preceded by "\>\>" and incoming messages are preceded - * by "\<\<" - */ - public void setDebugging(PrintStream s) { - debugOutput = new PrintWriter(s, true); - } - - /** Set the EventHandler object that will be notified of any - * events Tor delivers to this connection. To make Tor send us - * events, call setEvents(). */ - public void setEventHandler(EventHandler handler) { - this.handler = handler; - } - - /** - * Start a thread to react to Tor's responses in the background. - * This is necessary to handle asynchronous events and synchronous - * responses that arrive independantly over the same socket. - */ - public synchronized Thread launchThread(boolean daemon) { - ControlParseThread th = new ControlParseThread(); - if (daemon) - th.setDaemon(true); - th.start(); - this.thread = th; - return th; - } - - protected class ControlParseThread extends Thread { - - @Override - public void run() { - try { - react(); - } catch (IOException ex) { - parseThreadException = ex; - } - } - } - - protected synchronized void checkThread() { - if (thread == null) - launchThread(true); - } - - /** helper: implement the main background loop. */ - protected void react() throws IOException { - while (true) { - ArrayList lst = readReply(); - if (lst.isEmpty()) { - // interrupted queued waiters, there won't be any response. - synchronized (waiters) { - if (!waiters.isEmpty()) { - for (Waiter w : waiters) { - w.interrupt(); - } - } - } - throw new IOException("Tor is no longer running"); - } - if ((lst.get(0)).status.startsWith("6")) - handleEvent(lst); - else { - synchronized (waiters) { - if (!waiters.isEmpty()) - { - Waiter w; - w = waiters.removeFirst(); - w.setResponse(lst); + List getResponse() throws InterruptedException { + LOG.info("Entering synchronized (waiter " + hashCode() + ")"); + synchronized (this) { + LOG.info("Entered synchronized (waiter " + hashCode() + ")"); + while (response == null) { + LOG.info("Waiter " + hashCode() + " waiting for response"); + wait(); + if (interrupted) { + LOG.info("Waiter " + hashCode() + " interrupted"); + throw new InterruptedException(); + } + } + LOG.info("Waiter " + hashCode() + " got response " + response); + LOG.info("Leaving synchronized (waiter " + hashCode() + ")"); + return response; + } } - } - } - } - } + void setResponse(List response) { + LOG.info("Entering synchronized (waiter " + hashCode() + ")"); + synchronized (this) { + LOG.info("Entered synchronized (waiter " + hashCode() + ")"); + LOG.info("Setting response for waiter " + hashCode() + ": " + + response); + this.response = response; + notifyAll(); + LOG.info("Leaving synchronized (waiter " + hashCode() + ")"); + } + } - /** Change the value of the configuration option 'key' to 'val'. - */ - public void setConf(String key, String value) throws IOException { - List lst = new ArrayList(); - lst.add(key+" "+value); - setConf(lst); - } + void interrupt() { + LOG.info("Entering synchronized (waiter " + hashCode() + ")"); + synchronized (this) { + LOG.info("Entered synchronized (waiter " + hashCode() + ")"); + LOG.info("Interrupting waiter " + hashCode()); + interrupted = true; + notifyAll(); + LOG.info("Leaving synchronized (waiter " + hashCode() + ")"); + } + } + } - /** Change the values of the configuration options stored in kvMap. */ - public void setConf(Map kvMap) throws IOException { - List lst = new ArrayList(); - for (Iterator> it = kvMap.entrySet().iterator(); it.hasNext(); ) { - Map.Entry ent = it.next(); - lst.add(ent.getKey()+" "+ent.getValue()+"\n"); - } - setConf(lst); - } + static class ReplyLine { - /** Changes the values of the configuration options stored in - * kvList. Each list element in kvList is expected to be - * String of the format "key value". - * - * Tor behaves as though it had just read each of the key-value pairs - * from its configuration file. Keywords with no corresponding values have - * their configuration values reset to their defaults. setConf is - * all-or-nothing: if there is an error in any of the configuration settings, - * Tor sets none of them. - * - * When a configuration option takes multiple values, or when multiple - * configuration keys form a context-sensitive group (see getConf below), then - * setting any of the options in a setConf command is taken to reset all of - * the others. For example, if two ORBindAddress values are configured, and a - * command arrives containing a single ORBindAddress value, the new - * command's value replaces the two old values. - * - * To remove all settings for a given option entirely (and go back to its - * default value), include a String in kvList containing the key and no value. - */ - public void setConf(Collection kvList) throws IOException { - if (kvList.size() == 0) - return; - StringBuffer b = new StringBuffer("SETCONF"); - for (Iterator it = kvList.iterator(); it.hasNext(); ) { - String kv = it.next(); - int i = kv.indexOf(' '); - if (i == -1) - b.append(" ").append(kv); - b.append(" ").append(kv.substring(0,i)).append("=") - .append(quote(kv.substring(i+1))); - } - b.append("\r\n"); - sendAndWaitForResponse(b.toString(), null); - } + final String status; + final String msg; + final String rest; - /** Try to reset the values listed in the collection 'keys' to their - * default values. - **/ - public void resetConf(Collection keys) throws IOException { - if (keys.size() == 0) - return; - StringBuffer b = new StringBuffer("RESETCONF"); - for (Iterator it = keys.iterator(); it.hasNext(); ) { - String key = it.next(); - b.append(" ").append(key); - } - b.append("\r\n"); - sendAndWaitForResponse(b.toString(), null); - } + ReplyLine(String status, String msg, String rest) { + this.status = status; + this.msg = msg; + this.rest = rest; + } - /** Return the value of the configuration option 'key' */ - public List getConf(String key) throws IOException { - List lst = new ArrayList(); - lst.add(key); - return getConf(lst); - } + @Override + public String toString() { + return status + " " + msg + " " + rest; + } + } - /** Requests the values of the configuration variables listed in keys. - * Results are returned as a list of ConfigEntry objects. - * - * If an option appears multiple times in the configuration, all of its - * key-value pairs are returned in order. - * - * Some options are context-sensitive, and depend on other options with - * different keywords. These cannot be fetched directly. Currently there - * is only one such option: clients should use the "HiddenServiceOptions" - * virtual keyword to get all HiddenServiceDir, HiddenServicePort, - * HiddenServiceNodes, and HiddenServiceExcludeNodes option settings. - */ - public List getConf(Collection keys) throws IOException { - StringBuffer sb = new StringBuffer("GETCONF"); - for (Iterator it = keys.iterator(); it.hasNext(); ) { - String key = it.next(); - sb.append(" ").append(key); - } - sb.append("\r\n"); - List lst = sendAndWaitForResponse(sb.toString(), null); - List result = new ArrayList(); - for (Iterator it = lst.iterator(); it.hasNext(); ) { - String kv = (it.next()).msg; - int idx = kv.indexOf('='); - if (idx >= 0) - result.add(new ConfigEntry(kv.substring(0, idx), - kv.substring(idx+1))); - else - result.add(new ConfigEntry(kv)); - } - return result; - } + /** + * Create a new TorControlConnection to communicate with Tor over + * a given socket. After calling this constructor, it is typical to + * call launchThread and authenticate. + */ + public TorControlConnection(Socket connection) throws IOException { + this(connection.getInputStream(), connection.getOutputStream()); + } - /** Request that the server inform the client about interesting events. - * Each element of events is one of the following Strings: - * ["CIRC" | "STREAM" | "ORCONN" | "BW" | "DEBUG" | - * "INFO" | "NOTICE" | "WARN" | "ERR" | "NEWDESC" | "ADDRMAP"] . - * - * Any events not listed in the events are turned off; thus, calling - * setEvents with an empty events argument turns off all event reporting. - */ - public void setEvents(List events) throws IOException { - StringBuffer sb = new StringBuffer("SETEVENTS"); - for (Iterator it = events.iterator(); it.hasNext(); ) { - sb.append(" ").append(it.next()); - } - sb.append("\r\n"); - sendAndWaitForResponse(sb.toString(), null); - } + /** + * Create a new TorControlConnection to communicate with Tor over + * an arbitrary pair of data streams. + */ + public TorControlConnection(InputStream i, OutputStream o) { + this(new InputStreamReader(i), new OutputStreamWriter(o)); + } - /** Authenticates the controller to the Tor server. - * - * By default, the current Tor implementation trusts all local users, and - * the controller can authenticate itself by calling authenticate(new byte[0]). - * - * If the 'CookieAuthentication' option is true, Tor writes a "magic cookie" - * file named "control_auth_cookie" into its data directory. To authenticate, - * the controller must send the contents of this file in auth. - * - * If the 'HashedControlPassword' option is set, auth must contain the salted - * hash of a secret password. The salted hash is computed according to the - * S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier. - * This is then encoded in hexadecimal, prefixed by the indicator sequence - * "16:". - * - * You can generate the salt of a password by calling - * 'tor --hash-password ' - * or by using the provided PasswordDigest class. - * To authenticate under this scheme, the controller sends Tor the original - * secret that was used to generate the password. - */ - public void authenticate(byte[] auth) throws IOException { - String cmd = "AUTHENTICATE " + Bytes.hex(auth) + "\r\n"; - sendAndWaitForResponse(cmd, null); - } + public TorControlConnection(Reader i, Writer o) { + this.output = o; + if (i instanceof BufferedReader) + this.input = (BufferedReader) i; + else + this.input = new BufferedReader(i); + this.waiters = new LinkedList<>(); + } - /** Instructs the server to write out its configuration options into its torrc. - */ - public void saveConf() throws IOException { - sendAndWaitForResponse("SAVECONF\r\n", null); - } + protected final void writeEscaped(String s) throws IOException { + StringTokenizer st = new StringTokenizer(s, "\n"); + while (st.hasMoreTokens()) { + String line = st.nextToken(); + if (line.startsWith(".")) + line = "." + line; + if (line.endsWith("\r")) + line += "\n"; + else + line += "\r\n"; + if (debugOutput != null) + debugOutput.print(">> " + line); + output.write(line); + } + output.write(".\r\n"); + if (debugOutput != null) + debugOutput.print(">> .\n"); + } - /** Sends a signal from the controller to the Tor server. - * signal is one of the following Strings: - *
    - *
  • "RELOAD" or "HUP" : Reload config items, refetch directory
  • - *
  • "SHUTDOWN" or "INT" : Controlled shutdown: if server is an OP, exit immediately. - * If it's an OR, close listeners and exit after 30 seconds
  • - *
  • "DUMP" or "USR1" : Dump stats: log information about open connections and circuits
  • - *
  • "DEBUG" or "USR2" : Debug: switch all open logs to loglevel debug
  • - *
  • "HALT" or "TERM" : Immediate shutdown: clean up and exit now
  • - *
- */ - public void signal(String signal) throws IOException { - String cmd = "SIGNAL " + signal + "\r\n"; - sendAndWaitForResponse(cmd, null); - } + protected static String quote(String s) { + StringBuffer sb = new StringBuffer("\""); + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + switch (c) { + case '\r': + case '\n': + case '\\': + case '\"': + sb.append('\\'); + } + sb.append(c); + } + sb.append('\"'); + return sb.toString(); + } - /** Send a signal to the Tor process to shut it down or halt it. - * Does not wait for a response. */ - public void shutdownTor(String signal) throws IOException { - String s = "SIGNAL " + signal + "\r\n"; - Waiter w = new Waiter(); - if (debugOutput != null) - debugOutput.print(">> "+s); - synchronized (waiters) { - output.write(s); - output.flush(); - } - } + protected final ArrayList readReply() throws IOException { + ArrayList reply = new ArrayList<>(); + char c; + do { + String line = input.readLine(); + if (line == null) { + // if line is null, the end of the stream has been reached, i.e. + // the connection to Tor has been closed! + if (reply.isEmpty()) { + // nothing received so far, can exit cleanly + return reply; + } + // received half of a reply before the connection broke down + throw new TorControlSyntaxError("Connection to Tor " + + " broke down while receiving reply!"); + } + if (debugOutput != null) + debugOutput.println("<< " + line); + if (line.length() < 4) + throw new TorControlSyntaxError( + "Line (\"" + line + "\") too short"); + String status = line.substring(0, 3); + c = line.charAt(3); + String msg = line.substring(4); + String rest = null; + if (c == '+') { + StringBuffer data = new StringBuffer(); + while (true) { + line = input.readLine(); + if (debugOutput != null) + debugOutput.print("<< " + line); + if (line.equals(".")) + break; + else if (line.startsWith(".")) + line = line.substring(1); + data.append(line).append('\n'); + } + rest = data.toString(); + } + reply.add(new ReplyLine(status, msg, rest)); + } while (c != ' '); - /** Tells the Tor server that future SOCKS requests for connections to a set of original - * addresses should be replaced with connections to the specified replacement - * addresses. Each element of kvLines is a String of the form - * "old-address new-address". This function returns the new address mapping. - * - * The client may decline to provide a body for the original address, and - * instead send a special null address ("0.0.0.0" for IPv4, "::0" for IPv6, or - * "." for hostname), signifying that the server should choose the original - * address itself, and return that address in the reply. The server - * should ensure that it returns an element of address space that is unlikely - * to be in actual use. If there is already an address mapped to the - * destination address, the server may reuse that mapping. - * - * If the original address is already mapped to a different address, the old - * mapping is removed. If the original address and the destination address - * are the same, the server removes any mapping in place for the original - * address. - * - * Mappings set by the controller last until the Tor process exits: - * they never expire. If the controller wants the mapping to last only - * a certain time, then it must explicitly un-map the address when that - * time has elapsed. - */ - public Map mapAddresses(Collection kvLines) throws IOException { - StringBuffer sb = new StringBuffer("MAPADDRESS"); - for (Iterator it = kvLines.iterator(); it.hasNext(); ) { - String kv = it.next(); - int i = kv.indexOf(' '); - sb.append(" ").append(kv.substring(0,i)).append("=") - .append(quote(kv.substring(i+1))); - } - sb.append("\r\n"); - List lst = sendAndWaitForResponse(sb.toString(), null); - Map result = new HashMap(); - for (Iterator it = lst.iterator(); it.hasNext(); ) { - String kv = (it.next()).msg; - int idx = kv.indexOf('='); - result.put(kv.substring(0, idx), - kv.substring(idx+1)); - } - return result; - } + return reply; + } - public Map mapAddresses(Map addresses) throws IOException { - List kvList = new ArrayList(); - for (Iterator> it = addresses.entrySet().iterator(); it.hasNext(); ) { - Map.Entry e = it.next(); - kvList.add(e.getKey()+" "+e.getValue()); - } - return mapAddresses(kvList); - } + protected List sendAndWaitForResponse(String s, + String rest) throws IOException { + LOG.info("Entering synchronized (connection)"); + synchronized (this) { + LOG.info("Entered synchronized (connection)"); + LOG.info("Sending '" + s + "', '" + rest + + "' and waiting for response"); + if (parseThreadException != null) { + LOG.info("Throwing previously caught exception " + + parseThreadException); + throw parseThreadException; + } + checkThread(); + Waiter w = new Waiter(); + LOG.info("Created waiter " + w.hashCode()); + if (debugOutput != null) + debugOutput.print(">> " + s); + LOG.info("Entering synchronized (waiters)"); + synchronized (waiters) { + LOG.info("Entered synchronized (waiters)"); + output.write(s); + LOG.info("Wrote '" + s + "'"); + if (rest != null) { + writeEscaped(rest); + LOG.info("Wrote escaped '" + rest + "'"); + } + output.flush(); + LOG.info("Flushed output"); + waiters.addLast(w); + LOG.info("Added waiter, " + waiters.size() + " waiting"); + LOG.info("Leaving synchronized (waiters)"); + } + List lst; + try { + LOG.info("Getting response from waiter " + w.hashCode()); + lst = w.getResponse(); + LOG.info("Got response from waiter " + w.hashCode() + ": " + + lst); + } catch (InterruptedException ex) { + throw new IOException("Interrupted"); + } + for (Iterator i = lst.iterator(); i.hasNext(); ) { + ReplyLine c = i.next(); + if (!c.status.startsWith("2")) + throw new TorControlError("Error reply: " + c.msg); + } + LOG.info("Leaving synchronized (connection)"); + return lst; + } + } - public String mapAddress(String fromAddr, String toAddr) throws IOException { - List lst = new ArrayList(); - lst.add(fromAddr+" "+toAddr+"\n"); - Map m = mapAddresses(lst); - return m.get(fromAddr); - } + /** + * Helper: decode a CMD_EVENT command and dispatch it to our + * EventHandler (if any). + */ + protected void handleEvent(ArrayList events) { + if (handler == null) + return; - /** Queries the Tor server for keyed values that are not stored in the torrc - * configuration file. Returns a map of keys to values. - * - * Recognized keys include: - *
    - *
  • "version" : The version of the server's software, including the name - * of the software. (example: "Tor 0.0.9.4")
  • - *
  • "desc/id/" or "desc/name/" : the latest server - * descriptor for a given OR, NUL-terminated. If no such OR is known, the - * corresponding value is an empty string.
  • - *
  • "network-status" : a space-separated list of all known OR identities. - * This is in the same format as the router-status line in directories; - * see tor-spec.txt for details.
  • - *
  • "addr-mappings/all"
  • - *
  • "addr-mappings/config"
  • - *
  • "addr-mappings/cache"
  • - *
  • "addr-mappings/control" : a space-separated list of address mappings, each - * in the form of "from-address=to-address". The 'config' key - * returns those address mappings set in the configuration; the 'cache' - * key returns the mappings in the client-side DNS cache; the 'control' - * key returns the mappings set via the control interface; the 'all' - * target returns the mappings set through any mechanism.
  • - *
  • "circuit-status" : A series of lines as for a circuit status event. Each line is of the form: - * "CircuitID CircStatus Path"
  • - *
  • "stream-status" : A series of lines as for a stream status event. Each is of the form: - * "StreamID StreamStatus CircID Target"
  • - *
  • "orconn-status" : A series of lines as for an OR connection status event. Each is of the - * form: "ServerID ORStatus"
  • - *
- */ - public Map getInfo(Collection keys) throws IOException { - StringBuffer sb = new StringBuffer("GETINFO"); - for (Iterator it = keys.iterator(); it.hasNext(); ) { - sb.append(" ").append(it.next()); - } - sb.append("\r\n"); - List lst = sendAndWaitForResponse(sb.toString(), null); - Map m = new HashMap(); - for (Iterator it = lst.iterator(); it.hasNext(); ) { - ReplyLine line = it.next(); - int idx = line.msg.indexOf('='); - if (idx<0) - break; - String k = line.msg.substring(0,idx); - String v; - if (line.rest != null) { - v = line.rest; - } else { - v = line.msg.substring(idx+1); - } - m.put(k, v); - } - return m; - } + for (Iterator i = events.iterator(); i.hasNext(); ) { + ReplyLine line = i.next(); + int idx = line.msg.indexOf(' '); + String tp = line.msg.substring(0, idx).toUpperCase(); + String rest = line.msg.substring(idx + 1); + if (tp.equals("CIRC")) { + List lst = Bytes.splitStr(null, rest); + handler.circuitStatus(lst.get(1), + lst.get(0), + lst.get(1).equals("LAUNCHED") + || lst.size() < 3 ? "" + : lst.get(2)); + } else if (tp.equals("STREAM")) { + List lst = Bytes.splitStr(null, rest); + handler.streamStatus(lst.get(1), + lst.get(0), + lst.get(3)); + // XXXX circID. + } else if (tp.equals("ORCONN")) { + List lst = Bytes.splitStr(null, rest); + handler.orConnStatus(lst.get(1), lst.get(0)); + } else if (tp.equals("BW")) { + List lst = Bytes.splitStr(null, rest); + handler.bandwidthUsed(Integer.parseInt(lst.get(0)), + Integer.parseInt(lst.get(1))); + } else if (tp.equals("NEWDESC")) { + List lst = Bytes.splitStr(null, rest); + handler.newDescriptors(lst); + } else if (tp.equals("DEBUG") || + tp.equals("INFO") || + tp.equals("NOTICE") || + tp.equals("WARN") || + tp.equals("ERR")) { + handler.message(tp, rest); + } else { + handler.unrecognized(tp, rest); + } + } + } + /** + * Sets w as the PrintWriter for debugging output, + * which writes out all messages passed between Tor and the controller. + * Outgoing messages are preceded by "\>\>" and incoming messages are preceded + * by "\<\<" + */ + public void setDebugging(PrintWriter w) { + debugOutput = w; + } - /** Return the value of the information field 'key' */ - public String getInfo(String key) throws IOException { - List lst = new ArrayList(); - lst.add(key); - Map m = getInfo(lst); - return m.get(key); - } + /** + * Sets s as the PrintStream for debugging output, + * which writes out all messages passed between Tor and the controller. + * Outgoing messages are preceded by "\>\>" and incoming messages are preceded + * by "\<\<" + */ + public void setDebugging(PrintStream s) { + debugOutput = new PrintWriter(s, true); + } - /** An extendCircuit request takes one of two forms: either the circID is zero, in - * which case it is a request for the server to build a new circuit according - * to the specified path, or the circID is nonzero, in which case it is a - * request for the server to extend an existing circuit with that ID according - * to the specified path. - * - * If successful, returns the Circuit ID of the (maybe newly created) circuit. - */ - public String extendCircuit(String circID, String path) throws IOException { - List lst = sendAndWaitForResponse( - "EXTENDCIRCUIT "+circID+" "+path+"\r\n", null); - return (lst.get(0)).msg; - } + /** + * Set the EventHandler object that will be notified of any + * events Tor delivers to this connection. To make Tor send us + * events, call setEvents(). + */ + public void setEventHandler(EventHandler handler) { + this.handler = handler; + } - /** Informs the Tor server that the stream specified by streamID should be - * associated with the circuit specified by circID. - * - * Each stream may be associated with - * at most one circuit, and multiple streams may share the same circuit. - * Streams can only be attached to completed circuits (that is, circuits that - * have sent a circuit status "BUILT" event or are listed as built in a - * getInfo circuit-status request). - * - * If circID is 0, responsibility for attaching the given stream is - * returned to Tor. - * - * By default, Tor automatically attaches streams to - * circuits itself, unless the configuration variable - * "__LeaveStreamsUnattached" is set to "1". Attempting to attach streams - * via TC when "__LeaveStreamsUnattached" is false may cause a race between - * Tor and the controller, as both attempt to attach streams to circuits. - */ - public void attachStream(String streamID, String circID) - throws IOException { - sendAndWaitForResponse("ATTACHSTREAM "+streamID+" "+circID+"\r\n", null); - } + /** + * Start a thread to react to Tor's responses in the background. + * This is necessary to handle asynchronous events and synchronous + * responses that arrive independantly over the same socket. + */ + public Thread launchThread(boolean daemon) { + LOG.info("Entering synchronized (connection)"); + synchronized (this) { + LOG.info("Entered synchronized (connection)"); + ControlParseThread th = new ControlParseThread(); + LOG.info("Launching parse thread " + th.hashCode()); + if (daemon) + th.setDaemon(true); + th.start(); + this.thread = th; + LOG.info("Leaving synchronized (connection)"); + return th; + } + } - /** Tells Tor about the server descriptor in desc. - * - * The descriptor, when parsed, must contain a number of well-specified - * fields, including fields for its nickname and identity. - */ - // More documentation here on format of desc? - // No need for return value? control-spec.txt says reply is merely "250 OK" on success... - public String postDescriptor(String desc) throws IOException { - List lst = sendAndWaitForResponse("+POSTDESCRIPTOR\r\n", desc); - return (lst.get(0)).msg; - } + protected class ControlParseThread extends Thread { - /** Tells Tor to change the exit address of the stream identified by streamID - * to address. No remapping is performed on the new provided address. - * - * To be sure that the modified address will be used, this event must be sent - * after a new stream event is received, and before attaching this stream to - * a circuit. - */ - public void redirectStream(String streamID, String address) throws IOException { - sendAndWaitForResponse("REDIRECTSTREAM "+streamID+" "+address+"\r\n", - null); - } + @Override + public void run() { + try { + react(); + } catch (IOException ex) { + LOG.info("Parse thread " + hashCode() + + " caught exception " + ex); + parseThreadException = ex; + } + } + } - /** Tells Tor to close the stream identified by streamID. - * reason should be one of the Tor RELAY_END reasons given in tor-spec.txt, as a decimal: - *
    - *
  • 1 -- REASON_MISC (catch-all for unlisted reasons)
  • - *
  • 2 -- REASON_RESOLVEFAILED (couldn't look up hostname)
  • - *
  • 3 -- REASON_CONNECTREFUSED (remote host refused connection)
  • - *
  • 4 -- REASON_EXITPOLICY (OR refuses to connect to host or port)
  • - *
  • 5 -- REASON_DESTROY (Circuit is being destroyed)
  • - *
  • 6 -- REASON_DONE (Anonymized TCP connection was closed)
  • - *
  • 7 -- REASON_TIMEOUT (Connection timed out, or OR timed out while connecting)
  • - *
  • 8 -- (unallocated)
  • - *
  • 9 -- REASON_HIBERNATING (OR is temporarily hibernating)
  • - *
  • 10 -- REASON_INTERNAL (Internal error at the OR)
  • - *
  • 11 -- REASON_RESOURCELIMIT (OR has no resources to fulfill request)
  • - *
  • 12 -- REASON_CONNRESET (Connection was unexpectedly reset)
  • - *
  • 13 -- REASON_TORPROTOCOL (Sent when closing connection because of Tor protocol violations)
  • - *
- * - * Tor may hold the stream open for a while to flush any data that is pending. - */ - public void closeStream(String streamID, byte reason) - throws IOException { - sendAndWaitForResponse("CLOSESTREAM "+streamID+" "+reason+"\r\n",null); - } + protected void checkThread() { + LOG.info("Entering synchronized (connection)"); + synchronized (this) { + LOG.info("Entered synchronized (connection)"); + if (thread == null) + launchThread(true); + LOG.info("Leaving synchronized (connection)"); + } + } - /** Tells Tor to close the circuit identified by circID. - * If ifUnused is true, do not close the circuit unless it is unused. - */ - public void closeCircuit(String circID, boolean ifUnused) throws IOException { - sendAndWaitForResponse("CLOSECIRCUIT "+circID+ - (ifUnused?" IFUNUSED":"")+"\r\n", null); - } + /** + * helper: implement the main background loop. + */ + protected void react() throws IOException { + while (true) { + ArrayList lst = readReply(); + LOG.info("Read reply: " + lst); + if (lst.isEmpty()) { + // interrupted queued waiters, there won't be any response. + LOG.info("Entering synchronized (waiters)"); + synchronized (waiters) { + LOG.info("Entered synchronized (waiters)"); + if (!waiters.isEmpty()) { + for (Waiter w : waiters) { + LOG.info("Interrupting waiter " + w.hashCode()); + w.interrupt(); + } + } else { + LOG.info("No waiters"); + } + LOG.info("Leaving synchronized (waiters)"); + } + throw new IOException("Tor is no longer running"); + } + if ((lst.get(0)).status.startsWith("6")) { + LOG.info("Reply is an event"); + handleEvent(lst); + } else { + LOG.info("Entering synchronized (waiters)"); + synchronized (waiters) { + LOG.info("Entered synchronized (waiters)"); + if (!waiters.isEmpty()) { + Waiter w; + w = waiters.removeFirst(); + LOG.info("Setting response for waiter " + w.hashCode()); + w.setResponse(lst); + } else { + LOG.info("No waiters"); + } + LOG.info("Leaving synchronized (waiters)"); + } - /** Tells Tor to exit when this control connection is closed. This command - * was added in Tor 0.2.2.28-beta. - */ - public void takeOwnership() throws IOException { - sendAndWaitForResponse("TAKEOWNERSHIP\r\n", null); - } + } + } + } - /** - * Tells Tor to generate and set up a new onion service using the best - * supported algorithm. - *

- * ADD_ONION was added in Tor 0.2.7.1-alpha. - */ - public Map addOnion(Map portLines) - throws IOException { - return addOnion("NEW:BEST", portLines, null); - } + /** + * Change the value of the configuration option 'key' to 'val'. + */ + public void setConf(String key, String value) throws IOException { + List lst = new ArrayList<>(); + lst.add(key + " " + value); + setConf(lst); + } - /** - * Tells Tor to generate and set up a new onion service using the best - * supported algorithm. - *

- * ADD_ONION was added in Tor 0.2.7.1-alpha. - */ - public Map addOnion(Map portLines, - boolean ephemeral, boolean detach) - throws IOException { - return addOnion("NEW:BEST", portLines, ephemeral, detach); - } + /** + * Change the values of the configuration options stored in kvMap. + */ + public void setConf(Map kvMap) throws IOException { + List lst = new ArrayList<>(); + for (Iterator> it = + kvMap.entrySet().iterator(); it.hasNext(); ) { + Map.Entry ent = it.next(); + lst.add(ent.getKey() + " " + ent.getValue() + "\n"); + } + setConf(lst); + } - /** - * Tells Tor to set up an onion service using the provided private key. - *

- * ADD_ONION was added in Tor 0.2.7.1-alpha. - */ - public Map addOnion(String privKey, - Map portLines) - throws IOException { - return addOnion(privKey, portLines, null); - } + /** + * Changes the values of the configuration options stored in + * kvList. Each list element in kvList is expected to be + * String of the format "key value". + *

+ * Tor behaves as though it had just read each of the key-value pairs + * from its configuration file. Keywords with no corresponding values have + * their configuration values reset to their defaults. setConf is + * all-or-nothing: if there is an error in any of the configuration settings, + * Tor sets none of them. + *

+ * When a configuration option takes multiple values, or when multiple + * configuration keys form a context-sensitive group (see getConf below), then + * setting any of the options in a setConf command is taken to reset all of + * the others. For example, if two ORBindAddress values are configured, and a + * command arrives containing a single ORBindAddress value, the new + * command's value replaces the two old values. + *

+ * To remove all settings for a given option entirely (and go back to its + * default value), include a String in kvList containing the key and no value. + */ + public void setConf(Collection kvList) throws IOException { + if (kvList.size() == 0) + return; + StringBuffer b = new StringBuffer("SETCONF"); + for (Iterator it = kvList.iterator(); it.hasNext(); ) { + String kv = it.next(); + int i = kv.indexOf(' '); + if (i == -1) + b.append(" ").append(kv); + b.append(" ").append(kv.substring(0, i)).append("=") + .append(quote(kv.substring(i + 1))); + } + b.append("\r\n"); + sendAndWaitForResponse(b.toString(), null); + } - /** - * Tells Tor to set up an onion service using the provided private key. - *

- * ADD_ONION was added in Tor 0.2.7.1-alpha. - */ - public Map addOnion(String privKey, - Map portLines, - boolean ephemeral, boolean detach) - throws IOException { - List flags = new ArrayList(); - if (ephemeral) - flags.add("DiscardPK"); - if (detach) - flags.add("Detach"); - return addOnion(privKey, portLines, flags); - } + /** + * Try to reset the values listed in the collection 'keys' to their + * default values. + **/ + public void resetConf(Collection keys) throws IOException { + if (keys.size() == 0) + return; + StringBuffer b = new StringBuffer("RESETCONF"); + for (Iterator it = keys.iterator(); it.hasNext(); ) { + String key = it.next(); + b.append(" ").append(key); + } + b.append("\r\n"); + sendAndWaitForResponse(b.toString(), null); + } - /** - * Tells Tor to set up an onion service. - *

- * ADD_ONION was added in Tor 0.2.7.1-alpha. - */ - public Map addOnion(String privKey, - Map portLines, - List flags) - throws IOException { - if (privKey.indexOf(':') < 0) - throw new IllegalArgumentException("Invalid privKey"); - if (portLines == null || portLines.size() < 1) - throw new IllegalArgumentException("Must provide at least one port line"); - StringBuilder b = new StringBuilder(); - b.append("ADD_ONION ").append(privKey); - if (flags != null && flags.size() > 0) { - b.append(" Flags="); - String separator = ""; - for (String flag : flags) { - b.append(separator).append(flag); - separator = ","; - } - } - for (Map.Entry portLine : portLines.entrySet()) { - int virtPort = portLine.getKey(); - String target = portLine.getValue(); - b.append(" Port=").append(virtPort); - if (target != null && target.length() > 0) - b.append(",").append(target); - } - b.append("\r\n"); - List lst = sendAndWaitForResponse(b.toString(), null); - Map ret = new HashMap(); - ret.put(HS_ADDRESS, (lst.get(0)).msg.split("=", 2)[1]); - if (lst.size() > 2) - ret.put(HS_PRIVKEY, (lst.get(1)).msg.split("=", 2)[1]); - return ret; - } + /** + * Return the value of the configuration option 'key' + */ + public List getConf(String key) throws IOException { + List lst = new ArrayList<>(); + lst.add(key); + return getConf(lst); + } - /** - * Tells Tor to take down an onion service previously set up with - * addOnion(). The hostname excludes the .onion extension. - *

- * DEL_ONION was added in Tor 0.2.7.1-alpha. - */ - public void delOnion(String hostname) throws IOException { - sendAndWaitForResponse("DEL_ONION " + hostname + "\r\n", null); - } + /** + * Requests the values of the configuration variables listed in keys. + * Results are returned as a list of ConfigEntry objects. + *

+ * If an option appears multiple times in the configuration, all of its + * key-value pairs are returned in order. + *

+ * Some options are context-sensitive, and depend on other options with + * different keywords. These cannot be fetched directly. Currently there + * is only one such option: clients should use the "HiddenServiceOptions" + * virtual keyword to get all HiddenServiceDir, HiddenServicePort, + * HiddenServiceNodes, and HiddenServiceExcludeNodes option settings. + */ + public List getConf(Collection keys) + throws IOException { + StringBuffer sb = new StringBuffer("GETCONF"); + for (Iterator it = keys.iterator(); it.hasNext(); ) { + String key = it.next(); + sb.append(" ").append(key); + } + sb.append("\r\n"); + List lst = sendAndWaitForResponse(sb.toString(), null); + List result = new ArrayList<>(); + for (Iterator it = lst.iterator(); it.hasNext(); ) { + String kv = (it.next()).msg; + int idx = kv.indexOf('='); + if (idx >= 0) + result.add(new ConfigEntry(kv.substring(0, idx), + kv.substring(idx + 1))); + else + result.add(new ConfigEntry(kv)); + } + return result; + } - /** Tells Tor to forget any cached client state relating to the hidden - * service with the given hostname (excluding the .onion extension). - */ - public void forgetHiddenService(String hostname) throws IOException { - sendAndWaitForResponse("HSFORGET " + hostname + "\r\n", null); - } + /** + * Request that the server inform the client about interesting events. + * Each element of events is one of the following Strings: + * ["CIRC" | "STREAM" | "ORCONN" | "BW" | "DEBUG" | + * "INFO" | "NOTICE" | "WARN" | "ERR" | "NEWDESC" | "ADDRMAP"] . + *

+ * Any events not listed in the events are turned off; thus, calling + * setEvents with an empty events argument turns off all event reporting. + */ + public void setEvents(List events) throws IOException { + StringBuffer sb = new StringBuffer("SETEVENTS"); + for (Iterator it = events.iterator(); it.hasNext(); ) { + sb.append(" ").append(it.next()); + } + sb.append("\r\n"); + sendAndWaitForResponse(sb.toString(), null); + } + + /** + * Authenticates the controller to the Tor server. + *

+ * By default, the current Tor implementation trusts all local users, and + * the controller can authenticate itself by calling authenticate(new byte[0]). + *

+ * If the 'CookieAuthentication' option is true, Tor writes a "magic cookie" + * file named "control_auth_cookie" into its data directory. To authenticate, + * the controller must send the contents of this file in auth. + *

+ * If the 'HashedControlPassword' option is set, auth must contain the salted + * hash of a secret password. The salted hash is computed according to the + * S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier. + * This is then encoded in hexadecimal, prefixed by the indicator sequence + * "16:". + *

+ * You can generate the salt of a password by calling + * 'tor --hash-password ' + * or by using the provided PasswordDigest class. + * To authenticate under this scheme, the controller sends Tor the original + * secret that was used to generate the password. + */ + public void authenticate(byte[] auth) throws IOException { + String cmd = "AUTHENTICATE " + Bytes.hex(auth) + "\r\n"; + sendAndWaitForResponse(cmd, null); + } + + /** + * Instructs the server to write out its configuration options into its torrc. + */ + public void saveConf() throws IOException { + sendAndWaitForResponse("SAVECONF\r\n", null); + } + + /** + * Sends a signal from the controller to the Tor server. + * signal is one of the following Strings: + *

    + *
  • "RELOAD" or "HUP" : Reload config items, refetch directory
  • + *
  • "SHUTDOWN" or "INT" : Controlled shutdown: if server is an OP, exit immediately. + * If it's an OR, close listeners and exit after 30 seconds
  • + *
  • "DUMP" or "USR1" : Dump stats: log information about open connections and circuits
  • + *
  • "DEBUG" or "USR2" : Debug: switch all open logs to loglevel debug
  • + *
  • "HALT" or "TERM" : Immediate shutdown: clean up and exit now
  • + *
+ */ + public void signal(String signal) throws IOException { + String cmd = "SIGNAL " + signal + "\r\n"; + sendAndWaitForResponse(cmd, null); + } + + /** + * Send a signal to the Tor process to shut it down or halt it. + * Does not wait for a response. + */ + public void shutdownTor(String signal) throws IOException { + String s = "SIGNAL " + signal + "\r\n"; + Waiter w = new Waiter(); + if (debugOutput != null) + debugOutput.print(">> " + s); + LOG.info("Entering synchronized (waiters)"); + synchronized (waiters) { + LOG.info("Entered synchronized (waiters)"); + output.write(s); + output.flush(); + LOG.info("Leaving synchronized (waiters)"); + } + } + + /** + * Tells the Tor server that future SOCKS requests for connections to a set of original + * addresses should be replaced with connections to the specified replacement + * addresses. Each element of kvLines is a String of the form + * "old-address new-address". This function returns the new address mapping. + *

+ * The client may decline to provide a body for the original address, and + * instead send a special null address ("0.0.0.0" for IPv4, "::0" for IPv6, or + * "." for hostname), signifying that the server should choose the original + * address itself, and return that address in the reply. The server + * should ensure that it returns an element of address space that is unlikely + * to be in actual use. If there is already an address mapped to the + * destination address, the server may reuse that mapping. + *

+ * If the original address is already mapped to a different address, the old + * mapping is removed. If the original address and the destination address + * are the same, the server removes any mapping in place for the original + * address. + *

+ * Mappings set by the controller last until the Tor process exits: + * they never expire. If the controller wants the mapping to last only + * a certain time, then it must explicitly un-map the address when that + * time has elapsed. + */ + public Map mapAddresses(Collection kvLines) + throws IOException { + StringBuffer sb = new StringBuffer("MAPADDRESS"); + for (Iterator it = kvLines.iterator(); it.hasNext(); ) { + String kv = it.next(); + int i = kv.indexOf(' '); + sb.append(" ").append(kv.substring(0, i)).append("=") + .append(quote(kv.substring(i + 1))); + } + sb.append("\r\n"); + List lst = sendAndWaitForResponse(sb.toString(), null); + Map result = new HashMap<>(); + for (Iterator it = lst.iterator(); it.hasNext(); ) { + String kv = (it.next()).msg; + int idx = kv.indexOf('='); + result.put(kv.substring(0, idx), + kv.substring(idx + 1)); + } + return result; + } + + public Map mapAddresses(Map addresses) + throws IOException { + List kvList = new ArrayList<>(); + for (Iterator> it = + addresses.entrySet().iterator(); it.hasNext(); ) { + Map.Entry e = it.next(); + kvList.add(e.getKey() + " " + e.getValue()); + } + return mapAddresses(kvList); + } + + public String mapAddress(String fromAddr, String toAddr) + throws IOException { + List lst = new ArrayList<>(); + lst.add(fromAddr + " " + toAddr + "\n"); + Map m = mapAddresses(lst); + return m.get(fromAddr); + } + + /** + * Queries the Tor server for keyed values that are not stored in the torrc + * configuration file. Returns a map of keys to values. + *

+ * Recognized keys include: + *

    + *
  • "version" : The version of the server's software, including the name + * of the software. (example: "Tor 0.0.9.4")
  • + *
  • "desc/id/" or "desc/name/" : the latest server + * descriptor for a given OR, NUL-terminated. If no such OR is known, the + * corresponding value is an empty string.
  • + *
  • "network-status" : a space-separated list of all known OR identities. + * This is in the same format as the router-status line in directories; + * see tor-spec.txt for details.
  • + *
  • "addr-mappings/all"
  • + *
  • "addr-mappings/config"
  • + *
  • "addr-mappings/cache"
  • + *
  • "addr-mappings/control" : a space-separated list of address mappings, each + * in the form of "from-address=to-address". The 'config' key + * returns those address mappings set in the configuration; the 'cache' + * key returns the mappings in the client-side DNS cache; the 'control' + * key returns the mappings set via the control interface; the 'all' + * target returns the mappings set through any mechanism.
  • + *
  • "circuit-status" : A series of lines as for a circuit status event. Each line is of the form: + * "CircuitID CircStatus Path"
  • + *
  • "stream-status" : A series of lines as for a stream status event. Each is of the form: + * "StreamID StreamStatus CircID Target"
  • + *
  • "orconn-status" : A series of lines as for an OR connection status event. Each is of the + * form: "ServerID ORStatus"
  • + *
+ */ + public Map getInfo(Collection keys) + throws IOException { + StringBuffer sb = new StringBuffer("GETINFO"); + for (Iterator it = keys.iterator(); it.hasNext(); ) { + sb.append(" ").append(it.next()); + } + sb.append("\r\n"); + List lst = sendAndWaitForResponse(sb.toString(), null); + Map m = new HashMap<>(); + for (Iterator it = lst.iterator(); it.hasNext(); ) { + ReplyLine line = it.next(); + int idx = line.msg.indexOf('='); + if (idx < 0) + break; + String k = line.msg.substring(0, idx); + String v; + if (line.rest != null) { + v = line.rest; + } else { + v = line.msg.substring(idx + 1); + } + m.put(k, v); + } + return m; + } + + + /** + * Return the value of the information field 'key' + */ + public String getInfo(String key) throws IOException { + List lst = new ArrayList<>(); + lst.add(key); + Map m = getInfo(lst); + return m.get(key); + } + + /** + * An extendCircuit request takes one of two forms: either the circID is zero, in + * which case it is a request for the server to build a new circuit according + * to the specified path, or the circID is nonzero, in which case it is a + * request for the server to extend an existing circuit with that ID according + * to the specified path. + *

+ * If successful, returns the Circuit ID of the (maybe newly created) circuit. + */ + public String extendCircuit(String circID, String path) throws IOException { + List lst = sendAndWaitForResponse( + "EXTENDCIRCUIT " + circID + " " + path + "\r\n", null); + return (lst.get(0)).msg; + } + + /** + * Informs the Tor server that the stream specified by streamID should be + * associated with the circuit specified by circID. + *

+ * Each stream may be associated with + * at most one circuit, and multiple streams may share the same circuit. + * Streams can only be attached to completed circuits (that is, circuits that + * have sent a circuit status "BUILT" event or are listed as built in a + * getInfo circuit-status request). + *

+ * If circID is 0, responsibility for attaching the given stream is + * returned to Tor. + *

+ * By default, Tor automatically attaches streams to + * circuits itself, unless the configuration variable + * "__LeaveStreamsUnattached" is set to "1". Attempting to attach streams + * via TC when "__LeaveStreamsUnattached" is false may cause a race between + * Tor and the controller, as both attempt to attach streams to circuits. + */ + public void attachStream(String streamID, String circID) + throws IOException { + sendAndWaitForResponse( + "ATTACHSTREAM " + streamID + " " + circID + "\r\n", null); + } + + /** + * Tells Tor about the server descriptor in desc. + *

+ * The descriptor, when parsed, must contain a number of well-specified + * fields, including fields for its nickname and identity. + */ + // More documentation here on format of desc? + // No need for return value? control-spec.txt says reply is merely "250 OK" on success... + public String postDescriptor(String desc) throws IOException { + List lst = + sendAndWaitForResponse("+POSTDESCRIPTOR\r\n", desc); + return (lst.get(0)).msg; + } + + /** + * Tells Tor to change the exit address of the stream identified by streamID + * to address. No remapping is performed on the new provided address. + *

+ * To be sure that the modified address will be used, this event must be sent + * after a new stream event is received, and before attaching this stream to + * a circuit. + */ + public void redirectStream(String streamID, String address) + throws IOException { + sendAndWaitForResponse( + "REDIRECTSTREAM " + streamID + " " + address + "\r\n", + null); + } + + /** + * Tells Tor to close the stream identified by streamID. + * reason should be one of the Tor RELAY_END reasons given in tor-spec.txt, as a decimal: + *

    + *
  • 1 -- REASON_MISC (catch-all for unlisted reasons)
  • + *
  • 2 -- REASON_RESOLVEFAILED (couldn't look up hostname)
  • + *
  • 3 -- REASON_CONNECTREFUSED (remote host refused connection)
  • + *
  • 4 -- REASON_EXITPOLICY (OR refuses to connect to host or port)
  • + *
  • 5 -- REASON_DESTROY (Circuit is being destroyed)
  • + *
  • 6 -- REASON_DONE (Anonymized TCP connection was closed)
  • + *
  • 7 -- REASON_TIMEOUT (Connection timed out, or OR timed out while connecting)
  • + *
  • 8 -- (unallocated)
  • + *
  • 9 -- REASON_HIBERNATING (OR is temporarily hibernating)
  • + *
  • 10 -- REASON_INTERNAL (Internal error at the OR)
  • + *
  • 11 -- REASON_RESOURCELIMIT (OR has no resources to fulfill request)
  • + *
  • 12 -- REASON_CONNRESET (Connection was unexpectedly reset)
  • + *
  • 13 -- REASON_TORPROTOCOL (Sent when closing connection because of Tor protocol violations)
  • + *
+ *

+ * Tor may hold the stream open for a while to flush any data that is pending. + */ + public void closeStream(String streamID, byte reason) + throws IOException { + sendAndWaitForResponse( + "CLOSESTREAM " + streamID + " " + reason + "\r\n", null); + } + + /** + * Tells Tor to close the circuit identified by circID. + * If ifUnused is true, do not close the circuit unless it is unused. + */ + public void closeCircuit(String circID, boolean ifUnused) + throws IOException { + sendAndWaitForResponse("CLOSECIRCUIT " + circID + + (ifUnused ? " IFUNUSED" : "") + "\r\n", null); + } + + /** + * Tells Tor to exit when this control connection is closed. This command + * was added in Tor 0.2.2.28-beta. + */ + public void takeOwnership() throws IOException { + sendAndWaitForResponse("TAKEOWNERSHIP\r\n", null); + } + + /** + * Tells Tor to generate and set up a new onion service using the best + * supported algorithm. + *

+ * ADD_ONION was added in Tor 0.2.7.1-alpha. + */ + public Map addOnion(Map portLines) + throws IOException { + return addOnion("NEW:BEST", portLines, null); + } + + /** + * Tells Tor to generate and set up a new onion service using the best + * supported algorithm. + *

+ * ADD_ONION was added in Tor 0.2.7.1-alpha. + */ + public Map addOnion(Map portLines, + boolean ephemeral, boolean detach) + throws IOException { + return addOnion("NEW:BEST", portLines, ephemeral, detach); + } + + /** + * Tells Tor to set up an onion service using the provided private key. + *

+ * ADD_ONION was added in Tor 0.2.7.1-alpha. + */ + public Map addOnion(String privKey, + Map portLines) + throws IOException { + return addOnion(privKey, portLines, null); + } + + /** + * Tells Tor to set up an onion service using the provided private key. + *

+ * ADD_ONION was added in Tor 0.2.7.1-alpha. + */ + public Map addOnion(String privKey, + Map portLines, + boolean ephemeral, boolean detach) + throws IOException { + List flags = new ArrayList<>(); + if (ephemeral) + flags.add("DiscardPK"); + if (detach) + flags.add("Detach"); + return addOnion(privKey, portLines, flags); + } + + /** + * Tells Tor to set up an onion service. + *

+ * ADD_ONION was added in Tor 0.2.7.1-alpha. + */ + public Map addOnion(String privKey, + Map portLines, + List flags) + throws IOException { + if (privKey.indexOf(':') < 0) + throw new IllegalArgumentException("Invalid privKey"); + if (portLines == null || portLines.size() < 1) + throw new IllegalArgumentException( + "Must provide at least one port line"); + StringBuilder b = new StringBuilder(); + b.append("ADD_ONION ").append(privKey); + if (flags != null && flags.size() > 0) { + b.append(" Flags="); + String separator = ""; + for (String flag : flags) { + b.append(separator).append(flag); + separator = ","; + } + } + for (Map.Entry portLine : portLines.entrySet()) { + int virtPort = portLine.getKey(); + String target = portLine.getValue(); + b.append(" Port=").append(virtPort); + if (target != null && target.length() > 0) + b.append(",").append(target); + } + b.append("\r\n"); + List lst = sendAndWaitForResponse(b.toString(), null); + Map ret = new HashMap<>(); + ret.put(HS_ADDRESS, (lst.get(0)).msg.split("=", 2)[1]); + if (lst.size() > 2) + ret.put(HS_PRIVKEY, (lst.get(1)).msg.split("=", 2)[1]); + return ret; + } + + /** + * Tells Tor to take down an onion service previously set up with + * addOnion(). The hostname excludes the .onion extension. + *

+ * DEL_ONION was added in Tor 0.2.7.1-alpha. + */ + public void delOnion(String hostname) throws IOException { + sendAndWaitForResponse("DEL_ONION " + hostname + "\r\n", null); + } + + /** + * Tells Tor to forget any cached client state relating to the hidden + * service with the given hostname (excluding the .onion extension). + */ + public void forgetHiddenService(String hostname) throws IOException { + sendAndWaitForResponse("HSFORGET " + hostname + "\r\n", null); + } }