[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[or-cvs] r11066: From Karsten: drop TorControlConnection0.java, and merge Tor (in torctl/trunk: . java/net/freehaven/tor/control)



Author: nickm
Date: 2007-08-08 16:44:14 -0400 (Wed, 08 Aug 2007)
New Revision: 11066

Removed:
   torctl/trunk/java/net/freehaven/tor/control/TorControlConnection0.java
   torctl/trunk/java/net/freehaven/tor/control/TorControlConnection1.java
Modified:
   torctl/trunk/
   torctl/trunk/ChangeLog
   torctl/trunk/java/net/freehaven/tor/control/TorControlConnection.java
Log:
 r14101@catbus:  nickm | 2007-08-08 16:43:33 -0400
 From Karsten: drop TorControlConnection0.java, and merge TorControlConnection1 into TorControlConnection.



Property changes on: torctl/trunk
___________________________________________________________________
 svk:merge ticket from /torctl/trunk [r14101] on 8246c3cf-6607-4228-993b-4d95d33730f1

Modified: torctl/trunk/ChangeLog
===================================================================
--- torctl/trunk/ChangeLog	2007-08-08 14:43:30 UTC (rev 11065)
+++ torctl/trunk/ChangeLog	2007-08-08 20:44:14 UTC (rev 11066)
@@ -8,4 +8,6 @@
   - Implement resetConf method in controllers.
   - Fix bug when sending a signal that kills Tor; avoid a
     NullPointerException when Tor exits. (java; from Karsten Loesing)
+  - Remove all v0 support; merge TorControlConnection1 into 
+    TorControlConnection.  (Java; from Karsten)
   

Modified: torctl/trunk/java/net/freehaven/tor/control/TorControlConnection.java
===================================================================
--- torctl/trunk/java/net/freehaven/tor/control/TorControlConnection.java	2007-08-08 14:43:30 UTC (rev 11065)
+++ torctl/trunk/java/net/freehaven/tor/control/TorControlConnection.java	2007-08-08 20:44:14 UTC (rev 11066)
@@ -7,13 +7,15 @@
 import java.net.SocketException;
 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. */
-public abstract class TorControlConnection// implements TorControlCommands {
+/** A connection to a running Tor process as specified in control-spec.txt. */
+public class TorControlConnection implements TorControlCommands
 {
 
     protected EventHandler handler;
@@ -22,6 +24,12 @@
 
     protected ControlParseThread thread;
 
+    protected java.io.BufferedReader input;
+    
+    protected java.io.Writer output;
+    
+    protected java.io.PrintWriter debugOutput;
+    
     static class Waiter {
         Object response;
         public synchronized Object getResponse() {
@@ -40,45 +48,220 @@
         }
     }
 
-    protected static int detectVersion(java.io.InputStream input,
-                                       java.io.OutputStream output)
-        throws IOException
-    {
-        java.io.DataInputStream dInput = new java.io.DataInputStream(input);
-        byte out[] = { 0, 0, 13, 10 };
-        output.write(out);
+    static class ReplyLine {
+        public String status;
+        public String msg;
+        public String rest;
 
-        int len = dInput.readUnsignedShort();
-        int tp = dInput.readUnsignedShort();
-        if (tp == 0) {
-            byte err[] = new byte[len];
-            dInput.readFully(err);
-            return 0;
-        } else if ((len & 0xff00) != 0x0a00 &&
-                   (len & 0x00ff) != 0x000a &&
-                   (tp  & 0xff00) != 0x0a00 &&
-                   (tp  & 0x00ff) != 0x000a) {
-            while (input.read() != '\n')
-                ;
+        ReplyLine(String status, String msg, String rest) {
+            this.status = status; this.msg = msg; this.rest = rest;
         }
-        return 1;
     }
-
+    
     public static TorControlConnection getConnection(java.net.Socket sock)
         throws IOException
     {
-        int version = detectVersion(sock.getInputStream(),
-                                    sock.getOutputStream());
-        if (version == 0)
-            return new TorControlConnection0(sock);
-        else
-            return new TorControlConnection1(sock);
+        return new TorControlConnection(sock);
     }
 
-    protected TorControlConnection() {
+    /** 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(java.net.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(java.io.InputStream i, java.io.OutputStream o)
+        throws IOException {
+        this(new java.io.InputStreamReader(i),
+             new java.io.OutputStreamWriter(o));
+    }
+
+    public TorControlConnection(java.io.Reader i, java.io.Writer o)
+        throws IOException {
+        this.output = o;
+        if (i instanceof java.io.BufferedReader)
+            this.input = (java.io.BufferedReader) i;
+        else
+            this.input = new java.io.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;
+                } else {
+                        // 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 ArrayList sendAndWaitForResponse(String s,String rest)
+        throws IOException {
+        checkThread();
+        Waiter w = new Waiter();
+        if (debugOutput != null)
+            debugOutput.print(">> "+s);
+        synchronized (waiters) {
+            output.write(s);
+            output.flush();
+            if (rest != null)
+                writeEscaped(rest);
+            waiters.addLast(w);
+        }
+        ArrayList lst = (ArrayList) w.getResponse();
+        for (Iterator i = lst.iterator(); i.hasNext(); ) {
+            ReplyLine c = (ReplyLine) 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 = (ReplyLine) 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((String)lst.get(1),
+                                      (String)lst.get(0),
+                                      (String)lst.get(2));
+            } else if (tp.equals("STREAM")) {
+                List lst = Bytes.splitStr(null, rest);
+                handler.streamStatus((String)lst.get(1),
+                                     (String)lst.get(0),
+                                     (String)lst.get(3));
+                // XXXX circID.
+            } else if (tp.equals("ORCONN")) {
+                List lst = Bytes.splitStr(null, rest);
+                handler.orConnStatus((String)lst.get(1), (String)lst.get(0));
+            } else if (tp.equals("BW")) {
+                List lst = Bytes.splitStr(null, rest);
+                handler.bandwidthUsed(Integer.parseInt((String)lst.get(0)),
+                                      Integer.parseInt((String)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 <b>w</b> 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(java.io.PrintWriter w) {
+        if (w instanceof java.io.PrintWriter)
+            debugOutput = (java.io.PrintWriter) w;
+        else
+            debugOutput = new java.io.PrintWriter(w, true);
+    }
+    
+    /** Sets <b>s</b> 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(java.io.PrintStream s) {
+        debugOutput = new java.io.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(). */
@@ -124,7 +307,25 @@
             launchThread(true);
     }
 
-    protected abstract void react() throws IOException;
+    /** 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 (((ReplyLine)lst.get(0)).status.startsWith("6"))
+                handleEvent(lst);
+            else {
+                Waiter w;
+                synchronized (waiters) {
+                    w = (Waiter) waiters.removeFirst();
+                }
+                w.setResponse(lst);
+            }
+        }
+    }
 
     /** Change the value of the configuration option 'key' to 'val'.
      */
@@ -144,14 +345,56 @@
         setConf(lst);
     }
 
-    /** Change the values of the configuration options stored in
-     * 'kvList'.  (The format is "key value"). */
-    public abstract void setConf(Collection kvList) throws IOException;
-
+    /** Changes the values of the configuration options stored in
+     * <b>kvList</b>.  Each list element in <b>kvList</b> 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 <b>kvList</b> 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 = (String) 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 abstract void resetConf(Collection keys) throws IOException;
+    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 = (String) 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 {
@@ -160,30 +403,169 @@
         return getConf(lst);
     }
 
-    /** Return a key-value map for the configuration options in 'keys' */
-    public abstract List getConf(Collection keys) throws IOException;
+    /** Requests the values of the configuration variables listed in <b>keys</b>.
+     * 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 = (String) it.next();
+            sb.append(" ").append(key);
+        }
+        sb.append("\r\n");
+        ArrayList lst = sendAndWaitForResponse(sb.toString(), null);
+        ArrayList result = new ArrayList();
+        for (Iterator it = lst.iterator(); it.hasNext(); ) {
+            String kv = ((ReplyLine) 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;
+    }
 
-    /** Tell Tor to begin sending us events of the types listed in 'events'.
-     * Elements must be one of the EVENT_* values from TorControlCommands */
-    public abstract void setEvents(List events) throws IOException;
+    /** Request that the server inform the client about interesting events.
+     * Each element of <b>events</b> is one of the following Strings: 
+     * ["CIRC" | "STREAM" | "ORCONN" | "BW" | "DEBUG" |
+     *  "INFO" | "NOTICE" | "WARN" | "ERR" | "NEWDESC" | "ADDRMAP"] .
+     * 
+     * Any events not listed in the <b>events</b> are turned off; thus, calling
+     * setEvents with an empty <b>events</b> 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(); ) {
+            Object event = it.next();
+            if (event instanceof String) {
+                sb.append(" ").append((String)event);
+            } else {
+                int i = ((Number) event).intValue();
+                sb.append(" ").append(EVENT_NAMES[i]);
+            }
+        }
+        sb.append("\r\n");
+        sendAndWaitForResponse(sb.toString(), null);
+    }
 
-    /** Send Tor an authentication sequence 'auth' */
-    // XXXX more info about how to set this up securely.
-    public abstract void authenticate(byte[] auth) throws IOException;
+    /** 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 <b>auth</b>.
+     * 
+     * If the 'HashedControlPassword' option is set, <b>auth</b> 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 <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);
+    }
 
-    /** Tell Tor to save the value of its configuration to disk. */
-    public abstract void saveConf() throws IOException;
+    /** Instructs the server to write out its configuration options into its torrc.
+     */
+    public void saveConf() throws IOException {
+        sendAndWaitForResponse("SAVECONF\r\n", null);
+    }
 
-    /** Send a signal to the Tor process. */
-    public abstract void signal(String signal) throws IOException;
+    /** Sends a signal from the controller to the Tor server.
+     * <b>signal</b> is one of the following Strings:
+     * <ul>
+     * <li>"RELOAD" or "HUP" :  Reload config items, refetch directory</li>
+     * <li>"SHUTDOWN" or "INT" : Controlled shutdown: if server is an OP, exit immediately.
+     *     If it's an OR, close listeners and exit after 30 seconds</li>
+     * <li>"DUMP" or "USR1" : Dump stats: log information about open connections and circuits</li>
+     * <li>"DEBUG" or "USR2" : Debug: switch all open logs to loglevel debug</li>
+     * <li>"HALT" or "TERM" : Immediate shutdown: clean up and exit now</li>
+     * </ul>
+     */
+    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 abstract void shutdownTor(String signal) throws IOException;
+    public void shutdownTor(String signal) throws IOException {
+        String s = "SIGNAL " + signal + "\r\n";
+        Waiter w = new Waiter();
+        if (debugOutput != null)
+            debugOutput.print(">> "+s);
+        if (this.thread != null) {
+            this.thread.stopListening();
+    	}
+        synchronized (waiters) {
+            output.write(s);
+            output.flush();
+            waiters.addLast(w); // Prevent react() from finding the list empty
+        }
+    }
 
-    /** Tell Tor to replace incoming addresses with those as listed in 'kvLines'.
+    /** 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 <b>kvLines</b> 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 abstract Map mapAddresses(Collection kvLines) throws IOException;
+    public Map mapAddresses(Collection kvLines) throws IOException {
+        StringBuffer sb = new StringBuffer("MAPADDRESS");
+        for (Iterator it = kvLines.iterator(); it.hasNext(); ) {
+            String kv = (String) it.next();
+            int i = kv.indexOf(' ');
+            sb.append(" ").append(kv.substring(0,i)).append("=")
+                .append(quote(kv.substring(i+1)));
+        }
+        sb.append("\r\n");
+        ArrayList lst = sendAndWaitForResponse(sb.toString(), null);
+        Map result = new HashMap();
+        for (Iterator it = lst.iterator(); it.hasNext(); ) {
+            String kv = ((ReplyLine) 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();
@@ -201,9 +583,63 @@
         return (String) m.get(fromAddr);
     }
 
-    /** Look up the information values listed in keys. */
-    public abstract Map getInfo(Collection keys) throws IOException;
-
+    /** 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:
+     * <ul>
+     * <li>"version" : The version of the server's software, including the name
+     *  of the software. (example: "Tor 0.0.9.4")</li>
+     * <li>"desc/id/<OR identity>" or "desc/name/<OR nickname>" : the latest server
+     * descriptor for a given OR, NUL-terminated.  If no such OR is known, the
+     * corresponding value is an empty string.</li>
+     * <li>"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.</li>
+     * <li>"addr-mappings/all"</li>
+     * <li>"addr-mappings/config"</li>
+     * <li>"addr-mappings/cache"</li>
+     * <li>"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.</li>
+     * <li>"circuit-status" : A series of lines as for a circuit status event. Each line is of the form:
+     * "CircuitID CircStatus Path"</li>
+     * <li>"stream-status" : A series of lines as for a stream status event.  Each is of the form:
+     * "StreamID StreamStatus CircID Target"</li>
+     * <li>"orconn-status" : A series of lines as for an OR connection status event.  Each is of the
+     * form: "ServerID ORStatus"</li>
+     * </ul>
+     */
+    public Map getInfo(Collection keys) throws IOException {
+        StringBuffer sb = new StringBuffer("GETINFO");
+        for (Iterator it = keys.iterator(); it.hasNext(); ) {
+            sb.append(" ").append((String)it.next());
+        }
+        sb.append("\r\n");
+        ArrayList lst = sendAndWaitForResponse(sb.toString(), null);
+        Map m = new HashMap();
+        for (Iterator it = lst.iterator(); it.hasNext(); ) {
+            ReplyLine line = (ReplyLine) it.next();
+            int idx = line.msg.indexOf('=');
+            if (idx<0)
+                break;
+            String k = line.msg.substring(0,idx);
+            Object 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();
@@ -212,34 +648,98 @@
         return (String) m.get(key);
     }
 
-    /**
-     * Tell Tor to extend the circuit identified by 'circID' through the
-     * servers named in the list 'path'.
+    /** An extendCircuit request takes one of two forms: either the <b>circID</b> is zero, in
+     * which case it is a request for the server to build a new circuit according
+     * to the specified path, or the <b>circID</b> is nonzero, in which case it is a
+     * request for the server to extend an existing circuit with that ID according
+     * to the specified <b>path</b>.
+     *
+     * If successful, returns the Circuit ID of the (maybe newly created) circuit.
      */
-    public abstract String extendCircuit(String circID, String path) throws IOException;
+    public String extendCircuit(String circID, String path) throws IOException {
+        ArrayList lst = sendAndWaitForResponse(
+                          "EXTENDCIRCUIT "+circID+" "+path+"\r\n", null);
+        return ((ReplyLine)lst.get(0)).msg;
+    }
+    
+    /** Informs the Tor server that the stream specified by <b>streamID</b> should be
+     * associated with the circuit specified by <b>circID</b>.  
+     * 
+     * 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 <b>circID</b> 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);
+    }
 
-    /**
-     * Tell Tor to attach the stream identified by 'streamID' to the circuit
-     * identified by 'circID'.
+    /** Tells Tor about the server descriptor in <b>desc</b>.
+     * 
+     * The descriptor, when parsed, must contain a number of well-specified
+     * fields, including fields for its nickname and identity.
      */
-    public abstract void attachStream(String streamID, String circID) throws IOException;
+    // 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 {
+        ArrayList lst = sendAndWaitForResponse("+POSTDESCRIPTOR\r\n", desc);
+        return ((ReplyLine)lst.get(0)).msg;
+    }
 
-    /** Tell Tor about the server descriptor in 'desc' */
-    public abstract String postDescriptor(String desc) throws IOException;
-
-    /** Tell Tor to change the target of the stream identified by 'streamID'
-     * to 'address'.
+    /** Tells Tor to change the exit address of the stream identified by <b>streamID</b>
+     * to <b>address</b>. 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 abstract void redirectStream(String streamID, String address) throws IOException;
+    public void redirectStream(String streamID, String address) throws IOException {
+        sendAndWaitForResponse("REDIRECTSTREAM "+streamID+" "+address+"\r\n",
+                               null);
+    }
 
-    /** Tell Tor to close the stream identified by 'streamID'.
+    /** Tells Tor to close the stream identified by <b>streamID</b>.
+     * <b>reason</b> should be one of the Tor RELAY_END reasons given in tor-spec.txt, as a decimal:
+     * <ul>
+     * <li>1 -- REASON_MISC           (catch-all for unlisted reasons)</li>
+     * <li>2 -- REASON_RESOLVEFAILED  (couldn't look up hostname)</li>
+     * <li>3 -- REASON_CONNECTREFUSED (remote host refused connection)</li>
+     * <li>4 -- REASON_EXITPOLICY     (OR refuses to connect to host or port)</li>
+     * <li>5 -- REASON_DESTROY        (Circuit is being destroyed)</li>
+     * <li>6 -- REASON_DONE           (Anonymized TCP connection was closed)</li>
+     * <li>7 -- REASON_TIMEOUT        (Connection timed out, or OR timed out while connecting)</li>
+     * <li>8 -- (unallocated)</li>
+     * <li>9 -- REASON_HIBERNATING    (OR is temporarily hibernating)</li>
+     * <li>10 -- REASON_INTERNAL       (Internal error at the OR)</li>
+     * <li>11 -- REASON_RESOURCELIMIT  (OR has no resources to fulfill request)</li>
+     * <li>12 -- REASON_CONNRESET      (Connection was unexpectedly reset)</li>
+     * <li>13 -- REASON_TORPROTOCOL    (Sent when closing connection because of Tor protocol violations)</li>
+     * </ul>
+     *
+     * Tor may hold the stream open for a while to flush any data that is pending.
      */
-    public abstract void closeStream(String streamID, byte reason)
-        throws IOException;
+    public void closeStream(String streamID, byte reason)
+        throws IOException {
+        sendAndWaitForResponse("CLOSESTREAM "+streamID+" "+reason+"\r\n",null);
+    }
 
-    /** Tell Tor to close the circuit identified by 'streamID'.
+    /** Tells Tor to close the circuit identified by <b>circID</b>.
+     * If <b>ifUnused</b> is true, do not close the circuit unless it is unused.
      */
-    public abstract void closeCircuit(String circID, boolean ifUnused) throws IOException;
-
+    public void closeCircuit(String circID, boolean ifUnused) throws IOException {
+        sendAndWaitForResponse("CLOSECIRCUIT "+circID+
+                               (ifUnused?" IFUNUSED":"")+"\r\n", null);
+    }
 }
 

Deleted: torctl/trunk/java/net/freehaven/tor/control/TorControlConnection0.java
===================================================================
--- torctl/trunk/java/net/freehaven/tor/control/TorControlConnection0.java	2007-08-08 14:43:30 UTC (rev 11065)
+++ torctl/trunk/java/net/freehaven/tor/control/TorControlConnection0.java	2007-08-08 20:44:14 UTC (rev 11066)
@@ -1,431 +0,0 @@
-// $Id$
-// Copyright 2005 Nick Mathewson, Roger Dingledine
-// See LICENSE file for copying information
-package net.freehaven.tor.control;
-
-import java.io.IOException;
-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;
-
-/** DOCDOC */
-public class TorControlConnection0 extends TorControlConnection
-    implements TorControlCommands
-{
-    protected java.io.DataOutputStream outStream;
-    protected java.io.DataInputStream inStream;
-
-    static class Cmd {
-        public int type;
-        public byte[] body;
-
-        Cmd(int t, byte[] b) { type = t; body = b; }
-        Cmd(int t, int l) { type = t; body = new byte[l]; };
-    }
-
-    /** 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 TorControlConnection0(java.net.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 TorControlConnection0(java.io.InputStream i, java.io.OutputStream o)
-        throws IOException {
-        this.outStream = new java.io.DataOutputStream(o);
-        this.inStream = new java.io.DataInputStream(i);
-        this.waiters = new LinkedList();
-    }
-
-    /** helper: sends a single (unfragmentable) command to Tor */
-    protected final void sendCommand0(int type, byte[] cmd)
-        throws IOException {
-        int length = cmd == null ? 0 : cmd.length;
-        outStream.writeShort((short)length);
-        outStream.writeShort(type);
-        if (cmd != null)
-            outStream.write(cmd);
-    }
-
-    /** helper: sends a single (possibly fragmented) command to Tor */
-    protected void sendCommand(short type, byte[] cmd) throws IOException {
-        synchronized(this.outStream) {
-            if (cmd == null || cmd.length <= 65535) {
-                sendCommand0(type, cmd);
-                return;
-            }
-            int length = cmd.length;
-            outStream.writeShort(65535);
-            outStream.writeShort(CMD_FRAGMENTHEADER);
-            outStream.writeShort(type);
-            outStream.writeInt(length);
-            outStream.write(cmd, 0, 65535);
-            for (int pos=65535; pos < length; pos += 65535) {
-                int flen = length-pos < 65535 ? length-pos : 65535;
-                outStream.writeShort(flen);
-                outStream.writeShort(CMD_FRAGMENT);
-                this.outStream.write(cmd, pos, flen);
-            }
-        }
-    }
-
-    /** helper: read a possibly fragmented command from Tor */
-    protected final Cmd readCommand0() throws IOException {
-        int len = this.inStream.readUnsignedShort();
-        int cmd = this.inStream.readUnsignedShort();
-        byte[] result = new byte[len];
-        this.inStream.readFully(result);
-        return new Cmd(cmd, result);
-    }
-
-    /** Read a command from Tor, defragmenting as necessary */
-    protected Cmd readCommand() throws IOException {
-        synchronized (inStream) {
-            Cmd c = readCommand0();
-            if (c.type != CMD_FRAGMENT && c.type != CMD_FRAGMENTHEADER)
-                return c;
-
-            if (c.type == CMD_FRAGMENT)
-                throw new TorControlSyntaxError("Fragment without header");
-
-            int realType = Bytes.getU16(c.body, 0);
-            int realLen = Bytes.getU32(c.body, 2);
-
-            Cmd out = new Cmd(realType, realLen);
-            System.arraycopy(c.body, 6, out.body, 0, c.body.length-6);
-            int pos = c.body.length-6;
-            while (pos < realLen) {
-                c = readCommand0();
-                if (c.type != CMD_FRAGMENT)
-                    throw new TorControlSyntaxError("Incomplete fragmented message");
-                System.arraycopy(c.body, 0, out.body, pos, c.body.length);
-                pos += c.body.length;
-            }
-            return out;
-        }
-    }
-
-    /** helper: implement the main background loop. */
-    protected void react() throws IOException {
-        while (true) {
-            Cmd c = readCommand();
-            if (c.type == CMD_EVENT)
-                handleEvent(c);
-            else {
-                Waiter w;
-                synchronized (waiters) {
-                    w = (Waiter) waiters.removeFirst();
-                }
-                w.setResponse(c);
-            }
-        }
-    }
-
-    /** helper: Send a command and wait for the next reponse type command
-     * to be received (in order) */
-    protected synchronized Cmd _sendAndWaitForResponse(short type, byte[] cmd)
-        throws IOException {
-        checkThread();
-        Waiter w = new Waiter();
-        synchronized (waiters) {
-            sendCommand(type, cmd);
-            waiters.addLast(w);
-        }
-        return (Cmd) w.getResponse();
-    }
-
-    /** Send a message to Tor, and wait for a respose.
-     *
-     * @throw TorControlError if Tor tells us about an error
-     * @throw TorControlSyntaxError if the response type wasn't exType1...4
-     **/
-    protected Cmd sendAndWaitForResponse(short type, byte[] cmd,
-                   short exType1, short exType2, short exType3, short exType4)
-        throws IOException {
-
-        Cmd c = _sendAndWaitForResponse(type, cmd);
-        if (c.type == CMD_ERROR)
-            throw new TorControlError(Bytes.getU16(c.body, 0),
-                                      Bytes.getNulTerminatedStr(c.body, 2));
-        if (c.type == exType1 || c.type == exType2 || c.type == exType3 ||
-            c.type == exType4)
-            return c;
-
-        throw new TorControlSyntaxError("Unexpected reply type: "+c.type);
-    }
-
-    protected Cmd sendAndWaitForResponse(short type, byte[] cmd)
-        throws IOException {
-        return sendAndWaitForResponse(type, cmd, CMD_DONE, CMD_DONE, CMD_DONE, CMD_DONE);
-    }
-
-    protected Cmd sendAndWaitForResponse(short type, byte[] cmd, short exType1)
-        throws IOException {
-        return sendAndWaitForResponse(type, cmd, exType1, exType1, exType1,
-                                      exType1);
-    }
-
-    protected Cmd sendAndWaitForResponse(short type, byte[] cmd,
-                                    short exType1, short exType2)
-        throws IOException {
-        return sendAndWaitForResponse(type, cmd, exType1, exType2, exType2,
-                                      exType2);
-    }
-
-    protected Cmd sendAndWaitForResponse(short type, byte[] cmd,
-                                   short exType1, short exType2, short exType3)
-        throws IOException {
-        return sendAndWaitForResponse(type, cmd, exType1, exType2, exType3,
-                                      exType3);
-    }
-
-    /** Helper: decode a CMD_EVENT command and dispatch it to our
-     * EventHandler (if any). */
-    protected void handleEvent(Cmd c) {
-        if (handler == null)
-            return;
-        int type = Bytes.getU16(c.body, 0);
-
-        switch (type) {
-          case EVENT_CIRCSTATUS:
-              handler.circuitStatus(CIRC_STATUS_NAMES[c.body[2]],
-                                    Bytes.getU32S(c.body, 3),
-                                    Bytes.getNulTerminatedStr(c.body, 7));
-              break;
-          case EVENT_STREAMSTATUS:
-              handler.streamStatus(STREAM_STATUS_NAMES[c.body[2]],
-                                   Bytes.getU32S(c.body, 3),
-                                   Bytes.getNulTerminatedStr(c.body, 7));
-              break;
-          case EVENT_ORCONNSTATUS:
-              handler.orConnStatus(OR_CONN_STATUS_NAMES[c.body[2]],
-                                   Bytes.getNulTerminatedStr(c.body, 3));
-              break;
-          case EVENT_BANDWIDTH:
-              handler.bandwidthUsed(Bytes.getU32(c.body, 2),
-                                    Bytes.getU32(c.body, 6));
-              break;
-          case EVENT_NEWDESCRIPTOR:
-              List lst = new ArrayList();
-              Bytes.splitStr(lst, c.body, 2, (byte)',');
-              handler.newDescriptors(lst);
-              break;
-          case EVENT_MSG_DEBUG:
-              handler.message("DEBUG", Bytes.getNulTerminatedStr(c.body, 2));
-              break;
-          case EVENT_MSG_INFO:
-              handler.message("INFO", Bytes.getNulTerminatedStr(c.body, 2));
-              break;
-          case EVENT_MSG_NOTICE:
-              handler.message("NOTICE", Bytes.getNulTerminatedStr(c.body, 2));
-              break;
-          case EVENT_MSG_WARN:
-              handler.message("WARN", Bytes.getNulTerminatedStr(c.body, 2));
-              break;
-          case EVENT_MSG_ERROR:
-              handler.message("ERR", Bytes.getNulTerminatedStr(c.body, 2));
-              break;
-          default:
-              throw new TorControlSyntaxError("Unrecognized event type.");
-        }
-    }
-
-    /** Change the values of the configuration options stored in
-     * 'kvList'.  (The format is "key value"). */
-    public void setConf(Collection kvList) throws IOException {
-        StringBuffer b = new StringBuffer();
-        for (Iterator it = kvList.iterator(); it.hasNext(); ) {
-            String kv = (String) it.next();
-            b.append(kv).append("\n");
-        }
-        sendAndWaitForResponse(CMD_SETCONF, b.toString().getBytes());
-    }
-
-    public void resetConf(Collection keylist) throws IOException {
-        setConf(keylist);
-    }
-
-    public List getConf(Collection keys) throws IOException {
-        StringBuffer s = new StringBuffer();
-        for (Iterator it = keys.iterator(); it.hasNext(); ) {
-            String key = (String) it.next();
-            s.append(key).append("\n");
-        }
-        Cmd c = sendAndWaitForResponse(CMD_GETCONF, s.toString().getBytes(),
-                                       CMD_CONFVALUE);
-        List lines = new ArrayList();
-        Bytes.splitStr(lines, c.body, 0, (byte)'\n');
-        List result = new ArrayList();
-        for (Iterator it = lines.iterator(); it.hasNext(); ) {
-            String kv = (String) it.next();
-            int idx = kv.indexOf(' ');
-            result.add(new ConfigEntry(kv.substring(0, idx),
-                                       kv.substring(idx+1)));
-        }
-        return result;
-    }
-
-    public void setEvents(List events) throws IOException {
-        byte[] ba = new byte[events.size() * 2];
-        int i;
-        Iterator it;
-        for(i=0, it = events.iterator(); it.hasNext(); i += 2) {
-            Object event = it.next();
-            short e = -1;
-            if (event instanceof Number) {
-                e = ((Number)event).shortValue();
-            } else {
-                String s = ((String) event).toUpperCase();
-                for (int j = 0; i < EVENT_NAMES.length; ++i) {
-                    if (EVENT_NAMES[j].equals(s)) {
-                        e = (short)j;
-                        break;
-                    }
-                }
-                if (e < 0)
-                    throw new TorControlError("Unknown v0 code for event '"+s+"'");
-            }
-            Bytes.setU16(ba, i, e);
-        }
-        sendAndWaitForResponse(CMD_SETEVENTS, ba);
-        System.out.println("OK");
-    }
-
-    public void authenticate(byte[] auth) throws IOException {
-        if (auth == null)
-            auth = new byte[0];
-        sendAndWaitForResponse(CMD_AUTH, auth);
-    }
-
-    public void saveConf() throws IOException {
-        sendAndWaitForResponse(CMD_SAVECONF, new byte[0]);
-    }
-
-    public void signal(String signal) throws IOException {
-        int sig;
-        signal = signal.toUpperCase();
-        if (signal.equals("HUP") || signal.equals("RELOAD"))
-            sig = SIGNAL_HUP;
-        else if (signal.equals("INT") || signal.equals("SHUTDOWN"))
-            sig = SIGNAL_INT;
-        else if (signal.equals("USR1") || signal.equals("DUMP"))
-            sig = SIGNAL_USR1;
-        else if (signal.equals("USR2") || signal.equals("DEBUG"))
-            sig = SIGNAL_USR2;
-        else if (signal.equals("TERM") || signal.equals("HALT"))
-            sig = SIGNAL_TERM;
-        else
-            throw new TorControlError("Unrecognized value for signal()");
-        byte[] ba = { (byte)sig };
-        sendAndWaitForResponse(CMD_SIGNAL, ba);
-    }
-    
-    /** 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 {
-                throw new RuntimeException("Method is not implemented!");
-        }
-
-    public Map mapAddresses(Collection kvLines) throws IOException {
-        StringBuffer sb = new StringBuffer();
-        for (Iterator it = kvLines.iterator(); it.hasNext(); ) {
-            sb.append((String)it.next()).append("\n");
-        }
-        Cmd c = sendAndWaitForResponse(CMD_MAPADDRESS, sb.toString().getBytes());
-        Map result = new HashMap();
-        List lst = new ArrayList();
-        Bytes.splitStr(lst, c.body, 0, (byte)'\n');
-        for (Iterator it = lst.iterator(); it.hasNext(); ) {
-            String kv = (String) it.next();
-            int idx = kv.indexOf(' ');
-            result.put(kv.substring(0, idx),
-                       kv.substring(idx+1));
-        }
-        return result;
-    }
-
-    public Map getInfo(Collection keys) throws IOException {
-        StringBuffer sb = new StringBuffer();
-        for (Iterator it = keys.iterator(); it.hasNext(); ) {
-            sb.append(((String)it.next())+"\n");
-        }
-        Cmd c = sendAndWaitForResponse(CMD_GETINFO, sb.toString().getBytes(),
-                                       CMD_INFOVALUE);
-        Map m = new HashMap();
-        List lst = new ArrayList();
-        Bytes.splitStr(lst, c.body, 0, (byte)0);
-        if ((lst.size() % 2) != 0)
-            throw new TorControlSyntaxError(
-                                     "Odd number of substrings from GETINFO");
-        for (Iterator it = lst.iterator(); it.hasNext(); ) {
-            Object k = it.next();
-            Object v = it.next();
-            m.put(k, v);
-        }
-        return m;
-    }
-
-    public String extendCircuit(String circID, String path) throws IOException {
-        byte[] p = path.getBytes();
-        byte[] ba = new byte[p.length+4];
-        Bytes.setU32(ba, 0, (int)Long.parseLong(circID));
-        System.arraycopy(p, 0, ba, 4, p.length);
-        Cmd c = sendAndWaitForResponse(CMD_EXTENDCIRCUIT, ba);
-        return Integer.toString(Bytes.getU32(c.body, 0));
-    }
-
-    public void attachStream(String streamID, String circID)
-        throws IOException {
-        byte[] ba = new byte[8];
-        Bytes.setU32(ba, 0, (int)Long.parseLong(streamID));
-        Bytes.setU32(ba, 4, (int)Long.parseLong(circID));
-        sendAndWaitForResponse(CMD_ATTACHSTREAM, ba);
-    }
-
-    /** Tell Tor about the server descriptor in 'desc' */
-    public String postDescriptor(String desc) throws IOException {
-        return new String(
-             sendAndWaitForResponse(CMD_POSTDESCRIPTOR, desc.getBytes()).body);
-    }
-
-    /** Tell Tor to change the target of the stream identified by 'streamID'
-     * to 'address'.
-     */
-    public void redirectStream(String streamID, String address) throws IOException {
-        byte[] addr = address.getBytes();
-        byte[] ba = new byte[addr.length+4];
-        Bytes.setU32(ba, 0, (int)Long.parseLong(streamID));
-        System.arraycopy(addr, 0, ba, 4, addr.length);
-        sendAndWaitForResponse(CMD_REDIRECTSTREAM, ba);
-    }
-
-    /** Tell Tor to close the stream identified by 'streamID'.
-     */
-    public void closeStream(String streamID, byte reason)
-        throws IOException {
-        byte[] ba = new byte[6];
-        Bytes.setU32(ba, 0, (int)Long.parseLong(streamID));
-        ba[4] = reason;
-        ba[5] = (byte)0;
-        sendAndWaitForResponse(CMD_CLOSESTREAM, ba);
-    }
-
-    /** Tell Tor to close the circuit identified by 'streamID'.
-     */
-    public void closeCircuit(String circID, boolean ifUnused) throws IOException {
-        byte[] ba = new byte[5];
-        Bytes.setU32(ba, 0, (int)Long.parseLong(circID));
-        ba[4] = (byte)(ifUnused? 1 : 0);
-        sendAndWaitForResponse(CMD_CLOSECIRCUIT, ba);
-    }
-
-}
-

Deleted: torctl/trunk/java/net/freehaven/tor/control/TorControlConnection1.java
===================================================================
--- torctl/trunk/java/net/freehaven/tor/control/TorControlConnection1.java	2007-08-08 14:43:30 UTC (rev 11065)
+++ torctl/trunk/java/net/freehaven/tor/control/TorControlConnection1.java	2007-08-08 20:44:14 UTC (rev 11066)
@@ -1,616 +0,0 @@
-// $Id$
-// Copyright 2005 Nick Mathewson, Roger Dingledine
-// See LICENSE file for copying information
-package net.freehaven.tor.control;
-
-import java.io.IOException;
-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;
-
-/** Extends the TorControlConnection class to implement Version 1
-* of the TC protocol, as specified in control-spec.txt.
-*/
-public class TorControlConnection1 extends TorControlConnection
-    implements TorControlCommands
-{
-    protected java.io.BufferedReader input;
-    protected java.io.Writer output;
-    protected java.io.PrintWriter debugOutput;
-
-    static class ReplyLine {
-        public String status;
-        public String msg;
-        public 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 TorControlConnection1(java.net.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 TorControlConnection1(java.io.InputStream i, java.io.OutputStream o)
-        throws IOException {
-        this(new java.io.InputStreamReader(i),
-             new java.io.OutputStreamWriter(o));
-    }
-
-    public TorControlConnection1(java.io.Reader i, java.io.Writer o)
-        throws IOException {
-        this.output = o;
-        if (i instanceof java.io.BufferedReader)
-            this.input = (java.io.BufferedReader) i;
-        else
-            this.input = new java.io.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;
-                } else {
-                        // 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;
-    }
-
-    /** 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 (((ReplyLine)lst.get(0)).status.startsWith("6"))
-                handleEvent(lst);
-            else {
-                Waiter w;
-                synchronized (waiters) {
-                    w = (Waiter) waiters.removeFirst();
-                }
-                w.setResponse(lst);
-            }
-        }
-    }
-
-    protected synchronized ArrayList sendAndWaitForResponse(String s,String rest)
-        throws IOException {
-        checkThread();
-        Waiter w = new Waiter();
-        if (debugOutput != null)
-            debugOutput.print(">> "+s);
-        synchronized (waiters) {
-            output.write(s);
-            output.flush();
-            if (rest != null)
-                writeEscaped(rest);
-            waiters.addLast(w);
-        }
-        ArrayList lst = (ArrayList) w.getResponse();
-        for (Iterator i = lst.iterator(); i.hasNext(); ) {
-            ReplyLine c = (ReplyLine) 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 = (ReplyLine) 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((String)lst.get(1),
-                                      (String)lst.get(0),
-                                      (String)lst.get(2));
-            } else if (tp.equals("STREAM")) {
-                List lst = Bytes.splitStr(null, rest);
-                handler.streamStatus((String)lst.get(1),
-                                     (String)lst.get(0),
-                                     (String)lst.get(3));
-                // XXXX circID.
-            } else if (tp.equals("ORCONN")) {
-                List lst = Bytes.splitStr(null, rest);
-                handler.orConnStatus((String)lst.get(1), (String)lst.get(0));
-            } else if (tp.equals("BW")) {
-                List lst = Bytes.splitStr(null, rest);
-                handler.bandwidthUsed(Integer.parseInt((String)lst.get(0)),
-                                      Integer.parseInt((String)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);
-            }
-        }
-    }
-
-    /** Changes the values of the configuration options stored in
-     * <b>kvList</b>.  Each list element in <b>kvList</b> 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 <b>kvList</b> 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 = (String) 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);
-    }
-
-    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 = (String) it.next();
-            b.append(" ").append(key);
-        }
-        b.append("\r\n");
-        sendAndWaitForResponse(b.toString(), null);
-    }
-
-    /** Sets <b>w</b> 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(java.io.PrintWriter w) {
-        if (w instanceof java.io.PrintWriter)
-            debugOutput = (java.io.PrintWriter) w;
-        else
-            debugOutput = new java.io.PrintWriter(w, true);
-    }
-    
-    /** Sets <b>s</b> 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(java.io.PrintStream s) {
-        debugOutput = new java.io.PrintWriter(s, true);
-    }
-    
-    /** Requests the values of the configuration variables listed in <b>keys</b>.
-    * 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 = (String) it.next();
-            sb.append(" ").append(key);
-        }
-        sb.append("\r\n");
-        ArrayList lst = sendAndWaitForResponse(sb.toString(), null);
-        ArrayList result = new ArrayList();
-        for (Iterator it = lst.iterator(); it.hasNext(); ) {
-            String kv = ((ReplyLine) 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 <b>events</b> is one of the following Strings: 
-    * ["CIRC" | "STREAM" | "ORCONN" | "BW" | "DEBUG" |
-    *  "INFO" | "NOTICE" | "WARN" | "ERR" | "NEWDESC" | "ADDRMAP"] .
-    * 
-    * Any events not listed in the <b>events</b> are turned off; thus, calling
-    * setEvents with an empty <b>events</b> 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(); ) {
-            Object event = it.next();
-            if (event instanceof String) {
-                sb.append(" ").append((String)event);
-            } else {
-                int i = ((Number) event).intValue();
-                sb.append(" ").append(EVENT_NAMES[i]);
-            }
-        }
-        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 <b>auth</b>.
-    * 
-    * If the 'HashedControlPassword' option is set, <b>auth</b> 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 <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.
-    * <b>signal</b> is one of the following Strings:
-    * <ul>
-    * <li>"RELOAD" or "HUP" :  Reload config items, refetch directory</li>
-    * <li>"SHUTDOWN" or "INT" : Controlled shutdown: if server is an OP, exit immediately.
-    *     If it's an OR, close listeners and exit after 30 seconds</li>
-    * <li>"DUMP" or "USR1" : Dump stats: log information about open connections and circuits</li>
-    * <li>"DEBUG" or "USR2" : Debug: switch all open logs to loglevel debug</li>
-    * <li>"HALT" or "TERM" : Immediate shutdown: clean up and exit now</li>
-    * </ul>
-    */
-    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);
-        if (this.thread != null) {
-            this.thread.stopListening();
-    	}
-        synchronized (waiters) {
-            output.write(s);
-            output.flush();
-            waiters.addLast(w); // Prevent react() from finding the list empty
-        }
-    }
-
-    /** 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 <b>kvLines</b> 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 = (String) it.next();
-            int i = kv.indexOf(' ');
-            sb.append(" ").append(kv.substring(0,i)).append("=")
-                .append(quote(kv.substring(i+1)));
-        }
-        sb.append("\r\n");
-        ArrayList lst = sendAndWaitForResponse(sb.toString(), null);
-        Map result = new HashMap();
-        for (Iterator it = lst.iterator(); it.hasNext(); ) {
-            String kv = ((ReplyLine) it.next()).msg;
-            int idx = kv.indexOf('=');
-            result.put(kv.substring(0, idx),
-                       kv.substring(idx+1));
-        }
-        return result;
-    }
-    
-    /** 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:
-    * <ul>
-    * <li>"version" : The version of the server's software, including the name
-    *  of the software. (example: "Tor 0.0.9.4")</li>
-    * <li>"desc/id/<OR identity>" or "desc/name/<OR nickname>" : the latest server
-    * descriptor for a given OR, NUL-terminated.  If no such OR is known, the
-    * corresponding value is an empty string.</li>
-    * <li>"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.</li>
-    * <li>"addr-mappings/all"</li>
-    * <li>"addr-mappings/config"</li>
-    * <li>"addr-mappings/cache"</li>
-    * <li>"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.</li>
-    * <li>"circuit-status" : A series of lines as for a circuit status event. Each line is of the form:
-    * "CircuitID CircStatus Path"</li>
-    * <li>"stream-status" : A series of lines as for a stream status event.  Each is of the form:
-    * "StreamID StreamStatus CircID Target"</li>
-    * <li>"orconn-status" : A series of lines as for an OR connection status event.  Each is of the
-    * form: "ServerID ORStatus"</li>
-    * </ul>
-    */
-    public Map getInfo(Collection keys) throws IOException {
-        StringBuffer sb = new StringBuffer("GETINFO");
-        for (Iterator it = keys.iterator(); it.hasNext(); ) {
-            sb.append(" ").append((String)it.next());
-        }
-        sb.append("\r\n");
-        ArrayList lst = sendAndWaitForResponse(sb.toString(), null);
-        Map m = new HashMap();
-        for (Iterator it = lst.iterator(); it.hasNext(); ) {
-            ReplyLine line = (ReplyLine) it.next();
-            int idx = line.msg.indexOf('=');
-            if (idx<0)
-                break;
-            String k = line.msg.substring(0,idx);
-            Object v;
-            if (line.rest != null) {
-                v = line.rest;
-            } else {
-                v = line.msg.substring(idx+1);
-            }
-            m.put(k, v);
-        }
-        return m;
-    }
-    
-    /** An extendCircuit request takes one of two forms: either the <b>circID</b> is zero, in
-    * which case it is a request for the server to build a new circuit according
-    * to the specified path, or the <b>circID</b> is nonzero, in which case it is a
-    * request for the server to extend an existing circuit with that ID according
-    * to the specified <b>path</b>.
-    *
-    * If successful, returns the Circuit ID of the (maybe newly created) circuit.
-    */
-    public String extendCircuit(String circID, String path) throws IOException {
-        ArrayList lst = sendAndWaitForResponse(
-                          "EXTENDCIRCUIT "+circID+" "+path+"\r\n", null);
-        return ((ReplyLine)lst.get(0)).msg;
-    }
-    
-    /** Informs the Tor server that the stream specified by <b>streamID</b> should be
-    * associated with the circuit specified by <b>circID</b>.  
-    * 
-    * 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 <b>circID</b> 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 <b>desc</b>.
-    * 
-    * 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 {
-        ArrayList lst = sendAndWaitForResponse("+POSTDESCRIPTOR\r\n", desc);
-        return ((ReplyLine)lst.get(0)).msg;
-    }
-
-    /** Tells Tor to change the exit address of the stream identified by <b>streamID</b>
-     * to <b>address</b>. 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 <b>streamID</b>.
-    * <b>reason</b> should be one of the Tor RELAY_END reasons given in tor-spec.txt, as a decimal:
-    * <ul>
-    * <li>1 -- REASON_MISC           (catch-all for unlisted reasons)</li>
-    * <li>2 -- REASON_RESOLVEFAILED  (couldn't look up hostname)</li>
-    * <li>3 -- REASON_CONNECTREFUSED (remote host refused connection)</li>
-    * <li>4 -- REASON_EXITPOLICY     (OR refuses to connect to host or port)</li>
-    * <li>5 -- REASON_DESTROY        (Circuit is being destroyed)</li>
-    * <li>6 -- REASON_DONE           (Anonymized TCP connection was closed)</li>
-    * <li>7 -- REASON_TIMEOUT        (Connection timed out, or OR timed out while connecting)</li>
-    * <li>8 -- (unallocated)</li>
-    * <li>9 -- REASON_HIBERNATING    (OR is temporarily hibernating)</li>
-    * <li>10 -- REASON_INTERNAL       (Internal error at the OR)</li>
-    * <li>11 -- REASON_RESOURCELIMIT  (OR has no resources to fulfill request)</li>
-    * <li>12 -- REASON_CONNRESET      (Connection was unexpectedly reset)</li>
-    * <li>13 -- REASON_TORPROTOCOL    (Sent when closing connection because of Tor protocol violations)</li>
-    * </ul>
-    *
-    * 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 <b>circID</b>.
-    * If <b>ifUnused</b> 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);
-    }
-
-}
-