diff --git a/net/freehaven/tor/control/Bytes.java b/net/freehaven/tor/control/Bytes.java
index 24f929210b85f8a3328375a6f8fc870c2d3ba7e6..6166e449bc211d1c97b4933002b0fa0d6fd73573 100644
--- a/net/freehaven/tor/control/Bytes.java
+++ b/net/freehaven/tor/control/Bytes.java
@@ -3,7 +3,9 @@
 // See LICENSE file for copying information
 package net.freehaven.tor.control;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.StringTokenizer;
 
 /**
  * Static class to do bytewise structure manipulation in Java.
@@ -79,5 +81,33 @@ final class Bytes {
         }
     }
 
+    /**
+     * Read bytes from 'ba' starting at 'pos', dividing them into strings
+     * along the character in 'split' and writing them into 'lst'
+     */
+    public static List splitStr(List lst, String str) {
+        if (lst == null)
+            lst = new ArrayList();
+        StringTokenizer st = new StringTokenizer(str);
+        while (st.hasMoreTokens())
+            lst.add(st.nextToken());
+        return lst;
+    }
+
+    private static final char[] NYBBLES = {
+        '0', '1', '2', '3', '4', '5', '6', '7',
+        '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+    };
+
+    public static final String hex(byte[] ba) {
+        StringBuffer buf = new StringBuffer();
+        for (int i = 0; i < ba.length; ++i) {
+            int b = ((int)ba[i]) & 0xff;
+            buf.append(NYBBLES[b >> 4]);
+            buf.append(NYBBLES[b&0x0f]);
+        }
+        return buf.toString();
+    }
+
     private Bytes() {};
 }
diff --git a/net/freehaven/tor/control/EventHandler.java b/net/freehaven/tor/control/EventHandler.java
index c5162f738084b29d2da03ec21615760a92aa7945..d908bd204a2b35a46dbb6c95e92f74adfb0528cc 100644
--- a/net/freehaven/tor/control/EventHandler.java
+++ b/net/freehaven/tor/control/EventHandler.java
@@ -37,5 +37,11 @@ public interface EventHandler {
     /**
      * Invoked when Tor logs a message.
      */
-    public void message(int type, String msg);
+    public void message(String severity, String msg);
+    /**
+     * Invoked in an unspecified handler.
+     */
+    public void unrecognized(String type, String msg);
+
 }
+
diff --git a/net/freehaven/tor/control/PasswordDigest.java b/net/freehaven/tor/control/PasswordDigest.java
index d58c5675dc446948b1d90c4f66d34765c504e617..f632e9d40001f9318c4fe46da9a98bd5a74cc338 100644
--- a/net/freehaven/tor/control/PasswordDigest.java
+++ b/net/freehaven/tor/control/PasswordDigest.java
@@ -97,13 +97,7 @@ public class PasswordDigest {
     /** Return a hexadecimal encoding of a byte array. */
     // XXX There must be a better way to do this in Java.
     private static final String encodeBytes(byte[] ba) {
-        StringBuffer buf = new StringBuffer();
-        for (int i = 0; i < ba.length; ++i) {
-            int b = ((int)ba[i]) & 0xff;
-            buf.append(NYBBLES[b >> 4]);
-            buf.append(NYBBLES[b&0x0f]);
-        }
-        return buf.toString();
+        return Bytes.hex(ba);
     }
 
 }
\ No newline at end of file
diff --git a/net/freehaven/tor/control/TorControlConnection.java b/net/freehaven/tor/control/TorControlConnection.java
index e0137c57d33e637120d6457b03367b2547703d6a..daf9d8f16e25a79bf74d74371d9cbe9f166d46df 100644
--- a/net/freehaven/tor/control/TorControlConnection.java
+++ b/net/freehaven/tor/control/TorControlConnection.java
@@ -41,7 +41,6 @@ public abstract class TorControlConnection// implements TorControlCommands {
         this.waiters = new LinkedList();
     }
 
-
     /** Set the EventHandler object that will be notified of any
      * events Tor delivers to this connection.  To make Tor send us
      * events, call listenForEvents(). */
@@ -155,7 +154,7 @@ public abstract class TorControlConnection// implements TorControlCommands {
      * Tell Tor to extend the circuit identified by 'circID' through the
      * servers named in the list 'path'.
      */
-    public abstract int extendCircuit(String circID, String path) throws IOException;
+    public abstract String extendCircuit(String circID, String path) throws IOException;
 
     /**
      * Tell Tor to attach the stream identified by 'streamID' to the circuit
@@ -173,11 +172,11 @@ public abstract class TorControlConnection// implements TorControlCommands {
 
     /** Tell Tor to close the stream identified by 'streamID'.
      */
-    public abstract void closeStream(String streamID, byte reason, byte flags)
+    public abstract void closeStream(String streamID, byte reason)
         throws IOException;
 
     /** Tell Tor to close the circuit identified by 'streamID'.
      */
-    public abstract void closeCircuit(String circID, byte flags) throws IOException;
+    public abstract void closeCircuit(String circID, boolean ifUnused) throws IOException;
 
 }
diff --git a/net/freehaven/tor/control/TorControlConnection0.java b/net/freehaven/tor/control/TorControlConnection0.java
index 29c935019da9055ffbb1d058e2abd18afbef4fe3..d3171b260953ec3a9ecab21707051395987caccb 100644
--- a/net/freehaven/tor/control/TorControlConnection0.java
+++ b/net/freehaven/tor/control/TorControlConnection0.java
@@ -27,8 +27,6 @@ public class TorControlConnection0 extends TorControlConnection
         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. */
@@ -222,11 +220,19 @@ public class TorControlConnection0 extends TorControlConnection
               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(type, Bytes.getNulTerminatedStr(c.body, 2));
