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

[minion-cvs] Integrate and new path selection logic; rename FWD to F...



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv15175/src/minion/lib/mixminion

Modified Files:
	BuildMessage.py ClientDirectory.py ClientMain.py 
	ClientUtils.py Config.py Packet.py ServerInfo.py test.py 
Log Message:
Integrate and new path selection logic; rename FWD to FWD_IPv4; start
work on reverse directory lookup; clean dead code from directory; add
new MMTPHostInfo routing type; have ServerInfo decide whom it can
relay to and how.



Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.58
retrieving revision 1.59
diff -u -d -r1.58 -r1.59
--- BuildMessage.py	31 Aug 2003 19:29:29 -0000	1.58
+++ BuildMessage.py	9 Oct 2003 15:26:15 -0000	1.59
@@ -286,7 +286,7 @@
                           paddingPRNG=Crypto.getCommonPRNG())
 
     return ReplyBlock(header, expiryTime,
-                      SWAP_FWD_TYPE,
+                      SWAP_FWD_IPV4_TYPE,
                       path[0].getRoutingInfo().pack(), sharedKey), secrets, tag
 
 # Maybe we shouldn't even allow this to be called with userKey==None.
@@ -334,7 +334,7 @@
     err = 0 # 0: no error. 1: 1st leg too big. 2: 1st leg okay, 2nd too big.
     if path1 is not None:
         try:
-            _getRouting(path1, SWAP_FWD_TYPE, path2[0].getRoutingInfo().pack())
+            _getRouting(path1, SWAP_FWD_IPV4_TYPE, path2[0].getRoutingInfo().pack())
         except MixError:
             err = 1
     # Add a dummy tag as needed to last exitinfo.
@@ -537,7 +537,7 @@
         path1exittype = reply.routingType
         path1exitinfo = reply.routingInfo
     else:
-        path1exittype = SWAP_FWD_TYPE
+        path1exittype = SWAP_FWD_IPV4_TYPE
         path1exitinfo = path2[0].getRoutingInfo().pack()
 
     # Generate secrets for path1.
@@ -726,7 +726,7 @@
        Raises MixError if the routing info is too big to fit into a single
        header. """
     # Construct a list 'routing' of exitType, exitInfo.
-    routing = [ (FWD_TYPE, node.getRoutingInfo().pack()) for
+    routing = [ (FWD_IPV4_TYPE, node.getRoutingInfo().pack()) for
                 node in path[1:] ]
     routing.append((exitType, exitInfo))
 

Index: ClientDirectory.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientDirectory.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- ClientDirectory.py	7 Oct 2003 21:57:46 -0000	1.3
+++ ClientDirectory.py	9 Oct 2003 15:26:15 -0000	1.4
@@ -8,7 +8,7 @@
    DOCDOC
      """
 
-__all__ = [ 'ClientDirectory', 'parsePath', 'parsePathLeg', 'parseAddress2' ]
+__all__ = [ 'ClientDirectory', 'parsePath', 'parseAddress' ]
 
 import cPickle
 import errno
@@ -25,13 +25,15 @@
 import mixminion.ClientMain #XXXX
 import mixminion.Config
 import mixminion.Crypto
+import mixminion.Packet
 import mixminion.ServerInfo
 
 from mixminion.Common import LOG, MixError, MixFatalError, UIError, \
      ceilDiv, createPrivateDir, formatDate, formatFnameTime, openUnique, \
      previousMidnight, readPickled, readPossiblyGzippedFile, \
-     replaceFile, tryUnlink, writePickled, floorDiv
-from mixminion.Packet import MBOX_TYPE, SMTP_TYPE, DROP_TYPE
+     replaceFile, tryUnlink, writePickled, floorDiv, isSMTPMailbox
+from mixminion.Packet import MBOX_TYPE, SMTP_TYPE, DROP_TYPE, FRAGMENT_TYPE, \
+     parseMBOXInfo, parseSMTPInfo, ParseError
 
 # FFFF This should be made configurable and adjustable.
 MIXMINION_DIRECTORY_URL = "http://mixminion.net/directory/Directory.gz";
@@ -52,6 +54,7 @@
     # digestMap: Map of (Digest -> 'D'|'D-'|'I:filename').
     # byNickname: Map from nickname.lower() to list of (ServerInfo, source)
     #   tuples.
+    # byKeyID: Map from desc.getKeyID() to list of ServerInfo.
     # byCapability: Map from capability ('mbox'/'smtp'/'relay'/None) to
     #    list of (ServerInfo, source) tuples.
     # allServers: Same as byCapability[None]
@@ -346,10 +349,12 @@
         return n
 
     def __rebuildTables(self):
-        """Helper method.  Reconstruct byNickname, allServers, and byCapability
-           from the internal start of this object.
-        """
+
+        """Helper method.  Reconstruct byNickname, byKeyID,
+           allServers, and byCapability from the internal start of
+           this object.  """
         self.byNickname = {}
