// Copyright 2005 Nick Mathewson, Roger Dingledine // See LICENSE file for copying information package org.torproject.android.control; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.PrintWriter; import java.io.Reader; import java.io.Writer; import java.net.Socket; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; /** 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 ControlParseThread thread; // Locking: this private volatile EventHandler handler; private volatile PrintWriter debugOutput; private volatile IOException parseThreadException; static class Waiter { List response; // Locking: this synchronized List getResponse() throws InterruptedException { while (response == null) { wait(); } return response; } synchronized void setResponse(List response) { this.response = response; 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()) { // connection has been closed remotely! end the loop! return; } if ((lst.get(0)).status.startsWith("6")) handleEvent(lst); else { synchronized (waiters) { if (!waiters.isEmpty()) { Waiter w; w = waiters.removeFirst(); w.setResponse(lst); } } } } } /** 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); } /** 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); } /** 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); } /** 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); } /** 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); } /** 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; } /** 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); synchronized (waiters) { output.write(s); output.flush(); } } /** 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); } }