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

[or-cvs] r9623: Refactored TorCtl into its own python package. Separated the (in torflow/trunk: . TorCtl)



Author: mikeperry
Date: 2007-02-23 14:06:32 -0500 (Fri, 23 Feb 2007)
New Revision: 9623

Added:
   torflow/trunk/TorCtl/
   torflow/trunk/TorCtl/PathSupport.py
   torflow/trunk/TorCtl/README
   torflow/trunk/TorCtl/TorCtl.py
   torflow/trunk/TorCtl/TorUtil.py
   torflow/trunk/TorCtl/__init__.py
Removed:
   torflow/trunk/TorCtl.py
   torflow/trunk/TorUtil.py
   torflow/trunk/unit.py
Modified:
   torflow/trunk/metatroller.py
   torflow/trunk/nodemon.py
Log:
Refactored TorCtl into its own python package. Separated the node selection
stuff out into PathSupport.py so as to make it easier to ignore it if you
want (but that would be foolish because it is awesome :)




Added: torflow/trunk/TorCtl/PathSupport.py
===================================================================
--- torflow/trunk/TorCtl/PathSupport.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/TorCtl/PathSupport.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -0,0 +1,456 @@
+#!/usr/bin/python
+
+import TorCtl
+import re
+import struct
+import random
+import socket
+import copy
+import datetime
+from TorUtil import *
+
+__all__ = ["NodeRestrictionList", "PathRestrictionList",
+"PercentileRestriction", "OSRestriction", "ConserveExitsRestriction",
+"FlagsRestriction", "MinBWRestriction", "VersionIncludeRestriction",
+"VersionExcludeRestriction", "ExitPolicyRestriction", "OrNodeRestriction",
+"AtLeastNNodeRestriction", "NotNodeRestriction", "Subnet16Restriction",
+"UniqueRestriction", "UniformGenerator", "OrderedExitGenerator",
+"PathSelector", "Connection"]
+
+#################### Path Support Interfaces #####################
+
+class NodeRestriction:
+    "Interface for node restriction policies"
+    def r_is_ok(self, r): return True    
+    def reset(self, router_list): pass
+
+class NodeRestrictionList:
+    def __init__(self, restrictions, sorted_r):
+        self.restrictions = restrictions
+        self.update_routers(sorted_r)
+
+    def __check_r(self, r):
+        for rst in self.restrictions:
+            if not rst.r_is_ok(r): return False
+        self.restricted_bw += r.bw
+        return True
+
+    def update_routers(self, sorted_r):
+        self._sorted_r = sorted_r
+        self.restricted_bw = 0
+        for rs in self.restrictions: rs.reset(sorted_r)
+        self.restricted_r = filter(self.__check_r, self._sorted_r)
+
+    def add_restriction(self, restr):
+        self.restrictions.append(restr)
+        for r in self.restricted_r:
+            if not restr.r_is_ok(r):
+                self.restricted_r.remove(r)
+                self.restricted_bw -= r.bw
+    
+    # XXX: This does not collapse meta restrictions..
+    def del_restriction(self, RestrictionClass):
+        self.restrictions = filter(
+                lambda r: not isinstance(r, RestrictionClass),
+                    self.restrictions)
+        self.update_routers(self._sorted_r)
+
+class PathRestriction:
+    "Interface for path restriction policies"
+    def r_is_ok(self, path, r): return True    
+    def entry_is_ok(self, path, r): return self.r_is_ok(path, r)
+    def middle_is_ok(self, path, r): return self.r_is_ok(path, r)
+    def exit_is_ok(self, path, r): return self.r_is_ok(path, r)
+
+class PathRestrictionList:
+    def __init__(self, restrictions):
+        self.restrictions = restrictions
+    
+    def entry_is_ok(self, path, r):
+        for rs in self.restrictions:
+            if not rs.entry_is_ok(path, r):
+                return False
+        return True
+
+    def middle_is_ok(self, path, r):
+        for rs in self.restrictions:
+            if not rs.middle_is_ok(path, r):
+                return False
+        return True
+
+    def exit_is_ok(self, path, r):
+        for rs in self.restrictions:
+            if not rs.exit_is_ok(path, r):
+                return False
+        return True
+
+    def add_restriction(self, rstr):
+        self.restrictions.append(rstr)
+
+    def del_restriction(self, RestrictionClass):
+        self.restrictions = filter(
+                lambda r: not isinstance(r, RestrictionClass),
+                    self.restrictions)
+
+class NodeGenerator:
+    "Interface for node generation"
+    def __init__(self, restriction_list):
+        self.restriction_list = restriction_list
+        self.rewind()
+
+    def rewind(self):
+        # TODO: Hrmm... Is there any way to handle termination other 
+        # than to make a list of routers that we pop from? Random generators 
+        # will not terminate if no node matches the selector without this..
+        # Not so much an issue now, but in a few years, the Tor network
+        # will be large enough that having all these list copies will
+        # be obscene... Possible candidate for a python list comprehension
+        self.routers = copy.copy(self.restriction_list.restricted_r)
+        self.bw = self.restriction_list.restricted_bw
+
+    def mark_chosen(self, r):
+        self.routers.remove(r)
+        self.bw -= r.bw
+
+    def all_chosen(self):
+        if not self.routers and self.bw or not self.bw and self.routers:
+            plog("WARN", str(len(self.routers))+" routers left but bw="
+                 +str(self.bw))
+        return not self.routers
+
+    def next_r(self): raise NotImplemented()
+
+class Connection(TorCtl.Connection):
+    def build_circuit(self, pathlen, path_sel):
+        circ = TorCtl.Circuit()
+        if pathlen == 1:
+            circ.exit = path_sel.exit_chooser(circ.path)
+            circ.path = [circ.exit]
+            circ.cid = self.extend_circuit(0, circ.id_path())
+        else:
+            circ.path.append(path_sel.entry_chooser(circ.path))
+            for i in xrange(1, pathlen-1):
+                circ.path.append(path_sel.middle_chooser(circ.path))
+            circ.exit = path_sel.exit_chooser(circ.path)
+            circ.path.append(circ.exit)
+            circ.cid = self.extend_circuit(0, circ.id_path())
+        circ.created_at = datetime.datetime.now()
+        return circ
+
+######################## Node Restrictions ########################
+
+# TODO: We still need more path support implementations
+#  - BwWeightedGenerator
+#  - NodeRestrictions:
+#    - Uptime
+#    - Published/Updated
+#    - GeoIP
+#      - NodeCountry
+#  - PathRestrictions
+#    - Family
+#    - GeoIP:
+#      - OceanPhobicRestrictor (avoids Pacific Ocean or two atlantic crossings)
+#        or ContinentRestrictor (avoids doing more than N continent crossings)
+#        - Mathematical/empirical study of predecessor expectation
+#          - If middle node on the same continent as exit, exit learns nothing
+#          - else, exit has a bias on the continent of origin of user
+#            - Language and browser accept string determine this anyway
+
+class PercentileRestriction(NodeRestriction):
+    """If used, this restriction MUST be FIRST in the RestrictionList."""
+    def __init__(self, pct_skip, pct_fast, r_list):
+        self.pct_skip = pct_skip
+        self.pct_fast = pct_fast
+        self.sorted_r = r_list
+        self.position = 0
+
+    def reset(self, r_list):
+        self.sorted_r = r_list
+        self.position = 0
+        
+    def r_is_ok(self, r):
+        ret = True
+        if self.position == len(self.sorted_r):
+            self.position = 0
+            plog("WARN", "Resetting PctFastRestriction")
+        if self.position != self.sorted_r.index(r): # XXX expensive?
+            plog("WARN", "Router"+r.nickname+" at mismatched index: "
+                         +self.position+" vs "+self.sorted_r.index(r))
+        
+        if self.position < len(self.sorted_r)*self.pct_skip/100:
+            ret = False
+        elif self.position > len(self.sorted_r)*self.pct_fast/100:
+            ret = False
+        
+        self.position += 1
+        return ret
+        
+class OSRestriction(NodeRestriction):
+    def __init__(self, ok, bad=[]):
+        self.ok = ok
+        self.bad = bad
+
+    def r_is_ok(self, r):
+        for y in self.ok:
+            if re.search(y, r.os):
+                return True
+        for b in self.bad:
+            if re.search(b, r.os):
+                return False
+        if self.ok: return False
+        if self.bad: return True
+
+class ConserveExitsRestriction(NodeRestriction):
+    def r_is_ok(self, r): return not "Exit" in r.flags
+
+class FlagsRestriction(NodeRestriction):
+    def __init__(self, mandatory, forbidden=[]):
+        self.mandatory = mandatory
+        self.forbidden = forbidden
+
+    def r_is_ok(self, router):
+        for m in self.mandatory:
+            if not m in router.flags: return False
+        for f in self.forbidden:
+            if f in router.flags: return False
+        return True
+
+class MinBWRestriction(NodeRestriction):
+    def __init__(self, minbw):
+        self.min_bw = minbw
+
+    def r_is_ok(self, router): return router.bw >= self.min_bw
+     
+class VersionIncludeRestriction(NodeRestriction):
+    def __init__(self, eq):
+        self.eq = map(TorCtl.RouterVersion, eq)
+    
+    def r_is_ok(self, router):
+        for e in self.eq:
+            if e == router.version:
+                return True
+        return False
+
+
+class VersionExcludeRestriction(NodeRestriction):
+    def __init__(self, exclude):
+        self.exclude = map(TorCtl.RouterVersion, exclude)
+    
+    def r_is_ok(self, router):
+        for e in self.exclude:
+            if e == router.version:
+                return False
+        return True
+
+class VersionRangeRestriction(NodeRestriction):
+    def __init__(self, gr_eq, less_eq=None):
+        self.gr_eq = TorCtl.RouterVersion(gr_eq)
+        if less_eq: self.less_eq = TorCtl.RouterVersion(less_eq)
+        else: self.less_eq = None
+    
+
+    def r_is_ok(self, router):
+        return (not self.gr_eq or router.version >= self.gr_eq) and \
+                (not self.less_eq or router.version <= self.less_eq)
+
+class ExitPolicyRestriction(NodeRestriction):
+    def __init__(self, to_ip, to_port):
+        self.to_ip = to_ip
+        self.to_port = to_port
+
+    def r_is_ok(self, r): return r.will_exit_to(self.to_ip, self.to_port)
+
+class MetaNodeRestriction(NodeRestriction):
+    # XXX: these should collapse the restriction and return a new
+    # instance for re-insertion (or None)
+    def next_rstr(self): raise NotImplemented()
+    def del_restriction(self, RestrictionClass): raise NotImplemented()
+
+class OrNodeRestriction(MetaNodeRestriction):
+    def __init__(self, rs):
+        self.rstrs = rs
+
+    def r_is_ok(self, r):
+        for rs in self.rstrs:
+            if rs.r_is_ok(r):
+                return True
+        return False
+
+class NotNodeRestriction(MetaNodeRestriction):
+    def __init__(self, a):
+        self.a = a
+
+    def r_is_ok(self, r): return not self.a.r_is_ok(r)
+
+class AtLeastNNodeRestriction(MetaNodeRestriction):
+    def __init__(self, rstrs, n):
+        self.rstrs = rstrs
+        self.n = n
+
+    def r_is_ok(self, r):
+        cnt = 0
+        for rs in self.rstrs:
+            if rs.r_is_ok(r):
+                cnt += 1
+        if cnt < self.n: return False
+        else: return True
+
+
+#################### Path Restrictions #####################
+
+class Subnet16Restriction(PathRestriction):
+    def r_is_ok(self, path, router):
+        mask16 = struct.unpack(">I", socket.inet_aton("255.255.0.0"))[0]
+        ip16 = router.ip & mask16
+        for r in path:
+            if ip16 == (r.ip & mask16):
+                return False
+        return True
+
+class UniqueRestriction(PathRestriction):
+    def r_is_ok(self, path, r): return not r in path
+
+
+#################### Node Generators ######################
+
+class UniformGenerator(NodeGenerator):
+    def next_r(self):
+        while not self.all_chosen():
+            r = random.choice(self.routers)
+            self.mark_chosen(r)
+            yield r
+
+class OrderedExitGenerator(NodeGenerator):
+    def __init__(self, restriction_list, to_port):
+        self.to_port = to_port
+        self.next_exit_by_port = {}
+        NodeGenerator.__init__(self, restriction_list)
+
+    def rewind(self):
+        NodeGenerator.rewind(self)
+        if self.to_port not in self.next_exit_by_port or not self.next_exit_by_port[self.to_port]:
+            self.next_exit_by_port[self.to_port] = 0
+            self.last_idx = len(self.routers)
+        else:
+            self.last_idx = self.next_exit_by_port[self.to_port]
+   
+    # Just in case: 
+    def mark_chosen(self, r): raise NotImplemented()
+    def all_chosen(self): raise NotImplemented()
+
+    def next_r(self):
+        while True: # A do..while would be real nice here..
+            if self.next_exit_by_port[self.to_port] >= len(self.routers):
+                self.next_exit_by_port[self.to_port] = 0
+            r = self.routers[self.next_exit_by_port[self.to_port]]
+            self.next_exit_by_port[self.to_port] += 1
+            yield r
+            if self.last_idx == self.next_exit_by_port[self.to_port]:
+                break
+
+####################### Secret Sauce ###########################
+
+class PathError(Exception):
+    pass
+
+class NoRouters(PathError):
+    pass
+
+class PathSelector:
+    "Implementation of path selection policies"
+    def __init__(self, entry_gen, mid_gen, exit_gen, path_restrict):
+        self.entry_gen = entry_gen
+        self.mid_gen = mid_gen
+        self.exit_gen = exit_gen
+        self.path_restrict = path_restrict
+
+    def entry_chooser(self, path):
+        self.entry_gen.rewind()
+        for r in self.entry_gen.next_r():
+            if self.path_restrict.entry_is_ok(path, r):
+                return r
+        raise NoRouters();
+        
+    def middle_chooser(self, path):
+        self.mid_gen.rewind()
+        for r in self.mid_gen.next_r():
+            if self.path_restrict.middle_is_ok(path, r):
+                return r
+        raise NoRouters();
+
+    def exit_chooser(self, path):
+        self.exit_gen.rewind()
+        for r in self.exit_gen.next_r():
+            if self.path_restrict.exit_is_ok(path, r):
+                return r
+        raise NoRouters();
+
+
+########################## Unit tests ##########################
+
+
+def do_unit(rst, r_list, plamb):
+    print "\n"
+    print "-----------------------------------"
+    print rst.r_is_ok.im_class
+    for r in r_list:
+        print r.nickname+" "+plamb(r)+"="+str(rst.r_is_ok(r))
+
+# TODO: Tests:
+#  - Test each NodeRestriction and print in/out lines for it
+#  - Test NodeGenerator and reapply NodeRestrictions
+#  - Same for PathSelector and PathRestrictions
+#    - Also Reapply each restriction by hand to path. Verify returns true
+
+if __name__ == '__main__':
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect(("127.0.0.1",9061))
+    c = Connection(s)
+    c.debug(file("control.log", "w"))
+    c.authenticate()
+    nslist = c.get_network_status()
+    sorted_r = c.read_routers(c.get_network_status())
+
+    pct_rst = PercentileRestriction(10, 20, sorted_r)
+    oss_rst = OSRestriction([r"[lL]inux", r"BSD", "Darwin"], [])
+    prop_rst = OSRestriction([], ["Windows", "Solaris"])
+
+   
+    do_unit(VersionRangeRestriction("0.1.2.0"), sorted_r,
+                  lambda r: str(r.version))
+    do_unit(VersionRangeRestriction("0.1.2.0", "0.1.2.5"), sorted_r,
+                  lambda r: str(r.version))
+    do_unit(VersionIncludeRestriction(["0.1.1.26-alpha", "0.1.2.7-ignored"]),
+                  sorted_r, lambda r: str(r.version))
+    do_unit(VersionExcludeRestriction(["0.1.1.26"]), sorted_r,
+                  lambda r: str(r.version))
+
+    do_unit(ConserveExitsRestriction(), sorted_r, lambda r: " ".join(r.flags))
+    do_unit(FlagsRestriction([], ["Valid"]), sorted_r, lambda r: " ".join(r.flags))
+
+    # TODO: Cross check ns exit flag with this list
+    #do_unit(ExitPolicyRestriction("255.255.255.255", 25), sorted_r)
+
+    #do_unit(pct_rst, sorted_r)
+    #do_unit(oss_rst, sorted_r)
+    #do_unit(alpha_rst, sorted_r)
+    
+    rl =  [AtLeastNNodeRestriction([ExitPolicyRestriction("255.255.255.255", 80), ExitPolicyRestriction("255.255.255.255", 443), ExitPolicyRestriction("255.255.255.255", 6667)], 2), FlagsRestriction([], ["BadExit"])]
+
+    exit_rstr = NodeRestrictionList(rl, sorted_r)
+
+    ug = UniformGenerator(exit_rstr)
+
+    rlist = []
+    for r in ug.next_r():
+        print "Checking: " + r.nickname
+        for rs in rl:
+            if not rs.r_is_ok(r):
+                raise PathException()
+            if not "Exit" in r.flags:
+                print "No exit in flags of "+r.nickname
+        rlist.append(r)
+    for r in sorted_r:
+        if "Exit" in r.flags and not r in rlist:
+            print r.nickname+" is an exit not in rl!"
+                