+        self.byKeyID = {}
         self.allServers = []
         self.byCapability = { 'mbox': [],
                               'smtp': [],
@@ -360,7 +365,9 @@
 
         for info, where in self.serverList:
             nn = info.getNickname().lower()
-            lists = [ self.allServers, self.byNickname.setdefault(nn, []) ]
+            keyid = info.getKeyID()
+            lists = [ self.allServers, self.byNickname.setdefault(nn, []),
+                      self.byKeyID.setdefault(info.getKeyID(), []) ]
             for c in info.getCaps():
                 lists.append( self.byCapability[c] )
             for lst in lists:
@@ -373,7 +380,6 @@
                 continue
             self.byNickname.setdefault(nn, []).append((info, where))
 
-
     def listServers(self):
         """Returns a linewise listing of the current servers and their caps.
             This will go away or get refactored in future versions once we
@@ -403,18 +409,6 @@
                 lines.append(line)
         return lines
 
-    def __findOne(self, lst, startAt, endAt):
-        """Helper method.  Given a list of (ServerInfo, where), return a
-           single element that is valid for all time between startAt and
-           endAt.
-
-           Watch out: this element is _not_ randomly chosen.
-           """
-        res = self.__find(lst, startAt, endAt)
-        if res:
-            return res[0]
-        return None
-
     def __find(self, lst, startAt, endAt):
         """Helper method.  Given a list of (ServerInfo, where), return all
            elements that are valid for all time between startAt and endAt.
@@ -439,6 +433,28 @@
 
         return u.values()
 
+    def getNicknameByKeyID(self, keyid):
+        s = self.bykeyID.get(keyid)
+        if not s:
+            return None
+        r = []
+        for d in s:
+            if d.getNickname().lower() not in r:
+                r.append(d.getNickname())
+        return "/".join(r)
+
+    def getNameByRelay(self, routingType, routingInfo):
+        assert routingType in (mixminion.Packet.FWD_IPV4_TYPE,
+                               mixminion.Packet.SWAP_FWD_IPV4_TYPE)
+        if type(routingInfo) == types.StringType:
+            routingInfo = mixminion.Packet.parseIPV4Info(routingInfo)
+        assert isinstance(routingInfo, mixminion.Packet.IPV4Info)
+        nn = self.getNicknameByKeyID(self, routingInfo.keyinfo)
+        if nn is None:
+            return "%s:%s"%(routingInfo.ip, routingInfo.port)
+        else:
+            return nn
+
     def getLiveServers(self, startAt=None, endAt=None):
         """DOCDOC"""
         if startAt is None:
@@ -447,24 +463,6 @@
             endAt = time.time()+self.DEFAULT_REQUIRED_LIFETIME
         return self.__find(self.serverList, startAt, endAt)
 
-    def findByExitTypeAndSize(self, exitType, size, nPackets):
-        #XXXX006 remove this method.  It's not really a good interface,
-        #XXXX006 and only gets used by the kludgy choose-a-new-last-hop logic
-        """Return a server that supports exitType 'exittype' (currently must be
-           SMTP_TYPE), and messages of size 'size' bytes."""
-        assert exitType == SMTP_TYPE
-        servers = self.__find(self.byCapability['smtp'], time.time(),
-                              time.time()+24*60*60)
-        servers = servers[:]
-        mixminion.Crypto.getCommonPRNG().shuffle(servers)
-        for s in servers:
-            maxSize = s['Delivery/SMTP']['Maximum-Size'] * 1024
-            maxPackets = s['Delivery/Fragmented'].get('Maximum-Fragments',1)
-            if maxSize >= size and maxPackets >= nPackets:
-                return s
-
-        return None
-
     def clean(self, now=None):
         """Remove all expired or superseded descriptors from DIR/servers."""
 
@@ -524,13 +522,14 @@
                 LOG.error("Server is not currently valid")
         elif self.byNickname.has_key(name.lower()):
             # If it's a nickname, return a serverinfo with that name.
-            s = self.__findOne(self.byNickname[name.lower()], startAt, endAt)
+            s = self.__find(self.byNickname[name.lower()], startAt, endAt)
 
             if not s:
                 raise UIError(
                     "Couldn't find any currently live descriptor with name %s"
                     % name)
 
+            s = s[0]
             if not self.goodServerNicknames.has_key(s.getNickname().lower()):
                 LOG.warn("Server %s is not recommended",name)
             
@@ -552,7 +551,8 @@
         else:
             return None
 
-    def generatePaths(self, nPaths, pathSpec, exitAddress, startAt=None, endAt=None,
+    def generatePaths(self, nPaths, pathSpec, exitAddress, 
+                      startAt=None, endAt=None,
                       prng=None):
         """Return a list of pairs of lists of serverinfo DOCDOC."""
 
@@ -567,7 +567,7 @@
             plausibleExits = exitAddress.getExitServers(self,startAt,endAt)
             if exitAddress.isSSFragmented:
                 # We _must_ have a single common last hop.
-                plauxibleExits = [ prng.pick(plausibleExits) ]
+                plausibleExits = [ prng.pick(plausibleExits) ]
 
         for _ in xrange(nPaths):
             p1 = []
@@ -590,12 +590,20 @@
             else:
                 n1 = len(p1)
 
-            path = self.getPath(None, p, startAt=startAt, endAt=endAt)
-            paths.append( (path[:n1], path[n1:]) )
-            
+            path = self.getPath(p, startAt=startAt, endAt=endAt)
+            path1,path2 = path[:n1], path[n1:]
+            paths.append( (path1,path2) )
+            if exitAddress.isReply or exitAddress.isSURB:
+                LOG.info("Selected path is %s",
+                         ",".join([s.getNickname() for s in path]))
+            else:
+                LOG.info("Selected path is %s:%s",
+                         ",".join([s.getNickname() for s in path1]),
+                         ",".join([s.getNickname() for s in path2]))
+
         return paths
     
-    def getPath(self, endCap, template, startAt=None, endAt=None, prng=None):
+    def getPath(self, template, startAt=None, endAt=None, prng=None):
         """Workhorse method for path selection.  Given a template, and
            a capability that must be supported by the exit node, return
            a list of serverinfos that 'matches' the template, and whose
@@ -606,9 +614,7 @@
            getPath should select a corresponding server.
 
            All servers are chosen to be valid continuously from
-           startAt to endAt.  The last server is not set) is selected
-           to have 'endCap' (typically 'mbox' or 'smtp').  Set endCap
-           to 'None' if you don't care.
+           startAt to endAt.
 
            The path selection algorithm perfers to choose without
            replacement it it can.
@@ -619,8 +625,9 @@
                considered equivalent if their nicknames are the same,
                ignoring case.
             """
-            n = [ inf.getNickname().lower() for inf in s2 ]
-            return [ inf for inf in s1 if inf.getNickname().lower() not in n]
+            n = [ inf.getNickname().lower() for inf in s2 if inf is not None ]
+            return [ inf for inf in s1 
+                     if inf is not None and inf.getNickname().lower() not in n]
 
         # Fill in startAt, endAt, prng if not provided
         if startAt is None:
@@ -638,33 +645,7 @@
             else:
                 servers.append(self.getServerInfo(name, startAt, endAt, 1))
 
-        # If we need to pick the last server, pick it first.
-        if servers[-1] is None:
-            # Who has the required exit capability....
-            endCandidates = self.__find(self.byCapability[endCap],
-                                        startAt,endAt)
-            if not endCandidates:
-                raise UIError("Can't build path: no %s servers known"%endCap)
-            # ....that we haven't used yet?
-            used = filter(None, servers)
-            unusedEndCandidates = setSub(endCandidates, used)
-            if unusedEndCandidates:
-                # Somebody with the capability is unused
-                endCandidates = unusedEndCandidates
-            elif len(endCandidates) > 1 and servers[-2] is not None:
-                # We can at least avoid of picking someone with the
-                # capability who isn't the penultimate node.
-                penultimate = servers[-2].getNickname().lower()
-                endCandidates = setSub(endCandidates, [penultimate])
-            else:
-                # We're on our own.
-                assert len(endCandidates)
-
-            # Finally, fill in the last server.
-            servers[-1] = prng.pick(endCandidates)
-
         # Now figure out which relays we haven't used yet.
-        used = filter(None, servers)
         relays = self.__find(self.byCapability['relay'], startAt, endAt)
         if not relays:
             raise UIError("No relays known")
@@ -679,37 +660,78 @@
                 continue
             # Find the servers adjacent to it, if any...
             if i>0:
-                abutters = filter(None,[ servers[i-1], servers[i+1]])
+                prev = servers[i-1]
             else:
-                abutters = filter(None,[ servers[i+1] ])
-            # ...and see if there are any relays left that aren't adjacent.
-            candidates = setSub(relays, abutters)
+                prev = None
+            if i+1<len(servers):
+                next = servers[i+1]
+            else:
+                next = None
+            # ...and see if there are any relays left that aren't adjacent?
+            candidates = []
+            for c in relays:
+                if ((prev and c.hasSameNicknameAs(prev)) or
+                    (next and c.hasSameNicknameAs(next)) or
+                    (prev and not prev.canRelayTo(c)) or 
+                    (next and not c.canRelayTo(next))):
+                    continue
+                candidates.append(c)                    
             if candidates:
                 # Good.  There are.
                 servers[i] = prng.pick(candidates)
             else:
-                # Nope.  Choose a random relay.
-                servers[i] = prng.pick(relays)
+                # Nope.  Can we duplicate a relay?
+                LOG.warn("Repeating a relay because of routing restrictions.")
+                if prev and next: 
+                    if prev.canRelayTo(next):
+                        servers[i] = prev
+                    elif next.canRelayTo(next):
+                        servers[i] = next
+                    else:
+                        raise UIError("Can't generate path %s->???->%s"%(
+                                      prev.getNickname(),next.getNickname()))
+                elif prev and not next:
+                    servers[i] = prev
+                elif next and not prev:
+                    servers[i] = next
+                else:
+                    raise UIError("No servers known.")
 
         # FFFF We need to make sure that the path isn't totally junky.
 
         return servers
 
-    def validatePath2(self, pathSpec, exitAddress, startAt=None, endAt=None):
+    def validatePath(self, pathSpec, exitAddress, startAt=None, endAt=None):
         """DOCDOC 
            takes pathspec; raises UIError or does nothing."""
         if startAt is None: startAt = time.time()
         if endAt is None: endAt = startAt+self.DEFAULT_REQUIRED_LIFETIME
 
         p = pathSpec.path1+pathSpec.path2
+        # Make sure all elements are valid.
         for e in p:
             e.validate(self, startAt, endAt)
+
+        # When there are 2 elements in a row, make sure each can route to
+        # the next.
+        prevFixed = None
+        for e in p:
+            fixed = e.getFixedServer(self, startAt, endAt)
+            if prevFixed and fixed and not prevFixed.canRelayTo(fixed):
+                raise UIError("Server %s can't relay to %s",
+                              prevFixed.getNickname(), fixed.getNickname())
+            prevFixed = fixed
+
         fs = p[-1].getFixedServer(self,startAt,endAt)
         lh = exitAddress.getLastHop()
         if lh is not None:
-            fs = self.getServerInfo(lh, startAt, endAt)
-            if fs is None:
+            lh_s = self.getServerInfo(lh, startAt, endAt)
+            if lh_s is None:
                 raise UIError("No known server descriptor named %s",lh)
+            if fs and not fs.canRelayTo(lh_s):
+                raise UIError("Server %s can't relay to %s",
+                              fs.getNickname(), lh)
+            fs = lh_s
         if fs is not None:
             exitAddress.checkSupportedByServer(fs)
         elif exitAddress.isServerRelative():
@@ -748,244 +770,6 @@
             LOG.warn("This software is newer than any version "
                      "on the recommended list.")
 
-def parsePath(directory, config, path, address, nHops=None,
-              startAt=None, endAt=None, halfPath=0,
-              defaultNHops=None):
-    """Wrap new path parsing methods to provide functionality of old parsePath
-       method.  The old methods has been (for now) renamed to 'parsePathOrig'.
-       """
-    isReply = halfPath and (address is None)
-    isSURB = halfPath and (address is not None)
-    if not isReply:
-        rt, ri, lastHop = address.getRouting()
-        exitAddress = ExitAddress(rt, ri, lastHop)
-    else:
-        exitAddress = ExitAddress(isReply=1)
-    pathSpec = parsePath2(config, path, nHops=nHops, isReply=isReply,
-                          isSURB=isSURB, defaultNHops=defaultNHops)
-    directory.validatePath2(pathSpec, exitAddress, startAt=startAt,endAt=endAt)
-    paths = directory.generatePaths(1, pathSpec, exitAddress, startAt,endAt)
-    assert len(paths) == 1
-    return paths[0]
-
-def parsePathOrig(directory, config, path, address, nHops=None,
-              startAt=None, endAt=None, halfPath=0,
-              defaultNHops=None):
-    """Resolve a path as specified on the command line.  Returns a
-       (path-leg-1, path-leg-2) tuple, where each leg is a list of ServerInfo.
-
-       directory -- the ClientDirectory to use.
-       config -- unused for now.
-       path -- the path, in a format described below.  If the path is
-          None, all servers are chosen as if the path were '*'.
-       address -- the address to deliver the message to; if it specifies
-          an exit node, the exit node is appended to the second leg of the
-          path and does not count against the number of hops.  If 'address'
-          is None, the exit node must support relay.
-       nHops -- the number of hops to use.  Defaults to defaultNHops.
-       startAt/endAt -- A time range during which all servers must be valid.
-       halfPath -- If true, we generate only the second leg of the path
-          and leave the first leg empty.
-       defaultNHops -- The default path length to use when we encounter a
-          wildcard in the path.  Defaults to 6.
-
-       Paths are ordinarily comma-separated lists of server nicknames or
-       server descriptor filenames, as in:
-             'foo,bar,./descriptors/baz,quux'.
-
-       You can use a colon as a separator to divides the first leg of the path
-       from the second:
-             'foo,bar:baz,quux'.
-       If nSwap and a colon are both used, they must match, or MixError is
-       raised.
-
-       You can use a question mark to indicate a randomly chosen server:
-             'foo,bar,?,quux,?'.
-       As an abbreviation, you can use star followed by a number to indicate
-       that number of randomly chosen servers:
-             'foo,bar,*2,quux'.
-       You can use a star without a number to specify a fill point
-       where randomly-selected servers will be added:
-             'foo,bar,*,quux'.
-       Finally, you can use a tilde followed by a number to specify an
-       approximate number of servers to add.  (The actual number will be
-       chosen randomly, according to a normal distribution with standard
-       deviation 1.5):
-             'foo,bar,~2,quux'
-
-       The nHops argument must be consistent with the path, if both are
-       specified.  Specifically, if nHops is used _without_ a star on the
-       path, nHops must equal the path length; and if nHops is used _with_ a
-       star on the path, nHops must be >= the path length.
-    """
-    if not path:
-        path = '*'
-    # Break path into a list of entries of the form:
-    #        Nickname
-    #     or "<swap>"
-    #     or "?"
-    p = []
-    while path:
-        if path[0] == "'":
-            m = re.match(r"'([^']+|\\')*'", path)
-            if not m: 
-                raise UIError("Mismatched quotes in path.")
-            p.append(m.group(1).replace("\\'", "'"))
-            path = path[m.end():]
-            if path and path[0] not in ":,":
-                raise UIError("Invalid quotes in path.")
-        elif path[0] == '"':
-            m = re.match(r'"([^"]+|\\")*"', path)
-            if not m: 
-                raise UIError("Mismatched quotes in path.")
-            p.append(m.group(1).replace('\\"', '"'))
-            path = path[m.end():]
-            if path and path[0] not in ":,":
-                raise UIError("Invalid quotes in path.")
-        else:
-            m = re.match(r"[^,:]+",path)
-            if not m:
-                raise UIError("Invalid path") 
-            p.append(m.group(0))
-            path = path[m.end():]
-        if not path:
-            break 
-        elif path[0] == ',':
-            path = path[1:]
-        elif path[0] == ':':
-            path = path[1:]
-            p.append("<swap>")
-
-    path = []
-    for ent in p:
-        if re.match(r'\*(\d+)', ent):
-            path.extend(["?"]*int(ent[1:]))
-        elif re.match(r'\~(\d+)', ent):
-            avg = int(ent[1:])
-            n = int(mixminion.Crypto.getCommonPRNG().getNormal(avg, 1.5)+0.5)
-            if n < 0: n = 0
-            path.extend(['?']*n)
-        else:
-            path.append(ent)
-
-    # set explicitSwap to true iff the user specified a swap point.
-    explicitSwap = path.count("<swap>")
-    # set colonPos to the index of the explicit swap point, if any.
-    if path.count("<swap>") > 1:
-        raise UIError("Can't specify swap point twice")
-
-    # set starPos to the index of the var-length wildcard, if any.
-    if path.count("*") > 1:
-        raise UIError("Can't have two variable-length wildcards in a path")
-    elif path.count("*") == 1:
-        starPos = path.index("*")
-    else:
-        starPos = None
-
-    # If there's a variable-length wildcard...
-    if starPos is not None:
-        # Find out how many hops we should have.
-        myNHops = nHops or defaultNHops or 6
-        # Figure out how many nodes we need to add.
-        haveHops = len(path) - 1
-        # A colon will throw the count off.
-        if explicitSwap:
-            haveHops -= 1
-        path[starPos:starPos+1] = ["?"]*max(0,myNHops-haveHops)
-
-    # Figure out how long the first leg should be.
-    if explicitSwap:
-        # Calculate colon position
-        colonPos = path.index("<swap>")
-        if halfPath:
-            raise UIError("Can't specify swap point with replies")
-        firstLegLen = colonPos
-        del path[colonPos]
-    elif halfPath:
-        firstLegLen = 0
-    else:
-        firstLegLen = ceilDiv(len(path), 2)
-
-    # Do we have the right # of hops?
-    if nHops is not None and len(path) != nHops:
-        raise UIError("Mismatch between specified path lengths")
-
-    # Replace all '?'s in path with [None].
-    for i in xrange(len(path)):
-        if path[i] == '?': path[i] = None
-
-    # Figure out what capability we need in our exit node, so that
-    # we can tell the directory.
-    if address is None:
-        rt, ri, exitNode = None, None, None
-        exitCap = 'relay'
-    else:
-        rt, ri, exitNode = address.getRouting()
-        if rt == MBOX_TYPE:
-            exitCap = 'mbox'
-        elif rt == SMTP_TYPE:
-            exitCap = 'smtp'
-        else:
-            exitCap = None
-
-    # If we have an explicit exit node from the address, append it.
-    if exitNode is not None:
-        path.append(exitNode)
-
-    # Get a list of serverinfo.
-    path = directory.getPath(endCap=exitCap,
-                             template=path, startAt=startAt, endAt=endAt)
-
-    # Now sanity-check the servers.
-
-    # Make sure all relay servers support relaying.
-    for server in path[:-1]:
-        if "relay" not in server.getCaps():
-            raise UIError("Server %s does not support relay"
-                          % server.getNickname())
-
-    # Make sure the exit server can support the exit capability.
-    if exitCap and exitCap not in path[-1].getCaps():
-        raise UIError("Server %s does not support %s capability"
-                      % (path[-1].getNickname(), exitCap))
-
-
-    # Split the path into 2 legs.
-    path1, path2 = path[:firstLegLen], path[firstLegLen:]
-    if not halfPath and len(path1)+len(path2) < 2:
-        raise UIError("Path is too short")
-    if not halfPath and (not path1 or not path2):
-        raise UIError("Each leg of the path must have at least 1 hop")
-
-    # Make sure the path can fit into the headers.
-    mixminion.BuildMessage.checkPathLength(path1, path2,
-                                           rt,ri,
-                                           explicitSwap)
-
-    # Return the two legs of the path.
-    return path1, path2
-
-def parsePathLeg(directory, config, path, nHops, address=None,
-                 startAt=None, endAt=None, defaultNHops=None):
-    """Parse a single leg of a path.  Used for generating SURBs (second leg
-       only) or reply messages (first leg only).  Returns a list of
-       ServerInfo.
-
-       directory -- the ClientDirectory to use.
-       config -- unused for now.
-       path -- The path, as described in parsePath, except that ':' is not
-           allowed.
-       nHops -- the number of hops to use.  Defaults to defaultNHops.
-       startAt/endAt -- A time range during which all servers must be valid.
-       defaultNHops -- The default path length to use when we encounter a
-          wildcard in the path.  Defaults to 6.
-       """
-    path1, path2 = parsePath(directory, config, path, address, nHops,
-                             startAt=startAt, endAt=endAt, halfPath=1,
-                             defaultNHops=defaultNHops)
-    assert path1 == []
-    return path2
-
 #----------------------------------------------------------------------
 
 KNOWN_STRING_EXIT_TYPES = [
@@ -995,7 +779,7 @@
 class ExitAddress:
     #FFFF Perhaps this crams too much into ExitAddress.
     def __init__(self,exitType=None,exitAddress=None,lastHop=None,isReply=0, 
-                 isSSFragmented=0):
+                 isSURB=0,isSSFragmented=0):
         if isReply:
             assert exitType is None
             assert exitAddress is None
@@ -1014,18 +798,22 @@
         self.exitAddress = exitAddress
         self.lastHop = lastHop
         self.isReply = isReply
+        self.isSURB = isSURB
         self.isSSFragmented = isSSFragmented #server-side frag reassembly only.
         self.nFragments = self.exitSize = 0
         self.headers = {}
+    def getFragmentedMessagePrefix(self):
+        routingType, routingInfo, _ = self.getRouting()
+        return mixminion.Packet.ServerSideFragmentedMessage(
+            routingType, routingInfo, "").pack()
+        
     def setFragmented(self, isSSFragmented, nFragments):
-        self.isSSFragmented = isFragmented
+        self.isSSFragmented = isSSFragmented
         self.nFragments = nFragments
     def setExitSize(self, exitSize):
         self.exitSize = exitSize
     def setHeaders(self, headers):
         self.headers = headers
-    def isReply(self):
-        return self.isReply
     def getLastHop(self):
         return self.lastHop
     def isSupportedByServer(self, desc):
@@ -1038,6 +826,14 @@
         if self.isReply:
             return
         nickname = desc.getNickname()
+
+        if self.headers:
+            #XXXX006 remove this eventually.
+            sware = desc['Server'].get("Software")
+            if (sware.startswith("Mixminion 0.0.4") or 
+                sware.startswith("Mixminion 0.0.5alpha1")):
+                raise UIError("Server %s is running old software that doesn't support exit headers.", nickname)
+
         if self.isSSFragmented:
             dfsec = desc['Delivery/Fragmented']
             if not dfsec.get("Version"):
@@ -1076,9 +872,9 @@
 
     def getPrettyExitType(self):
         if type(self.exitType) == types.IntType:
-            prettyExit = "0x%04X"%self.exitType
+            return "0x%04X"%self.exitType
         else:
-            prettyExit = `self.exitType`
+            return self.exitType
 
     def isServerRelative(self):
         return self.exitType in ('mbox', MBOX_TYPE)
@@ -1092,12 +888,24 @@
                    if self.isSupportedByServer(desc) ]
         return result
 
-    def getRouting(self, desc):
+    def getRouting(self):
         """DOCDOC"""
-        #XXXX
-        assert 0
+        ri = self.exitAddress
+        if self.isSSFragmented:
+            rt = FRAGMENT_TYPE
+            ri = ""
+        elif self.exitType == 'smtp':
+            rt = SMTP_TYPE
+        elif self.exitType == 'drop':
+            rt = DROP_TYPE
+        elif self.exitType == 'mbox':
+            rt = MBOX_TYPE
+        else:
+            assert type(self.exitType) == types.IntType
+            rt = self.exitType
+        return rt, ri, self.lastHop
 
-def parseAddress2(s):
+def parseAddress(s):
     """Parse and validate an address; takes a string, and returns an
        ExitAddress object.
 
@@ -1144,15 +952,13 @@
 
 class PathElement:
     def validate(self, directory, start, end):
-        raise NotImplemented
-    def getServers(self, directory, start, end):
-        raise NotImplemented
+        raise NotImplemented()
     def getFixedServer(self, directory, start, end):
-        raise NotImplemented
+        raise NotImplemented()
     def getServerNames(self):
-        raise NotImplemented
+        raise NotImplemented()
     def getMinLength(self):
-        raise NotImplemented
+        raise NotImplemented()
 
 class ServerPathElement(PathElement):
     def __init__(self, nickname):
@@ -1226,6 +1032,7 @@
         self.isReply=isReply
         self.isSURB=isSURB
         self.lateSplit=lateSplit
+
     def getFixedLastServer(self,directory,startAt,endAt):
         """DOCDOC"""
         if self.path2:
@@ -1234,9 +1041,57 @@
             return None
 
 #----------------------------------------------------------------------
-def parsePath2(config, path, nHops=None, isReply=0, isSURB=0,
-               defaultNHops=None):
-    """DOCDOC ; Returns a pathSpecifier.
+def parsePath(config, path, nHops=None, isReply=0, isSURB=0,
+              defaultNHops=None):
+    """DOCDOC ; Returns a pathSpecifier.  This documentation is no longer
+       even vaguely accurate.
+
+       Resolve a path as specified on the command line.  Returns a
+       (path-leg-1, path-leg-2) tuple, where each leg is a list of ServerInfo.
+
+       directory -- the ClientDirectory to use.
+       config -- unused for now.
+       path -- the path, in a format described below.  If the path is
+          None, all servers are chosen as if the path were '*'.
+       address -- the address to deliver the message to; if it specifies
+          an exit node, the exit node is appended to the second leg of the
+          path and does not count against the number of hops.  If 'address'
+          is None, the exit node must support relay.
+       nHops -- the number of hops to use.  Defaults to defaultNHops.
+       startAt/endAt -- A time range during which all servers must be valid.
+       halfPath -- If true, we generate only the second leg of the path
+          and leave the first leg empty.
+       defaultNHops -- The default path length to use when we encounter a
+          wildcard in the path.  Defaults to 6.
+
+       Paths are ordinarily comma-separated lists of server nicknames or
+       server descriptor filenames, as in:
+             'foo,bar,./descriptors/baz,quux'.
+
+       You can use a colon as a separator to divides the first leg of the path
+       from the second:
+             'foo,bar:baz,quux'.
+       If nSwap and a colon are both used, they must match, or MixError is
+       raised.
+
+       You can use a question mark to indicate a randomly chosen server:
+             'foo,bar,?,quux,?'.
+       As an abbreviation, you can use star followed by a number to indicate
+       that number of randomly chosen servers:
+             'foo,bar,*2,quux'.
+       You can use a star without a number to specify a fill point
+       where randomly-selected servers will be added:
+             'foo,bar,*,quux'.
+       Finally, you can use a tilde followed by a number to specify an
+       approximate number of servers to add.  (The actual number will be
+       chosen randomly, according to a normal distribution with standard
+       deviation 1.5):
+             'foo,bar,~2,quux'
+
+       The nHops argument must be consistent with the path, if both are
+       specified.  Specifically, if nHops is used _without_ a star on the
+       path, nHops must equal the path length; and if nHops is used _with_ a
+       star on the path, nHops must be >= the path length.
     """
     halfPath = isReply or isSURB
     if not path:
@@ -1300,7 +1155,7 @@
             raise UIError("Only one '*' is permitted in a single path")
         approxHops = reduce(operator.add,
                             [ ent.getMinLength() for ent in pathEntries
-                              if ent not in ("*", "<swap>") ])
+                              if ent not in ("*", "<swap>") ], 0)
         myNHops = nHops or defaultNHops or 6
         extraHops = max(myNHops-approxHops, 0)
         pathEntries[starPos:starPos+1] =[RandomServersPathElement(n=extraHops)]