+              handler.message("ERR", Bytes.getNulTerminatedStr(c.body, 2));
               break;
           default:
               throw new TorControlSyntaxError("Unrecognized event type.");
@@ -345,13 +351,13 @@ public class TorControlConnection0 extends TorControlConnection
         return m;
     }
 
-    public int extendCircuit(String circID, String path) throws IOException {
+    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 Bytes.getU32(c.body, 0);
+        return Integer.toString(Bytes.getU32(c.body, 0));
     }
 
     public void attachStream(String streamID, String circID) 
@@ -381,21 +387,21 @@ public class TorControlConnection0 extends TorControlConnection
 
     /** Tell Tor to close the stream identified by 'streamID'.
      */
-    public void closeStream(String streamID, byte reason, byte flags)
+    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] = flags;
+        ba[5] = (byte)0;
         sendAndWaitForResponse(CMD_CLOSESTREAM, ba);
     }
 
     /** Tell Tor to close the circuit identified by 'streamID'.
      */
-    public void closeCircuit(String circID, byte flags) throws IOException {
+    public void closeCircuit(String circID, boolean ifUnused) throws IOException {
         byte[] ba = new byte[5];
         Bytes.setU32(ba, 0, (int)Long.parseLong(circID));
-        ba[4] = flags;
+        ba[4] = (byte)(ifUnused? 1 : 0);
         sendAndWaitForResponse(CMD_CLOSECIRCUIT, ba);
     }
 
diff --git a/net/freehaven/tor/control/TorControlConnection1.java b/net/freehaven/tor/control/TorControlConnection1.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed72bdb05872474e751867f2a98ab40b9e7b4c8c
--- /dev/null
+++ b/net/freehaven/tor/control/TorControlConnection1.java
@@ -0,0 +1,346 @@
+// $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;
+
+/** DOCDOC */
+public class TorControlConnection1 extends TorControlConnection
+    implements TorControlCommands
+{
+    protected java.io.BufferedReader input;
+    protected java.io.Writer output;
+
+    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("."))
+                output.write(".");
+            output.write(line);
+            if (line.endsWith("\r"))
+                output.write("\n");
+            else
+                output.write("\r\n");
+        }
+        output.write(".\r\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.length() < 4)
+                throw new TorControlSyntaxError("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 (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 (((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 {
+        Waiter w = new Waiter();
+        synchronized (waiters) {
+            output.write(s);
+            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);
+            }
+        }
+    }
+
+    /** Change the values of the configuration options stored in
+     * 'kvList'.  (The format is "key 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 Map 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);
+        Map result = new HashMap();
+        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 void setEvents(List events) throws IOException {
+        StringBuffer sb = new StringBuffer("SETEVENTS");
+        for (Iterator it = events.iterator(); it.hasNext(); ) {
+            String event = (String) it.next();
+            sb.append(" ").append(event);
+        }
+        sb.append("\r\n");
+        sendAndWaitForResponse(sb.toString(), null);
+    }
+
+    public void authenticate(byte[] auth) throws IOException {
+        String cmd = "AUTHENTICATE " + Bytes.hex(auth) + "\r\n";
+        sendAndWaitForResponse(cmd, null);
+    }
+
+    public void saveConf() throws IOException {
+        sendAndWaitForResponse("SAVECONF\r\n", null);
+    }
+
+    public void signal(String signal) throws IOException {
+        String cmd = "AUTHENTICATE " + signal + "\r\n";
+        sendAndWaitForResponse(cmd, null);
+    }
+
+    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 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;
+    }
+
+    public String extendCircuit(String circID, String path) throws IOException {
+        ArrayList lst = sendAndWaitForResponse(
+                          "EXTENDCIRCUIT "+circID+" "+path+"\r\n", null);
+        return ((ReplyLine)lst.get(0)).msg;
+    }
+
+    public void attachStream(String streamID, String circID)
+        throws IOException {
+        sendAndWaitForResponse("ATTACHSTREAM "+streamID+" "+circID+"\r\n", null);
+    }
+
+    /** Tell Tor about the server descriptor in 'desc' */
+    public String postDescriptor(String desc) throws IOException {
+        ArrayList lst = sendAndWaitForResponse("+POSTDESCRIPTOR\r\n", desc);
+        return ((ReplyLine)lst.get(0)).msg;
+    }
+
+    /** Tell Tor to change the target of the stream identified by 'streamID'
+     * to 'address'.
+     */
+    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'.
+     */
+    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'.
+     */
+    public void closeCircuit(String circID, boolean ifUnused) throws IOException {
+        sendAndWaitForResponse("CLOSECIRCUIT "+circID+
+                               (ifUnused?" IFUNUSED":"")+"\r\n", null);
+    }
+
+}
diff --git a/net/freehaven/tor/control/TorControlError.java b/net/freehaven/tor/control/TorControlError.java
index 7244784d3d0093ae7be495b1512b1d2544fedcb8..1d04c80a24cec9cc481fb2446eaf763a8f5121da 100644
--- a/net/freehaven/tor/control/TorControlError.java
+++ b/net/freehaven/tor/control/TorControlError.java
@@ -12,11 +12,16 @@ public class TorControlError extends RuntimeException {
         super(s);
         errorType = type;
     }
+    public TorControlError(String s) {
+        this(-1, s);
+    }
     public int getErrorType() {
         return errorType;
     }
     public String getErrorMsg() {
         try {
+            if (errorType == -1)
+                return null;
             return TorControlCommands.ERROR_MSGS[errorType];
         } catch (ArrayIndexOutOfBoundsException ex) {
             return "Unrecongized error #"+errorType;