Added: torflow/trunk/TorCtl/README
===================================================================
--- torflow/trunk/TorCtl/README	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/TorCtl/README	2007-02-23 19:06:32 UTC (rev 9623)
@@ -0,0 +1 @@
+TODO: Write me.

Added: torflow/trunk/TorCtl/TorCtl.py
===================================================================
--- torflow/trunk/TorCtl/TorCtl.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/TorCtl/TorCtl.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -0,0 +1,968 @@
+#!/usr/bin/python
+# TorCtl.py -- Python module to interface with Tor Control interface.
+# Copyright 2005 Nick Mathewson
+# Copyright 2007 Mike Perry. See LICENSE file.
+
+"""
+TorCtl -- Library to control Tor processes.
+"""
+
+# XXX: Docstring all exported classes/interfaces. Also need __all__
+
+import os
+import re
+import struct
+import sys
+import threading
+import Queue
+import datetime
+import traceback
+import socket
+import binascii
+import types
+import time
+from TorUtil import *
+
+# Types of "EVENT" message.
+EVENT_TYPE = Enum2(
+                    CIRC="CIRC",
+                    STREAM="STREAM",
+                    ORCONN="ORCONN",
+                    BW="BW",
+                    NS="NS",
+                    NEWDESC="NEWDESC",
+                    DEBUG="DEBUG",
+                    INFO="INFO",
+                    NOTICE="NOTICE",
+                    WARN="WARN",
+                    ERR="ERR")
+
+class TorCtlError(Exception):
+    "Generic error raised by TorControl code."
+    pass
+
+class TorCtlClosed(TorCtlError):
+    "Raised when the controller connection is closed by Tor (not by us.)"
+    pass
+
+class ProtocolError(TorCtlError):
+    "Raised on violations in Tor controller protocol"
+    pass
+
+class ErrorReply(TorCtlError):
+    "Raised when Tor controller returns an error"
+    pass
+
+class NetworkStatus:
+    "Filled in during NS events"
+    def __init__(self, nickname, idhash, orhash, updated, ip, orport, dirport, flags):
+        self.nickname = nickname
+        self.idhash = idhash
+        self.orhash = orhash
+        self.ip = ip
+        self.orport = int(orport)
+        self.dirport = int(dirport)
+        self.flags = flags
+        self.idhex = (self.idhash + "=").decode("base64").encode("hex")
+        m = re.search(r"(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)", updated)
+        self.updated = datetime.datetime(*map(int, m.groups()))
+
+class NetworkStatusEvent:
+    def __init__(self, event_name, nslist):
+        self.event_name = event_name
+        self.nslist = nslist # List of NetworkStatus objects
+
+class NewDescEvent:
+    def __init__(self, event_name, idlist):
+        self.event_name = event_name
+        self.idlist = idlist
+
+class CircuitEvent:
+    def __init__(self, event_name, circ_id, status, path, reason,
+                 remote_reason):
+        self.event_name = event_name
+        self.circ_id = circ_id
+        self.status = status
+        self.path = path
+        self.reason = reason
+        self.remote_reason = remote_reason
+
+class StreamEvent:
+    def __init__(self, event_name, strm_id, status, circ_id, target_host,
+                 target_port, reason, remote_reason):
+        self.event_name = event_name
+        self.strm_id = strm_id
+        self.status = status
+        self.circ_id = circ_id
+        self.target_host = target_host
+        self.target_port = int(target_port)
+        self.reason = reason
+        self.remote_reason = remote_reason
+
+class ORConnEvent:
+    def __init__(self, event_name, status, endpoint, age, read_bytes,
+                 wrote_bytes, reason, ncircs):
+        self.event_name = event_name
+        self.status = status
+        self.endpoint = endpoint
+        self.age = age
+        self.read_bytes = read_bytes
+        self.wrote_bytes = wrote_bytes
+        self.reason = reason
+        self.ncircs = ncircs
+
+class LogEvent:
+    def __init__(self, level, msg):
+        self.event_name = self.level = level
+        self.msg = msg
+
+class AddrMapEvent:
+    def __init__(self, event_name, from_addr, to_addr, when, by_exit):
+        self.event_name = event_name
+        self.from_addr = from_addr
+        self.to_addr = to_addr
+        self.when = when
+        self.by_exit = by_exit # XOXOXOX <3 ;) @ nickm
+
+class BWEvent:
+    def __init__(self, event_name, read, written):
+        self.event_name = event_name
+        self.read = read
+        self.written = written
+
+class UnknownEvent:
+    def __init__(self, event_name, event_string):
+        self.event_name = event_name
+        self.event_string = event_string
+
+class ExitPolicyLine:
+    def __init__(self, match, ip_mask, port_low, port_high):
+        self.match = match
+        if ip_mask == "*":
+            self.ip = 0
+            self.netmask = 0
+        else:
+            if not "/" in ip_mask:
+                self.netmask = 0xFFFFFFFF
+                ip = ip_mask
+            else:
+                ip, mask = ip_mask.split("/")
+                if re.match(r"\d+.\d+.\d+.\d+", mask):
+                    self.netmask=struct.unpack(">I", socket.inet_aton(mask))[0]
+                else:
+                    self.netmask = ~(2**(32 - int(mask)) - 1)
+            self.ip = struct.unpack(">I", socket.inet_aton(ip))[0]
+        self.ip &= self.netmask
+        if port_low == "*":
+            self.port_low,self.port_high = (0,65535)
+        else:
+            if not port_high:
+                port_high = port_low
+            self.port_low = int(port_low)
+            self.port_high = int(port_high)
+    
+    def check(self, ip, port):
+        ip = struct.unpack(">I", socket.inet_aton(ip))[0]
+        if (ip & self.netmask) == self.ip:
+            if self.port_low <= port and port <= self.port_high:
+                return self.match
+        return -1
+
+class RouterVersion:
+    def __init__(self, version):
+        v = re.search("^(\d+).(\d+).(\d+).(\d+)", version).groups()
+        self.version = int(v[0])*0x1000000 + int(v[1])*0x10000 + int(v[2])*0x100 + int(v[3])
+        self.ver_string = version
+
+    def __lt__(self, other): return self.version < other.version
+    def __gt__(self, other): return self.version > other.version
+    def __ge__(self, other): return self.version >= other.version
+    def __le__(self, other): return self.version <= other.version
+    def __eq__(self, other): return self.version == other.version
+    def __ne__(self, other): return self.version != other.version
+    def __str__(self): return self.ver_string
+
+class Router:
+    def __init__(self, idhex, name, bw, down, exitpolicy, flags, ip, version, os):
+        self.idhex = idhex
+        self.nickname = name
+        self.bw = bw
+        self.exitpolicy = exitpolicy
+        self.guard = "Guard" in flags
+        self.badexit = "BadExit" in flags
+        self.valid = "Valid" in flags
+        self.fast = "Fast" in flags
+        self.flags = flags
+        self.down = down
+        self.ip = struct.unpack(">I", socket.inet_aton(ip))[0]
+        self.version = RouterVersion(version)
+        self.os = os
+
+    def will_exit_to(self, ip, port):
+        for line in self.exitpolicy:
+            ret = line.check(ip, port)
+            if ret != -1:
+                return ret
+        plog("NOTICE", "No matching exit line for "+self.nickname)
+        return False
+    
+    def __eq__(self, other): return self.idhex == other.idhex
+    def __ne__(self, other): return self.idhex != other.idhex
+
+class Circuit:
+    def __init__(self):
+        self.cid = 0
+        self.created_at = 0 # time
+        self.path = [] # routers
+        self.exit = 0
+    
+    def id_path(self): return map(lambda r: r.idhex, self.path)
+
+
+class Connection:
+    """A Connection represents a connection to the Tor process."""
+    def __init__(self, sock):
+        """Create a Connection to communicate with the Tor process over the
+           socket 'sock'.
+        """
+        self._handler = None
+        self._handleFn = None
+        self._sendLock = threading.RLock()
+        self._queue = Queue.Queue()
+        self._thread = None
+        self._closedEx = None
+        self._closed = 0
+        self._closeHandler = None
+        self._eventThread = None
+        self._eventQueue = Queue.Queue()
+        self._s = BufSock(sock)
+        self._debugFile = None
+
+    def set_close_handler(self, handler):
+        """Call 'handler' when the Tor process has closed its connection or
+           given us an exception.  If we close normally, no arguments are
+           provided; otherwise, it will be called with an exception as its
+           argument.
+        """
+        self._closeHandler = handler
+
+    def close(self):
+        """Shut down this controller connection"""
+        self._sendLock.acquire()
+        try:
+            self._queue.put("CLOSE")
+            self._eventQueue.put("CLOSE")
+            self._s.close()
+            self._s = None
+            self._closed = 1
+        finally:
+            self._sendLock.release()
+
+    def launch_thread(self, daemon=1):
+        """Launch a background thread to handle messages from the Tor process."""
+        assert self._thread is None
+        t = threading.Thread(target=self._loop)
+        if daemon:
+            t.setDaemon(daemon)
+        t.start()
+        self._thread = t
+        t = threading.Thread(target=self._eventLoop)
+        if daemon:
+            t.setDaemon(daemon)
+        t.start()
+        self._eventThread = t
+        return self._thread
+
+    def _loop(self):
+        """Main subthread loop: Read commands from Tor, and handle them either
+           as events or as responses to other commands.
+        """
+        while 1:
+            try:
+                isEvent, reply = self._read_reply()
+            except:
+                self._err(sys.exc_info())
+                return
+
+            if isEvent:
+                if self._handler is not None:
+                    self._eventQueue.put(reply)
+            else:
+                cb = self._queue.get()
+                cb(reply)
+
+    def _err(self, (tp, ex, tb), fromEventLoop=0):
+        """DOCDOC"""
+        # silent death is bad :(
+        traceback.print_exception(tp, ex, tb)
+        if self._s:
+            try:
+                self.close()
+            except:
+                pass
+        self._sendLock.acquire()
+        try:
+            self._closedEx = ex
+            self._closed = 1
+        finally:
+            self._sendLock.release()
+        while 1:
+            try:
+                cb = self._queue.get(timeout=0)
+                if cb != "CLOSE":
+                    cb("EXCEPTION")
+            except Queue.Empty:
+                break
+        if self._closeHandler is not None:
+            self._closeHandler(ex)
+        return
+
+    def _eventLoop(self):
+        """DOCDOC"""
+        while 1:
+            reply = self._eventQueue.get()
+            if reply == "CLOSE":
+                return
+            try:
+                self._handleFn(reply)
+            except:
+                self._err(sys.exc_info(), 1)
+                return
+
+    def _sendImpl(self, sendFn, msg):
+        """DOCDOC"""
+        if self._thread is None:
+            self.launch_thread(1)
+        # This condition will get notified when we've got a result...
+        condition = threading.Condition()
+        # Here's where the result goes...
+        result = []
+
+        if self._closedEx is not None:
+            raise self._closedEx
+        elif self._closed:
+            raise TorCtlClosed()
+
+        def cb(reply,condition=condition,result=result):
+            condition.acquire()
+            try:
+                result.append(reply)
+                condition.notify()
+            finally:
+                condition.release()
+
+        # Sends a message to Tor...
+        self._sendLock.acquire()
+        try:
+            self._queue.put(cb)
+            sendFn(msg) # _doSend(msg)
+        finally:
+            self._sendLock.release()
+
+        # Now wait till the answer is in...
+        condition.acquire()
+        try:
+            while not result:
+                condition.wait()
+        finally:
+            condition.release()
+
+        # ...And handle the answer appropriately.
+        assert len(result) == 1
+        reply = result[0]
+        if reply == "EXCEPTION":
+            raise self._closedEx
+
+        return reply
+
+
+    def debug(self, f):
+        """DOCDOC"""
+        self._debugFile = f
+
+    def set_event_handler(self, handler):
+        """Cause future events from the Tor process to be sent to 'handler'.
+        """
+        self._handler = handler
+        self._handleFn = handler.handle1
+
+    def _read_reply(self):
+        lines = []
+        while 1:
+            line = self._s.readline().strip()
+            if self._debugFile:
+                self._debugFile.write("    %s\n" % line)
+            if len(line)<4:
+                raise ProtocolError("Badly formatted reply line: Too short")
+            code = line[:3]
+            tp = line[3]
+            s = line[4:]
+            if tp == "-":
+                lines.append((code, s, None))
+            elif tp == " ":
+                lines.append((code, s, None))
+                isEvent = (lines and lines[0][0][0] == '6')
+                return isEvent, lines
+            elif tp != "+":
+                raise ProtocolError("Badly formatted reply line: unknown type %r"%tp)
+            else:
+                more = []
+                while 1:
+                    line = self._s.readline()
+                    if self._debugFile:
+                        self._debugFile.write("+++ %s" % line)
+                    if line in (".\r\n", ".\n"):
+                        break
+                    more.append(line)
+                lines.append((code, s, unescape_dots("".join(more))))
+                isEvent = (lines and lines[0][0][0] == '6')
+                if isEvent: # Need "250 OK" if it's not an event. Otherwise, end
+                    return (isEvent, lines)
+
+        # XXX: Notreached
+        isEvent = (lines and lines[0][0][0] == '6')
+        return (isEvent, lines)
+
+    def _doSend(self, msg):
+        if self._debugFile:
+            amsg = msg
+            lines = amsg.split("\n")
+            if len(lines) > 2:
+                amsg = "\n".join(lines[:2]) + "\n"
+            self._debugFile.write(">>> %s" % amsg)
+        self._s.write(msg)
+
+    def sendAndRecv(self, msg="", expectedTypes=("250", "251")):
+        """Helper: Send a command 'msg' to Tor, and wait for a command
+           in response.  If the response type is in expectedTypes,
+           return a list of (tp,body,extra) tuples.  If it is an
+           error, raise ErrorReply.  Otherwise, raise ProtocolError.
+        """
+        if type(msg) == types.ListType:
+            msg = "".join(msg)
+        assert msg.endswith("\r\n")
+
+        lines = self._sendImpl(self._doSend, msg)
+        # print lines
+        for tp, msg, _ in lines:
+            if tp[0] in '45':
+                raise ErrorReply("%s %s"%(tp, msg))
+            if tp not in expectedTypes:
+                raise ProtocolError("Unexpectd message type %r"%tp)
+
+        return lines
+
+    def authenticate(self, secret=""):
+        """Send an authenticating secret to Tor.  You'll need to call this
+           method before Tor can start.
+        """
+        hexstr = binascii.b2a_hex(secret)
+        self.sendAndRecv("AUTHENTICATE %s\r\n"%hexstr)
+
+    def get_option(self, name):
+        """Get the value of the configuration option named 'name'.  To
+           retrieve multiple values, pass a list for 'name' instead of
+           a string.  Returns a list of (key,value) pairs.
+           Refer to section 3.3 of control-spec.txt for a list of valid names.
+        """
+        if not isinstance(name, str):
+            name = " ".join(name)
+        lines = self.sendAndRecv("GETCONF %s\r\n" % name)
+
+        r = []
+        for _,line,_ in lines:
+            try:
+                key, val = line.split("=", 1)
+                r.append((key,val))
+            except ValueError:
+                r.append((line, None))
+
+        return r
+
+    def set_option(self, key, value):
+        """Set the value of the configuration option 'key' to the value 'value'.
+        """
+        self.set_options([(key, value)])
+
+    def set_options(self, kvlist):
+        """Given a list of (key,value) pairs, set them as configuration
+           options.
+        """
+        if not kvlist:
+            return
+        msg = " ".join(["%s=%s"%(k,quote(v)) for k,v in kvlist])
+        self.sendAndRecv("SETCONF %s\r\n"%msg)
+
+    def reset_options(self, keylist):
+        """Reset the options listed in 'keylist' to their default values.
+
+           Tor started implementing this command in version 0.1.1.7-alpha;
+           previous versions wanted you to set configuration keys to "".
+           That no longer works.
+        """
+        self.sendAndRecv("RESETCONF %s\r\n"%(" ".join(keylist)))
+
+    def get_network_status(self, who="all"):
+        """Get the entire network status list"""
+        return parse_ns_body(self.sendAndRecv("GETINFO ns/"+who+"\r\n")[0][2])
+
+    def get_router(self, ns):
+        """Fill in a Router class corresponding to a given NS class"""
+        desc = self.sendAndRecv("GETINFO desc/id/" + ns.idhex + "\r\n")[0][2].split("\n")
+        line = desc.pop(0)
+        m = re.search(r"^router\s+(\S+)\s+(\S+)", line)
+        router,ip = m.groups()
+        exitpolicy = []
+        dead = not ("Running" in ns.flags)
+        bw_observed = 0
+        if router != ns.nickname:
+            plog("NOTICE", "Got different names " + ns.nickname + " vs " +
+                         router + " for " + ns.idhex)
+        for line in desc:
+            pl = re.search(r"^platform Tor (\S+) on (\S+)", line)
+            ac = re.search(r"^accept (\S+):([^-]+)(?:-(\d+))?", line)
+            rj = re.search(r"^reject (\S+):([^-]+)(?:-(\d+))?", line)
+            bw = re.search(r"^bandwidth \d+ \d+ (\d+)", line)
+            if re.search(r"^opt hibernating 1", line):
+                dead = 1 # XXX: Technically this may be stale..
+                if ("Running" in ns.flags):
+                    plog("NOTICE", "Hibernating router is running..")
+            if ac:
+                exitpolicy.append(ExitPolicyLine(True, *ac.groups()))
+            elif rj:
+                exitpolicy.append(ExitPolicyLine(False, *rj.groups()))
+            elif bw:
+                bw_observed = int(bw.group(1))
+            elif pl:
+                version, os = pl.groups()
+        if not bw_observed and not dead and ("Valid" in ns.flags):
+            plog("NOTICE", "No bandwidth for live router " + ns.nickname)
+        return Router(ns.idhex, ns.nickname, bw_observed, dead, exitpolicy,
+                ns.flags, ip, version, os)
+
+    def read_routers(self, nslist):
+        bad_key = 0
+        new = []
+        for ns in nslist:
+            try:
+                r = self.get_router(ns)
+                new.append(r)
+            except ErrorReply:
+                bad_key += 1
+                if "Running" in ns.flags:
+                    plog("NOTICE", "Running router "+ns.nickname+"="
+                         +ns.idhex+" has no descriptor")
+            except:
+                traceback.print_exception(*sys.exc_info())
+                continue
+    
+        return new
+
+    def get_info(self, name):
+        """Return the value of the internal information field named 'name'.
+           Refer to section 3.9 of control-spec.txt for a list of valid names.
+           DOCDOC
+        """
+        if not isinstance(name, str):
+            name = " ".join(name)
+        lines = self.sendAndRecv("GETINFO %s\r\n"%name)
+        d = {}
+        for _,msg,more in lines:
+            if msg == "OK":
+                break
+            try:
+                k,rest = msg.split("=",1)
+            except ValueError:
+                raise ProtocolError("Bad info line %r",msg)
+            if more:
+                d[k] = more
+            else:
+                d[k] = rest
+        return d
+
+    def set_events(self, events, extended=False):
+        """Change the list of events that the event handler is interested
+           in to those in 'events', which is a list of event names.
+           Recognized event names are listed in section 3.3 of the control-spec
+        """
+        if extended:
+            plog ("DEBUG", "SETEVENTS EXTENDED %s\r\n" % " ".join(events))
+            self.sendAndRecv("SETEVENTS EXTENDED %s\r\n" % " ".join(events))
+        else:
+            self.sendAndRecv("SETEVENTS %s\r\n" % " ".join(events))
+
+    def save_conf(self):
+        """Flush all configuration changes to disk.
+        """
+        self.sendAndRecv("SAVECONF\r\n")
+
+    def send_signal(self, sig):
+        """Send the signal 'sig' to the Tor process; The allowed values for
+           'sig' are listed in section 3.6 of control-spec.
+        """
+        sig = { 0x01 : "HUP",
+                0x02 : "INT",
+                0x0A : "USR1",
+                0x0C : "USR2",
+                0x0F : "TERM" }.get(sig,sig)
+        self.sendAndRecv("SIGNAL %s\r\n"%sig)
+
+    def map_address(self, kvList):
+        if not kvList:
+            return
+        m = " ".join([ "%s=%s" for k,v in kvList])
+        lines = self.sendAndRecv("MAPADDRESS %s\r\n"%m)
+        r = []
+        for _,line,_ in lines:
+            try:
+                key, val = line.split("=", 1)
+            except ValueError:
+                raise ProtocolError("Bad address line %r",v)
+            r.append((key,val))
+        return r
+
+    def extend_circuit(self, circid, hops):
+        """Tell Tor to extend the circuit identified by 'circid' through the
+           servers named in the list 'hops'.
+        """
+        if circid is None:
+            circid = "0"
+        plog("DEBUG", "Extending circuit")
+        lines = self.sendAndRecv("EXTENDCIRCUIT %d %s\r\n"
+                                  %(circid, ",".join(hops)))
+        tp,msg,_ = lines[0]
+        m = re.match(r'EXTENDED (\S*)', msg)
+        if not m:
+            raise ProtocolError("Bad extended line %r",msg)
+        plog("DEBUG", "Circuit extended")
+        return int(m.group(1))
+
+    def redirect_stream(self, streamid, newaddr, newport=""):
+        """DOCDOC"""
+        if newport:
+            self.sendAndRecv("REDIRECTSTREAM %d %s %s\r\n"%(streamid, newaddr, newport))
+        else:
+            self.sendAndRecv("REDIRECTSTREAM %d %s\r\n"%(streamid, newaddr))
+
+    def attach_stream(self, streamid, circid):
+        """DOCDOC"""
+        plog("DEBUG", "Attaching stream: "+str(streamid)+" to "+str(circid))
+        self.sendAndRecv("ATTACHSTREAM %d %d\r\n"%(streamid, circid))
+
+    def close_stream(self, streamid, reason=0, flags=()):
+        """DOCDOC"""
+        self.sendAndRecv("CLOSESTREAM %d %s %s\r\n"
+                          %(streamid, reason, "".join(flags)))
+
+    def close_circuit(self, circid, reason=0, flags=()):
+        """DOCDOC"""
+        self.sendAndRecv("CLOSECIRCUIT %d %s %s\r\n"
+                          %(circid, reason, "".join(flags)))
+
+    def post_descriptor(self, desc):
+        self.sendAndRecv("+POSTDESCRIPTOR\r\n%s"%escape_dots(desc))
+
+def parse_ns_body(data):
+    "Parse the body of an NS event or command."
+    nsgroups = re.compile(r"^r ", re.M).split(data)
+    nsgroups.pop(0)
+    nslist = []
+    for nsline in nsgroups:
+        m = re.search(r"^s((?:\s\S*)+)", nsline, re.M)
+        flags = m.groups()
+        flags = flags[0].strip().split(" ")
+        m = re.match(r"(\S+)\s(\S+)\s(\S+)\s(\S+\s\S+)\s(\S+)\s(\d+)\s(\d+)", nsline)
+        nslist.append(NetworkStatus(*(m.groups() + (flags,))))
+    return nslist
+
+class EventHandler:
+    """An 'EventHandler' wraps callbacks for the events Tor can return."""
+    def __init__(self):
+        """Create a new EventHandler."""
+        self._map1 = {
+            "CIRC" : self.circ_status_event,
+            "STREAM" : self.stream_status_event,
+            "ORCONN" : self.or_conn_status_event,
+            "BW" : self.bandwidth_event,
+            "DEBUG" : self.msg_event,
+            "INFO" : self.msg_event,
+            "NOTICE" : self.msg_event,
+            "WARN" : self.msg_event,
+            "ERR" : self.msg_event,
+            "NEWDESC" : self.new_desc_event,
+            "ADDRMAP" : self.address_mapped_event,
+            "NS" : self.ns_event
+            }
+
+    def handle1(self, lines):
+        """Dispatcher: called from Connection when an event is received."""
+        for code, msg, data in lines:
+            event = self.decode1(msg, data)
+            self.heartbeat_event()
+            self._map1.get(event.event_name, self.unknown_event)(event)
+
+    def decode1(self, body, data):
+        """Unpack an event message into a type/arguments-tuple tuple."""
+        if " " in body:
+            evtype,body = body.split(" ",1)
+        else:
+            evtype,body = body,""
+        evtype = evtype.upper()
+        if evtype == "CIRC":
+            m = re.match(r"(\d+)\s+(\S+)(\s\S+)?(\s\S+)?(\s\S+)?", body)
+            if not m:
+                raise ProtocolError("CIRC event misformatted.")
+            ident,status,path,reason,remote = m.groups()
+            ident = int(ident)
+            if path:
+                if "REASON=" in path:
+                    remote = reason
+                    reason = path
+                    path=[]
+                else:
+                    path = path.strip().split(",")
+            else:
+                path = []
+            if reason: reason = reason[8:]
+            if remote: remote = remote[15:]
+            event = CircuitEvent(evtype, ident, status, path, reason,
+                                       remote)
+        elif evtype == "STREAM":
+            m = re.match(r"(\S+)\s+(\S+)\s+(\S+)\s+(\S+):(\d+)(\s\S+)?(\s\S+)?", body)
+            if not m:
+                raise ProtocolError("STREAM event misformatted.")
+            ident,status,circ,target_host,target_port,reason,remote = m.groups()
+            ident,circ = map(int, (ident,circ))
+            if reason: reason = reason[8:]
+            if remote: remote = remote[15:]
+            event = StreamEvent(evtype, ident, status, circ, target_host,
+                                      int(target_port), reason, remote)
+        elif evtype == "ORCONN":
+            m = re.match(r"(\S+)\s+(\S+)(\sAGE=\S+)?(\sREAD=\S+)?(\sWRITTEN=\S+)?(\sREASON=\S+)?(\sNCIRCS=\S+)?", body)
+            if not m:
+                raise ProtocolError("ORCONN event misformatted.")
+            target, status, age, read, wrote, reason, ncircs = m.groups()
+
+            #plog("DEBUG", "ORCONN: "+body)
+            if ncircs: ncircs = int(ncircs[8:])
+            else: ncircs = 0
+            if reason: reason = reason[8:]
+            if age: age = int(age[5:])
+            else: age = 0
+            if read: read = int(read[6:])
+            else: read = 0
+            if wrote: wrote = int(wrote[9:])
+            else: wrote = 0
+            event = ORConnEvent(evtype, status, target, age, read, wrote,
+                                reason, ncircs)
+        elif evtype == "BW":
+            m = re.match(r"(\d+)\s+(\d+)", body)
+            if not m:
+                raise ProtocolError("BANDWIDTH event misformatted.")
+            read, written = map(long, m.groups())
+            event = BWEvent(evtype, read, written)
+        elif evtype in ("DEBUG", "INFO", "NOTICE", "WARN", "ERR"):
+            event = LogEvent(evtype, body)
+        elif evtype == "NEWDESC":
+            event = NewDescEvent(evtype, body.split(" "))
+        elif evtype == "ADDRMAP":
+            m = re.match(r'(\S+)\s+(\S+)\s+(\"[^"]+\"|\w+)', body)
+            if not m:
+                raise ProtocolError("BANDWIDTH event misformatted.")
+            fromaddr, toaddr, when = m.groups()
+            if when.upper() == "NEVER":
+                when = None
+            else:
+                when = time.localtime(
+                    time.strptime(when[1:-1], "%Y-%m-%d %H:%M:%S"))
+            event = AddrMapEvent(evtype, fromaddr, toaddr, when, "Unknown")
+        elif evtype == "NS":
+            event = NetworkStatusEvent(evtype, parse_ns_body(data))
+        else:
+            event = UnknownEvent(evtype, body)
+
+        return event
+
+    def heartbeat_event(self):
+        """Called every time any event is recieved. Convenience function
+           for any cleanup you may need to do.
+        """
+        pass
+
+    def unknown_event(self, event):
+        """Called when we get an event type we don't recognize.  This
+           is almost alwyas an error.
+        """
+        raise NotImplemented()
+
+    def circ_status_event(self, event):
+        """Called when a circuit status changes if listening to CIRCSTATUS
+           events.  'status' is a member of CIRC_STATUS; circID is a numeric
+           circuit ID, and 'path' is the circuit's path so far as a list of
+           names.
+        """
+        raise NotImplemented()
+
+    def stream_status_event(self, event):
+        """Called when a stream status changes if listening to STREAMSTATUS
+           events.  'status' is a member of STREAM_STATUS; streamID is a
+           numeric stream ID, and 'target' is the destination of the stream.
+        """
+        raise NotImplemented()
+
+    def or_conn_status_event(self, event):
+        """Called when an OR connection's status changes if listening to
+           ORCONNSTATUS events. 'status' is a member of OR_CONN_STATUS; target
+           is the OR in question.
+        """
+        raise NotImplemented()
+
+    def bandwidth_event(self, event):
+        """Called once a second if listening to BANDWIDTH events.  'read' is
+           the number of bytes read; 'written' is the number of bytes written.
+        """
+        raise NotImplemented()
+
+    def new_desc_event(self, event):
+        """Called when Tor learns a new server descriptor if listenting to
+           NEWDESC events.
+        """
+        raise NotImplemented()
+
+    def msg_event(self, event):
+        """Called when a log message of a given severity arrives if listening
+           to INFO_MSG, NOTICE_MSG, WARN_MSG, or ERR_MSG events."""
+        raise NotImplemented()
+
+    def ns_event(self, event):
+        raise NotImplemented()
+
+    def address_mapped_event(self, event):
+        """Called when Tor adds a mapping for an address if listening
+           to ADDRESSMAPPED events.
+        """
+        raise NotImplemented()
+
+
+class DebugEventHandler(EventHandler):
+    """Trivial debug event handler: reassembles all parsed events to stdout."""
+    def circ_status_event(self, circ_event): # CircuitEvent()
+        output = [circ_event.event_name, str(circ_event.circ_id),
+                  circ_event.status]
+        if circ_event.path:
+            output.append(",".join(circ_event.path))
+        if circ_event.reason:
+            output.append("REASON=" + circ_event.reason)
+        if circ_event.remote_reason:
+            output.append("REMOTE_REASON=" + circ_event.remote_reason)
+        print " ".join(output)
+
+    def stream_status_event(self, strm_event):
+        output = [strm_event.event_name, str(strm_event.strm_id),
+                  strm_event.status, str(strm_event.circ_id),
+                  strm_event.target_host, str(strm_event.target_port)]
+        if strm_event.reason:
+            output.append("REASON=" + strm_event.reason)
+        if strm_event.remote_reason:
+            output.append("REMOTE_REASON=" + strm_event.remote_reason)
+        print " ".join(output)
+
+    def ns_event(self, ns_event):
+        for ns in ns_event.nslist:
+            print " ".join((ns_event.event_name, ns.nickname, ns.idhash,
+              ns.updated.isoformat(), ns.ip, str(ns.orport),
+              str(ns.dirport), " ".join(ns.flags)))
+
+    def new_desc_event(self, newdesc_event):
+        print " ".join((newdesc_event.event_name, " ".join(newdesc_event.idlist)))
+   
+    def or_conn_status_event(self, orconn_event):
+        if orconn_event.age: age = "AGE="+str(orconn_event.age)
+        else: age = ""
+        if orconn_event.read_bytes: read = "READ="+str(orconn_event.read_bytes)
+        else: read = ""
+        if orconn_event.wrote_bytes: wrote = "WRITTEN="+str(orconn_event.wrote_bytes)
+        else: wrote = ""
+        if orconn_event.reason: reason = "REASON="+orconn_event.reason
+        else: reason = ""
+        if orconn_event.ncircs: ncircs = "NCIRCS="+str(orconn_event.ncircs)
+        else: ncircs = ""
+        print " ".join((orconn_event.event_name, orconn_event.endpoint,
+                        orconn_event.status, age, read, wrote, reason, ncircs))
+
+    def msg_event(self, log_event):
+        print log_event.event_name+" "+log_event.msg
+    
+    def bandwidth_event(self, bw_event):
+        print bw_event.event_name+" "+str(bw_event.read)+" "+str(bw_event.written)
+
+def parseHostAndPort(h):
+    """Given a string of the form 'address:port' or 'address' or
+       'port' or '', return a two-tuple of (address, port)
+    """
+    host, port = "localhost", 9100
+    if ":" in h:
+        i = h.index(":")
+        host = h[:i]
+        try:
+            port = int(h[i+1:])
+        except ValueError:
+            print "Bad hostname %r"%h
+            sys.exit(1)
+    elif h:
+        try:
+            port = int(h)
+        except ValueError:
+            host = h
+
+    return host, port
+
+def run_example(host,port):
+    print "host is %s:%d"%(host,port)
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect((host,port))
+    c = Connection(s)
+    c.set_event_handler(DebugEventHandler())
+    th = c.launch_thread()
+    c.authenticate()
+    print "nick",`c.get_option("nickname")`
+    print `c.get_info("version")`
+    #print `c.get_info("desc/name/moria1")`
+    print `c.get_info("network-status")`
+    print `c.get_info("addr-mappings/all")`
+    print `c.get_info("addr-mappings/config")`
+    print `c.get_info("addr-mappings/cache")`
+    print `c.get_info("addr-mappings/control")`
+
+    print `c.extend_circuit(0,["moria1"])`
+    try:
+        print `c.extend_circuit(0,[""])`
+    except ErrorReply: # wtf?
+        print "got error. good."
+    except:
+        print "Strange error", sys.exc_info()[0]
+   
+    #send_signal(s,1)
+    #save_conf(s)
+
+    #set_option(s,"1")
+    #set_option(s,"bandwidthburstbytes 100000")
+    #set_option(s,"runasdaemon 1")
+    #set_events(s,[EVENT_TYPE.WARN])
+#    c.set_events([EVENT_TYPE.ORCONN], True)
+    c.set_events([EVENT_TYPE.STREAM, EVENT_TYPE.CIRC,
+                  EVENT_TYPE.NS, EVENT_TYPE.NEWDESC,
+                  EVENT_TYPE.ORCONN, EVENT_TYPE.BW], True)
+
+    th.join()
+    return
+
+if __name__ == '__main__':
+    if len(sys.argv) > 2:
+        print "Syntax: TorControl.py torhost:torport"
+        sys.exit(0)
+    else:
+        sys.argv.append("localhost:9051")
+    sh,sp = parseHostAndPort(sys.argv[1])
+    run_example(sh,sp)
+