@@ -1333,7 +1188,7 @@
             raise UIError("Each leg of the path must have at least 1 hop")
     else:
         minLen = reduce(operator.add,
-                        [ ent.getMinLength() for ent in pathEntries ])
+                        [ ent.getMinLength() for ent in pathEntries ], 0)
         if halfPath and minLen < 1:
             raise UIError("The path must have at least 1 hop")
         if not halfPath and minLen < 2:

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.118
retrieving revision 1.119
diff -u -d -r1.118 -r1.119
--- ClientMain.py	7 Oct 2003 21:57:46 -0000	1.118
+++ ClientMain.py	9 Oct 2003 15:26:15 -0000	1.119
@@ -11,6 +11,7 @@
 
 import getopt
 import os
+import socket
 import sys
 import time
 from types import ListType
@@ -26,12 +27,11 @@
 from mixminion.Common import LOG, Lockfile, LockfileLocked, MixError, \
      MixFatalError, MixProtocolBadAuth, MixProtocolError, UIError, \
      UsageError, createPrivateDir, isPrintingAscii, isSMTPMailbox, readFile, \
-     stringContains, succeedingMidnight, writeFile
+     stringContains, succeedingMidnight, writeFile, previousMidnight
 from mixminion.Packet import encodeMailHeaders, ParseError, parseMBOXInfo, \
      parseReplyBlocks, parseSMTPInfo, parseTextEncodedMessages, \
-     parseTextReplyBlocks, ReplyBlock, MBOX_TYPE, SMTP_TYPE, DROP_TYPE
-from mixminion.ClientDirectory import ClientDirectory, parsePath, \
-     parsePathLeg
+     parseTextReplyBlocks, ReplyBlock, MBOX_TYPE, SMTP_TYPE, DROP_TYPE, \
+     parseMessageAndHeaders
 
 #----------------------------------------------------------------------
 # Global variable; holds an instance of Common.Lockfile used to prevent
@@ -203,8 +203,16 @@
         self.prng = mixminion.Crypto.getCommonPRNG()
         self.queue = mixminion.ClientUtils.ClientQueue(os.path.join(userdir, "queue"))
 
-    def sendForwardMessage(self, address, payload, servers1, servers2,
-                           forceQueue=0, forceNoQueue=0):
+    def _sortPackets(self, packets):
+        """[(packet,firstHop),...] -> [ (routing, [packet,...]), ...]"""
+        r = {}
+        for packet, firstHop in packets:
+            ri = firstHop.getRoutingInfo()
+            r.setdefault(ri,[]).append(packet)
+        return r.items()
+
+    def sendForwardMessage(self, directory, address, pathSpec, message,
+                           startAt, endAt, forceQueue=0, forceNoQueue=0):
         """Generate and send a forward message.
             address -- the results of a parseAddress call
             payload -- the contents of the message to send
@@ -216,17 +224,17 @@
                fails."""
         assert not (forceQueue and forceNoQueue)
 
-        for packet, firstHop in self.generateForwardMessage(
-            address, payload, servers1, servers2):
-
-            routing = firstHop.getRoutingInfo()
+        allPackets = self.generateForwardPayloads(
+            directory, address, pathSpec, message, startAt, endAt)
 
+        for routing, packets in self._sortPackets(allPackets):
             if forceQueue:
-                self.queueMessages([packet], routing)
+                self.queueMessages(packets, routing)
             else:
-                self.sendMessages([packet], routing, noQueue=forceNoQueue)
+                self.sendMessages(packets, routing, noQueue=forceNoQueue)
 