Property changes on: torflow/trunk/TorCtl/TorCtl.py
___________________________________________________________________
Name: svn:executable
   + *

Added: torflow/trunk/TorCtl/TorUtil.py
===================================================================
--- torflow/trunk/TorCtl/TorUtil.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/TorCtl/TorUtil.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -0,0 +1,193 @@
+#!/usr/bin/python
+# TorCtl.py -- Python module to interface with Tor Control interface.
+# Copyright 2007 Mike Perry -- See LICENSE for licensing information.
+# Portions Copyright 2005 Nick Matthewson
+
+"""
+TorUtil -- Support functions for TorCtl.py and metatroller
+"""
+
+import os
+import re
+import sys
+import socket
+import binascii
+import sha
+
+__all__ = ["Enum", "Enum2", "quote", "escape_dots", "unescape_dots",
+            "BufSock", "secret_to_key", "urandom_rng", "s2k_gen", "s2k_check",
+            "plog", "ListenSocket"]
+
+class Enum:
+    # Helper: define an ordered dense name-to-number 1-1 mapping.
+    def __init__(self, start, names):
+        self.nameOf = {}
+        idx = start
+        for name in names:
+            setattr(self,name,idx)
+            self.nameOf[idx] = name
+            idx += 1
+
+class Enum2:
+    # Helper: define an ordered sparse name-to-number 1-1 mapping.
+    def __init__(self, **args):
+        self.__dict__.update(args)
+        self.nameOf = {}
+        for k,v in args.items():
+            self.nameOf[v] = k
+
+def quote(s):
+    return re.sub(r'([\r\n\\\"])', r'\\\1', s)
+
+def escape_dots(s, translate_nl=1):
+    if translate_nl:
+        lines = re.split(r"\r?\n", s)
+    else:
+        lines = s.split("\r\n")
+    if lines and not lines[-1]:
+        del lines[-1]
+    for i in xrange(len(lines)):
+        if lines[i].startswith("."):
+            lines[i] = "."+lines[i]
+    lines.append(".\r\n")
+    return "\r\n".join(lines)
+
+def unescape_dots(s, translate_nl=1):
+    lines = s.split("\r\n")
+
+    for i in xrange(len(lines)):
+        if lines[i].startswith("."):
+            lines[i] = lines[i][1:]
+
+    if lines and lines[-1]:
+        lines.append("")
+
+    if translate_nl:
+        return "\n".join(lines)
+    else:
+        return "\r\n".join(lines)
+
+class BufSock:
+    def __init__(self, s):
+        self._s = s
+        self._buf = []
+
+    def readline(self):
+        if self._buf:
+            idx = self._buf[0].find('\n')
+            if idx >= 0:
+                result = self._buf[0][:idx+1]
+                self._buf[0] = self._buf[0][idx+1:]
+                return result
+
+        while 1:
+            s = self._s.recv(128)
+            if not s: return None
+            # XXX: This really does need an exception
+            #    raise ConnectionClosed()
+            idx = s.find('\n')
+            if idx >= 0:
+                self._buf.append(s[:idx+1])
+                result = "".join(self._buf)
+                rest = s[idx+1:]
+                if rest:
+                    self._buf = [ rest ]
+                else:
+                    del self._buf[:]
+                return result
+            else:
+                self._buf.append(s)
+
+    def write(self, s):
+        self._s.send(s)
+
+    def close(self):
+        self._s.close()
+
+# SocketServer.TCPServer is nuts.. 
+class ListenSocket:
+    def __init__(self, listen_ip, port):
+        msg = None
+        self.s = None
+        for res in socket.getaddrinfo(listen_ip, port, socket.AF_UNSPEC,
+                          socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
+            af, socktype, proto, canonname, sa = res
+            try:
+                self.s = socket.socket(af, socktype, proto)
+                self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            except socket.error, msg:
+                self.s = None
+                continue
+            try:
+                self.s.bind(sa)
+                self.s.listen(1)
+            except socket.error, msg:
+                self.s.close()
+                self.s = None
+                continue
+            break
+        if self.s is None:
+            raise socket.error(msg)
+
+    def accept(self):
+        conn, addr = self.s.accept()
+        return conn
+
+    def close(self):
+        self.s.close()
+
+
+def secret_to_key(secret, s2k_specifier):
+    """Used to generate a hashed password string. DOCDOC."""
+    c = ord(s2k_specifier[8])
+    EXPBIAS = 6
+    count = (16+(c&15)) << ((c>>4) + EXPBIAS)
+
+    d = sha.new()
+    tmp = s2k_specifier[:8]+secret
+    slen = len(tmp)
+    while count:
+        if count > slen:
+            d.update(tmp)
+            count -= slen
+        else:
+            d.update(tmp[:count])
+            count = 0
+    return d.digest()
+
+def urandom_rng(n):
+    """Try to read some entropy from the platform entropy source."""
+    f = open('/dev/urandom', 'rb')
+    try:
+        return f.read(n)
+    finally:
+        f.close()
+
+def s2k_gen(secret, rng=None):
+    """DOCDOC"""
+    if rng is None:
+        if hasattr(os, "urandom"):
+            rng = os.urandom
+        else:
+            rng = urandom_rng
+    spec = "%s%s"%(rng(8), chr(96))
+    return "16:%s"%(
+        binascii.b2a_hex(spec + secret_to_key(secret, spec)))
+
+def s2k_check(secret, k):
+    """DOCDOC"""
+    assert k[:3] == "16:"
+
+    k =  binascii.a2b_hex(k[3:])
+    return secret_to_key(secret, k[:9]) == k[9:]
+
+
+## XXX: Make this a class?
+loglevel = "DEBUG"
+loglevels = {"DEBUG" : 0, "INFO" : 1, "NOTICE" : 2, "WARN" : 3, "ERROR" : 4}
+
+def plog(level, msg): # XXX: Timestamps
+    if(loglevels[level] >= loglevels[loglevel]):
+        print level + ": " + msg
+        sys.stdout.flush()
+

Added: torflow/trunk/TorCtl/__init__.py
===================================================================
--- torflow/trunk/TorCtl/__init__.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/TorCtl/__init__.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -0,0 +1,2 @@
+
+__all__ = ["TorUtil", "PathSupport", "TorCtl"]

Deleted: torflow/trunk/TorCtl.py
===================================================================
--- torflow/trunk/TorCtl.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/TorCtl.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -1,1111 +0,0 @@
-#!/usr/bin/python
-# TorCtl.py -- Python module to interface with Tor Control interface.
-# Copyright 2005 Nick Mathewson
-# Copyright 2007 Mike Perry. See LICENSE file.
-
-"""
-TorCtl -- Library to control Tor processes.
-"""
-
-# XXX: Docstring all exported classes/interfaces. Also need __all__
-
-import os
-import re
-import struct
-import sys
-import threading
-import Queue
-import datetime
-import traceback
-import socket
-import binascii
-import types
-import time
-import copy
-from TorUtil import *
-
-# Types of "EVENT" message.
-EVENT_TYPE = Enum2(
-                    CIRC="CIRC",
-                    STREAM="STREAM",
-                    ORCONN="ORCONN",
-                    BW="BW",
-                    NS="NS",
-                    NEWDESC="NEWDESC",
-                    DEBUG="DEBUG",
-                    INFO="INFO",
-                    NOTICE="NOTICE",
-                    WARN="WARN",
-                    ERR="ERR")
-
-class TorCtlError(Exception):
-    "Generic error raised by TorControl code."
-    pass
-
-class TorCtlClosed(TorCtlError):
-    "Raised when the controller connection is closed by Tor (not by us.)"
-    pass
-
-class ProtocolError(TorCtlError):
-    "Raised on violations in Tor controller protocol"
-    pass
-
-class ErrorReply(TorCtlError):
-    "Raised when Tor controller returns an error"
-    pass
-
-class NodeError(TorCtlError):
-    "Raise when we have no nodes satisfying restrictions"
-    pass
-
-class NetworkStatus:
-    "Filled in during NS events"
-    def __init__(self, nickname, idhash, orhash, updated, ip, orport, dirport, flags):
-        self.nickname = nickname
-        self.idhash = idhash
-        self.orhash = orhash
-        self.ip = ip
-        self.orport = int(orport)
-        self.dirport = int(dirport)
-        self.flags = flags
-        self.idhex = (self.idhash + "=").decode("base64").encode("hex")
-        m = re.search(r"(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)", updated)
-        self.updated = datetime.datetime(*map(int, m.groups()))
-
-class NetworkStatusEvent:
-    def __init__(self, event_name, nslist):
-        self.event_name = event_name
-        self.nslist = nslist # List of NetworkStatus objects
-
-class NewDescEvent:
-    def __init__(self, event_name, idlist):
-        self.event_name = event_name
-        self.idlist = idlist
-
-class CircuitEvent:
-    def __init__(self, event_name, circ_id, status, path, reason,
-                 remote_reason):
-        self.event_name = event_name
-        self.circ_id = circ_id
-        self.status = status
-        self.path = path
-        self.reason = reason
-        self.remote_reason = remote_reason
-
-class StreamEvent:
-    def __init__(self, event_name, strm_id, status, circ_id, target_host,
-                 target_port, reason, remote_reason):
-        self.event_name = event_name
-        self.strm_id = strm_id
-        self.status = status
-        self.circ_id = circ_id
-        self.target_host = target_host
-        self.target_port = int(target_port)
-        self.reason = reason
-        self.remote_reason = remote_reason
-
-class ORConnEvent:
-    def __init__(self, event_name, status, endpoint, age, read_bytes,
-                 wrote_bytes, reason, ncircs):
-        self.event_name = event_name
-        self.status = status
-        self.endpoint = endpoint
-        self.age = age
-        self.read_bytes = read_bytes
-        self.wrote_bytes = wrote_bytes
-        self.reason = reason
-        self.ncircs = ncircs
-
-class LogEvent:
-    def __init__(self, level, msg):
-        self.event_name = self.level = level
-        self.msg = msg
-
-class AddrMapEvent:
-    def __init__(self, event_name, from_addr, to_addr, when, by_exit):
-        self.event_name = event_name
-        self.from_addr = from_addr
-        self.to_addr = to_addr
-        self.when = when
-        self.by_exit = by_exit # XOXOXOX <3 ;) @ nickm
-
-class BWEvent:
-    def __init__(self, event_name, read, written):
-        self.event_name = event_name
-        self.read = read
-        self.written = written
-
-class UnknownEvent:
-    def __init__(self, event_name, event_string):
-        self.event_name = event_name
-        self.event_string = event_string
-
-class PathRestriction:
-    "Interface for path restriction policies"
-    def r_is_ok(self, path, r): return True    
-    def entry_is_ok(self, path, r): return self.r_is_ok(path, r)
-    def middle_is_ok(self, path, r): return self.r_is_ok(path, r)
-    def exit_is_ok(self, path, r): return self.r_is_ok(path, r)
-
-class PathRestrictionList:
-    def __init__(self, restrictions):
-        self.restrictions = restrictions
-    
-    def entry_is_ok(self, path, r):
-        for rs in self.restrictions:
-            if not rs.entry_is_ok(path, r):
-                return False
-        return True
-
-    def middle_is_ok(self, path, r):
-        for rs in self.restrictions:
-            if not rs.middle_is_ok(path, r):
-                return False
-        return True
-
-    def exit_is_ok(self, path, r):
-        for rs in self.restrictions:
-            if not rs.exit_is_ok(path, r):
-                return False
-        return True
-
-    def add_restriction(self, rstr):
-        self.restrictions.append(rstr)
-
-    def del_restriction(self, RestrictionClass):
-        # im_class actually returns current base class, not
-        # implementing class. We abuse this fact here. 
-        # XXX: Is this a standard, or a bug?
-        self.restrictions = filter(
-                lambda r: r.r_is_ok.im_class != RestrictionClass,
-                    self.restrictions)
-
-class NodeRestriction:
-    "Interface for node restriction policies"
-    def r_is_ok(self, r): return True    
-    def reset(self, router_list): pass
-
-class NodeRestrictionList:
-    def __init__(self, restrictions, sorted_r):
-        self.restrictions = restrictions
-        self.update_routers(sorted_r)
-
-    def __check_r(self, r):
-        for rst in self.restrictions:
-            if not rst.r_is_ok(r): return False
-        self.restricted_bw += r.bw
-        return True
-
-    def update_routers(self, sorted_r):
-        self._sorted_r = sorted_r
-        self.restricted_bw = 0
-        for rs in self.restrictions: rs.reset(sorted_r)
-        self.restricted_r = filter(self.__check_r, self._sorted_r)
-
-    def add_restriction(self, restr):
-        self.restrictions.append(restr)
-        for r in self.restricted_r:
-            if not restr.r_is_ok(r):
-                self.restricted_r.remove(r)
-                self.restricted_bw -= r.bw
-    
-    # XXX: This does not collapse And/Or restrictions.. That is non-trivial
-    # in teh general case
-    def del_restriction(self, RestrictionClass):
-        self.restrictions = filter(
-                lambda r: r.r_is_ok.im_class != RestrictionClass,
-                    self.restrictions)
-        self.update_routers(self._sorted_r)
-
-
-class NodeGenerator:
-    "Interface for node generation"
-    def __init__(self, restriction_list):
-        self.restriction_list = restriction_list
-        self.rewind()
-
-    def rewind(self):
-        # TODO: Hrmm... Is there any way to handle termination other 
-        # than to make a list of routers that we pop from? Random generators 
-        # will not terminate if no node matches the selector without this..
-        # Not so much an issue now, but in a few years, the Tor network
-        # will be large enough that having all these list copies will
-        # be obscene... Possible candidate for a python list comprehension
-        self.routers = copy.copy(self.restriction_list.restricted_r)
-        self.bw = self.restriction_list.restricted_bw
-
-    def mark_chosen(self, r):
-        self.routers.remove(r)
-        self.bw -= r.bw
-
-    def all_chosen(self):
-        if not self.routers and self.bw or not self.bw and self.routers:
-            plog("WARN", str(len(self.routers))+" routers left but bw="
-                 +str(self.bw))
-        return not self.routers
-
-    def next_r(self): raise NotImplemented()
-
-class PathSelector:
-    "Implementation of path selection policies"
-    def __init__(self, entry_gen, mid_gen, exit_gen, path_restrict):
-        self.entry_gen = entry_gen
-        self.mid_gen = mid_gen
-        self.exit_gen = exit_gen
-        self.path_restrict = path_restrict
-
-    def entry_chooser(self, path):
-        self.entry_gen.rewind()
-        for r in self.entry_gen.next_r():
-            if self.path_restrict.entry_is_ok(path, r):
-                return r
-        raise NodeError();
-        
-    def middle_chooser(self, path):
-        self.mid_gen.rewind()
-        for r in self.mid_gen.next_r():
-            if self.path_restrict.middle_is_ok(path, r):
-                return r
-        raise NodeError();
-
-    def exit_chooser(self, path):
-        self.exit_gen.rewind()
-        for r in self.exit_gen.next_r():
-            if self.path_restrict.exit_is_ok(path, r):
-                return r
-        raise NodeError();
-
-class ExitPolicyLine:
-    def __init__(self, match, ip_mask, port_low, port_high):
-        self.match = match
-        if ip_mask == "*":
-            self.ip = 0
-            self.netmask = 0
-        else:
-            if not "/" in ip_mask:
-                self.netmask = 0xFFFFFFFF
-                ip = ip_mask
-            else:
-                ip, mask = ip_mask.split("/")
-                if re.match(r"\d+.\d+.\d+.\d+", mask):
-                    self.netmask=struct.unpack(">I", socket.inet_aton(mask))[0]
-                else:
-                    self.netmask = ~(2**(32 - int(mask)) - 1)
-            self.ip = struct.unpack(">I", socket.inet_aton(ip))[0]
-        self.ip &= self.netmask
-        if port_low == "*":
-            self.port_low,self.port_high = (0,65535)
-        else:
-            if not port_high:
-                port_high = port_low
-            self.port_low = int(port_low)
-            self.port_high = int(port_high)
-    
-    def check(self, ip, port):
-        ip = struct.unpack(">I", socket.inet_aton(ip))[0]
-        if (ip & self.netmask) == self.ip:
-            if self.port_low <= port and port <= self.port_high:
-                return self.match
-        return -1
-
-class RouterVersion:
-    def __init__(self, version):
-        v = re.search("^(\d+).(\d+).(\d+).(\d+)", version).groups()
-        self.version = int(v[0])*0x1000000 + int(v[1])*0x10000 + int(v[2])*0x100 + int(v[3])
-        self.ver_string = version
-
-    def __lt__(self, other): return self.version < other.version
-    def __gt__(self, other): return self.version > other.version
-    def __ge__(self, other): return self.version >= other.version
-    def __le__(self, other): return self.version <= other.version
-    def __eq__(self, other): return self.version == other.version
-    def __ne__(self, other): return self.version != other.version
-    def __str__(self): return self.ver_string
-
-class Router:
-    def __init__(self, idhex, name, bw, down, exitpolicy, flags, ip, version, os):
-        self.idhex = idhex
-        self.name = name
-        self.bw = bw
-        self.exitpolicy = exitpolicy
-        self.guard = "Guard" in flags
-        self.badexit = "BadExit" in flags
-        self.valid = "Valid" in flags
-        self.fast = "Fast" in flags
-        self.flags = flags
-        self.down = down
-        self.ip = struct.unpack(">I", socket.inet_aton(ip))[0]
-        self.version = RouterVersion(version)
-        self.os = os
-
-    def will_exit_to(self, ip, port):
-        for line in self.exitpolicy:
-            ret = line.check(ip, port)
-            if ret != -1:
-                return ret
-        plog("NOTICE", "No matching exit line for "+self.name)
-        return False
-    
-    def __eq__(self, other): return self.idhex == other.idhex
-    def __ne__(self, other): return self.idhex != other.idhex
-
-class Circuit:
-    def __init__(self):
-        self.cid = 0
-        self.created_at = 0 # time
-        self.path = [] # routers
-        self.exit = 0
-    
-    def id_path(self): return map(lambda r: r.idhex, self.path)
-
-
-class Connection:
-    """A Connection represents a connection to the Tor process."""
-    def __init__(self, sock):
-        """Create a Connection to communicate with the Tor process over the
-           socket 'sock'.
-        """
-        self._handler = None
-        self._handleFn = None
-        self._sendLock = threading.RLock()
-        self._queue = Queue.Queue()
-        self._thread = None
-        self._closedEx = None
-        self._closed = 0
-        self._closeHandler = None
-        self._eventThread = None
-        self._eventQueue = Queue.Queue()
-        self._s = BufSock(sock)
-        self._debugFile = None
-
-    def set_close_handler(self, handler):
-        """Call 'handler' when the Tor process has closed its connection or
-           given us an exception.  If we close normally, no arguments are
-           provided; otherwise, it will be called with an exception as its
-           argument.
-        """
-        self._closeHandler = handler
-
-    def close(self):
-        """Shut down this controller connection"""
-        self._sendLock.acquire()
-        try:
-            self._queue.put("CLOSE")
-            self._eventQueue.put("CLOSE")
-            self._s.close()
-            self._s = None
-            self._closed = 1
-        finally:
-            self._sendLock.release()
-
-    def launch_thread(self, daemon=1):
-        """Launch a background thread to handle messages from the Tor process."""
-        assert self._thread is None
-        t = threading.Thread(target=self._loop)
-        if daemon:
-            t.setDaemon(daemon)
-        t.start()
-        self._thread = t
-        t = threading.Thread(target=self._eventLoop)
-        if daemon:
-            t.setDaemon(daemon)
-        t.start()
-        self._eventThread = t
-        return self._thread
-
-    def _loop(self):
-        """Main subthread loop: Read commands from Tor, and handle them either
-           as events or as responses to other commands.
-        """
-        while 1:
-            try:
-                isEvent, reply = self._read_reply()
-            except:
-                self._err(sys.exc_info())
-                return
-
-            if isEvent:
-                if self._handler is not None:
-                    self._eventQueue.put(reply)
-            else:
-                cb = self._queue.get()
-                cb(reply)
-
-    def _err(self, (tp, ex, tb), fromEventLoop=0):
-        """DOCDOC"""
-        # silent death is bad :(
-        traceback.print_exception(tp, ex, tb)
-        if self._s:
-            try:
-                self.close()
-            except:
-                pass
-        self._sendLock.acquire()
-        try:
-            self._closedEx = ex
-            self._closed = 1
-        finally:
-            self._sendLock.release()
-        while 1:
-            try:
-                cb = self._queue.get(timeout=0)
-                if cb != "CLOSE":
-                    cb("EXCEPTION")
-            except Queue.Empty:
-                break
-        if self._closeHandler is not None:
-            self._closeHandler(ex)
-        return
-
-    def _eventLoop(self):
-        """DOCDOC"""
-        while 1:
-            reply = self._eventQueue.get()
-            if reply == "CLOSE":
-                return
-            try:
-                self._handleFn(reply)
-            except:
-                self._err(sys.exc_info(), 1)
-                return
-
-    def _sendImpl(self, sendFn, msg):
-        """DOCDOC"""
-        if self._thread is None:
-            self.launch_thread(1)
-        # This condition will get notified when we've got a result...
-        condition = threading.Condition()
-        # Here's where the result goes...
-        result = []
-
-        if self._closedEx is not None:
-            raise self._closedEx
-        elif self._closed:
-            raise TorCtlClosed()
-
-        def cb(reply,condition=condition,result=result):
-            condition.acquire()
-            try:
-                result.append(reply)
-                condition.notify()
-            finally:
-                condition.release()
-
-        # Sends a message to Tor...
-        self._sendLock.acquire()
-        try:
-            self._queue.put(cb)
-            sendFn(msg) # _doSend(msg)
-        finally:
-            self._sendLock.release()
-
-        # Now wait till the answer is in...
-        condition.acquire()
-        try:
-            while not result:
-                condition.wait()
-        finally:
-            condition.release()
-
-        # ...And handle the answer appropriately.
-        assert len(result) == 1
-        reply = result[0]
-        if reply == "EXCEPTION":
-            raise self._closedEx
-
-        return reply
-
-
-    def debug(self, f):
-        """DOCDOC"""
-        self._debugFile = f
-
-    def set_event_handler(self, handler):
-        """Cause future events from the Tor process to be sent to 'handler'.
-        """
-        self._handler = handler
-        self._handleFn = handler.handle1
-
-    def _read_reply(self):
-        lines = []
-        while 1:
-            line = self._s.readline().strip()
-            if self._debugFile:
-                self._debugFile.write("    %s\n" % line)
-            if len(line)<4:
-                raise ProtocolError("Badly formatted reply line: Too short")
-            code = line[:3]
-            tp = line[3]
-            s = line[4:]
-            if tp == "-":
-                lines.append((code, s, None))
-            elif tp == " ":
-                lines.append((code, s, None))
-                isEvent = (lines and lines[0][0][0] == '6')
-                return isEvent, lines
-            elif tp != "+":
-                raise ProtocolError("Badly formatted reply line: unknown type %r"%tp)
-            else:
-                more = []
-                while 1:
-                    line = self._s.readline()
-                    if self._debugFile:
-                        self._debugFile.write("+++ %s" % line)
-                    if line in (".\r\n", ".\n"):
-                        break
-                    more.append(line)
-                lines.append((code, s, unescape_dots("".join(more))))
-                isEvent = (lines and lines[0][0][0] == '6')
-                if isEvent: # Need "250 OK" if it's not an event. Otherwise, end
-                    return (isEvent, lines)
-
-        # XXX: Notreached
-        isEvent = (lines and lines[0][0][0] == '6')
-        return (isEvent, lines)
-
-    def _doSend(self, msg):
-        if self._debugFile:
-            amsg = msg
-            lines = amsg.split("\n")
-            if len(lines) > 2:
-                amsg = "\n".join(lines[:2]) + "\n"
-            self._debugFile.write(">>> %s" % amsg)
-        self._s.write(msg)
-
-    def sendAndRecv(self, msg="", expectedTypes=("250", "251")):
-        """Helper: Send a command 'msg' to Tor, and wait for a command
-           in response.  If the response type is in expectedTypes,
-           return a list of (tp,body,extra) tuples.  If it is an
-           error, raise ErrorReply.  Otherwise, raise ProtocolError.
-        """
-        if type(msg) == types.ListType:
-            msg = "".join(msg)
-        assert msg.endswith("\r\n")
-
-        lines = self._sendImpl(self._doSend, msg)
-        # print lines
-        for tp, msg, _ in lines:
-            if tp[0] in '45':
-                raise ErrorReply("%s %s"%(tp, msg))
-            if tp not in expectedTypes:
-                raise ProtocolError("Unexpectd message type %r"%tp)
-
-        return lines
-
-    def authenticate(self, secret=""):
-        """Send an authenticating secret to Tor.  You'll need to call this
-           method before Tor can start.
-        """
-        hexstr = binascii.b2a_hex(secret)
-        self.sendAndRecv("AUTHENTICATE %s\r\n"%hexstr)
-
-    def get_option(self, name):
-        """Get the value of the configuration option named 'name'.  To
-           retrieve multiple values, pass a list for 'name' instead of
-           a string.  Returns a list of (key,value) pairs.
-           Refer to section 3.3 of control-spec.txt for a list of valid names.
-        """
-        if not isinstance(name, str):
-            name = " ".join(name)
-        lines = self.sendAndRecv("GETCONF %s\r\n" % name)
-
-        r = []
-        for _,line,_ in lines:
-            try:
-                key, val = line.split("=", 1)
-                r.append((key,val))
-            except ValueError:
-                r.append((line, None))
-
-        return r
-
-    def set_option(self, key, value):
-        """Set the value of the configuration option 'key' to the value 'value'.
-        """
-        self.set_options([(key, value)])
-
-    def set_options(self, kvlist):
-        """Given a list of (key,value) pairs, set them as configuration
-           options.
-        """
-        if not kvlist:
-            return
-        msg = " ".join(["%s=%s"%(k,quote(v)) for k,v in kvlist])
-        self.sendAndRecv("SETCONF %s\r\n"%msg)
-
-    def reset_options(self, keylist):
-        """Reset the options listed in 'keylist' to their default values.
-
-           Tor started implementing this command in version 0.1.1.7-alpha;
-           previous versions wanted you to set configuration keys to "".
-           That no longer works.
-        """
-        self.sendAndRecv("RESETCONF %s\r\n"%(" ".join(keylist)))
-
-    def get_network_status(self, who="all"):
-        """Get the entire network status list"""
-        return parse_ns_body(self.sendAndRecv("GETINFO ns/"+who+"\r\n")[0][2])
-
-    def get_router(self, ns):
-        """Fill in a Router class corresponding to a given NS class"""
-        desc = self.sendAndRecv("GETINFO desc/id/" + ns.idhex + "\r\n")[0][2].split("\n")
-        line = desc.pop(0)
-        m = re.search(r"^router\s+(\S+)\s+(\S+)", line)
-        router,ip = m.groups()
-        exitpolicy = []
-        dead = not ("Running" in ns.flags)
-        bw_observed = 0
-        if router != ns.nickname:
-            plog("NOTICE", "Got different names " + ns.nickname + " vs " +
-                         router + " for " + ns.idhex)
-        for line in desc:
-            pl = re.search(r"^platform Tor (\S+) on (\S+)", line)
-            ac = re.search(r"^accept (\S+):([^-]+)(?:-(\d+))?", line)
-            rj = re.search(r"^reject (\S+):([^-]+)(?:-(\d+))?", line)
-            bw = re.search(r"^bandwidth \d+ \d+ (\d+)", line)
-            if re.search(r"^opt hibernating 1", line):
-                dead = 1 # XXX: Technically this may be stale..
-                if ("Running" in ns.flags):
-                    plog("NOTICE", "Hibernating router is running..")
-            if ac:
-                exitpolicy.append(ExitPolicyLine(True, *ac.groups()))
-            elif rj:
-                exitpolicy.append(ExitPolicyLine(False, *rj.groups()))
-            elif bw:
-                bw_observed = int(bw.group(1))
-            elif pl:
-                version, os = pl.groups()
-        if not bw_observed and not dead and ("Valid" in ns.flags):
-            plog("NOTICE", "No bandwidth for live router " + ns.nickname)
-        return Router(ns.idhex, ns.nickname, bw_observed, dead, exitpolicy,
-                ns.flags, ip, version, os)
-
-    def get_info(self, name):
-        """Return the value of the internal information field named 'name'.
-           Refer to section 3.9 of control-spec.txt for a list of valid names.
-           DOCDOC
-        """
-        if not isinstance(name, str):
-            name = " ".join(name)
-        lines = self.sendAndRecv("GETINFO %s\r\n"%name)
-        d = {}
-        for _,msg,more in lines:
-            if msg == "OK":
-                break
-            try:
-                k,rest = msg.split("=",1)
-            except ValueError:
-                raise ProtocolError("Bad info line %r",msg)
-            if more:
-                d[k] = more
-            else:
-                d[k] = rest
-        return d
-
-    def set_events(self, events, extended=False):
-        """Change the list of events that the event handler is interested
-           in to those in 'events', which is a list of event names.
-           Recognized event names are listed in section 3.3 of the control-spec
-        """
-        if extended:
-            plog ("DEBUG", "SETEVENTS EXTENDED %s\r\n" % " ".join(events))
-            self.sendAndRecv("SETEVENTS EXTENDED %s\r\n" % " ".join(events))
-        else:
-            self.sendAndRecv("SETEVENTS %s\r\n" % " ".join(events))
-
-    def save_conf(self):
-        """Flush all configuration changes to disk.
-        """
-        self.sendAndRecv("SAVECONF\r\n")
-
-    def send_signal(self, sig):
-        """Send the signal 'sig' to the Tor process; The allowed values for
-           'sig' are listed in section 3.6 of control-spec.
-        """
-        sig = { 0x01 : "HUP",
-                0x02 : "INT",
-                0x0A : "USR1",
-                0x0C : "USR2",
-                0x0F : "TERM" }.get(sig,sig)
-        self.sendAndRecv("SIGNAL %s\r\n"%sig)
-
-    def map_address(self, kvList):
-        if not kvList:
-            return
-        m = " ".join([ "%s=%s" for k,v in kvList])
-        lines = self.sendAndRecv("MAPADDRESS %s\r\n"%m)
-        r = []
-        for _,line,_ in lines:
-            try:
-                key, val = line.split("=", 1)
-            except ValueError:
-                raise ProtocolError("Bad address line %r",v)
-            r.append((key,val))
-        return r
-
-    def extend_circuit(self, circid, hops):
-        """Tell Tor to extend the circuit identified by 'circid' through the
-           servers named in the list 'hops'.
-        """
-        if circid is None:
-            circid = "0"
-        plog("DEBUG", "Extending circuit")
-        lines = self.sendAndRecv("EXTENDCIRCUIT %d %s\r\n"
-                                  %(circid, ",".join(hops)))
-        tp,msg,_ = lines[0]
-        m = re.match(r'EXTENDED (\S*)', msg)
-        if not m:
-            raise ProtocolError("Bad extended line %r",msg)
-        plog("DEBUG", "Circuit extended")
-        return int(m.group(1))
-
-    def build_circuit(self, pathlen, path_sel):
-        circ = Circuit()
-        if pathlen == 1:
-            circ.exit = path_sel.exit_chooser(circ.path)
-            circ.path = [circ.exit]
-            circ.cid = self.extend_circuit(0, circ.id_path())
-        else:
-            circ.path.append(path_sel.entry_chooser(circ.path))
-            for i in xrange(1, pathlen-1):
-                circ.path.append(path_sel.middle_chooser(circ.path))
-            circ.exit = path_sel.exit_chooser(circ.path)
-            circ.path.append(circ.exit)
-            circ.cid = self.extend_circuit(0, circ.id_path())
-        circ.created_at = datetime.datetime.now()
-        return circ
-
-    def redirect_stream(self, streamid, newaddr, newport=""):
-        """DOCDOC"""
-        if newport:
-            self.sendAndRecv("REDIRECTSTREAM %d %s %s\r\n"%(streamid, newaddr, newport))
-        else:
-            self.sendAndRecv("REDIRECTSTREAM %d %s\r\n"%(streamid, newaddr))
-
-    def attach_stream(self, streamid, circid):
-        """DOCDOC"""
-        plog("DEBUG", "Attaching stream: "+str(streamid)+" to "+str(circid))
-        self.sendAndRecv("ATTACHSTREAM %d %d\r\n"%(streamid, circid))
-
-    def close_stream(self, streamid, reason=0, flags=()):
-        """DOCDOC"""
-        self.sendAndRecv("CLOSESTREAM %d %s %s\r\n"
-                          %(streamid, reason, "".join(flags)))
-
-    def close_circuit(self, circid, reason=0, flags=()):
-        """DOCDOC"""
-        self.sendAndRecv("CLOSECIRCUIT %d %s %s\r\n"
-                          %(circid, reason, "".join(flags)))
-
-    def post_descriptor(self, desc):
-        self.sendAndRecv("+POSTDESCRIPTOR\r\n%s"%escape_dots(desc))
-
-def parse_ns_body(data):
-    "Parse the body of an NS event or command."
-    nsgroups = re.compile(r"^r ", re.M).split(data)
-    nsgroups.pop(0)
-    nslist = []
-    for nsline in nsgroups:
-        m = re.search(r"^s((?:\s\S*)+)", nsline, re.M)
-        flags = m.groups()
-        flags = flags[0].strip().split(" ")
-        m = re.match(r"(\S+)\s(\S+)\s(\S+)\s(\S+\s\S+)\s(\S+)\s(\d+)\s(\d+)", nsline)
-        nslist.append(NetworkStatus(*(m.groups() + (flags,))))
-    return nslist
-
-class EventHandler:
-    """An 'EventHandler' wraps callbacks for the events Tor can return."""
-    def __init__(self):
-        """Create a new EventHandler."""
-        self._map1 = {
-            "CIRC" : self.circ_status_event,
-            "STREAM" : self.stream_status_event,
-            "ORCONN" : self.or_conn_status_event,
-            "BW" : self.bandwidth_event,
-            "DEBUG" : self.msg_event,
-            "INFO" : self.msg_event,
-            "NOTICE" : self.msg_event,
-            "WARN" : self.msg_event,
-            "ERR" : self.msg_event,
-            "NEWDESC" : self.new_desc_event,
-            "ADDRMAP" : self.address_mapped_event,
-            "NS" : self.ns_event
-            }
-
-    def handle1(self, lines):
-        """Dispatcher: called from Connection when an event is received."""
-        for code, msg, data in lines:
-            event = self.decode1(msg, data)
-            self.heartbeat_event()
-            self._map1.get(event.event_name, self.unknown_event)(event)
-
-    def decode1(self, body, data):
-        """Unpack an event message into a type/arguments-tuple tuple."""
-        if " " in body:
-            evtype,body = body.split(" ",1)
-        else:
-            evtype,body = body,""
-        evtype = evtype.upper()
-        if evtype == "CIRC":
-            m = re.match(r"(\d+)\s+(\S+)(\s\S+)?(\s\S+)?(\s\S+)?", body)
-            if not m:
-                raise ProtocolError("CIRC event misformatted.")
-            ident,status,path,reason,remote = m.groups()
-            ident = int(ident)
-            if path:
-                if "REASON=" in path:
-                    remote = reason
-                    reason = path
-                    path=[]
-                else:
-                    path = path.strip().split(",")
-            else:
-                path = []
-            if reason: reason = reason[8:]
-            if remote: remote = remote[15:]
-            event = CircuitEvent(evtype, ident, status, path, reason,
-                                       remote)
-        elif evtype == "STREAM":
-            m = re.match(r"(\S+)\s+(\S+)\s+(\S+)\s+(\S+):(\d+)(\s\S+)?(\s\S+)?", body)
-            if not m:
-                raise ProtocolError("STREAM event misformatted.")
-            ident,status,circ,target_host,target_port,reason,remote = m.groups()
-            ident,circ = map(int, (ident,circ))
-            if reason: reason = reason[8:]
-            if remote: remote = remote[15:]
-            event = StreamEvent(evtype, ident, status, circ, target_host,
-                                      int(target_port), reason, remote)
-        elif evtype == "ORCONN":
-            m = re.match(r"(\S+)\s+(\S+)(\sAGE=\S+)?(\sREAD=\S+)?(\sWRITTEN=\S+)?(\sREASON=\S+)?(\sNCIRCS=\S+)?", body)
-            if not m:
-                raise ProtocolError("ORCONN event misformatted.")
-            target, status, age, read, wrote, reason, ncircs = m.groups()
-
-            #plog("DEBUG", "ORCONN: "+body)
-            if ncircs: ncircs = int(ncircs[8:])
-            else: ncircs = 0
-            if reason: reason = reason[8:]
-            if age: age = int(age[5:])
-            else: age = 0
-            if read: read = int(read[6:])
-            else: read = 0
-            if wrote: wrote = int(wrote[9:])
-            else: wrote = 0
-            event = ORConnEvent(evtype, status, target, age, read, wrote,
-                                reason, ncircs)
-        elif evtype == "BW":
-            m = re.match(r"(\d+)\s+(\d+)", body)
-            if not m:
-                raise ProtocolError("BANDWIDTH event misformatted.")
-            read, written = map(long, m.groups())
-            event = BWEvent(evtype, read, written)
-        elif evtype in ("DEBUG", "INFO", "NOTICE", "WARN", "ERR"):
-            event = LogEvent(evtype, body)
-        elif evtype == "NEWDESC":
-            event = NewDescEvent(evtype, body.split(" "))
-        elif evtype == "ADDRMAP":
-            m = re.match(r'(\S+)\s+(\S+)\s+(\"[^"]+\"|\w+)', body)
-            if not m:
-                raise ProtocolError("BANDWIDTH event misformatted.")
-            fromaddr, toaddr, when = m.groups()
-            if when.upper() == "NEVER":
-                when = None
-            else:
-                when = time.localtime(
-                    time.strptime(when[1:-1], "%Y-%m-%d %H:%M:%S"))
-            event = AddrMapEvent(evtype, fromaddr, toaddr, when, "Unknown")
-        elif evtype == "NS":
-            event = NetworkStatusEvent(evtype, parse_ns_body(data))
-        else:
-            event = UnknownEvent(evtype, body)
-
-        return event
-
-    def heartbeat_event(self):
-        """Called every time any event is recieved. Convenience function
-           for any cleanup you may need to do.
-        """
-        pass
-
-    def unknown_event(self, event):
-        """Called when we get an event type we don't recognize.  This
-           is almost alwyas an error.
-        """
-        raise NotImplemented()
-
-    def circ_status_event(self, event):
-        """Called when a circuit status changes if listening to CIRCSTATUS
-           events.  'status' is a member of CIRC_STATUS; circID is a numeric
-           circuit ID, and 'path' is the circuit's path so far as a list of
-           names.
-        """
-        raise NotImplemented()
-
-    def stream_status_event(self, event):
-        """Called when a stream status changes if listening to STREAMSTATUS
-           events.  'status' is a member of STREAM_STATUS; streamID is a
-           numeric stream ID, and 'target' is the destination of the stream.
-        """
-        raise NotImplemented()
-
-    def or_conn_status_event(self, event):
-        """Called when an OR connection's status changes if listening to
-           ORCONNSTATUS events. 'status' is a member of OR_CONN_STATUS; target
-           is the OR in question.
-        """
-        raise NotImplemented()
-
-    def bandwidth_event(self, event):
-        """Called once a second if listening to BANDWIDTH events.  'read' is
-           the number of bytes read; 'written' is the number of bytes written.
-        """
-        raise NotImplemented()
-
-    def new_desc_event(self, event):
-        """Called when Tor learns a new server descriptor if listenting to
-           NEWDESC events.
-        """
-        raise NotImplemented()
-
-    def msg_event(self, event):
-        """Called when a log message of a given severity arrives if listening
-           to INFO_MSG, NOTICE_MSG, WARN_MSG, or ERR_MSG events."""
-        raise NotImplemented()
-
-    def ns_event(self, event):
-        raise NotImplemented()
-
-    def address_mapped_event(self, event):
-        """Called when Tor adds a mapping for an address if listening
-           to ADDRESSMAPPED events.
-        """
-        raise NotImplemented()
-
-
-class DebugEventHandler(EventHandler):
-    """Trivial debug event handler: reassembles all parsed events to stdout."""
-    def circ_status_event(self, circ_event): # CircuitEvent()
-        output = [circ_event.event_name, str(circ_event.circ_id),
-                  circ_event.status]
-        if circ_event.path:
-            output.append(",".join(circ_event.path))
-        if circ_event.reason:
-            output.append("REASON=" + circ_event.reason)
-        if circ_event.remote_reason:
-            output.append("REMOTE_REASON=" + circ_event.remote_reason)
-        print " ".join(output)
-
-    def stream_status_event(self, strm_event):
-        output = [strm_event.event_name, str(strm_event.strm_id),
-                  strm_event.status, str(strm_event.circ_id),
-                  strm_event.target_host, str(strm_event.target_port)]
-        if strm_event.reason:
-            output.append("REASON=" + strm_event.reason)
-        if strm_event.remote_reason:
-            output.append("REMOTE_REASON=" + strm_event.remote_reason)
-        print " ".join(output)
-
-    def ns_event(self, ns_event):
-        for ns in ns_event.nslist:
-            print " ".join((ns_event.event_name, ns.nickname, ns.idhash,
-              ns.updated.isoformat(), ns.ip, str(ns.orport),
-              str(ns.dirport), " ".join(ns.flags)))
-
-    def new_desc_event(self, newdesc_event):
-        print " ".join((newdesc_event.event_name, " ".join(newdesc_event.idlist)))
-   
-    def or_conn_status_event(self, orconn_event):
-        if orconn_event.age: age = "AGE="+str(orconn_event.age)
-        else: age = ""
-        if orconn_event.read_bytes: read = "READ="+str(orconn_event.read_bytes)
-        else: read = ""
-        if orconn_event.wrote_bytes: wrote = "WRITTEN="+str(orconn_event.wrote_bytes)
-        else: wrote = ""
-        if orconn_event.reason: reason = "REASON="+orconn_event.reason
-        else: reason = ""
-        if orconn_event.ncircs: ncircs = "NCIRCS="+str(orconn_event.ncircs)
-        else: ncircs = ""
-        print " ".join((orconn_event.event_name, orconn_event.endpoint,
-                        orconn_event.status, age, read, wrote, reason, ncircs))
-
-    def msg_event(self, log_event):
-        print log_event.event_name+" "+log_event.msg
-    
-    def bandwidth_event(self, bw_event):
-        print bw_event.event_name+" "+str(bw_event.read)+" "+str(bw_event.written)
-
-def parseHostAndPort(h):
-    """Given a string of the form 'address:port' or 'address' or
-       'port' or '', return a two-tuple of (address, port)
-    """
-    host, port = "localhost", 9100
-    if ":" in h:
-        i = h.index(":")
-        host = h[:i]
-        try:
-            port = int(h[i+1:])
-        except ValueError:
-            print "Bad hostname %r"%h
-            sys.exit(1)
-    elif h:
-        try:
-            port = int(h)
-        except ValueError:
-            host = h
-
-    return host, port
-
-def get_connection(sock):
-    """Given a socket attached to a Tor control port, detect the version of Tor
-       and return an appropriate 'Connection' object."""
-    return Connection(sock)
-
-def run_example(host,port):
-    print "host is %s:%d"%(host,port)
-    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-    s.connect((host,port))
-    c = get_connection(s)
-    c.set_event_handler(DebugEventHandler())
-    th = c.launch_thread()
-    c.authenticate()
-    print "nick",`c.get_option("nickname")`
-    print `c.get_info("version")`
-    #print `c.get_info("desc/name/moria1")`
-    print `c.get_info("network-status")`
-    print `c.get_info("addr-mappings/all")`
-    print `c.get_info("addr-mappings/config")`
-    print `c.get_info("addr-mappings/cache")`
-    print `c.get_info("addr-mappings/control")`
-
-    print `c.extend_circuit(0,["moria1"])`
-    try:
-        print `c.extend_circuit(0,[""])`
-    except ErrorReply: # wtf?
-        print "got error. good."
-    except:
-        print "Strange error", sys.exc_info()[0]
-   
-    #send_signal(s,1)
-    #save_conf(s)
-
-    #set_option(s,"1")
-    #set_option(s,"bandwidthburstbytes 100000")
-    #set_option(s,"runasdaemon 1")
-    #set_events(s,[EVENT_TYPE.WARN])
-#    c.set_events([EVENT_TYPE.ORCONN], True)
-    c.set_events([EVENT_TYPE.STREAM, EVENT_TYPE.CIRC,
-                  EVENT_TYPE.NS, EVENT_TYPE.NEWDESC,
-                  EVENT_TYPE.ORCONN, EVENT_TYPE.BW], True)
-
-    th.join()
-    return
-
-if __name__ == '__main__':
-    if len(sys.argv) > 2:
-        print "Syntax: TorControl.py torhost:torport"
-        sys.exit(0)
-    else:
-        sys.argv.append("localhost:9051")
-    sh,sp = parseHostAndPort(sys.argv[1])
-    run_example(sh,sp)
-

Deleted: torflow/trunk/TorUtil.py
===================================================================
--- torflow/trunk/TorUtil.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/TorUtil.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -1,193 +0,0 @@
-#!/usr/bin/python
-# TorCtl.py -- Python module to interface with Tor Control interface.
-# Copyright 2007 Mike Perry -- See LICENSE for licensing information.
-# Portions Copyright 2005 Nick Matthewson
-
-"""
-TorUtil -- Support functions for TorCtl.py and metatroller
-"""
-
-import os
-import re
-import sys
-import socket
-import binascii
-import sha
-
-__all__ = ["Enum", "Enum2", "quote", "escape_dots", "unescape_dots",
-            "BufSock", "secret_to_key", "urandom_rng", "s2k_gen", "s2k_check",
-            "plog", "ListenSocket"]
-
-class Enum:
-    # Helper: define an ordered dense name-to-number 1-1 mapping.
-    def __init__(self, start, names):
-        self.nameOf = {}
-        idx = start
-        for name in names:
-            setattr(self,name,idx)
-            self.nameOf[idx] = name
-            idx += 1
-
-class Enum2:
-    # Helper: define an ordered sparse name-to-number 1-1 mapping.
-    def __init__(self, **args):
-        self.__dict__.update(args)
-        self.nameOf = {}
-        for k,v in args.items():
-            self.nameOf[v] = k
-
-def quote(s):
-    return re.sub(r'([\r\n\\\"])', r'\\\1', s)
-
-def escape_dots(s, translate_nl=1):
-    if translate_nl:
-        lines = re.split(r"\r?\n", s)
-    else:
-        lines = s.split("\r\n")
-    if lines and not lines[-1]:
-        del lines[-1]
-    for i in xrange(len(lines)):
-        if lines[i].startswith("."):
-            lines[i] = "."+lines[i]
-    lines.append(".\r\n")
-    return "\r\n".join(lines)
-
-def unescape_dots(s, translate_nl=1):
-    lines = s.split("\r\n")
-
-    for i in xrange(len(lines)):
-        if lines[i].startswith("."):
-            lines[i] = lines[i][1:]
-
-    if lines and lines[-1]:
-        lines.append("")
-
-    if translate_nl:
-        return "\n".join(lines)
-    else:
-        return "\r\n".join(lines)
-
-class BufSock:
-    def __init__(self, s):
-        self._s = s
-        self._buf = []
-
-    def readline(self):
-        if self._buf:
-            idx = self._buf[0].find('\n')
-            if idx >= 0:
-                result = self._buf[0][:idx+1]
-                self._buf[0] = self._buf[0][idx+1:]
-                return result
-
-        while 1:
-            s = self._s.recv(128)
-            if not s: return None
-            # XXX: This really does need an exception
-            #    raise ConnectionClosed()
-            idx = s.find('\n')
-            if idx >= 0:
-                self._buf.append(s[:idx+1])
-                result = "".join(self._buf)
-                rest = s[idx+1:]
-                if rest:
-                    self._buf = [ rest ]
-                else:
-                    del self._buf[:]
-                return result
-            else:
-                self._buf.append(s)
-
-    def write(self, s):
-        self._s.send(s)
-
-    def close(self):
-        self._s.close()
-
-# SocketServer.TCPServer is nuts.. 
-class ListenSocket:
-    def __init__(self, listen_ip, port):
-        msg = None
-        self.s = None
-        for res in socket.getaddrinfo(listen_ip, port, socket.AF_UNSPEC,
-                          socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
-            af, socktype, proto, canonname, sa = res
-            try:
-                self.s = socket.socket(af, socktype, proto)
-                self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-            except socket.error, msg:
-                self.s = None
-                continue
-            try:
-                self.s.bind(sa)
-                self.s.listen(1)
-            except socket.error, msg:
-                self.s.close()
-                self.s = None
-                continue
-            break
-        if self.s is None:
-            raise socket.error(msg)
-
-    def accept(self):
-        conn, addr = self.s.accept()
-        return conn
-
-    def close(self):
-        self.s.close()
-
-
-def secret_to_key(secret, s2k_specifier):
-    """Used to generate a hashed password string. DOCDOC."""
-    c = ord(s2k_specifier[8])
-    EXPBIAS = 6
-    count = (16+(c&15)) << ((c>>4) + EXPBIAS)
-
-    d = sha.new()
-    tmp = s2k_specifier[:8]+secret
-    slen = len(tmp)
-    while count:
-        if count > slen:
-            d.update(tmp)
-            count -= slen
-        else:
-            d.update(tmp[:count])
-            count = 0
-    return d.digest()
-
-def urandom_rng(n):
-    """Try to read some entropy from the platform entropy source."""
-    f = open('/dev/urandom', 'rb')
-    try:
-        return f.read(n)
-    finally:
-        f.close()
-
-def s2k_gen(secret, rng=None):
-    """DOCDOC"""
-    if rng is None:
-        if hasattr(os, "urandom"):
-            rng = os.urandom
-        else:
-            rng = urandom_rng
-    spec = "%s%s"%(rng(8), chr(96))
-    return "16:%s"%(
-        binascii.b2a_hex(spec + secret_to_key(secret, spec)))
-
-def s2k_check(secret, k):
-    """DOCDOC"""
-    assert k[:3] == "16:"
-
-    k =  binascii.a2b_hex(k[3:])
-    return secret_to_key(secret, k[:9]) == k[9:]
-
-
-## XXX: Make this a class?
-loglevel = "DEBUG"
-loglevels = {"DEBUG" : 0, "INFO" : 1, "NOTICE" : 2, "WARN" : 3, "ERROR" : 4}
-
-def plog(level, msg): # XXX: Timestamps
-    if(loglevels[level] >= loglevels[loglevel]):
-        print level + ": " + msg
-        sys.stdout.flush()
-

Modified: torflow/trunk/metatroller.py
===================================================================
--- torflow/trunk/metatroller.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/metatroller.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -5,7 +5,6 @@
 Metatroller - Tor Meta controller
 """
 
-import TorCtl
 import atexit
 import sys
 import socket
@@ -15,18 +14,19 @@
 import datetime
 import threading
 import struct
-from TorUtil import *
+from TorCtl import *
+from TorCtl.TorUtil import *
+from TorCtl.PathSupport import *
 
 routers = {} # indexed by idhex
 name_to_key = {}
-key_to_name = {}
 
 sorted_r = []
 
 circuits = {} # map from ID # to circuit object
 streams = {} # map from stream id to circuit
 
-version = "0.1.0-dev"
+mt_version = "0.1.0-dev"
 
 # TODO: Move these to config file
 # TODO: Option to ignore guard flag
@@ -83,195 +83,6 @@
         self.host = host
         self.port = port
 
-# TODO: We still need more path support implementations
-#  - BwWeightedGenerator
-#  - NodeRestrictions:
-#    - Uptime
-#    - GeoIP
-#      - NodeCountry
-#  - PathRestrictions
-#    - Family
-#    - GeoIP:
-#      - OceanPhobicRestrictor (avoids Pacific Ocean or two atlantic crossings)
-#        or ContinentRestrictor (avoids doing more than N continent crossings)
-#        - Mathematical/empirical study of predecessor expectation
-#          - If middle node on the same continent as exit, exit learns nothing
-#          - else, exit has a bias on the continent of origin of user
-#            - Language and browser accept string determine this anyway
-
-class PercentileRestriction(TorCtl.NodeRestriction):
-    """If used, this restriction MUST be FIRST in the RestrictionList."""
-    def __init__(self, pct_skip, pct_fast, r_list):
-        self.pct_skip = pct_skip
-        self.pct_fast = pct_fast
-        self.sorted_r = r_list
-        self.position = 0
-
-    def reset(self, r_list):
-        self.sorted_r = r_list
-        self.position = 0
-        
-    def r_is_ok(self, r):
-        ret = True
-        if self.position == len(self.sorted_r):
-            self.position = 0
-            plog("WARN", "Resetting PctFastRestriction")
-        if self.position != self.sorted_r.index(r): # XXX expensive?
-            plog("WARN", "Router"+r.name+" at mismatched index: "
-                         +self.position+" vs "+self.sorted_r.index(r))
-        
-        if self.position < len(self.sorted_r)*self.pct_skip/100:
-            ret = False
-        elif self.position > len(self.sorted_r)*self.pct_fast/100:
-            ret = False
-        
-        self.position += 1
-        return ret
-        
-class OSRestriction(TorCtl.NodeRestriction):
-    def __init__(self, ok, bad=[]):
-        self.ok = ok
-        self.bad = bad
-
-    def r_is_ok(self, r):
-        for y in self.ok:
-            if re.search(y, r.os):
-                return True
-        for b in self.bad:
-            if re.search(b, r.os):
-                return False
-        if self.ok: return False
-        if self.bad: return True
-
-class ConserveExitsRestriction(TorCtl.NodeRestriction):
-    def r_is_ok(self, r): return not "Exit" in r.flags
-
-class FlagsRestriction(TorCtl.NodeRestriction):
-    def __init__(self, mandatory, forbidden=[]):
-        self.mandatory = mandatory
-        self.forbidden = forbidden
-
-    def r_is_ok(self, router):
-        for m in self.mandatory:
-            if not m in router.flags: return False
-        for f in self.forbidden:
-            if f in router.flags: return False
-        return True
-        
-
-class MinBWRestriction(TorCtl.NodeRestriction):
-    def __init__(self, minbw):
-        self.min_bw = minbw
-
-    def r_is_ok(self, router): return router.bw >= self.min_bw
-     
-class VersionIncludeRestriction(TorCtl.NodeRestriction):
-    def __init__(self, eq):
-        self.eq = map(TorCtl.RouterVersion, eq)
-    
-    def r_is_ok(self, router):
-        for e in self.eq:
-            if e == router.version:
-                return True
-        return False
-
-
-class VersionExcludeRestriction(TorCtl.NodeRestriction):
-    def __init__(self, exclude):
-        self.exclude = map(TorCtl.RouterVersion, exclude)
-    
-    def r_is_ok(self, router):
-        for e in self.exclude:
-            if e == router.version:
-                return False
-        return True
-
-class VersionRangeRestriction(TorCtl.NodeRestriction):
-    def __init__(self, gr_eq, less_eq=None):
-        self.gr_eq = TorCtl.RouterVersion(gr_eq)
-        if less_eq: self.less_eq = TorCtl.RouterVersion(less_eq)
-        else: self.less_eq = None
-    
-
-    def r_is_ok(self, router):
-        return (not self.gr_eq or router.version >= self.gr_eq) and \
-                (not self.less_eq or router.version <= self.less_eq)
-
-class ExitPolicyRestriction(TorCtl.NodeRestriction):
-    def __init__(self, to_ip, to_port):
-        self.to_ip = to_ip
-        self.to_port = to_port
-
-    def r_is_ok(self, r):
-        return r.will_exit_to(self.to_ip, self.to_port)
-
-class AndRestriction(TorCtl.NodeRestriction):
-    def __init__(self, a, b):
-        self.a = a
-        self.b = b
-
-    def r_is_ok(self, r): return self.a.r_is_ok(r) and self.b.r_is_ok(r)
-
-class OrRestriction(TorCtl.NodeRestriction):
-    def __init__(self, a, b):
-        self.a = a
-        self.b = b
-
-    def r_is_ok(self, r): return self.a.r_is_ok(r) or self.b.r_is_ok(r)
-
-class NotRestriction(TorCtl.NodeRestriction):
-    def __init__(self, a):
-        self.a = a
-
-    def r_is_ok(self, r): return not self.a.r_is_ok(r)
-
-class Subnet16Restriction(TorCtl.PathRestriction):
-    def r_is_ok(self, path, router):
-        mask16 = struct.unpack(">I", socket.inet_aton("255.255.0.0"))[0]
-        ip16 = router.ip & mask16
-        for r in path:
-            if ip16 == (r.ip & mask16):
-                return False
-        return True
-
-class UniqueRestriction(TorCtl.PathRestriction):
-    def r_is_ok(self, path, r): return not r in path
-
-class UniformGenerator(TorCtl.NodeGenerator):
-    def next_r(self):
-        while not self.all_chosen():
-            r = random.choice(self.routers)
-            self.mark_chosen(r)
-            yield r
-
-class OrderedExitGenerator(TorCtl.NodeGenerator):
-    next_exit_by_port = {} # class member (aka C++ 'static')
-    def __init__(self, restriction_list, to_port):
-        self.to_port = to_port
-        TorCtl.NodeGenerator.__init__(self, restriction_list)
-
-    def rewind(self):
-        TorCtl.NodeGenerator.rewind(self)
-        if self.to_port not in self.next_exit_by_port or not self.next_exit_by_port[self.to_port]:
-            self.next_exit_by_port[self.to_port] = 0
-            self.last_idx = len(self.routers)
-        else:
-            self.last_idx = self.next_exit_by_port[self.to_port]
-   
-    # Just in case: 
-    def mark_chosen(self, r): raise NotImplemented()
-    def all_chosen(self): raise NotImplemented()
-
-    def next_r(self):
-        while True: # A do..while would be real nice here..
-            if self.next_exit_by_port[self.to_port] >= len(sorted_r):
-                self.next_exit_by_port[self.to_port] = 0
-            r = self.routers[self.next_exit_by_port[self.to_port]]
-            self.next_exit_by_port[self.to_port] += 1
-            yield r
-            if self.last_idx == self.next_exit_by_port[self.to_port]:
-                break
-        
 # TODO: Make passive mode so people can get aggregate node reliability 
 # stats for normal usage without us attaching streams
 
@@ -283,49 +94,36 @@
         nslist = c.get_network_status()
         self.read_routers(nslist)
         plog("INFO", "Read "+str(len(sorted_r))+"/"+str(len(nslist))+" routers")
-        self.path_rstr = TorCtl.PathRestrictionList(
+        self.path_rstr = PathRestrictionList(
                  [Subnet16Restriction(), UniqueRestriction()])
-        self.entry_rstr = TorCtl.NodeRestrictionList(
+        self.entry_rstr = NodeRestrictionList(
             [PercentileRestriction(percent_skip, percent_fast, sorted_r),
              ConserveExitsRestriction(),
              FlagsRestriction(["Guard", "Valid", "Running"], [])], sorted_r)
-        self.mid_rstr = TorCtl.NodeRestrictionList(
+        self.mid_rstr = NodeRestrictionList(
             [PercentileRestriction(percent_skip, percent_fast, sorted_r),
              ConserveExitsRestriction(),
              FlagsRestriction(["Valid", "Running"], [])], sorted_r)
-        self.exit_rstr = TorCtl.NodeRestrictionList(
+        self.exit_rstr = NodeRestrictionList(
             [PercentileRestriction(percent_skip, percent_fast, sorted_r),
              FlagsRestriction(["Valid", "Running", "Exit"], ["BadExit"])],
              sorted_r)
-        self.path_selector = TorCtl.PathSelector(
+        self.path_selector = PathSelector(
              UniformGenerator(self.entry_rstr),
              UniformGenerator(self.mid_rstr),
              OrderedExitGenerator(self.exit_rstr, 80), self.path_rstr)
 
     def read_routers(self, nslist):
-        bad_key = 0
-        for ns in nslist:
-            try:
-                key_to_name[ns.idhex] = ns.nickname
-                name_to_key[ns.nickname] = ns.idhex
-                r = MetaRouter(self.c.get_router(ns))
-                if ns.idhex in routers:
-                    if routers[ns.idhex].name != r.name:
-                        plog("NOTICE", "Router "+r.idhex+" changed names from "
-                             +routers[ns.idhex].name+" to "+r.name)
-                    sorted_r.remove(routers[ns.idhex])
-                routers[ns.idhex] = r
-                sorted_r.append(r)
-            except TorCtl.ErrorReply:
-                bad_key += 1
-                if "Running" in ns.flags:
-                    plog("NOTICE", "Running router "+ns.nickname+"="
-                         +ns.idhex+" has no descriptor")
-                pass
-            except:
-                traceback.print_exception(*sys.exc_info())
-                continue
-    
+        new_routers = map(MetaRouter, self.c.read_routers(nslist))
+        for r in new_routers:
+            if r.idhex in routers:
+                if routers[r.idhex].nickname != r.nickname:
+                    plog("NOTICE", "Router "+r.idhex+" changed names from "
+                         +routers[r.idhex].nickname+" to "+r.nickname)
+                sorted_r.remove(routers[r.idhex])
+            routers[r.idhex] = r
+            name_to_key[r.nickname] = r.idhex
+        sorted_r.extend(new_routers)
         sorted_r.sort(lambda x, y: cmp(y.bw, x.bw))
 
     def attach_stream_any(self, stream, badcircs):
@@ -498,9 +296,8 @@
         self.mid_rstr.update_routers(sorted_r)
         self.exit_rstr.update_routers(sorted_r)
         
-
 def commandloop(s):
-    s.write("220 Welcome to the Tor Metatroller "+version+"! Try HELP for Info\r\n\r\n")
+    s.write("220 Welcome to the Tor Metatroller "+mt_version+"! Try HELP for Info\r\n\r\n")
     while 1:
         buf = s.readline()
         if not buf: break
@@ -513,7 +310,7 @@
         (command, arg) = m.groups()
         if command == "GETLASTEXIT":
             le = last_exit # Consistency (avoids need for lock w/ GIL)
-            s.write("250 LASTEXIT=$"+le.idhex.upper()+" ("+le.name+") OK\r\n")
+            s.write("250 LASTEXIT=$"+le.idhex.upper()+" ("+le.nickname+") OK\r\n")
         elif command == "NEWEXIT" or command == "NEWNYM":
             global new_nym
             new_nym = True
@@ -614,7 +411,7 @@
 def startup():
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     s.connect((control_host,control_port))
-    c = TorCtl.get_connection(s)
+    c = PathSupport.Connection(s)
     c.debug(file("control.log", "w"))
     c.authenticate()
     c.set_event_handler(SnakeHandler(c))

Modified: torflow/trunk/nodemon.py
===================================================================
--- torflow/trunk/nodemon.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/nodemon.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -56,9 +56,9 @@
             r = RouterStats(c.get_router(ns))
             if ns.nickname in errors:
                 if errors[ns.nickname].idhex != r.idhex:
-                    plog("NOTICE", "Router "+r.name+" has multiple keys: "
+                    plog("NOTICE", "Router "+r.nickname+" has multiple keys: "
                          +errors[ns.nickname].idhex+" and "+r.idhex)
-            errors[r.name] = r # XXX: We get names only from ORCONN :(
+            errors[r.nickname] = r # XXX: We get names only from ORCONN :(
         except TorCtl.ErrorReply:
             bad_key += 1
             if "Running" in ns.flags:
@@ -77,7 +77,7 @@
         TorCtl.EventHandler.__init__(self)
         self.c = c
 
-    def or_conn_status(self, o):
+    def or_conn_status_event(self, o):
         # XXX: Count all routers as one?
         if re.search(r"^\$", o.endpoint):
             if o.endpoint not in key_to_name:
@@ -93,7 +93,7 @@
             if o.endpoint not in errors:
                 plog("NOTICE", "Buh?? No "+o.endpoint)
                 errors[o.endpoint] = RouterStats()
-                errors[o.endpoint].name = o.endpoint
+                errors[o.endpoint].nickname = o.endpoint
             errors[o.endpoint].running_read += o.read_bytes
             errors[o.endpoint].running_wrote += o.wrote_bytes
             errors_lock.release()
@@ -104,7 +104,7 @@
             if o.endpoint not in errors:
                 plog("NOTICE", "Buh?? No "+o.endpoint)
                 errors[o.endpoint] = RouterStats()
-                errors[o.endpoint].name = o.endpoint
+                errors[o.endpoint].nickname = o.endpoint
             if o.status == "FAILED" and not errors[o.endpoint].down:
                 o.status = o.status + "(Running)"
             o.reason = o.status+":"+o.reason
@@ -134,10 +134,10 @@
                 " ".join((o.event_name, o.endpoint, o.status, age, read, wrote,
                            reason, ncircs)))
 
-    def ns(self, n):
+    def ns_event(self, n):
         read_routers(self.c, n.nslist)
  
-    def new_desc(self, d):
+    def new_desc_event(self, d):
         for i in d.idlist: # Is this too slow?
             read_routers(self.c, self.c.get_network_status("id/"+i))
 
@@ -146,7 +146,7 @@
     routers.sort(lambda x,y: cmp(key(y), key(x))) # Python < 2.4 hack
 
     for r in routers:
-        f.write(r.name+"="+str(key(r))+"\n")
+        f.write(r.nickname+"="+str(key(r))+"\n")
     
     f.close()
     
@@ -187,7 +187,7 @@
     routers.sort(notlambda)
 
     for r in routers:
-        f.write(r.name+" " +str(r.tot_ncircs)+"/"+str(r.tot_count)+"\n")
+        f.write(r.nickname+" " +str(r.tot_ncircs)+"/"+str(r.tot_count)+"\n")
         for reason in r.reasons.itervalues():
             f.write("\t"+reason.reason+" "+str(reason.ncircs)+
                      "/"+str(reason.count)+"\n")

Deleted: torflow/trunk/unit.py
===================================================================
--- torflow/trunk/unit.py	2007-02-23 18:34:35 UTC (rev 9622)
+++ torflow/trunk/unit.py	2007-02-23 19:06:32 UTC (rev 9623)
@@ -1,59 +0,0 @@
-#!/usr/bin/python
-# Metatroller and TorCtl Unit Tests
-
-"""
-Unit tests
-"""
-
-import metatroller
-import copy
-import TorCtl
-c = metatroller.startup()
-
-print "Done!"
-
-# TODO: Tests:
-#  - Test each NodeRestriction and print in/out lines for it
-#  - Test NodeGenerator and reapply NodeRestrictions
-#  - Same for PathSelector and PathRestrictions
-#    - Also Reapply each restriction by hand to path. Verify returns true
-
-def do_unit(rst, r_list):
-    print "\n"
-    print rst.r_is_ok.im_class
-    for r in r_list:
-        print r.name+" "+r.os+" "+str(r.version)+"="+str(rst.r_is_ok(r))
-
-# Need copy for threadsafeness (XXX: hopefully it is atomic)
-sorted_r = copy.copy(metatroller.sorted_r)
-pct_rst = metatroller.PercentileRestriction(10, 20, sorted_r)
-oss_rst = metatroller.OSRestriction([r"[lL]inux", r"BSD", "Darwin"], [])
-prop_rst = metatroller.OSRestriction([], ["Windows", "Solaris"])
-
-#do_unit(metatroller.VersionRangeRestriction("0.1.2.0"), sorted_r)
-#do_unit(metatroller.VersionRangeRestriction("0.1.2.0", "0.1.2.5"), sorted_r)
-#do_unit(metatroller.VersionIncludeRestriction(["0.1.1.26-alpha"]), sorted_r)
-#do_unit(metatroller.VersionExcludeRestriction(["0.1.1.26"]), sorted_r)
-
-#do_unit(metatroller.ConserveExitsRestriction(), sorted_r)
-
-#do_unit(metatroller.FlagsRestriction([], ["Valid"]), sorted_r)
-
-# TODO: Cross check ns exit flag with this list
-#do_unit(metatroller.ExitPolicyRestriction("255.255.255.255", 25), sorted_r)
-
-#do_unit(pct_rst, sorted_r)
-#do_unit(oss_rst, sorted_r)
-#do_unit(alpha_rst, sorted_r)
-    
-rl =  [metatroller.ExitPolicyRestriction("255.255.255.255", 80), metatroller.OrRestriction(metatroller.ExitPolicyRestriction("255.255.255.255", 443), metatroller.ExitPolicyRestriction("255.255.255.255", 6667)), metatroller.FlagsRestriction([], ["BadExit"])]
-
-exit_rstr = TorCtl.NodeRestrictionList(rl, sorted_r)
-
-ug = metatroller.UniformGenerator(exit_rstr)
-
-for r in ug.next_r():
-    print "Checking: " + r.name
-    for rs in rl:
-        if not rs.r_is_ok(r):
-            raise FuxxorateException()