-    def sendReplyMessage(self, payload, servers, surbList, forceQueue=0,
+    def sendReplyMessage(self, directory, address, pathSpec, surbList, message,
+                         startAt, endAt, forceQueue=0,
                          forceNoQueue=0):
         """Generate and send a reply message.
             payload -- the contents of the message to send
@@ -237,17 +245,18 @@
             forceQueue -- if true, do not try to send the message; simply
                queue it and exit.
             forceNoQueue -- if true, do not queue the message even if delivery
-               fails."""
-        #XXXX write unit tests
-        message, firstHop = \
-                 self.generateReplyMessage(payload, servers, surbList)
+               fails.
 
-        routing = firstHop.getRoutingInfo()
+               DOCDOC args are wrong."""
+        #XXXX write unit tests
+        allPackets = self.generateReplyMessage(
+            directory, address, pathSpec, message, surbList, startAt, endAt)
 
-        if forceQueue:
-            self.queueMessages([message], routing)
-        else:
-            self.sendMessages([message], routing, noQueue=forceNoQueue)
+        for routing, packets in self._sortPackets(allPackets):
+            if forceQueue:
+                self.queueMessages(packets, routing)
+            else:
+                self.sendMessages(packets, routing, noQueue=forceNoQueue)
 
     def generateReplyBlock(self, address, servers, name="", expiryTime=0):
         """Generate an return a new ReplyBlock object.
@@ -265,42 +274,44 @@
 
         return block
 
-    def generateForwardMessage(self, address, message, servers1, servers2):
+    def generateForwardPayloads(self, directory, address, pathSpec, message,
+                               startAt, endAt):
         """Generate a forward message, but do not send it.  Returns a
            list of tuples of (the packet body, a ServerInfo for the
            first hop.)
 
-            address -- the results of a parseAddress call
-            message -- the contents of the message to send  (None for DROP
-              messages)
-            servers1,servers2 -- lists of ServerInfo.
+           DOCDOC
             """
-        routingType, routingInfo, _ = address.getRouting()
+
+        #XXXX006 handle user-side fragmentation.
 
         #XXXX006 we need to factor this long-message logic out to the
         #XXXX006 common code.  For now, this is a temporary measure.
-        fragmentedMessagePrefix = mixminion.Packet.ServerSideFragmentedMessage(
-            routingType, routingInfo, "").pack()
+        fragmentedMessagePrefix = address.getFragmentedMessagePrefix()
         LOG.info("Generating payload(s)...")
         r = []
         payloads = mixminion.BuildMessage.encodeMessage(message, 0,
                             fragmentedMessagePrefix)
         if len(payloads) > 1:
-            routingType = mixminion.Packet.FRAGMENT_TYPE
-            routingInfo = ""
-            if servers2[-1]['Delivery/Fragmented'].get('Maximum-Fragments',1) < len(payloads):
-                raise UIError("Oops; %s won't reassable a message this large."%
-                              servers2[-1].getNickname())
+            address.setFragmented(1,len(payloads))
+        else:
+            address.setFragmented(0,1)
+        routingType, routingInfo, _ = address.getRouting()
+        
+        directory.validatePath(pathSpec, address, startAt, endAt)
+        
+        for p, (path1,path2) in zip(payloads, directory.generatePaths(
+            len(payloads), pathSpec, address, startAt, endAt)):
 
-        #XXXX006 don't use the same path for all the packets!
-        for p in payloads:
             msg = mixminion.BuildMessage._buildForwardMessage(
-                p, routingType, routingInfo, servers1, servers2,
+                p, routingType, routingInfo, path1, path2,
                 self.prng)
-            r.append( (msg, servers1[0]) )
+            r.append( (msg, path1[0]) )
+
         return r
 
-    def generateReplyMessage(self, payload, servers, surbList, now=None):
+    def generateReplyMessage(self, directory, address, pathSpec, message,
+                             surbList, startAt, endAt):
         """Generate a forward message, but do not send it.  Returns
            a tuple of (the message body, a ServerInfo for the first hop.)
 
@@ -313,20 +324,30 @@
                used, and mark it used.
             """
         #XXXX write unit tests
-        if now is None:
-            now = time.time()
+        assert address.isReply
+
+        payloads = mixminion.BuildMessage.encodeMessage(message, 0, "")
+        
         surbLog = self.openSURBLog() # implies lock
+        result = []
         try:
-            surb = surbLog.findUnusedSURB(surbList, verbose=1, now=now)
-            if surb is None:
-                raise UIError("No usable reply blocks found; all were used or expired.")
-
-            LOG.info("Generating packet...")
-            msg = mixminion.BuildMessage.buildReplyMessage(
-                payload, servers, surb, self.prng)
+            surbs = surbLog.findUnusedSURB(surbList, len(payloads), 
+                                           verbose=1, now=startAt)
+            if len(surbs) <= len(payloads):
+                raise UIError("Not enough usable reply blocks found; all were used or expired.")
+            
 
-            surbLog.markSURBUsed(surb)
-            return msg, servers[0]
+            for (surb,payload,(path1,path2)) in zip(surbs,payloads,
+                  directory.generatePaths(len(payloads),pathSpec, address,
+                                          startAt,endAt)):
+                assert path1 and not path2
+                LOG.info("Generating packet...")
+                msg = mixminion.BuildMessage.buildReplyMessage(
+                    payload, path1, surb, self.prng)
+                
+                surbLog.markSURBUsed(surb)
+                result.append( (msg, path1[0]) )
+            
         finally:
             surbLog.close() #implies unlock
 
@@ -365,6 +386,8 @@
 
            If warnIfLost is true, log a warning if we fail to deliver
            the message, and we don't queue it.
+
+           DOCDOC never raises
            """
         #XXXX write unit tests
         timeout = self.config['Network'].get('ConnectionTimeout')
@@ -392,6 +415,7 @@
                                                   timeout)
                 LOG.info("... %s sent", mword)
             except:
+                e = sys.exc_info()[1]
                 if noQueue and warnIfLost:
                     LOG.error("Error with queueing disabled: %s lost", mword)
                 elif lazyQueue:
@@ -401,7 +425,8 @@
                 else:
                     LOG.info("Error while delivering %s; leaving in queue",
                              mword)
-                raise
+
+                LOG.info("Error was: %s",e)
             try:
                 clientLock()
                 for h in handles:
@@ -528,63 +553,6 @@
                     raise UIError("Not writing binary message to terminal: Use -F to do it anyway.")
         return results
 
-def parseAddress(s):
-    """Parse and validate an address; takes a string, and returns an Address
-       object.
-
-       Accepts strings of the format:
-              mbox:<mailboxname>@<server>
-           OR smtp:<email address>
-           OR <email address> (smtp is implicit)
-           OR drop
-           OR 0x<routing type>:<routing info>
-    """
-    # ???? Should this should get refactored into clientmodules, or someplace?
-    if s.lower() == 'drop':
-        return Address(DROP_TYPE, "", None)
-    elif s.lower() == 'test':
-        return Address(0xFFFE, "", None)
-    elif ':' not in s:
-        if isSMTPMailbox(s):
-            return Address(SMTP_TYPE, s, None)
-        else:
-            raise ParseError("Can't parse address %s"%s)
-    tp,val = s.split(':', 1)
-    tp = tp.lower()
-    if tp.startswith("0x"):
-        try:
-            tp = int(tp[2:], 16)
-        except ValueError:
-            raise ParseError("Invalid hexidecimal value %s"%tp)
-        if not (0x0000 <= tp <= 0xFFFF):
-            raise ParseError("Invalid type: 0x%04x"%tp)
-        return Address(tp, val, None)
-    elif tp == 'mbox':
-        if "@" in val:
-            mbox, server = val.split("@",1)
-            return Address(MBOX_TYPE, parseMBOXInfo(mbox).pack(), server)
-        else:
-            return Address(MBOX_TYPE, parseMBOXInfo(val).pack(), None)
-    elif tp == 'smtp':
-        # May raise ParseError
-        return Address(SMTP_TYPE, parseSMTPInfo(val).pack(), None)
-    elif tp == 'test':
-        return Address(0xFFFE, val, None)
-    else:
-        raise ParseError("Unrecognized address type: %s"%s)
-
-class Address:
-    """Represents the target address for a Mixminion message.
-       Consists of the exitType for the final hop, the routingInfo for
-       the last hop, and (optionally) a server to use as the last hop.
-       """
-    def __init__(self, exitType, exitAddress, lastHop=None):
-        self.exitType = exitType
-        self.exitAddress = exitAddress
-        self.lastHop = lastHop
-    def getRouting(self):
-        return self.exitType, self.exitAddress, self.lastHop
-
 def readConfigFile(configFile):
     """Given a configuration file (possibly none) as specified on the command
        line, return a ClientConfig object.
@@ -713,7 +681,7 @@
 
         self.path = None
         self.nHops = None
-        self.address = None
+        self.exitAddress = None
         self.lifetime = None
         self.replyBlockFiles = []
 
@@ -744,10 +712,10 @@
                 self.download = dl
             elif o in ('-t', '--to'):
                 assert wantForwardPath or wantReplyPath
-                if self.address is not None:
+                if self.exitAddress is not None:
                     raise UIError("Multiple addresses specified.")
                 try:
-                    self.address = parseAddress(v)
+                    self.exitAddress = mixminion.ClientDirectory.parseAddress(v)
                 except ParseError, e:
                     raise UsageError(str(e))
             elif o in ('-R', '--reply-block'):
@@ -818,7 +786,7 @@
         if self.wantClientDirectory:
             assert self.wantConfig
             LOG.debug("Configuring server list")
-            self.directory = ClientDirectory(userdir)
+            self.directory = mixminion.ClientDirectory.ClientDirectory(userdir)
 
         if self.wantDownload:
             assert self.wantClientDirectory
@@ -833,22 +801,20 @@
             self.directory.checkClientVersion()
 
     def parsePath(self):
-        """Parse the path specified on the command line and generate a
-           new list of servers to be retrieved by getForwardPath or
-           getReplyPath."""
-        if self.wantReplyPath and self.address is None:
+        # Sets: exitAddress, pathSpec.
+        if self.wantReplyPath and self.exitAddress is None:
             address = self.config['Security'].get('SURBAddress')
             if address is None:
                 raise UIError("No recipient specified; exiting.  (Try "
                               "using -t <your-address>)")
             try:
-                self.address = parseAddress(address)
+                self.exitAddress = mixminion.ClientDirectory.parseAddress(address)
             except ParseError, e:
                 raise UIError("Error in SURBAddress:"+str(e))
-        elif self.address is None and self.replyBlockFiles == []:
+        elif self.exitAddress is None and self.replyBlockFiles == []:
             raise UIError("No recipients specified; exiting. (Try using "
                           "-t <recipient-address>")
-        elif self.address is not None and self.replyBlockFiles:
+        elif self.exitAddress is not None and self.replyBlockFiles:
             raise UIError("Cannot use both a recipient and a reply block")
         elif self.replyBlockFiles:
             useRB = 1
@@ -866,66 +832,44 @@
                         surbs.extend(parseReplyBlocks(s))
                 except ParseError, e:
                         raise UIError("Error parsing %s: %s" % (fn, e))
+            self.surbList = surbs
         else:
-            assert self.address is not None
+            assert self.exitAddress is not None
             useRB = 0
 
+        isSURB = isReply = 0
+        if self.wantReplyPath:
+            p = 'SURBPath'; isSURB = 1
+            defHops = self.config['Security'].get("SURBPathLength", 4)
+        elif useRB:
+            p = 'ReplyPath'; isReply = 1
+            defHops = self.config['Security'].get("PathLength", 6)
+        else:
+            p = 'ForwardPath'
+            defHops = self.config['Security'].get("PathLength", 6)
         if self.path is None:
-            if self.wantReplyPath:
-                p = 'SURBPath'
-            elif useRB:
-                p = 'ReplyPath'
-            else:
-                p = 'ForwardPath'
             self.path = self.config['Security'].get(p, "*")
 
-        if self.wantReplyPath:
+        if isSURB:
             if self.lifetime is not None:
                 duration = self.lifetime * 24*60*60
             else:
                 duration = int(self.config['Security']['SURBLifetime'])
-
-            self.endTime = succeedingMidnight(time.time() + duration)
-
-            defHops = self.config['Security'].get("SURBPathLength", 4)
-            self.path1 = parsePathLeg(self.directory, self.config, self.path,
-                                      self.nHops, self.address,
-                                      startAt=time.time(),
-                                      endAt=self.endTime,
-                                      defaultNHops=defHops)
-            self.path2 = None
-            LOG.info("Selected path is %s",
-                     ",".join([ s.getNickname() for s in self.path1 ]))
-        elif useRB:
-            assert self.wantForwardPath
-            defHops = self.config['Security'].get("PathLength", 6)
-            self.path1 = parsePathLeg(self.directory, self.config, self.path,
-                                      self.nHops, defaultNHops=defHops)
-            self.path2 = surbs
-            self.usingSURBList = 1
-            LOG.info("Selected path is %s:<reply block>",
-                     ",".join([ s.getNickname() for s in self.path1 ]))
         else:
-            assert self.wantForwardPath
-            defHops = self.config['Security'].get("PathLength", 6)
-            self.path1, self.path2 = \
-                        parsePath(self.directory, self.config, self.path,
-                                  self.address, self.nHops,
-                                  defaultNHops=defHops)
-            self.usingSURBList = 0
-            LOG.info("Selected path is %s:%s",
-                     ",".join([ s.getNickname() for s in self.path1 ]),
-                     ",".join([ s.getNickname() for s in self.path2 ]))
+            duration = 24*60*60
 
-    def getForwardPath(self):
-        """Return a 2-tuple of lists of ServerInfo for the most recently
-           parsed forward path."""
-        return self.path1, self.path2
+        self.startAt = time.time()
+        self.endAt = previousMidnight(self.startAt+duration)
 
-    def getReplyPath(self):
-        """Return a list of ServerInfo for the most recently parsed reply
-           block path."""
-        return self.path1
+        self.pathSpec = mixminion.ClientDirectory.parsePath(
+            self.config, self.path, self.nHops, isReply=isReply, isSURB=isSURB,
+            defaultNHops = defHops)
+        self.directory.validatePath2(self.pathSpec, self.exitAddress,
+                                     self.startAt, self.endAt)
+
+    def generatePaths(self, n):
+        return self.directory.generatePaths(n,self.pathSpec,self.exitAddress,
+                                            self.startAt,self.endAt)
 
 _SEND_USAGE = """\
 Usage: %(cmd)s [options] <-t address>|<--to=address>|
@@ -1067,35 +1011,12 @@
 
     parser.init()
     client = parser.client
-
-    #XXXX006 the logic here is wrong for large messages.  Instead of
-    #XXXX006 [parse pathspec, parse address, generate path, read message,
-    #XXXX006 encode message, build packets, send packets], it should be
-    #XXXX006 [parse pathspec, parse address, check pathspec, read message,
-    #XXXX006 encode message, generate paths, build packets, send packets].
     parser.parsePath()
-
-    path1, path2 = parser.getForwardPath()
-    address = parser.address
-
-    #XXXX006 remove these ad hoc checks
-    if not parser.usingSURBList and len(headerStr) > 2:
-        sware = path2[-1]['Server'].get('Software', "")
-        if sware.startswith("Mixminion 0.0.4") or sware.startswith("Mixminion 0.0.5alpha1"):
-            LOG.warn("Exit server %s is running old software that may not support headers correctly.", path2[-1].getNickname())
-    elif not parser.usingSURBList and h_from:
-        sware = path2[-1]['Server'].get('Software', "")
-        if sware != 'Mixminion 0.0.5':
-            bad = 0
-            if address.getRouting()[0] == SMTP_TYPE and not path2[-1]['Delivery/SMTP'].get("Allow-From"):
-                bad = 1
-            elif address.getRouting()[0] == MBOX_TYPE and not path2[-1]['Delivery/MBOX'].get("Allow-From"):
-                bad = 1
-            if bad:
-                LOG.warn("Exit server %s does not support user-supplied From addresses", path2[-1].getNickname())
+    address = parser.exitAddress
+    address.setHeaders(parseMessageAndHeaders(headerStr+"\n")[1])
 
     # Get our surb, if any.
-    if parser.usingSURBList and inFile in ('-', None):
+    if address.isReply and inFile in ('-', None):
         # We check to make sure that we have a valid SURB before reading
         # from stdin.
         surblog = client.openSURBLog()
@@ -1106,6 +1027,7 @@
         finally:
             surblog.close()
 
+    # Read the message.
     # XXXX Clean up this ugly control structure.
     if address and inFile is None and address.getRouting()[0] == DROP_TYPE:
         message = None
@@ -1128,44 +1050,22 @@
             print "Interrupted.  Message not sent."
             sys.exit(1)
 
-    message = "%s%s" % (headerStr, message)
+        message = "%s%s" % (headerStr, message)
 
-    if parser.usingSURBList:
-        assert isinstance(path2, ListType)
-        client.sendReplyMessage(message, path1, path2,
-                                forceQueue, forceNoQueue)
+        address.setExitSize(len(message))
+
+
+    if parser.exitAddress.isReply:
+        client.sendReplyMessage(
+            parser.directory, parser.exitAddress, parser.pathSpec,
+            parser.surbList, message, 
+            parser.startAt, parser.endAt, forceQueue, forceNoQueue)
     else:
-        # If our message is too large for the exit node to reconstruct,
-        # either choose a new exit node (for SMTP) or bail (for MBOX or
-        # other).
-        #
-        #XXXX006 This logic is wrong; when we refactor paths again, it'll
-        #XXXX006 have to be fixed.  
-        if message:
-            msgLen = len(message)
-            if address.exitType == SMTP_TYPE:
-                maxLen = path2[-1]["Delivery/SMTP"]["Maximum-Size"] * 1024
-                if msgLen > maxLen:
-                    LOG.warn("Message is too long for server %s--looking for another..."
-                             % path2[-1].getNickname())
-                    LOG.warn("(This behavior is a hack, and will go away in 0.0.6.)")
-                    server = parser.directory.findByExitTypeAndSize(SMTP_TYPE, msgLen, 1)
-                    if not server:
-                        raise UIError("No such server found")
-                    LOG.warn("Replacing %s with %s",
-                             path2[-1].getNickname(),
-                             server.getNickname())
-                    path2[-1] = server
-            elif address.exitType == MBOX_TYPE:
-                maxLen = path2[-1]["Delivery/MBOX"]["Maximum-Size"] * 1024
-                if msgLen > maxLen:
-                    raise UIError("Message is too long for MBOX server %s, and client-side reconstruction is not yet supported"
-                                  % path2[-1].getNickname())
-            elif msgLen > 32*1024:
-                LOG.warn("Delivering long message via unrecognized delivery type")
-        
-        client.sendForwardMessage(address, message, path1, path2,
-                                  forceQueue, forceNoQueue)
+        client.sendForwardMessage(
+            parser.directory, parser.exitAddress, parser.pathSpec,
+            message, parser.startAt, parser.endAt, forceQueue, forceNoQueue)
+            
+            
 
 _PING_USAGE = """\
 Usage: mixminion ping [options] serverName
@@ -1495,9 +1395,6 @@
 
     parser.parsePath()
 
-    path1 = parser.getReplyPath()
-    address = parser.address
-
     if outputFile == '-':
         out = sys.stdout
     elif binary:
@@ -1505,16 +1402,15 @@
     else:
         out = open(outputFile, 'w')
 
-    for i in xrange(count):
-        surb = client.generateReplyBlock(address, path1, name=identity,
-                                         expiryTime=parser.endTime)
+    for path1,path2 in parser.generatePaths(count):
+        assert path2 and not path1
+        surb = client.generateReplyBlock(parser.exitAddress, path2, 
+                                         name=identity,
+                                         expiryTime=parser.endAt)
         if binary:
             out.write(surb.pack())
         else:
             out.write(surb.packAsText())
-        if i != count-1:
-            parser.parsePath()
-            path1 = parser.getReplyPath()
 
     out.close()
 

Index: ClientUtils.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientUtils.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- ClientUtils.py	28 Sep 2003 05:27:56 -0000	1.2
+++ ClientUtils.py	9 Oct 2003 15:26:16 -0000	1.3
@@ -234,14 +234,14 @@
             self.clean()
         self.sync()
 
-    def findUnusedSURB(self, surbList, verbose=0, now=None):
+    def findUnusedSURBs(self, surbList, nSURBs=1, verbose=0, now=None):
         """Given a list of ReplyBlock objects, find the first that is neither
            expired, about to expire, or used in the past.  Return None if
-           no such reply block exists."""
+           no such reply block exists. DOCDOC returns list, nSurbs"""
         if now is None:
             now = time.time()
         nUsed = nExpired = nShortlived = 0
-        result = None
+        result = []
         for surb in surbList: 
             expiry = surb.timestamp
             timeLeft = expiry - now
@@ -252,8 +252,9 @@
             elif timeLeft < 3*60*60:
                 nShortlived += 1
             else:
-                result = surb
-                break
+                result.append(surb)
+                if len(result) >= nSURBs:
+                    break
 
         if verbose:
             if nUsed:

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.59
retrieving revision 1.60
diff -u -d -r1.59 -r1.60
--- Config.py	6 Oct 2003 20:53:02 -0000	1.59
+++ Config.py	9 Oct 2003 15:26:16 -0000	1.60
@@ -67,7 +67,7 @@
 import mixminion.Crypto
 
 from mixminion.Common import MixError, LOG, ceilDiv, englishSequence, \
-   isPrintingAscii, stripSpace, stringContains
+   isPrintingAscii, stripSpace, stringContains, UIError
 
 class ConfigError(MixError):
     """Thrown when an error is found in a configuration file."""

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.60
retrieving revision 1.61
diff -u -d -r1.60 -r1.61
--- Packet.py	31 Aug 2003 19:29:29 -0000	1.60
+++ Packet.py	9 Oct 2003 15:26:16 -0000	1.61
@@ -12,14 +12,14 @@
 __all__ = [ 'compressData', 'CompressedDataTooLong', 'DROP_TYPE',
             'ENC_FWD_OVERHEAD', 'ENC_SUBHEADER_LEN',
             'encodeMailHeaders', 'encodeMessageHeaders',
-            'FRAGMENT_PAYLOAD_OVERHEAD', 'FWD_TYPE', 'FragmentPayload',
+            'FRAGMENT_PAYLOAD_OVERHEAD', 'FWD_IPV4_TYPE', 'FragmentPayload',
             'FRAGMENT_MESSAGEID_LEN', 'FRAGMENT_TYPE', 
             'HEADER_LEN', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
             'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE',
             'MIN_SUBHEADER_LEN', 'Packet',
             'OAEP_OVERHEAD', 'PAYLOAD_LEN', 'ParseError', 'ReplyBlock',
             'ReplyBlock', 'SECRET_LEN', 'SINGLETON_PAYLOAD_OVERHEAD',
-            'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_TYPE', 'SingletonPayload',
+            'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_IPV4_TYPE', 'SingletonPayload',
             'Subheader', 'TAG_LEN', 'TextEncodedMessage',
             'parseHeader', 'parseIPV4Info',
             'parseMBOXInfo', 'parsePacket', 'parseMessageAndHeaders',
@@ -75,9 +75,11 @@
 #----------------------------------------------------------------------
 # Values for the 'Routing type' subheader field
 # Mixminion types
-DROP_TYPE      = 0x0000  # Drop the current message
-FWD_TYPE       = 0x0001  # Forward the msg to an IPV4 addr via MMTP
-SWAP_FWD_TYPE  = 0x0002  # SWAP, then forward the msg to an IPV4 addr via MMTP
+DROP_TYPE          = 0x0000     # Drop the current message
+FWD_IPV4_TYPE      = 0x0001 # Forward the msg to an IPV4 addr via MMTP
+SWAP_FWD_IPV4_TYPE = 0x0002 # SWAP, then FWD_IPV4
+FWD_HOST_TYPE      = 0x0003 # Forward the msg to a hostname, via MMTP.
+SWAP_FWD_HOST_TYPE = 0x0004 # SWAP, then FWD_HOST
 
 # Exit types
 MIN_EXIT_TYPE  = 0x0100  # The numerically first exit type.
@@ -93,6 +95,9 @@
 # XXXX006 needless tag field to every fragment routing info.  
 _TYPES_WITHOUT_TAGS = { FRAGMENT_TYPE : 1 }
 
+def typeIsSwap(tp):
+    return tp in (SWAP_FWD_IPV4_TYPE,SWAP_FWD_HOST_TYPE)
+
 class ParseError(MixError):
     """Thrown when a message or portion thereof is incorrectly formatted."""
     pass
@@ -525,7 +530,7 @@
     def format(self):
         hash = binascii.b2a_hex(sha1(self.pack()))
         expiry = formatTime(self.timestamp)
-        if self.routingType == SWAP_FWD_TYPE:
+        if self.routingType == SWAP_FWD_IPV4_TYPE:
             server = parseIPV4Info(self.routingInfo).format()
         else:
             server = "????"
@@ -598,6 +603,52 @@
         r = cmp(type(self), type(other))
         if r: return r
         r = cmp(self.ip, other.ip)
+        if r: return r
+        r = cmp(self.port, other.port)
+        if r: return r
+        return cmp(self.keyinfo, other.keyinfo)
+
+MMTP_HOST_PAT = "!H%ds" % DIGEST_LEN
+
+def parseMMTPHostInfo(s):
+    """DOCDOC"""
+    if len(s) < 2+DIGEST_LEN+1:
+        raise ParseError("Routing information is too short.")
+    try:
+        port, keyinfo = struct.unpack(MMTP_HOST_PAT, s[:2+DIGEST_LEN])
+    except struct.error:
+        raise ParseError("Misformatted routing info")
+    return MMTPHostInfo(s[2+DIGEST_LEN:], port, keyinfo)
+
+class MMTPHostInfo:
+    """DOCDOC"""
+    def __init__(self, hostname, port, keyinfo):
+        """Construct a new IPV4Info"""
+        assert 0 <= port <= 65535
+        self.hostname = hostname
+        self.port = port
+        self.keyinfo = keyinfo
+
+    def format(self):
+        return "%s:%s (keyid=%s)"%(self.hostname, self.port,
+                                   binascii.b2a_hex(self.keyinfo))
+
+    def pack(self):
+        """Return the routing info for this address"""
+        assert len(self.keyinfo) == DIGEST_LEN
+        return struct.pack(MMTP_HOST_PAT,self.port,self.keyinfo)+self.hostname
+
+    def __repr__(self):
+        return "MMTPHostInfo(%r, %r, %r)"%(
+            self.hostname,self.port,self.keyinfo)
+
+    def __hash__(self):
+        return hash(self.pack())
+
+    def __cmp__(self, other):
+        r = cmp(type(self), type(other))
+        if r: return r
+        r = cmp(self.hostname, other.hostname)
         if r: return r
         r = cmp(self.port, other.port)
         if r: return r

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.56
retrieving revision 1.57
diff -u -d -r1.56 -r1.57
--- ServerInfo.py	6 Oct 2003 20:55:06 -0000	1.56
+++ ServerInfo.py	9 Oct 2003 15:26:16 -0000	1.57
@@ -15,6 +15,7 @@
 
 import mixminion.Config
 import mixminion.Crypto
+import mixminion.Packet
 
 from mixminion.Common import IntervalSet, LOG, MixError, createPrivateDir, \
     formatBase64, formatDate, formatTime, readPossiblyGzippedFile
@@ -266,10 +267,34 @@
     def getIdentity(self):
         return self['Server']['Identity']
 
-    def canDeliverTo(self, otherDesc):
-        #DOCDOC
-        return 1
+    def canRelayTo(self, otherDesc):
+        """DOCDOC"""
+        if self.hasSameNicknameAs(otherDesc):
+            return 1
+        myOut = self['Outgoing/MMTP']
+        if not myOut.get("Version"):
+            return 0
+        otherIn = otherDesc['Incoming/MMTP']
+        if not otherIn.get("Version"):
+            return 0
+        myOutProtocols = [ s.strip() for s in ",".split(myOut["Protocols"]) ]
+        otherInProtocols = [s.strip() for s in ",".split(otherIn["Protocols"])]
+        for out in myOutProtocols:
+            if out in otherInProtocols:
+                return 1
+        return 0
 
+    def getRoutingFor(self, otherDesc, swap=0):
+        """DOCDOC"""
+        #XXXX006 use this
+        assert self.canRelayTo(otherDesc)
+        if swap:
+            rt = mixminion.Packet.SWAP_FWD_IPV4_TYPE
+        else:
+            rt = mixminion.Packet.FWD_IPV4_TYPE
+        ri = other.getRoutingInfo().pack()
+        return rt, ri
+        
     def getCaps(self):
         # FFFF refactor this once we have client addresses.
         caps = []
@@ -285,6 +310,14 @@
         if self['Delivery/Fragmented'].get('Version'):
             caps.append('frag')
         return caps
+
+    def isSameDescriptorAs(self, other):
+        """DOCDOC"""
+        return self.getDigest() == other.getDigest()
+
+    def hasSameNicknameAs(self, other):
+        """DOCDOC"""
+        return self.getNickname().lower() == other.getNickname().lower()
 
     def isValidated(self):
         """Return true iff this ServerInfo has been validated"""

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.156
retrieving revision 1.157
diff -u -d -r1.156 -r1.157
--- test.py	7 Oct 2003 21:57:46 -0000	1.156
+++ test.py	9 Oct 2003 15:26:16 -0000	1.157
@@ -1754,7 +1754,7 @@
         self.do_header_test(head,
                             (self.pk1, self.pk2),
                             ["9"*16, "1"*16],
-                            (FWD_TYPE, 99),
+                            (FWD_IPV4_TYPE, 99),
                             (ipv4("127.0.0.2",3,"Z"*20).pack(),
                              "Hi mom"))
 
@@ -1765,7 +1765,7 @@
         head = bhead([self.server1, self.server2, self.server3], secrets,
                       99, "Hi mom", AESCounterPRNG())
         pks = (self.pk1,self.pk2,self.pk3)
-        rtypes = (FWD_TYPE, FWD_TYPE, 99)
+        rtypes = (FWD_IPV4_TYPE, FWD_IPV4_TYPE, 99)
         rinfo = (mixminion.Packet.IPV4Info("127.0.0.2", 3, "Z"*20).pack(),
                  mixminion.Packet.IPV4Info("127.0.0.3", 5, "Q"*20).pack(),
                  "Hi mom")
@@ -1880,7 +1880,7 @@
                      longStr,
                      AESCounterPRNG())
         pks = (self.pk2,self.pk1)
-        rtypes = (FWD_TYPE,99)
+        rtypes = (FWD_IPV4_TYPE,99)
         rinfo = (tag+longStr2,longStr)
         self.do_header_test(head, pks, secrets, rtypes, rinfo)
 
@@ -2009,11 +2009,11 @@
 
         self.do_message_test(m,
                              ( (self.pk1, self.pk2), None,
-                               (FWD_TYPE, SWAP_FWD_TYPE),
+                               (FWD_IPV4_TYPE, SWAP_FWD_IPV4_TYPE),
                                (self.server2.getRoutingInfo().pack(),
                                 self.server3.getRoutingInfo().pack()) ),
                              ( (self.pk3, self.pk2), None,
-                               (FWD_TYPE, 500),
+                               (FWD_IPV4_TYPE, 500),
                                (self.server2.getRoutingInfo().pack(),
                                 "Goodbye") ),
                              "Hello!!!!")
@@ -2027,7 +2027,7 @@
 
         self.do_message_test(m,
                              ( (self.pk1,), None,
-                               (SWAP_FWD_TYPE,),
+                               (SWAP_FWD_IPV4_TYPE,),
                                (self.server3.getRoutingInfo().pack(),) ),
                              ( (self.pk3,), None,
                                (500,),
@@ -2045,7 +2045,7 @@
 
         self.do_message_test(m,
                              ( (self.pk1,), None,
-                               (SWAP_FWD_TYPE,),
+                               (SWAP_FWD_IPV4_TYPE,),
                                (self.server3.getRoutingInfo().pack(),) ),
                              ( (self.pk3,), None,
                                (DROP_TYPE,),
@@ -2068,11 +2068,11 @@
                 return payload.getUncompressedContents()
             self.do_message_test(m,
                                  ( (self.pk1, self.pk2), None,
-                                   (FWD_TYPE, SWAP_FWD_TYPE),
+                                   (FWD_IPV4_TYPE, SWAP_FWD_IPV4_TYPE),
                                    (self.server2.getRoutingInfo().pack(),
                                     self.server3.getRoutingInfo().pack()) ),
                                  ( (self.pk3, self.pk2), None,
-                                   (FWD_TYPE, 500),
+                                   (FWD_IPV4_TYPE, 500),
                                    (self.server2.getRoutingInfo().pack(),
                                     "Phello") ),
                                  payload,
@@ -2136,11 +2136,11 @@
 
         self.do_message_test(m,
                              ((self.pk3, self.pk1), None,
-                              (FWD_TYPE,SWAP_FWD_TYPE),
+                              (FWD_IPV4_TYPE,SWAP_FWD_IPV4_TYPE),
                               (self.server1.getRoutingInfo().pack(),
                                self.server3.getRoutingInfo().pack())),
                              (pks_1, hsecrets,
-                              (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,SMTP_TYPE),
+                              (FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,SMTP_TYPE),
                               infos+("no-such-user@invalid",)),
                              "Information???",
                              decoder=decoder)
@@ -2150,14 +2150,14 @@
                      "fred", "Tyrone Slothrop", 3)
 
         sec,(loc,), _ = self.do_header_test(reply.header, pks_1, None,
-                            (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
+                            (FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,MBOX_TYPE),
                             infos+(None,))
 
         self.assertEquals(loc[20:], "fred")
 
         # (Test reply block formats)
         self.assertEquals(reply.timestamp, 3)
-        self.assertEquals(reply.routingType, SWAP_FWD_TYPE)
+        self.assertEquals(reply.routingType, SWAP_FWD_IPV4_TYPE)
         self.assertEquals(reply.routingInfo,
                           self.server3.getRoutingInfo().pack())
         self.assertEquals(reply.pack(),
@@ -2214,11 +2214,11 @@
         
         self.do_message_test(m,
                              ((self.pk3, self.pk1), None,
-                              (FWD_TYPE,SWAP_FWD_TYPE),
+                              (FWD_IPV4_TYPE,SWAP_FWD_IPV4_TYPE),
                               (self.server1.getRoutingInfo().pack(),
                                self.server3.getRoutingInfo().pack())),
                              (pks_1, None,
-                              (FWD_TYPE,FWD_TYPE,FWD_TYPE,FWD_TYPE,MBOX_TYPE),
+                              (FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,MBOX_TYPE),
                               infos+("fred",)),
                              payload,
                              decoder=decoder2)
@@ -2430,7 +2430,7 @@
             res = sp.processMessage(m)
             self.assert_(isinstance(res, DeliveryPacket) or
                          isinstance(res, RelayedPacket))
-            if rt in (FWD_TYPE, SWAP_FWD_TYPE):
+            if rt in (FWD_IPV4_TYPE, SWAP_FWD_IPV4_TYPE):
                 self.assert_(not res.isDelivery())
                 self.assertEquals(res.getAddress().pack(), ri)
                 m = res.getPacket()
@@ -2454,7 +2454,7 @@
 
         self.do_test_chain(m,
                            [self.sp1,self.sp2,self.sp3],
-                           [FWD_TYPE, FWD_TYPE, SMTP_TYPE],
+                           [FWD_IPV4_TYPE, FWD_IPV4_TYPE, SMTP_TYPE],
                            [self.server2.getRoutingInfo().pack(),
                             self.server3.getRoutingInfo().pack(),
                             "nobody@invalid"],
@@ -2466,7 +2466,7 @@
 
         self.do_test_chain(m,
                            [self.sp1,self.sp3],
-                           [FWD_TYPE, SMTP_TYPE],
+                           [FWD_IPV4_TYPE, SMTP_TYPE],
                            [self.server3.getRoutingInfo().pack(),
                             "nobody@invalid"],
                            p)
@@ -2474,7 +2474,7 @@
         # Try servers with multiple keys
         m = bfm("\n"+p,
                 SMTP_TYPE, "nobody@invalid", [self.server2], [self.server3])
-        self.do_test_chain(m, [self.sp2_3, self.sp2_3], [FWD_TYPE, SMTP_TYPE],
+        self.do_test_chain(m, [self.sp2_3, self.sp2_3], [FWD_IPV4_TYPE, SMTP_TYPE],
                            [self.server3.getRoutingInfo().pack(),
                             "nobody@invalid"], p)
 
@@ -2488,8 +2488,8 @@
             self.do_test_chain(m,
                                [self.sp1,self.sp2,self.sp1,
                                 self.sp3,self.sp1,self.sp2],
-                               [FWD_TYPE,FWD_TYPE,FWD_TYPE,
-                                FWD_TYPE,FWD_TYPE,SMTP_TYPE],
+                               [FWD_IPV4_TYPE,FWD_IPV4_TYPE,FWD_IPV4_TYPE,
+                                FWD_IPV4_TYPE,FWD_IPV4_TYPE,SMTP_TYPE],
                                [self.server2.getRoutingInfo().pack(),
                                 self.server1.getRoutingInfo().pack(),
                                 self.server3.getRoutingInfo().pack(),
@@ -2510,7 +2510,7 @@
 
         pkt = self.do_test_chain(m,
                                  [self.sp1,self.sp3],
-                                 [FWD_TYPE, SMTP_TYPE],
+                                 [FWD_IPV4_TYPE, SMTP_TYPE],
                                  [self.server3.getRoutingInfo().pack(),
                                   "nobody@invalid"],
                                  p)
@@ -2534,7 +2534,7 @@
                 [self.server1], [self.server3])
         pkt = self.do_test_chain(m,
                                  [self.sp1,self.sp3],
-                                 [FWD_TYPE, SMTP_TYPE],
+                                 [FWD_IPV4_TYPE, SMTP_TYPE],
                                  [self.server3.getRoutingInfo().pack(),
                                   "nobody@invalid"],
                                  pbin)
@@ -2550,7 +2550,7 @@
                 [self.server1], [self.server3])
         pkt = self.do_test_chain(m,
                                  [self.sp1,self.sp3],
-                                 [FWD_TYPE, SMTP_TYPE],
+                                 [FWD_IPV4_TYPE, SMTP_TYPE],
                                  [self.server3.getRoutingInfo().pack(),
                                   "nobody@invalid"],
                                  "")
@@ -2564,7 +2564,7 @@
                  [self.server3], getRSAKey(0,1024))
         pkt = self.do_test_chain(m,
                                  [self.sp1,self.sp3],
-                                 [FWD_TYPE, SMTP_TYPE],
+                                 [FWD_IPV4_TYPE, SMTP_TYPE],
                                  [self.server3.getRoutingInfo().pack(),
                                   "nobody@invalid"],
                                  "")
@@ -2582,7 +2582,7 @@
         m = bfm(p, SMTP_TYPE, "nobody@invalid",[self.server1], [self.server3])
         pkt = self.do_test_chain(m, 
                                  [self.sp1, self.sp3],
-                                 [FWD_TYPE, SMTP_TYPE],
+                                 [FWD_IPV4_TYPE, SMTP_TYPE],
                                  [self.server3.getRoutingInfo().pack(),
                                   "nobody@invalid"],
                                  "")
@@ -2597,7 +2597,7 @@
         m = bfm(p, SMTP_TYPE, "nobody@invalid",[self.server1], [self.server3])
         pkt = self.do_test_chain(m, 
                                  [self.sp1, self.sp3],
-                                 [FWD_TYPE, SMTP_TYPE],
+                                 [FWD_IPV4_TYPE, SMTP_TYPE],
                                  [self.server3.getRoutingInfo().pack(),
                                   "nobody@invalid"],
                                  "")
@@ -2674,16 +2674,21 @@
             # (We temporarily override the setting from 'BuildMessage',
             #  not Packet; BuildMessage has already imported a copy of this
             #  constant.)
-            save = mixminion.BuildMessage.SWAP_FWD_TYPE
-            mixminion.BuildMessage.SWAP_FWD_TYPE = 50
+            save = mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE
+            mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE = 50
             m_x = bfm("Z", 500, "", [self.server1], [self.server2])
         finally:
-            mixminion.BuildMessage.SWAP_FWD_TYPE = save
+            mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE = save
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
 
-        # Subhead we can't parse
+        # Subhead with bad length
         m_x = pk_encrypt("foo", self.pk1)+m[256:]
-        self.failUnlessRaises(ParseError, self.sp1.processMessage, m_x)
+        self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
+
+        # Subhead we can't parse.
+        m_x = pk_encrypt("f"*(256-42), self.pk1)+m[256:]
+        self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
+
 
         # Bad IPV4 info
         subh_real = pk_decrypt(m[:256], self.pk1)
@@ -6046,26 +6051,25 @@
         try:
             ### Try out getPath.
             # 1. Fully-specified paths.
-            p = ks.getPath(None, ['Joe', 'Lisa', 'Alice', 'Joe'])
+            p = ks.getPath(['Joe', 'Lisa', 'Alice', 'Joe'])
 
             # 2. Partly-specified paths...
             # 2a. With plenty of servers
-            p = ks.getPath(None, [None, None])
+            p = ks.getPath([None, None])
             eq(2, len(p))
             neq(p[0].getNickname(), p[1].getNickname())
 
-            p = ks.getPath(None, ["Joe", None, None])
+            p = ks.getPath(["Joe", None, None])
             eq(3, len(p))
             self.assertSameSD(p[0], joe[0])
             neq(p[1].getNickname(), "Joe")
-            neq(p[2].getNickname(), "Joe")
             neq(p[1].getNickname(), p[2].getNickname())
 
-            p = ks.getPath(None, [None, None, "Joe"])
+            p = ks.getPath([None, None, "Joe"])
             eq(3, len(p))
             self.assertSameSD(joe[0], p[2])
 
-            p = ks.getPath(None, ["Alice", None, None, "Joe"])
+            p = ks.getPath(["Alice", None, None, "Joe"])
             eq(4, len(p))
             self.assertSameSD(alice[0], p[0])
             self.assertSameSD(joe[0], p[3])
@@ -6074,7 +6078,7 @@
             self.assert_(nicks.count("Joe")>=1)
             neq(nicks[1],nicks[2])
 
-            p = ks.getPath(None, ["Joe", None, "Alice", "Joe"])
+            p = ks.getPath(["Joe", None, "Alice", "Joe"])
             eq(4, len(p))
             self.assertSameSD(alice[0], p[2])
             self.assertSameSD(joe[0], p[0])
@@ -6088,22 +6092,22 @@
             ks2.importFromFile(os.path.join(impdirname, "Lisa1"))
             ks2.importFromFile(os.path.join(impdirname, "Bob0"))
 
-            p = ks2.getPath(None, [None]*9)
+            p = ks2.getPath([None]*9)
             eq(9, len(p))
             self.failIf(nRuns([s.getNickname() for s in p]))
 
-            p = ks2.getPath(None, ["Joe"]+[None]*6+["Joe"])
+            p = ks2.getPath(["Joe"]+[None]*6+["Joe"])
             self.failIf(nRuns([s.getNickname() for s in p]))
             eq(8, len(p))
             self.assertSameSD(joe[0], p[0])
             self.assertSameSD(joe[0], p[-1])
 
-            p = ks2.getPath(None, ["Joe"]+[None]*6)
+            p = ks2.getPath(["Joe"]+[None]*6)
             self.failIf(nRuns([s.getNickname() for s in p]))
             eq(7, len(p))
             self.assertSameSD(joe[0], p[0])
 
-            p = ks2.getPath(None, [None]*6+["Joe"])
+            p = ks2.getPath([None]*6+["Joe"])
             self.failIf(nRuns([s.getNickname() for s in p]))
             eq(7, len(p))
             self.assertSameSD(joe[0], p[-1])
@@ -6111,56 +6115,49 @@
             # 2c. With 2 servers
             ks2.expungeByNickname("Alice")
             ks2.expungeByNickname("Bob")
-            p = ks2.getPath(None, [None]*4)
+            p = ks2.getPath([None]*4)
             self.failIf(nRuns([s.getNickname() for s in p]) > 1)
 
-            p = ks2.getPath(None, ["Joe",None,None,None])
+            p = ks2.getPath(["Joe",None,None,None])
 
             self.failIf(nRuns([s.getNickname() for s in p]) > 2)
-            p = ks2.getPath(None, [None, None, None, "Joe"])
+            p = ks2.getPath([None, None, None, "Joe"])
             self.failIf(nRuns([s.getNickname() for s in p]) > 1)
 
-            p = ks2.getPath(None, [None,None,None,None,None, "Joe"])
+            p = ks2.getPath([None,None,None,None,None, "Joe"])
             self.failIf(nRuns([s.getNickname() for s in p]) > 1)
 
             # 2d. With only 1.
             ks2.expungeByNickname("Lisa")
-            p = ks2.getPath(None,[None]*4)
+            p = ks2.getPath([None]*4)
             eq(len(p), 4)
-            p = ks2.getPath(None,["Joe",None,None,None])
+            p = ks2.getPath(["Joe",None,None,None])
             eq(len(p), 4)
-            p = ks2.getPath(None,[None,None,None,"Joe"])
+            p = ks2.getPath([None,None,None,"Joe"])
             eq(len(p), 4)
 
             # 2e. With 0
             self.assertRaises(MixError, ks.getPath,
-                              None, [None]*4, startAt=now+100*oneDay)
+                              [None]*4, startAt=now+100*oneDay)
         finally:
             s = resumeLog()
         self.assertEquals(4, s.count("to avoid same-server hops"))
         self.assertEquals(3, s.count("Only one relay known"))
 
-        # 3. With capabilities.
-        p = ks.getPath("smtp", [None]*5)
-        eq(5, len(p))
-        self.assertSameSD(p[-1], joe[0]) # Only Joe has SMTP
-
-        p = ks.getPath("mbox", [None]*4)
-        eq(4, len(p))
-        self.assertSameSD(p[-1], lola[1]) # Only Lola has MBOX
-
-        p = ks.getPath("mbox", ["Alice", None, None, None, None])
-        eq(5, len(p))
-        self.assertSameSD(p[-1], lola[1]) # Only Lola has MBOX
-        self.assertSameSD(p[0], alice[0])
-
-        p = ks.getPath("mbox", [None,None,None,None, "Alice"])
-        eq(5, len(p))
-        self.assertSameSD(p[-1], alice[0]) # We ignore endCap with endServers
-
-        ### Now try parsePath.  This should exercise resolvePath as well.
-        ppath = mixminion.ClientDirectory.parsePath
-        paddr = mixminion.ClientMain.parseAddress
+        # wrap path parsing and verification and generation.
+        def ppath(dir, cfg, path, addr, nHops=None, startAt=None, endAt=None,
+                  halfPath=0, defaultNHops=None):
+            isReply = halfPath and (addr is None)
+            isSURB = halfPath and (addr is not None)
+            pathSpec = mixminion.ClientDirectory.parsePath(
+                cfg, path, nHops=nHops, isReply=isReply,
+                isSURB=isSURB, defaultNHops=defaultNHops)
+            dir.validatePath(pathSpec, addr, startAt=startAt, endAt=endAt)
+            paths = dir.generatePaths(1, pathSpec, addr, startAt,endAt)
+            assert len(paths) == 1
+            return paths[0]
+        
+        paddr = mixminion.ClientDirectory.parseAddress
         email = paddr("smtp:lloyd@dobler.com")
         mboxWithServer = paddr("mbox:Granola@Lola")
         mboxWithoutServer = paddr("mbox:Granola")
@@ -6369,7 +6366,7 @@
     def testAddress(self):
         def parseEq(s, tp, addr, server, eq=self.assertEquals):
             "Helper: return true iff parseAddress(s).getRouting() == t,s,a."
-            t, a, s = mixminion.ClientMain.parseAddress(s).getRouting()
+            t, a, s = mixminion.ClientDirectory.parseAddress(s).getRouting()
             eq(t, tp)
             eq(s, server)
             eq(a, addr)
@@ -6391,7 +6388,7 @@
         parseEq("0x999:", 0x999, "", None)
 
         def parseFails(s, f=self.failUnlessRaises):
-            f(ParseError, mixminion.ClientMain.parseAddress, s)
+            f(ParseError, mixminion.ClientDirectory.parseAddress, s)
 
         # Check failing cases
         parseFails("sxtp:foo@bar.com") # unknown module
@@ -6437,11 +6434,16 @@
             s = SURBLog(fname)
             self.assert_(s.isSURBUsed(surbs[0]))
             self.assert_(not s.isSURBUsed(surbs[1]))
-            self.assert_(s.findUnusedSURB(surbs) is surbs[1])
+            self.assert_(s.findUnusedSURBs(surbs)[0] is surbs[1])
+            one = s.findUnusedSURBs(surbs,1)
+            self.assertEquals(len(one),1)
+            two = s.findUnusedSURBs(surbs,2)
+            self.assert_(two[0] is surbs[1])
+            self.assert_(two[1] is surbs[2])
             s.markSURBUsed(surbs[1])
-            self.assert_(s.findUnusedSURB(surbs) is surbs[2])
+            self.assert_(s.findUnusedSURBs(surbs)[0] is surbs[2])
             s.markSURBUsed(surbs[2])
-            self.assert_(s.findUnusedSURB(surbs) is None)
+            self.assert_(s.findUnusedSURBs(surbs) == [])
         finally:
             s.close()
 
@@ -6478,22 +6480,39 @@
 
     def testMixminionClient(self):
         # Create and configure a MixminionClient object...
-        parseAddress = mixminion.ClientMain.parseAddress
+        parseAddress = mixminion.ClientDirectory.parseAddress
+        parsePath = mixminion.ClientDirectory.parsePath
         userdir = mix_mktemp()
         usercfgstr = "[User]\nUserDir: %s\n[DirectoryServers]\n"%userdir
         usercfg = mixminion.Config.ClientConfig(string=usercfgstr)
         client = mixminion.ClientMain.MixminionClient(usercfg)
 
+        # Create a directory...
+        dirname = mix_mktemp()
+        directory = mixminion.ClientDirectory.ClientDirectory(dirname)
+        
+        edesc = getExampleServerDescriptors()
+        fname = mix_mktemp()
+        for server, descriptors in edesc.items():
+            for d in descriptors:
+                writeFile(fname, d)
+                try:
+                    directory.importFromFile(fname)
+                except UIError:
+                    pass
+
         # Now try with some servers...
         edesc = getExampleServerDescriptors()
         ServerInfo = mixminion.ServerInfo.ServerInfo
         Lola = ServerInfo(string=edesc["Lola"][1], assumeValid=1)
         Joe = ServerInfo(string=edesc["Joe"][0], assumeValid=1)
         Alice = ServerInfo(string=edesc["Alice"][1], assumeValid=1)
-
+        
         # ... and for now, we need to restart the client.
         client = mixminion.ClientMain.MixminionClient(usercfg)
 
+        pathSpec1 = parsePath(usercfg, "lola,joe:alice,joe")
+
         ##  Test generateForwardMessage.
         # We replace 'buildForwardMessage' to make this easier to test.
         replaceFunction(mixminion.BuildMessage, "buildForwardMessage",
@@ -6504,14 +6523,17 @@
             # First, two forward messages that end with 'joe' and go via
             # SMTP
             payload = "Hey Joe, where you goin' with that gun in your hand?"
-            client.generateForwardMessage(
+            client.generateForwardPayloads(
+                directory,
                 parseAddress("joe@cledonism.net"),
-                payload,
-                servers1=[Lola, Joe], servers2=[Alice, Joe])
-            client.generateForwardMessage(
+                pathSpec1,
+                payload, time.time(), time.time()+200)
+            client.generateForwardPayloads(
+                directory,
                 parseAddress("smtp:joe@cledonism.net"),
+                pathSpec1,
                 "Hey Joe, where you goin' with that gun in your hand?",
-                servers1=[Lola, Joe], servers2=[Alice, Joe])
+                time.time(), time.time()+200)
 
             for fn, args, kwargs in getCalls():
                 self.assertEquals(fn, "buildForwardMessage")
@@ -6524,15 +6546,18 @@
 
             # Now try an mbox message, with an explicit last hop.
             payload = "Hey, Lo', where you goin' with that pun in your hand?"
-            client.generateForwardMessage(
+            client.generateForwardPayloads(
+                directory,
                 parseAddress("mbox:granola"),
-                payload,
-                servers1=[Lola, Joe], servers2=[Alice, Lola])
+                parsePath(usercfg, "lola,joe:alice,lola"),
+                payload, time.time(), time.time()+200)
             # And an mbox message with a last hop implicit in the address
-            client.generateForwardMessage(
+            client.generateForwardPayloads(
+                directory,
                 parseAddress("mbox:granola@Lola"),
-                payload,
-                servers1=[Lola, Joe], servers2=[Alice, Lola])
+                parsePath(usercfg, "Lola,Joe:Alice"),
+                payload, time.time(), time.time()+200)
+                
 
             for fn, args, kwargs in getCalls():
                 self.assertEquals(fn, "buildForwardMessage")
@@ -6547,11 +6572,6 @@
             clearCalls()
 
         ### Now try some failing cases for generateForwardMessage:
-        # Empty path...
-        self.assertRaises(MixError,
-                          client.generateForwardMessage,
-                          parseAddress("0xFFFE:zed"),
-                          "Z", [], [Alice])
 
         # Temporarily replace BlockingClientConnection so we can try the client
         # without hitting the network.
@@ -6577,9 +6597,11 @@
                          FakeBCC)
         try:
             client.sendForwardMessage(
+                directory,
                 parseAddress("mbox:granola@Lola"),
+                parsePath(usercfg,"alice,lola,joe,alice:joe,alice"),
                 "You only give me your information.",
-                [Alice, Lola, Joe, Alice], [Joe, Alice])
+                time.time(), time.time()+300)
             bcc = BCC_INSTANCE
             # first hop is alice
             self.assertEquals(bcc.addr, "10.0.0.9")