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

[minion-cvs] Several days worth of hacking. Highlights: Key rotatio...



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

Modified Files:
	BuildMessage.py ClientMain.py Common.py Config.py Crypto.py 
	MMTPClient.py Main.py Packet.py __init__.py benchmark.py 
	test.py testSupport.py 
Log Message:
Several days worth of hacking.  Highlights: Key rotation, robust queues.

TODO:
- Update status, add time estimates
- Break down directory work

etc/mixminiond.conf:
- Rename PublicKeySloppiness to PublicKeyOverlap

*:
- Whitespace normalization

ClientMain:
- Improve path syntax to include ?, *n,  Allow choice-with-replacement
- Use new readPickled functionality from Common
- Add -n argument for flush command
- Add default-path options to ClientConfig
- Be more specific about causes of failure when flushing; be more specific
  about # messages flushed.
- Remove --swap-at option: now path syntax is adequate.

Config, ClientMain, Common:
- Change duration from a 3-tuple to an independent class.  Now we 
  can say duration.getSeconds() rather than duration[2], which makes
  some stuff more readable.

Common:
- Debug checkPrivateFile
- Add AtomicFile class to help with standard create/rename pattern.
- Add readPickled/writePickled wrappers

MMTPClient:
- Document PeerCertificateCache

Packet:
- Correct documentation on overflow, underflow.

benchmark:
- Improve format of printed sizes
- Improve pk timing; time with bizarre exponent.
- Add Timing for ServerQueues

test:
- Add tests for encodeBase64
- Correct tests for new DeliveryQueue implementation
- Add tests for checkPrivateFile
- Revise tests for _parseInterval in response to new Duration class.
- Add tests for generating new descriptors with existing keys
- Fix test for directory with bad signature: make it fail for the
  right reason
- Deal with new validateConfig in Module
- Add test for scheduler.
- Tests for new path selection code

testSupport: 
- Module code uses new interface

EventStats:
- Document, clean

MMTPServer:
- Better warning on TLSClosed while connecting.
- Document new functionality

Modules:
- validateConfig function no longer needs 'sections' and 'entries':
  make it follow the same interface as other validation fns
- _deliverMessages: use new DeliveryQueue interface

PacketHandler:
- Always take a list of keys, never a single one.

ServerConfig:
- Refactor validateRetrySchedule
- Use new Duration class
- Rename PublicKeySloppiness to PublicKeyOverlap

ServerKeys: ***
- Implement key rotation:
   - Notice when to add and remove keys from PacketHandlers, MMTPServer
   - Set keys in packethandlers, mmtpserver 
   - Note that 512-bit DH moduli are kinda silly 
- More code and debugging for descriptor regenration

ServerMain:
- Documentation
- Key rotation
- Respond to refactoring in DeliveryQueue
- Use lambdas to wrap EventStats rotation
- Separate reset method
- Remove obsolete commands

ServerQueue: ***
- Refactor DeliveryQueue so that it has a prayer of working: Keep
  message delivery state in a separate file, and update separately.
  Remember time of queueing for each method, and last attempted
  delivery; n_retries is gone.  This allows us to change the retry schedule
  without putting messages in an inconsistent state.

  An earlier version put the state for _all_ queued objects in a
  single file: this turned out to be screamingly inefficient.

crypt.c, tls.c:
- Documentation fixes




Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.44
retrieving revision 1.45
diff -u -d -r1.44 -r1.45
--- BuildMessage.py	5 May 2003 00:38:45 -0000	1.44
+++ BuildMessage.py	17 May 2003 00:08:41 -0000	1.45
@@ -59,7 +59,7 @@
                    ",".join([s.getNickname() for s in path1]),
                    ",".join([s.getNickname() for s in path2]))
     LOG.debug("  Delivering to %04x:%r", exitType, exitInfo)
-    
+
     # Choose a random decoding tag.
     if not suppressTag:
         tag = _getRandomTag(paddingPRNG)
@@ -265,7 +265,7 @@
     elif err:
         raise UIError("Address and %s leg of path will not fit in one header",
                       ["first", "second"][err-1])
-    
+
 #----------------------------------------------------------------------
 # MESSAGE DECODING
 
@@ -486,8 +486,8 @@
         # Node i+1 sees the junk that node i saw, plus the junk that i appends,
         # all encrypted by i.
 
-        prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE) 
-    
+        prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE)
+
         # newJunk is the junk that node i will append. (It's as long as
         #   the data that i removes.)
         newJunk = Crypto.prng(prngKey,size)

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.72
retrieving revision 1.73
diff -u -d -r1.72 -r1.73
--- ClientMain.py	5 May 2003 00:38:45 -0000	1.72
+++ ClientMain.py	17 May 2003 00:08:41 -0000	1.73
@@ -15,7 +15,9 @@
 import getopt
 import getpass
 import os
+import re
 import stat
+import string
 import sys
 import time
 import urllib
@@ -24,13 +26,13 @@
 import mixminion.BuildMessage
[...1042 lines suppressed...]
+    count=None
+    for o,v in options:
+        if o in ('-n','--count'):
+            try:
+                count = int(v)
+            except ValueError:
+                print "ERROR: %s expects an integer" % o
+                sys.exit(1)
     try:
         parser = CLIArgumentParser(options, wantConfig=1, wantLog=1,
                                    wantClient=1)
@@ -2637,7 +2591,7 @@
     parser.init()
     client = parser.client
 
-    client.flushQueue()
+    client.flushQueue(count)
 
 
 _LIST_QUEUE_USAGE = """\

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.72
retrieving revision 1.73
diff -u -d -r1.72 -r1.73
--- Common.py	5 May 2003 00:41:57 -0000	1.72
+++ Common.py	17 May 2003 00:08:42 -0000	1.73
@@ -8,16 +8,18 @@
 __all__ = [ 'IntervalSet', 'Lockfile', 'LOG', 'LogStream', 'MixError',
             'MixFatalError', 'MixProtocolError', 'UIError', 'UsageError',
             'ceilDiv', 'checkPrivateDir', 'checkPrivateFile',
-            'createPrivateDir',
-            'encodeBase64', 'floorDiv',
-            'formatBase64', 'formatDate', 'formatFnameTime', 'formatTime',
+            'createPrivateDir', 'encodeBase64', 'floorDiv', 'formatBase64',
+            'formatDate', 'formatFnameTime', 'formatTime',
             'installSIGCHLDHandler', 'isSMTPMailbox', 'openUnique',
-            'previousMidnight', 'readPossiblyGzippedFile', 'secureDelete',
-            'stringContains', 'succeedingMidnight', 'waitForChildren' ]
+            'previousMidnight', 'readPickled', 'readPossiblyGzippedFile',
+            'secureDelete', 'stringContains', 'succeedingMidnight',
+            'waitForChildren', 'writePickled' ]
 
 import binascii
 import bisect
 import calendar
+import cPickle
+import errno
 import fcntl
 import gzip
 import os
@@ -138,13 +140,12 @@
     def formatBase64(s):
         """Convert 's' to a one-line base-64 representation."""
         return encodeBase64(s, 64, 1)
-    
+
 def encodeBase64(s, lineWidth=64, oneline=0):
     """Convert 's' to a multiline base-64 representation.  Improves upon
        base64.encodestring by having a variable line width.  Implementation
        is based upon that function.
     """
-    # XXXX004 test me
     pieces = []
     bytesPerLine = floorDiv(lineWidth, 4) * 3
     for i in xrange(0, len(s), bytesPerLine):
@@ -154,18 +155,22 @@
         return "".join([ s.strip() for s in pieces ])
     else:
         return "".join(pieces)
-    
+
 #----------------------------------------------------------------------
 def checkPrivateFile(fn, fix=1):
     """Checks whether f is a file owned by this uid, set to mode 0600 or
        0700, and all its parents pass checkPrivateDir.  Raises MixFatalError
        if the assumtions are not met; else return None.  If 'fix' is true,
        repair permissions on the file rather than raising MixFatalError."""
-    #XXXX004 testme
     parent, _ = os.path.split(fn)
-    if not checkPrivateDir(parent):
-        return None
-    st = os.stat(fn)
+    checkPrivateDir(parent)
+    try:
+        st = os.stat(fn)
+    except OSError, e:
+        if e.errno == errno.EEXIST:
+            raise MixFatalError("Nonexistant file %s" % fn)
+        else:
+            raise MixFatalError("Could't stat file %s: %s" % (fn, e))
     if not st:
         raise MixFatalError("Nonexistant file %s" % fn)
     if not os.path.isfile(fn):
@@ -176,7 +181,7 @@
     mode = st[stat.ST_MODE] & 0777
     if mode not in (0700, 0600):
         if not fix:
-            raise MixFatalError("Bad mode %o on file %s" % mode)
+            raise MixFatalError("Bad mode %o on file %s" % (mode & 0777, fn))
         newmode = {0:0600,0100:0700}[(mode & 0100)]
         LOG.warn("Repairing permissions on file %s" % fn)
         os.chmod(fn, newmode)
@@ -246,6 +251,56 @@
             _WARNED_DIRECTORIES[d] = 1
 
 #----------------------------------------------------------------------
+# File helpers
+class AtomicFile:
+    """Wrapper around open/write/rename to encapsulate writing to a temporary
+       file, then moving to the final filename on close"""
+    def __init__(self, fname, mode='w'):
+        self.fname = fname
+        self.tmpname = fname + ".tmp"
+        fd = os.open(self.tmpname, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0600)
+        self.f = os.fdopen(fd, mode)
+
+    def write(self, s):
+        self.f.write(s)
+
+    def close(self):
+        """Close the underlying file and replace the destination file."""
+        os.rename(self.tmpname, self.fname)
+        self.f.close()
+
+    def discard(self):
+        """Discard changes to the temporary file."""
+        self.f.close()
+        os.unlink(self.tmpname)
+
+def readPickled(fn):
+    """Given the name of a file containing a pickled object, return the pickled
+       object."""
+    f = open(fn, 'rb')
+    try:
+        return cPickle.load(f)
+    finally:
+        f.close()
+
+def writePickled(fn, obj):
+    """Given a filename and an object to be pickled, pickles the object into
+       a temporary file, then replaces the file with the temporary file.
+    """
+    tmpname = fn + ".tmp"
+    f, tmpname = openUnique(tmpname, 'wb')
+    try:
+        try:
+            cPickle.dump(obj, f, 1)
+        finally:
+            f.close()
+    except:
+        if os.path.exists(tmpname): os.unlink(tmpname)
+        raise
+
+    os.rename(tmpname, fn)
+
+#----------------------------------------------------------------------
 # Secure filesystem operations.
 
 # A 'shred' command to overwrite and unlink files.  It should accept an
@@ -637,10 +692,10 @@
             LOG.log(self.severity, "->%s: %s", self.name, line)
             del self.buf[:]
             s = s[idx+1:]
-            
+
         if s:
             self.buf.append(s)
-                            
+
     def flush(self): pass
     def close(self): pass
 
@@ -690,6 +745,64 @@
     if when is None:
         when = time.time()
     return time.strftime("%Y%m%d%H%M%S", time.localtime(when))
+
+#----------------------------------------------------------------------
+class Duration:
+    """A Duration is a number of time units, such as '1.5 seconds' or
+       '2 weeks'.  Durations are stored internally as a number of seconds.
+    """
+    ## Fields:
+    # seconds: the number of seconds in this duration
+    # unitName: the name of the units comprising this duration.
+    # nUnits: the number of units in this duration
+    def __init__(self, seconds, unitName=None, nUnits=None):
+        """Initialize a new Duration with a given number of seconds."""
+        self.seconds = seconds
+        if unitName:
+            self.unitName = unitName
+            self.nUnits = nUnits
+        else:
+            self.unitName = "second"
+            self.nUnits = seconds
+
+    def __str__(self):
+        s = ""
+        if self.nUnits != 1:
+            s = "s"
+        return "%s %s%s" % (self.nUnits, self.unitName, s)
+
+    def __repr__(self):
+        return "Duration(%r, %r, %r)" % (self.seconds, self.unitName,
+                                         self.nUnits)
+
+    def __float__(self):
+        """Return the number of seconds in this duration"""
+        return self.seconds
+
+    def __int__(self):
+        """Return the number of seconds in this duration"""
+        return int(self.seconds)
+
+    def getSeconds(self):
+        """Return the number of seconds in this duration"""
+        return self.seconds
+
+    def reduce(self):
+        """Change the representation of this object to its clearest form"""
+        s = self.seconds
+        for n,u in [(60*60*24*365,'year'),
+                    (60*60*24*30, 'month'),
+                    (60*60*24*7,  'week'),
+                    (60*60*24,    'day'),
+                    (60*60,       'hour'),
+                    (60,          'minute')]:
+            if s % n == 0:
+                self.nUnits = floorDiv(s,n)
+                self.unitName = u
+                return
+        self.nUnits = s
+        self.unitName = 'second'
+        return self
 
 #----------------------------------------------------------------------
 # InteralSet

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.41
retrieving revision 1.42
diff -u -d -r1.41 -r1.42
--- Config.py	26 Apr 2003 14:39:58 -0000	1.41
+++ Config.py	17 May 2003 00:08:42 -0000	1.42
@@ -118,16 +118,19 @@
 _canonical_unit_names = { 'sec' : 'second', 'min': 'minute', 'mon' : 'month' }
 def _parseInterval(interval):
     """Validation function.  Converts a config value to an interval of time,
-       in the format (number of units, name of unit, total number of seconds).
-       Raises ConfigError on failure."""
+       returning a Duration object. Raises ConfigError on failure."""
     inter = interval.strip().lower()
     m = _interval_re.match(inter)
     if not m:
         raise ConfigError("Unrecognized interval %r" % inter)
-    num, unit = float(m.group(1)), m.group(2)
+    num, unit = m.group(1), m.group(2)
+    if '.' in num:
+        num = float(num)
+    else:
+        num = int(num)
     nsec = num * _seconds_per_unit[unit]
-    return num, _canonical_unit_names.get(unit,unit), nsec
-
+    return mixminion.Common.Duration(nsec,
+                    _canonical_unit_names.get(unit,unit), num)
 
 def _parseIntervalList(s):
     """Validation functions. Parse a list of comma-separated intervals
@@ -141,8 +144,8 @@
             if item.startswith("every "):
                 item = item[6:]
             interval, duration = item.split(" for ", 1)
-            interval = int(_parseInterval(interval)[2])
-            duration = int(_parseInterval(duration)[2])
+            interval = int(_parseInterval(interval))
+            duration = int(_parseInterval(duration))
             if interval < 1:
                 raise ConfigError("Repeated interval too small in %s"%s)
 
@@ -152,7 +155,7 @@
                 "Bad syntax on interval %s. (Did you mean %s for X days?)",
                 item, item)
         else:
-            interval = _parseInterval(item)[2]
+            interval = int(_parseInterval(item))
             ilist.append(interval)
     return ilist
 
@@ -512,7 +515,7 @@
            steps.  (Use this to load a file that's already been checked as
            valid.)"""
         assert (filename is None) != (string is None)
-        
+
         if not hasattr(self, '_callbacks'):
             self._callbacks = {}
 
@@ -571,7 +574,7 @@
                         raise ConfigError("Unrecognized key %s on line %s" %
                                           (k, line))
                     else:
-                        LOG.warn("Unregognized key %s on line %s", k, line)
+                        LOG.warn("Unrecognized key %s on line %s", k, line)
                         continue
 
                 # Parse and validate the value of this entry.
@@ -700,7 +703,11 @@
         'Security' : { 'PathLength' : ('ALLOW', _parseInt, "8"),
                        'SURBAddress' : ('ALLOW', None, None),
                        'SURBPathLength' : ('ALLOW', _parseInt, "4"),
-                       'SURBLifetime' : ('ALLOW', _parseInterval, "7 days") },
+                       'SURBLifetime' : ('ALLOW', _parseInterval, "7 days"),
+                       'ForwardPath' : ('ALLOW', None, "*"),
+                       'ReplyPath' : ('ALLOW', None, "*"),
+                       'SURBPath' : ('ALLOW', None, "*"),
+                       },
         'Network' : { 'ConnectionTimeout' : ('ALLOW', _parseInterval, None) }
         }
     def __init__(self, fname=None, string=None):
@@ -719,9 +726,9 @@
 
         t = self['Network'].get('ConnectionTimeout')
         if t:
-            if t[2] < 5:
+            if int(t) < 5:
                 LOG.warn("Very short connection timeout")
-            elif t[2] > 60:
+            elif int(t) > 60:
                 LOG.warn("Very long connection timeout")
 
 def _validateHostSection(sec):

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.42
retrieving revision 1.43
diff -u -d -r1.42 -r1.43
--- Crypto.py	26 Apr 2003 14:39:58 -0000	1.42
+++ Crypto.py	17 May 2003 00:08:42 -0000	1.43
@@ -481,7 +481,7 @@
            bytes at a time."""
         self.bytes = ""
         self.chunksize = chunksize
-        
+
     def getBytes(self, n):
         """Returns a string of 'n' random bytes."""
 
@@ -702,7 +702,7 @@
            bytes at a time"""
         RNG.__init__(self,n)
         self.__lock = threading.Lock()
-        
+
     def _prng(self,n):
         "Returns n fresh bytes from our true RNG."
         if _TRNG_FILENAME is None:

Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.30
retrieving revision 1.31
diff -u -d -r1.30 -r1.31
--- MMTPClient.py	5 May 2003 00:42:49 -0000	1.30
+++ MMTPClient.py	17 May 2003 00:08:43 -0000	1.31
@@ -94,7 +94,7 @@
             assert sig == signal.SIGALRM
         if connectTimeout:
             signal.signal(signal.SIGALRM, sigalarmHandler)
-        
+
         # Connect to the server
         self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         self.sock.setblocking(1)
@@ -114,7 +114,7 @@
         finally:
             if connectTimeout:
                 signal.alarm(0)
-            
+
         LOG.debug("Handshaking with %s:%s",self.targetIP, self.targetPort)
         self.tls = self.context.sock(self.sock.fileno())
         self.tls.connect()
@@ -164,7 +164,7 @@
         self._sendPacket(packet,
                          control="JUNK\r\n", serverControl="RECEIVED\r\n",
                          hashExtra="JUNK", serverHashExtra="RECEIVED JUNK")
-        
+
     def _sendPacket(self, packet,
                     control="SEND\r\n", serverControl="RECEIVED\r\n",
                     hashExtra="SEND",serverHashExtra="RECEIVED"):
@@ -199,7 +199,7 @@
             LOG.debug("ACK received; packet successfully delivered")
         except (socket.error, _ml.TLSError, _ml.TLSClosed), e:
             self._raise(e, "sending packet")
-            
+
     def shutdown(self):
         """Close this connection."""
         LOG.debug("Shutting down connection to %s:%s",
@@ -256,28 +256,37 @@
 
 def pingServer(routing, connectTimeout=5):
     """Try to connect to a server and send a junk packet.
-    
+
        May raise MixProtocolBadAuth, or other MixProtocolError if server
        isn't up."""
     sendMessages(routing, ["JUNK"], connectTimeout=connectTimeout)
-    
+
 class PeerCertificateCache:
-    "DOCDOC"
+    """A PeerCertificateCache validates certificate chains from MMTP servers,
+       and remembers which chains we've already seen and validated."""
+    ## Fieleds
+    # cache: A map from peer (temporary) KeyID's to a (signing) KeyID.
     def __init__(self):
-        self.cache = {} # hashed peer pk -> identity keyid that it is valid for
+        self.cache = {}
 
     def check(self, tls, targetKeyID, address):
-        "DOCDOC"
-        if targetKeyID is None:
-            return
-
+        """Check whether the certificate chain on the TLS connection 'tls'
+           is valid, current, and matches the keyID 'targetKeyID'.  If so,
+           return.  If not, raise MixProtocolBadAuth.
+        """
+        # First, make sure the certificate is neither premature nor expired.
         try:
             tls.check_cert_alive()
         except _ml.TLSError, e:
             raise MixProtocolBadAuth("Invalid certificate: %s", str(e))
 
-        peer_pk = tls.get_peer_cert_pk()
-        hashed_peer_pk = sha1(peer_pk.encode_key(public=1))
+        # If we don't care whom we're talking to, we don't need to check
+        # them out.
+        if targetKeyID is None:
+            return
+
+        # Get the KeyID for the peer (temporary) key.
+        hashed_peer_pk = sha1(tls.get_peer_cert_pk().encode_key(public=1))
         # Before 0.0.4alpha, a server's keyID was a hash of its current
         # TLS public key.  In 0.0.4alpha, we allowed this for backward
         # compatibility.  As of 0.0.4alpha2, since we've dropped backward
@@ -287,28 +296,41 @@
             raise MixProtocolBadAuth(
                "Pre-0.0.4 (non-rotatable) certificate from server at %s",
                address)
+
         try:
-            if self.cache[hashed_peer_pk] == targetKeyID:
+            if targetKeyID == self.cache[hashed_peer_pk]:
+                # We recognize the key, and have already seen it to be
+                # signed by the target identity.
                 LOG.trace("Got a cached certificate from server at %s",
                           address)
                 return # All is well.
             else:
+                # We recognize the key, but some other identity signed it.
                 raise MixProtocolBadAuth(
                     "Mismatch between expected and actual key id")
         except KeyError:
-            # We haven't found an identity for this pk yet.
             pass
 
+        # We haven't found an identity for this pk yet.  Try to check the
+        # signature on it.
         try:
             identity = tls.verify_cert_and_get_identity_pk()
         except _ml.TLSError, e:
             raise MixProtocolBadAuth("Invalid KeyID from server at %s: %s"
                                    %(address, e))
 
+        # Okay, remember who has signed this certificate.
         hashed_identity = sha1(identity.encode_key(public=1))
         LOG.trace("Remembering valid certificate for server at %s",
                   address)
         self.cache[hashed_peer_pk] = hashed_identity
+
+        # Note: we don't need to worry about two identities signing the
+        # same certificate.  While this *is* possible to do, it's useless:
+        # You could get someone else's certificate and sign it, but you
+        # couldn't start up a TLS connection with that certificate without
+        # stealing their private key too.
+
+        # Was the signer the right person?
         if hashed_identity != targetKeyID:
             raise MixProtocolBadAuth("Invalid KeyID for server at %s" %address)
-

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- Main.py	26 Apr 2003 14:39:58 -0000	1.40
+++ Main.py	17 May 2003 00:08:43 -0000	1.41
@@ -119,7 +119,7 @@
     "client" :         ( 'mixminion.ClientMain', 'runClient' ),
     # XXXX Obsolete; use "queue"; remove in 0.0.5
     "pool" :           ( 'mixminion.ClientMain', 'runClient' ),
-    "queue" :          ( 'mixminion.ClientMain', 'runClient' ),    
+    "queue" :          ( 'mixminion.ClientMain', 'runClient' ),
     "import-server" :  ( 'mixminion.ClientMain', 'importServer' ),
     "list-servers" :   ( 'mixminion.ClientMain', 'listServers' ),
     "update-servers" : ( 'mixminion.ClientMain', 'updateServers' ),
@@ -132,7 +132,7 @@
     "inspect-queue" :   ( 'mixminion.ClientMain', 'listQueue' ),
     # XXXX Obsolete; use "inspect-queue"; remove in 0.0.5
     "inspect-pool" :   ( 'mixminion.ClientMain', 'listQueue' ),
-    "ping" :           ( 'mixminion.ClientMain', 'runPing' ),    
+    "ping" :           ( 'mixminion.ClientMain', 'runPing' ),
     # XXXX Obsolete; use "server-start"; remove in 0.0.5
     "server" :         ( 'mixminion.server.ServerMain', 'runServer' ),
     "server-start" :   ( 'mixminion.server.ServerMain', 'runServer' ),

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- Packet.py	5 May 2003 00:38:45 -0000	1.40
+++ Packet.py	17 May 2003 00:08:43 -0000	1.41
@@ -142,21 +142,23 @@
         ri, underflow = ri[:rlen], ri[rlen:]
     if rt >= MIN_EXIT_TYPE and rlen < 20:
         raise ParseError("Subheader missing tag")
-    #XXXX004 test underflow
     return Subheader(major,minor,secret,digest,rt,ri,rlen,underflow)
 
 class Subheader:
-    """Represents a decoded Mixminion subheader
+    """Represents a decoded Mixminion subheader.
 
        Fields: major, minor, secret, digest, routinglen, routinginfo,
                routingtype.
 
        A Subheader can exist in a half-initialized state where routing
-       info has been read from the first header, but not from the
-       extened headers.  If this is so, routinglen will be > len(routinginfo).
-       DOCDOC underflow
-       """
+       info has been read from the first RSA-encrypted data, but not
+       from the symmetrically encrypted data in the rest of the
+       header.  If this is so, routinglen will be > len(routinginfo).
 
+       If 'underflow' is present, it contains material that does not
+       belong to this subheader that was provided to 'parseSubheader'
+       anyway.
+       """
     def __init__(self, major, minor, secret, digest, routingtype,
                  routinginfo, routinglen=None, underflow=""):
         """Initialize a new subheader"""
@@ -198,26 +200,27 @@
         self.routinglen = len(info)
 
     def appendOverflow(self, data):
-        """Given a string containing additional 
-           routing info, add it to the routinginfo of this
-           object.
-           DOCDOC
-        """
-        #XXXX004 test
+        """Given a string containing additional routing info, add it
+           to the routinginfo of this object.  """
         self.routinginfo += data
         assert len(self.routinginfo) <= self.routinglen
 
     def getUnderflowLength(self):
+        """Return the number of bytes from the rest of the header that should
+           be included in the RSA-encrypted part of the header.
+        """
         return max(0, MAX_ROUTING_INFO_LEN - self.routinglen)
 
     def getOverflowLength(self):
-        """DOCDOC"""
-        #XXXX004 test
+        """Return the length of the data from routinginfo that will
+           not fit in the RSA-encrypted part of the header.
+        """
         return max(0, self.routinglen - MAX_ROUTING_INFO_LEN)
 
     def getOverflow(self):
-        """DOCDOC"""
-        #XXXX004 test
+        """Return the portion of routinginfo that doesn't fit into the
+           RSA-encrypted part of the header.
+        """
         return self.routinginfo[MAX_ROUTING_INFO_LEN:]
 
     def pack(self):
@@ -473,7 +476,7 @@
         if not text.endswith("\n"):
             text += "\n"
         return "%s\nVersion: 0.1\n\n%s%s\n"%(RB_TEXT_START,text,RB_TEXT_END)
-    
+
 #----------------------------------------------------------------------
 # Routing info
 
@@ -691,7 +694,7 @@
             if (c.startswith("Decoding-handle:") or
                 c.startswith("Message-type:")):
                 preNL = "\n"
-                
+
         if self.messageType == 'TXT':
             tagLine = ""
         elif self.messageType == 'ENC':

Index: __init__.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/__init__.py,v
retrieving revision 1.31
retrieving revision 1.32
diff -u -d -r1.31 -r1.32
--- __init__.py	26 Apr 2003 14:39:59 -0000	1.31
+++ __init__.py	17 May 2003 00:08:43 -0000	1.32
@@ -90,7 +90,7 @@
         raise ValueError, "Can't compare versions"
 
     return cmp(a[4],b[4])
-    
+
 assert __version__ == version_tuple_to_string(version_info)
 assert parse_version_string(__version__) == version_info
 assert cmp_versions(version_info, version_info) == 0

Index: benchmark.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/benchmark.py,v
retrieving revision 1.33
retrieving revision 1.34
diff -u -d -r1.33 -r1.34
--- benchmark.py	26 Apr 2003 14:39:59 -0000	1.33
+++ benchmark.py	17 May 2003 00:08:43 -0000	1.34
@@ -19,8 +19,9 @@
 import threading
 from time import time
 
-
 import mixminion._minionlib as _ml
+import mixminion.server.ServerQueue
+
 from mixminion.BuildMessage import _buildHeader, buildForwardMessage, \
      compressData, uncompressData, _encodePayload, decodePayload
 from mixminion.Common import secureDelete, installSIGCHLDHandler, \
@@ -83,9 +84,9 @@
 
 def spacestr(n):
     """Converts number of bytes to readable representation)"""
-    if abs(n) < 1e4:
-        return "%d bytes" %n
-    elif abs(n) < 1e7:
+    if abs(n) < 1024:
+        return "%d B" %n
+    elif abs(n) < 1048576:
         return "%d KB" % (n >> 10)
     elif abs(n) < 1e10:
         return "%d MB" % (n >> 20)
@@ -223,6 +224,8 @@
     print "bear D (32K)", timeit((
         lambda bkey=bkey: bear_decrypt(s32K, bkey)), 100)
 
+def rsaTiming():
+    c = AESCounterPRNG()
     if hasattr(_ml, 'add_oaep_padding'):
         print "OAEP_add (70->128B) (C)",
         print timeit((lambda: _ml.add_oaep_padding(s70b,OAEP_PARAMETER,128)),
@@ -265,7 +268,7 @@
 
     print "RSA generate (1024 bit,e=65535)", timeit((lambda: pk_generate(1024,
                                                                   65535)),10)
-    rsa = pk_generate()
+    rsa = pk_generate(1024,65535)
     print "Pad+RSA public encrypt",
     print timeit((lambda rsa=rsa: pk_encrypt(s70b, rsa)),1000)
     enc = pk_encrypt(s70b, rsa)
@@ -274,7 +277,17 @@
 
     print "RSA generate (1024 bit,e=3)", timeit((lambda: pk_generate(1024,
                                                                   3)),10)
-    rsa = pk_generate()
+    rsa = pk_generate(1024,3)
+    print "Pad+RSA public encrypt",
+    print timeit((lambda rsa=rsa: pk_encrypt(s70b, rsa)),1000)
+    enc = pk_encrypt(s70b, rsa)
+    print "Pad+RSA private decrypt", \
+          timeit((lambda enc=enc,rsa=rsa: pk_decrypt(enc, rsa)),100)
+
+    print "RSA generate (1024 bit,e=100073471)", timeit(
+        lambda: pk_generate(1024, 100073471), 10)
+                             
+    rsa = pk_generate(1024, 100073471)
     print "Pad+RSA public encrypt",
     print timeit((lambda rsa=rsa: pk_encrypt(s70b, rsa)),1000)
     enc = pk_encrypt(s70b, rsa)
@@ -381,7 +394,7 @@
     keyring = ServerKeyring(config)
     keyring.getIdentityKey()
     print "Create and sign server descriptor", timeit(keyring.createKeys, 10)
-    liveKey = keyring.getServerKeyset()
+    liveKey = keyring.getServerKeysets()[0]
     descFile = liveKey.getDescriptorFileName()
     desc = open(descFile).read()
 ##     for _ in xrange(2000):
@@ -463,6 +476,70 @@
     bm(16,16,10)
 
 #----------------------------------------------------------------------
+def serverQueueTiming():
+    print "#================= SERVER QUEUES ====================="    
+    Queue = mixminion.server.ServerQueue.Queue
+    DeliveryQueue = mixminion.server.ServerQueue.DeliveryQueue
+    d1 = mix_mktemp()
+    q1 = Queue(d1, create=1)
+
+    d2 = mix_mktemp()
+    os.mkdir(d2,0700)
+    getCommonPRNG().getBytes(1)
+    
+    #for ln,it in (32*1024,100),(128,400),(1024,400), (32*1024,100):
+    for ln,it in ():
+        msg = "z"*ln
+        def y(msg=msg,idx=[0],d2=d2):
+            fn = os.path.join(d2,"k_"+str(idx[0]))
+            idx[0] += 1
+            f = open(fn, 'wb')
+            f.write(msg)
+            f.close()
+        def x(msg=msg,d2=d2):
+            f,b=getCommonPRNG().openNewFile(d2,"k_",1)
+            f.write(msg)
+            f.close()
+        print "Base: write %s file: %s" %(
+            spacestr(ln), timestr(timeit_(y, it)))
+        for p in os.listdir(d2):
+            os.unlink(os.path.join(d2,p))
+        print "Base: write %s file with random name: %s" %(
+            spacestr(ln), timestr(timeit_(x, it)))
+        for p in os.listdir(d2):
+            os.unlink(os.path.join(d2,p))
+
+        tm = timeit_(lambda q1=q1,msg=msg:q1.queueMessage(msg),  it)
+        print "Queue %s message: %s" %(spacestr(ln), timestr(tm))
+        t2 = time()
+        q1.removeAll()
+        q1.cleanQueue()
+        t2 = time() - t2
+        print "Scrub %s message: %s" %(spacestr(ln), timestr(t2/float(it)))
+
+        msg = [ 123, 414, msg ]
+        tm = timeit_(lambda q1=q1,msg=msg:q1.queueObject(msg), it)
+        print "Pickle %s message: %s" %(spacestr(ln), timestr(tm))
+        q1.removeAll()
+        q1.cleanQueue()
+
+    for ln,it in (128,400),(1024,400), (32*1024,100):
+        q1 = DeliveryQueue(d1, [100,100,100,100])
+        msg = "z"*ln
+        print "Delivery queue: %s message: %s" %(
+            spacestr(ln),
+            timeit(lambda q1=q1,msg=msg: q1.queueDeliveryMessage(msg), it))
+        print "            (repOK):", \
+              timeit(lambda q1=q1: q1._repOk(), it*10)
+#        q1._bs2()
+#        print "          (set metadata 2):", \
+#              timeit(lambda q1=q1: q1._saveState2(), it)
+
+        for p in os.listdir(d1):
+            os.unlink(os.path.join(d1,p))
+
+
+#----------------------------------------------------------------------
 class DummyLog:
     def seenHash(self,h): return 0
     def logHash(self,h): pass
@@ -472,7 +549,7 @@
 
     pk = pk_generate(2048)
     server = FakeServerInfo("127.0.0.1", 1, pk, "X"*20)
-    sp = PacketHandler(pk, DummyLog())
+    sp = PacketHandler([pk], [DummyLog()])
 
     m_noswap = buildForwardMessage("Hello world", SMTP_TYPE, "f@invalid",
                                    [server, server], [server, server])
@@ -494,7 +571,7 @@
     t = prng.getBytes(20)
     print "Decode short payload", timeit(
         lambda p=p,t=t: decodePayload(p, t), 1000)
-    
+
     k20 = prng.getBytes(20*1024)
     p = _encodePayload(k20, 0, prng)
     t = prng.getBytes(20)
@@ -510,7 +587,7 @@
         except CompressedDataTooLong:
             pass
     print "Decode overcompressed payload", timeit(decode, 1000)
-       
+
 #----------------------------------------------------------------------
 def timeEfficiency():
     print "#================= ACTUAL v. IDEAL ====================="
@@ -580,7 +657,7 @@
     prng_128b = timeit_((lambda k=aeskey: prng(k,128)),10000)
 
     server = FakeServerInfo("127.0.0.1", 1, pk, "X"*20)
-    sp = PacketHandler(pk, DummyLog())
+    sp = PacketHandler([pk], [DummyLog()])
 
     m_noswap = buildForwardMessage("Hello world", SMTP_TYPE, "f@invalid",
                                    [server, server], [server, server])
@@ -620,7 +697,7 @@
         lockfile.release()
     t = time()-t1
     print "Lockfile: lock+unlock", timestr(t/2000.)
-    
+
     for i in xrange(200):
         f = open(os.path.join(dname, str(i)), 'wb')
         f.write(s32K)
@@ -745,14 +822,14 @@
             context = _ml.TLSContext_new(fn, p, dh)
             s1 = context.sock(0, 0)
             s2 = context.sock(0, 1)
-            
+
 def testLeaks5():
     from mixminion.test import _getMMTPServer
     server, listener, messagesIn, keyid = _getMMTPServer(1)
     #t = threading.Thread(None, testLeaks5_send,
     #                     args=(keyid,))
     #t.start()
-        
+
     while 1:
         server.process(0.5)
         #if messagesIn:
@@ -789,7 +866,7 @@
         _ml.generate_dh_parameters(dh, 1, 512)
     print "OK"
     context = _ml.TLSContext_new(fn, p)#XXXX, dh)
-    
+
     listenSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     listenSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     listenSock.bind(("127.0.0.1", 48999))
@@ -827,10 +904,12 @@
 #----------------------------------------------------------------------
 def timeAll(name, args):
     cryptoTiming()
+    rsaTiming()
     buildMessageTiming()
     directoryTiming()
     fileOpsTiming()
     encodingTiming()
+    serverQueueTiming()
     serverProcessTiming()
     hashlogTiming()
     timeEfficiency()

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.103
retrieving revision 1.104
diff -u -d -r1.103 -r1.104
--- test.py	5 May 2003 02:52:00 -0000	1.103
+++ test.py	17 May 2003 00:08:43 -0000	1.104
@@ -491,11 +491,6 @@
         LF2.release()
         LF1.acquire(blocking=1)
 
-        # XXXX004 reenable this once we figure out how to do so
-        #         happily on *BSD.  (The issue is that a blocking
-        #         flock seems to block _all_ the threads in this
-        #         process, not just this one.)
-
         # Now try a blocking lock.  We need to block in another process
         # because of some platforms' implementations of threading.
         releasedFile = mix_mktemp()
@@ -515,6 +510,18 @@
         os.waitpid(pid, 0)
         self.assertEquals("GOOD", readFile(releasedFile))
 
+    def test_encodeBase64(self):
+        longish = "xyzzyasjklsadjsakldjsakldjsakldjskljaskldjsadkljsa"*10
+        d32 = encodeBase64(longish, lineWidth=32)
+        d64 = encodeBase64(longish, lineWidth=64)
+        self.assertEquals(longish, base64.decodestring(d32))
+        self.assertEquals(longish, base64.decodestring(d64))
+        for enc, max in ((d32, 33), (d64,65)):
+            lines = enc.split("\n")
+            for line in lines[-1]:
+                self.assertEquals(len(line), max)
+            self.assert_(len(lines[-1]) <= max)
+
     def _intervalEq(self, a, *others):
         eq = self.assertEquals
         for b in others:
@@ -1356,7 +1363,7 @@
     def __init__(self, addr, port, key, keyid):
         assert key.get_modulus_bytes() == 256
         self.addr = addr
-        self.port = port        
+        self.port = port
         self.key = key
         self.keyid = keyid
 
@@ -1741,12 +1748,12 @@
 
         # Drop message gets no tag, random payload
         m = bfm(payload, DROP_TYPE, "", [self.server1], [self.server3])
-        
+
         def decoderDrop(p,t,self=self):
             self.assertEquals(None, t)
             self.failIf(BuildMessage._checkPayload(p))
             return ""
-            
+
         self.do_message_test(m,
                              ( (self.pk1,), None,
                                (SWAP_FWD_TYPE,),
@@ -1756,7 +1763,7 @@
                                ("",) ),
                              "",
                              decoder=decoderDrop)
-        
+
 
         # Encrypted forward message
         rsa1, rsa2 = self.pk1, self.pk512
@@ -1891,7 +1898,7 @@
               "Version: 0.1\n"+
               "xyz\n"+
               "== END TYPE III REPLY BLOCK ==\n")
-        
+
         # Test decoding
         seed = loc[:20]
         prng = AESCounterPRNG(sha1(seed+"Tyrone SlothropGenerate")[:16])
@@ -2097,9 +2104,9 @@
         self.server1 = FakeServerInfo("127.0.0.1", 1, self.pk1, "X"*20)
         self.server2 = FakeServerInfo("127.0.0.2", 3, self.pk2, "Z"*20)
         self.server3 = FakeServerInfo("127.0.0.3", 5, self.pk3, "Q"*20)
-        self.sp1 = PacketHandler(self.pk1, h)
-        self.sp2 = PacketHandler(self.pk2, h)
-        self.sp3 = PacketHandler(self.pk3, h)
+        self.sp1 = PacketHandler([self.pk1], [h])
+        self.sp2 = PacketHandler([self.pk2], [h])
+        self.sp3 = PacketHandler([self.pk3], [h])
         self.sp2_3 = PacketHandler((self.pk2,self.pk3), (h,h))
 
     def tearDown(self):
@@ -2151,14 +2158,14 @@
 
         # A one-hop/one-hop message.
         m = bfm(p, SMTP_TYPE, "nobody@invalid", [self.server1], [self.server3])
-        
+
         self.do_test_chain(m,
                            [self.sp1,self.sp3],
                            [FWD_TYPE, SMTP_TYPE],
                            [self.server3.getRoutingInfo().pack(),
                             "nobody@invalid"],
                            p)
-        
+
         # Try servers with multiple keys
         m = bfm(p, SMTP_TYPE, "nobody@invalid", [self.server2], [self.server3])
         self.do_test_chain(m, [self.sp2_3, self.sp2_3], [FWD_TYPE, SMTP_TYPE],
@@ -2192,7 +2199,7 @@
 
         p = "That gum you like, it's coming back in style."
         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],
@@ -2255,7 +2262,7 @@
         self.assertEquals(len(pkt.getContents()), 28*1024)
         self.assertEquals(encodeBase64(pkt.getContents()),
                           pkt.getAsciiContents())
-        
+
     def test_rejected(self):
         bfm = BuildMessage.buildForwardMessage
         brm = BuildMessage.buildReplyMessage
@@ -2367,8 +2374,8 @@
 
 
 class TestDeliveryQueue(DeliveryQueue):
-    def __init__(self,d):
-        DeliveryQueue.__init__(self,d)
+    def __init__(self,d,now=None):
+        DeliveryQueue.__init__(self,d,now=now)
         self._msgs = None
     def sendReadyMessages(self, *x, **y):
         self._msgs = None
@@ -2511,28 +2518,29 @@
     def testDeliveryQueues(self):
         d_d = mix_mktemp("qd")
 
-        queue = TestDeliveryQueue(d_d)
+        now = 10000 # time.time()
+        queue = TestDeliveryQueue(d_d, now)
         queue.setRetrySchedule([10, 10, 10, 10]) # Retry up to 40 sec.
-        now = time.time()
         # First, make sure the queue stores messages correctly.
-        h1 = queue.queueDeliveryMessage("Message 1")
-        h2 = queue.queueDeliveryMessage("Message 2")
-        self.assertEquals((0, "Message 1", 0), queue.get(h1))
+        h1 = queue.queueDeliveryMessage("Message 1", now)
+        h2 = queue.queueDeliveryMessage("Message 2", now)
+        self.assertEquals(("Message 1", now, None, now), queue._inspect(h1))
+        self.assertEquals(("Message 2", now, None, now), queue._inspect(h2))
 
         # Call sendReadyMessages to begin 'sending' msg1 and msg2.
         queue.sendReadyMessages(now)
         msgs = queue._msgs
         self.assertEquals(2, len(msgs))
         # _deliverMessages should have gotten them both.
-        self.failUnless((h1, "Message 1", 0) in msgs)
-        self.failUnless((h2, "Message 2", 0) in msgs)
+        self.failUnless((h1, "Message 1") in msgs)
+        self.failUnless((h2, "Message 2") in msgs)
         # Add msg3, and acknowledge that msg1 succeeded.  msg2 is now in limbo
-        h3 = queue.queueDeliveryMessage("Message 3")
+        h3 = queue.queueDeliveryMessage("Message 3", now)
         queue.deliverySucceeded(h1)
         # Only msg3 should get sent out, since msg2 is still in progress.
         queue.sendReadyMessages(now+1)
         msgs = queue._msgs
-        self.assertEquals([(h3, "Message 3", 0)], msgs)
+        self.assertEquals([(h3, "Message 3")], msgs)
 
         # Now, make sure that msg1 is gone from the pool.
         allHandles = queue.getAllMessages()
@@ -2544,11 +2552,23 @@
         # Now, fail msg2 retriably, and fail msg3 hard.  Only one message
         # should be left.  (It will have a different handle from the old
         # msg2.)
-        queue.deliveryFailed(h2, retriable=1)
-        queue.deliveryFailed(h3, retriable=0)
+        queue.deliveryFailed(h2, retriable=1, now=now+4)
+        queue.deliveryFailed(h3, retriable=0, now=now+4)
         allHandles = queue.getAllMessages()
         h4 = allHandles[0]
+        queue.cleanQueue()
+        files = os.listdir(d_d)
+        files.sort()
+        self.assertEquals(files, ["meta_"+h4, "msg_"+h4])
         self.assertEquals([h4], queue.getAllMessages())
+        self.assertEquals(("Message 2", now, now, now+10), queue._inspect(h2))
+
+        # Reload the queue; and try that again.
+        queue = TestDeliveryQueue(d_d, now+4)
+        queue.setRetrySchedule([10, 10, 10, 10]) # Retry up to 40 sec.
+        self.assertEquals([h4], queue.getAllMessages())
+        self.assertEquals(("Message 2", now, now, now+10), queue._inspect(h2))
+
         # When we try to send messages again after 5 seconds, nothing happens.
         queue.sendReadyMessages(now+5)
         msgs = queue._msgs
@@ -2556,30 +2576,47 @@
         # When we try to send again after after 11 seconds, message 2 fires.
         queue.sendReadyMessages(now+11)
         msgs = queue._msgs
-        self.assertEquals([(h4, "Message 2", 1)], msgs)
-        self.assertNotEquals(h2, h4)
-        queue.deliveryFailed(h4, retriable=1)
-        # At 30 seconds, message 2 fires.
+        self.assertEquals([(h4, "Message 2")], msgs)
+        self.assertEquals(h2, h4)
+        queue.deliveryFailed(h4, retriable=1, now=now+15)
+        self.assertEquals(("Message 2", now, now+11, now+20),
+                          queue._inspect(h2))
+        # At 31 seconds, message 2 fires.
         h5 = queue.getAllMessages()[0]
-        queue.sendReadyMessages(now+30)
+        queue.sendReadyMessages(now+31)
         msgs = queue._msgs
-        self.assertEquals([(h5, "Message 2", 2)], msgs)
-        self.assertNotEquals(h5, h4)
-        queue.deliveryFailed(h5, retriable=1)
+        self.assertEquals([(h5, "Message 2")], msgs)
+        self.assertEquals(h5, h4)
+        queue.deliveryFailed(h5, retriable=1, now=now+33)
+        self.assertEquals(("Message 2", now, now+31, now+40),
+                          queue._inspect(h2))
         # At 45 sec, it fires one last time.  It will have gotten up to #4
         # already.
         h6 = queue.getAllMessages()[0]
         queue.sendReadyMessages(now+45)
         msgs = queue._msgs
-        self.assertEquals([(h6, "Message 2", 4)], msgs)
-        self.assertNotEquals(h6, h5)
-        queue.deliveryFailed(h6, retriable=1)
+        self.assertEquals([(h6, "Message 2")], msgs)
+        self.assertEquals(h6, h5)
+        queue.deliveryFailed(h6, retriable=1, now=now+100)
         # Now Message 2 is timed out.
         self.assertEquals([], queue.getAllMessages())
-        
+
         queue.removeAll()
         queue.cleanQueue()
 
+        # Make sure old-style messages get nuked.
+        writePickled(os.path.join(d_d, "msg_ABCDEFGH"),
+                     (5, None, "xyzzy", 6))
+        try:
+            suspendLog("TRACE")
+            queue = TestDeliveryQueue(d_d, now+4)
+        finally:
+            s = resumeLog()
+        self.assert_(stringContains(s, "No metadata for file handle ABCDEFGH"))
+        self.assert_(stringContains(s, "Removing item ABCDEFGH"))
+        queue.setRetrySchedule([10, 10, 10, 10]) # Retry up to 40 sec.
+        self.assertEquals([], queue.getAllMessages())
+
     def testMixPools(self):
         d_m = mix_mktemp("qm")
 
@@ -2711,17 +2748,21 @@
 
 
 class FileParanoiaTests(unittest.TestCase):
-    def testPrivateDirs(self):
-        # Pick a private directory under tempdir, but don't create it.
-        noia = mix_mktemp("noia")
+    def ensureParanoia(self, whatkind):
         tempdir = mixminion.testSupport._MM_TESTING_TEMPDIR
 
         # If our tempdir doesn't exist and isn't private, we can't go on.
         try:
             checkPrivateDir(tempdir)
         except MixFatalError, e:
-            self.fail("Can't test directory paranoia, because something's\n"
-                      +" wrong with %s: %s"%(tempdir, str(e)))
+            self.fail(("Can't test %s paranoia, because something's\n"
+                      +" wrong with %s: %s")%(whatkind, tempdir, str(e)))
+
+    def testPrivateDirs(self):
+        self.ensureParanoia("directory")
+
+        # Pick a private directory under tempdir, but don't create it.
+        noia = mix_mktemp("noia")
 
         # Nonexistant directory.
         self.failUnlessRaises(MixFatalError, checkPrivateDir, noia)
@@ -2809,6 +2850,61 @@
             if old_mask is not None:
                 os.umask(old_mask)
 
+    def testPrivateFiles(self):
+        self.ensureParanoia("file")
+        dir = mix_mktemp()
+        os.mkdir(dir, 0700)
+        subdir = os.path.join(dir, "subdir")
+        os.mkdir(subdir, 0777)
+
+        # File doesn't exist.
+        self.failUnlessRaises(MixFatalError,
+                              checkPrivateFile, os.path.join(dir, "x"))
+
+        # Parent not private.
+        subdir_x = os.path.join(subdir, "x")
+        writeFile(subdir_x, "zzz")
+        os.chmod(subdir_x, 0600)
+        self.failUnlessRaises(MixFatalError,
+                              checkPrivateFile, subdir_x)
+
+        # File not owned by us. (???)
+        if os.getuid() == 0:
+            os.chmod(subdir_x, 0600)
+            os.chown(subdir_x, 1)
+            self.failUnlessRaises(MixFatalError, checkPrivateFile, subdir_x)
+            os.chown(subdir_x, 0)
+        else:
+            if os.path.exists("/etc/shadow"):
+                self.failUnlessRaises(MixFatalError, checkPrivateFile,
+                                      "/etc/shadow")
+
+        # File not private (fix, noex)
+        os.chmod(subdir, 0700)
+        os.chmod(subdir_x, 0777)
+        try:
+            suspendLog()
+            checkPrivateFile(subdir_x)
+        finally:
+            s = resumeLog()
+        self.assertEquals(0700, os.stat(subdir_x)[stat.ST_MODE] & 0777)
+        self.assert_(stringContains(s,
+                             "Repairing permissions on file "+subdir_x))
+
+        os.chmod(subdir_x, 0606)
+        try:
+            suspendLog()
+            checkPrivateFile(subdir_x)
+        finally:
+            s = resumeLog()
+        self.assertEquals(0600, os.stat(subdir_x)[stat.ST_MODE] & 0777)
+        self.assert_(stringContains(s,
+                             "Repairing permissions on file "+subdir_x))
+
+        # File not private, nofix.
+        os.chmod(subdir_x, 0701)
+        self.failUnlessRaises(MixFatalError, checkPrivateFile, subdir_x, 0)
+
 #----------------------------------------------------------------------
 # SIGHANDLERS
 # FFFF Write tests here
@@ -2816,7 +2912,6 @@
 
 #----------------------------------------------------------------------
 # MMTP
-# FFFF Write more tests
 
 # Run on a different port so we don't conflict with any actual servers
 # running on this machine.
@@ -2971,12 +3066,12 @@
         t.join()
 
     def testStallingTransmission(self):
-        # XXXX004 I know this works, but there doesn't seem to be a good
-        # XXXX004 way to test it.  It's hard to open a connection that
-        # XXXX004 will surely stall.  For now, I'm disabling this test.
+        # XXXX I know this works, but there doesn't seem to be a good
+        # XXXX way to test it.  It's hard to open a connection that
+        # XXXX will surely stall.  For now, I'm disabling this test.
         if 1:
             return
-        
+
         def threadfn(pausing):
             # helper fn to run in a different thread: bind a socket,
             # but don't listen.
@@ -2994,7 +3089,7 @@
         pausing = [4]
         t = threading.Thread(None, threadfn, args=(pausing,))
         t.start()
-        
+
         now = time.time()
         timedout = 0
         try:
@@ -3009,7 +3104,7 @@
             passed = time.time() - now
             pausing[0] = 0
             t.join()
-            
+
         self.assert_(passed < 2)
         self.assert_(timedout)
 
@@ -3053,7 +3148,7 @@
         self.failUnless(len(c) == 1)
         self.failUnless(startTime <= c[0].lastActivity <= endTime)
         self.assertEquals(2, server.nJunkPackets)
-        
+
         # Again, with bad keyid.
         clientcon = mixminion.server.MMTPServer.MMTPClientConnection(
            _getTLSContext(0), "127.0.0.1", TEST_PORT, "Z"*20,
@@ -3074,7 +3169,7 @@
             t.join()
         finally:
             resumeLog()  #unsuppress warning
-        
+
     def _testTimeout(self):
         server, listener, messagesIn, keyid = _getMMTPServer()
         self.listener = listener
@@ -3160,7 +3255,7 @@
             except mixminion.Common.MixProtocolReject:
                 ok[0] = 1
             done[0] = 1
-            
+
         t = threading.Thread(None, _t)
         t.start()
         while not done[0]:
@@ -3348,14 +3443,20 @@
         self.assertEquals(C._parseServerMode(" relay "), "relay")
         self.assertEquals(C._parseServerMode("Local"), "local")
         # interval
-        self.assertEquals(C._parseInterval(" 1 sec "), (1,"second", 1))
-        self.assertEquals(C._parseInterval(" 99 sec "), (99,"second", 99))
-        self.failUnless(floatEq(C._parseInterval("1.5 minutes")[2],
+        self.assertEquals(str(C._parseInterval(" 1 sec ")),"1 second")
+        self.assertEquals(str(C._parseInterval(" 99 sec ")),"99 seconds")
+        self.failUnless(floatEq(float(C._parseInterval("1.5 minutes")),
                                 90))
-        self.assertEquals(C._parseInterval("2 houRS"), (2,"hour",7200))
+        h2 = C._parseInterval("2 houRS")
+        m120 = C._parseInterval("120 minutes")
+        self.assertEquals(str(h2), "2 hours")
+        self.assertEquals(str(m120), "120 minutes")
+        self.assert_(int(h2) == int(m120) == 7200)
+        m120.reduce()
+        self.assertEquals(str(m120), "2 hours")
         # IntervalList
         self.assertEquals(C._parseIntervalList(" 5 sec, 1 min, 2 hours"),
-                          [ 5, 60, 7200 ])#XXXX mode
+                          [ 5, 60, 7200 ])
         self.assertEquals([5,5,5,5,5,5, 8*3600,8*3600,8*3600,8*3600,],
               C._parseIntervalList("5 sec for 30 sec, 8 hours for 1.3 days"))
         self.assertEquals([60], C._parseIntervalList("1 min for 1 min"))
@@ -3482,12 +3583,11 @@
 
         # IntervalSet validation
         def warns(mixInterval, retryList, self=self):
-            ents = { "Section":
-               [('Retry', mixminion.Config._parseIntervalList(retryList))]}
+            ent = mixminion.Config._parseIntervalList(retryList)
             try:
                 suspendLog()
                 mixminion.server.ServerConfig._validateRetrySchedule(
-                    mixInterval, ents, "Section")
+                    mixInterval, ent, "Section")
             finally:
                 r = resumeLog()
             self.assert_(stringContains(r, "[WARN]"))
@@ -3650,7 +3750,12 @@
         # Make sure we accept an extra key.
         inf2 = inf+"Unexpected-Key: foo\n"
         inf2 = mixminion.ServerInfo.signServerInfo(inf2, identity)
-        mixminion.ServerInfo.ServerInfo(string=inf2)
+        try:
+            suspendLog()
+            mixminion.ServerInfo.ServerInfo(string=inf2)
+        finally:
+            s = resumeLog()
+        self.assert_(stringContains(s,"Unrecognized key Unexpected-Key on line"))
 
         # Now make sure everything was saved properly
         keydir = os.path.join(d, "key_key1")
@@ -3736,6 +3841,40 @@
         except ConfigError, p:
             self.assertEquals(str(p), "Unrecognized descriptor version 0.99")
 
+        # Try regenerating server descriptor with existing keys.
+        key2 = mixminion.server.ServerKeys.ServerKeyset(d, "key2", d)
+        key2.load()
+        try:
+            suspendLog()
+            conf = mixminion.server.ServerConfig.ServerConfig(
+                string=(SERVER_CONFIG_SHORT%mix_mktemp())+
+                                           """[Incoming/MMTP]
+Enabled: yes
+IP: 192.168.100.3
+[Delivery/SMTP]
+Enabled: yes
+ReturnAddress: X@Y.Z
+""")
+        finally:
+            resumeLog()
+
+        inf3 = generateServerDescriptorAndKeys(conf,
+                                               identity,
+                                               d,
+                                               "key2",
+                                               d,
+                                               useServerKeys=1)
+
+        key3 = mixminion.server.ServerKeys.ServerKeyset(d, "key2", d)
+        key3.load()
+        info3 = key3.getServerDescriptor()
+        self.assertEquals(key3.getMMTPKey().get_public_key(),
+                          key2.getMMTPKey().get_public_key())
+        self.assertEquals(key3.getPacketKey().get_public_key(),
+                          key2.getPacketKey().get_public_key())
+        eq(info3['Incoming/MMTP']['IP'], "192.168.100.3")
+        self.assert_('smtp' in info3.getCaps())
+
     def test_directory(self):
         eq = self.assertEquals
         examples = getExampleServerDescriptors()
@@ -3794,7 +3933,7 @@
         self.failUnlessRaises(ConfigError, ServerDirectory, dBad)
         # Bad signature.
         dBad = re.compile(r"^DirectorySignature: ........", re.M).sub(
-            "Directory: ZZZZZZZZ", d)
+            "DirectorySignature: ZZZZZZZZ", d)
         self.failUnlessRaises(ConfigError, ServerDirectory, dBad)
 
         # Can we use messed-up spaces and line-endings?
@@ -4014,7 +4153,7 @@
         ES.log.lastSave = pm-1800
         ES.log._setNextRotation(now=pm-1800)
         eq(ES.log.getNextRotation(), pm+3600)
-        
+
         # 2) Rotation interval is not a multiple of hours: We don't round
         # 2A) Accumulated time << interval
         ES.log.rotateInterval = 40*60
@@ -4046,10 +4185,12 @@
     def getConfigSyntax(self):
         return { "Example" : { "Foo" : ("REQUIRE",
                                         mixminion.Config._parseInt, None) } }
-    def validateConfig(self, cfg, entries, lines, contents):
+    def validateConfig(self, cfg, lines, contents):
         if cfg['Example'] is not None:
             if cfg['Example'].get('Foo',1) % 2 == 0:
                 raise ConfigError("Foo was even")
+    def getRetrySchedule(self):
+        return [.1] *100
     def configure(self,cfg, manager):
         if cfg['Example']:
             self.enabled = 1
@@ -4141,6 +4282,7 @@
         # It should have processed all three.
         self.assertEquals(3, len(exampleMod.processedMessages))
         # If we try to send agin, the second message should get re-sent.
+        time.sleep(.15) # so we retry.
         manager.sendReadyMessages()
         self.assertEquals(1, queue.count())
         self.assertEquals(4, len(exampleMod.processedMessages))
@@ -4555,7 +4697,8 @@
         module.configure({'Testing/DirectoryDump' : {}}, manager)
         self.assert_(not manager.queues.has_key('Testing_DirectoryDump'))
         module.configure({'Testing/DirectoryDump' :
-                          {'Location': dir, 'UseQueue' : 0}}, manager)
+                          {'Location': dir, 'UseQueue' : 0,
+                           'Retry': [60]}}, manager)
         # Try sending a couple of messages.
         queue = manager.queues['Testing_DirectoryDump']
         p1 = FDP('plain',0xFFFE, "addr1","this is the message","t"*20)
@@ -4594,7 +4737,8 @@
         module = mixminion.testSupport.DirectoryStoreModule()
         # This time, use a queue.
         module.configure({'Testing/DirectoryDump' :
-                          {'Location': dir, 'UseQueue' : 1}}, manager)
+                          {'Location': dir, 'UseQueue' : 1,
+                           'Retry': [60]}}, manager)
         # Do we skip over the missing messages?
         self.assertEquals(module.next, 91)
         self.assertEquals(len(os.listdir(dir)), 3)
@@ -4603,7 +4747,7 @@
                 FDP('plain',0xFFFE, "addr91", "This is message 91"))
         queue.queueDeliveryMessage(
                 FDP('plain',0xFFFE, "addr92", "This is message 92"))
-        queue.queueDeliveryMessage(
+        h3 = queue.queueDeliveryMessage(
                 FDP('plain',0xFFFE, "fail", "This is message 93"))
         queue.queueDeliveryMessage(
                 FDP('plain',0xFFFE, "FAIL!", "This is message 94"))
@@ -4666,6 +4810,7 @@
 
 class ServerKeysTests(unittest.TestCase):
     def testServerKeyring(self):
+        #XXXX004 rethink this
         keyring = _getServerKeyring()
         home = _FAKE_HOME
 
@@ -4699,17 +4844,17 @@
         keyring.createKeys(2)
 
         # Check the first key we created
-        va, vu, curKey = keyring._getLiveKey()
+        va, vu, curKey = keyring._getLiveKeys()[0]
         self.assertEquals(va, start)
         self.assertEquals(vu, finish)
-        self.assertEquals(vu, keyring.getNextKeyRotation())
         self.assertEquals(curKey, "0001")
-        keyset = keyring.getServerKeyset()
+        keyset = keyring.getServerKeysets()[0]
         self.assertEquals(keyset.getHashLogFileName(),
                           os.path.join(home, "work", "hashlogs", "hash_0001"))
+        self.assertEquals(vu, keyring.getNextKeyRotation())
 
         # Check the second key we created.
-        va, vu, curKey = keyring._getLiveKey(vu + 3600)
+        va, vu, curKey = keyring._getLiveKeys(vu + 3600)[0]
         self.assertEquals(va, finish)
         self.assertEquals(vu, mixminion.Common.previousMidnight(
             finish+10*24*60*60+60))
@@ -4734,15 +4879,47 @@
             self.assertEquals(f, f2)
 
             # Test getTLSContext
-            keyring.getTLSContext()
+            keyring._getTLSContext()
 
         # Test getPacketHandler
-        _ = keyring.getPacketHandler()
+        #_ = keyring.getPacketHandler()
 
 
 #----------------------------------------------------------------------
 
 class ServerMainTests(unittest.TestCase):
+    def testScheduler(self):
+        _Scheduler = mixminion.server.ServerMain._Scheduler
+        _RecurringEvent = mixminion.server.ServerMain._RecurringEvent
+        lst=[]
+        def a(lst=lst): lst.append('a')
+        def b(lst=lst): lst.append('b')
+        def c(lst=lst): lst.append('c')
+        def d(lst=lst): lst.append('d')
+        def e(lst=lst): lst.append('e')
+        s = _Scheduler()
+        self.assertEquals(s.firstEventTime(), -1)
+        tm = time.time()
+
+        s.scheduleOnce(tm+1, "A", a)
+        s.scheduleOnce(tm+2, "B", b)
+        self.assertEquals(s.firstEventTime(), tm+1)
+        s.processEvents(tm+1)
+        self.assertEquals(['a'],lst)
+        del lst[:]
+
+        s.scheduleRecurring(tm+1.5, 1, "C", c)
+        s.scheduleOnce(tm+1.9, "D", d)
+        self.assertEquals(s.firstEventTime(), tm+1.5)
+        s.processEvents(tm+1.5)
+        self.assertEquals(["c"], lst)
+        diff = abs(s.firstEventTime()-(time.time()+1))
+        self.assert_(diff < 0.01)
+
+        s.processEvents(tm+5)
+        self.assertEquals(["c", "c", "d", "b"], lst)
+
+
     def testMixPool(self):
         ServerConfig = mixminion.server.ServerConfig.ServerConfig
         MixPool = mixminion.server.ServerMain.MixPool
@@ -5029,12 +5206,6 @@
                     n += 1
             return n
 
-        def allUnique(lst):
-            d = {}
-            for item in lst:
-                d[item] = 1
-            return len(d) == len(lst)
-
         # Override ks.DEFAULT_REQUIRED_LIFETIME so we don't need to
         # explicitly specify a really early endAt all the time.
         ks.DEFAULT_REQUIRED_LIFETIME = 1
@@ -5048,43 +5219,35 @@
         try:
             ### Try out getPath.
             # 1. Fully-specified paths.
-            p = ks.getPath(startServers=("Joe", "Lisa"),
-                           endServers=("Alice", "Joe"))
-            p = ks.getPath(startServers=("Joe", "Lisa", "Alice", "Joe"))
-            p = ks.getPath(endServers=("Joe", "Lisa", "Alice", "Joe"))
+            p = ks.getPath(None, ['Joe', 'Lisa', 'Alice', 'Joe'])
 
             # 2. Partly-specified paths...
             # 2a. With plenty of servers
-            p = ks.getPath(length=2)
+            p = ks.getPath(None, [None, None])
             eq(2, len(p))
             neq(p[0].getNickname(), p[1].getNickname())
 
-            p = ks.getPath(startServers=("Joe",), length=3)
+            p = ks.getPath(None, ["Joe", None, None])
             eq(3, len(p))
             self.assertSameSD(p[0], joe[0])
-            self.assert_(allUnique([s.getNickname() for s in p]))
             neq(p[1].getNickname(), "Joe")
             neq(p[2].getNickname(), "Joe")
             neq(p[1].getNickname(), p[2].getNickname())
 
-            p = ks.getPath(endServers=("Joe",), length=3)
+            p = ks.getPath(None, [None, None, "Joe"])
             eq(3, len(p))
             self.assertSameSD(joe[0], p[2])
-            self.assert_(allUnique([s.getNickname() for s in p]))
 
-            p = ks.getPath(startServers=("Alice",),endServers=("Joe",),
-                           length=4)
+            p = ks.getPath(None, ["Alice", None, None, "Joe"])
             eq(4, len(p))
             self.assertSameSD(alice[0], p[0])
             self.assertSameSD(joe[0], p[3])
             nicks = [ s.getNickname() for s in p ]
-            eq(1, nicks.count("Alice"))
-            eq(1, nicks.count("Joe"))
+            self.assert_(nicks.count("Alice")>=1)
+            self.assert_(nicks.count("Joe")>=1)
             neq(nicks[1],nicks[2])
-            self.assert_(allUnique([s.getNickname() for s in p]))
 
-            p = ks.getPath(startServers=("Joe",),endServers=("Alice","Joe"),
-                           length=4)
+            p = ks.getPath(None, ["Joe", None, "Alice", "Joe"])
             eq(4, len(p))
             self.assertSameSD(alice[0], p[2])
             self.assertSameSD(joe[0], p[0])
@@ -5098,23 +5261,22 @@
             ks2.importFromFile(os.path.join(impdirname, "Lisa1"))
             ks2.importFromFile(os.path.join(impdirname, "Bob0"))
 
-            p = ks2.getPath(length=9)
+            p = ks2.getPath(None, [None]*9)
             eq(9, len(p))
             self.failIf(nRuns([s.getNickname() for s in p]))
 
-            p = ks2.getPath(startServers=("Joe",),endServers=("Joe",),
-                            length=8)
+            p = ks2.getPath(None, ["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(startServers=("Joe",),length=7)
+            p = ks2.getPath(None, ["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(endServers=("Joe",),length=7)
+            p = ks2.getPath(None, [None]*6+["Joe"])
             self.failIf(nRuns([s.getNickname() for s in p]))
             eq(7, len(p))
             self.assertSameSD(joe[0], p[-1])
@@ -5122,30 +5284,30 @@
             # 2c. With 2 servers
             ks2.expungeByNickname("Alice")
             ks2.expungeByNickname("Bob")
-            p = ks2.getPath(length=4)
+            p = ks2.getPath(None, [None]*4)
             self.failIf(nRuns([s.getNickname() for s in p]) > 1)
 
-            p = ks2.getPath(length=4,startServers=("Joe",))
+            p = ks2.getPath(None, ["Joe",None,None,None])
 
             self.failIf(nRuns([s.getNickname() for s in p]) > 2)
-            p = ks2.getPath(length=4, endServers=("Joe",))
+            p = ks2.getPath(None, [None, None, None, "Joe"])
             self.failIf(nRuns([s.getNickname() for s in p]) > 1)
 
-            p = ks2.getPath(length=6, endServers=("Joe",))
+            p = ks2.getPath(None, [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(length=4)
-            eq(len(p), 2)
-            p = ks2.getPath(length=4, startServers=("Joe",))
-            eq(len(p), 3)
-            p = ks2.getPath(length=4, endServers=("Joe",))
-            eq(len(p), 2)
+            p = ks2.getPath(None,[None]*4)
+            eq(len(p), 4)
+            p = ks2.getPath(None,["Joe",None,None,None])
+            eq(len(p), 4)
+            p = ks2.getPath(None,[None,None,None,"Joe"])
+            eq(len(p), 4)
 
             # 2e. With 0
             self.assertRaises(MixError, ks.getPath,
-                              length=4, startAt=now+100*oneDay)
+                              None, [None]*4, startAt=now+100*oneDay)
         finally:
             s = resumeLog()
         self.assertEquals(4, s.count("Not enough servers for distinct"))
@@ -5153,22 +5315,20 @@
         self.assertEquals(3, s.count("Only one relay known"))
 
         # 3. With capabilities.
-        p = ks.getPath(length=5, endCap="smtp", midCap="relay")
+        p = ks.getPath("smtp", [None]*5)
         eq(5, len(p))
         self.assertSameSD(p[-1], joe[0]) # Only Joe has SMTP
 
-        p = ks.getPath(length=4, endCap="mbox", midCap="relay")
+        p = ks.getPath("mbox", [None]*4)
         eq(4, len(p))
         self.assertSameSD(p[-1], lola[1]) # Only Lola has MBOX
 
-        p = ks.getPath(length=5, endCap="mbox", midCap="relay",
-                       startServers=("Alice",))
+        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(length=5, endCap="mbox", midCap="relay",
-                       endServers=("Alice",))
+        p = ks.getPath("mbox", [None,None,None,None, "Alice"])
         eq(5, len(p))
         self.assertSameSD(p[-1], alice[0]) # We ignore endCap with endServers
 
@@ -5212,19 +5372,15 @@
         fredfile = os.path.join(impdirname, "Fred1")
         p1,p2 = ppath(ks, None, "Alice,%s,Bob,Joe"%fredfile, email)
         pathIs((p1,p2), ((alice,fred),(bob,joe)))
-        p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Joe", email, nHops=4, nSwap=1)
-        pathIs((p1,p2), ((alice,fred),(bob,joe)))
-        p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Lola,Joe", email, nHops=5,
-                      nSwap=1)
-        pathIs((p1,p2), ((alice,fred),(bob,lola,joe)))
         p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Lola,Joe", email, nHops=5)
         pathIs((p1,p2), ((alice,fred,bob),(lola,joe)))
         p1,p2 = ppath(ks, None, "Alice,Fred,Bob", mboxWithServer)
         pathIs((p1,p2), ((alice,fred),(bob,lola)))
         p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Lola", mboxWithoutServer)
         pathIs((p1,p2), ((alice,fred),(bob,lola)))
-        p1,p2 = ppath(ks, None, "Alice,Fred,Bob", mboxWithServer, nSwap=0)
-        pathIs((p1,p2), ((alice,),(fred,bob,lola)))
+        p1,p2 = ppath(ks, None, "Alice,?,?,Bob", mboxWithServer)
+        eq((len(p1),len(p2)), (2,3))
+        pathIs((p1[:1],p2[-2:]), ((alice,),(bob,lola)))
 
         # 1b. Colon, no star
         p1,p2 = ppath(ks, None, "Alice:Fred,Joe", email)
@@ -5233,10 +5389,11 @@
         pathIs((p1,p2), ((alice,),(bob,fred,joe)))
         p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", email)
         pathIs((p1,p2), ((alice,bob,fred),(joe,)))
+        p1,p2 = ppath(ks, None, "Alice,Bob,?:Joe", email)
+        eq((len(p1),len(p2)), (3,1))
+        pathIs((p1[:-1],p2), ((alice,bob),(joe,)))
         p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", email, nHops=4)
         pathIs((p1,p2), ((alice,bob,fred),(joe,)))
-        p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", email, nSwap=2)
-        pathIs((p1,p2), ((alice,bob,fred),(joe,)))
         p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", mboxWithServer)
         pathIs((p1,p2), ((alice,bob,fred),(joe,lola)))
         p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Lola", mboxWithoutServer)
@@ -5244,71 +5401,67 @@
 
         # 1c. Star, no colon
         p1,p2 = ppath(ks, None, 'Alice,*,Joe', email, nHops=5)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p1[0],p2[-1]), (alice, joe))
         eq((len(p1),len(p2)), (3,2))
 
         p1,p2 = ppath(ks, None, 'Alice,Bob,*,Joe', email, nHops=6)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p1[0],p1[1],p2[-1]), (alice, bob, joe))
         eq((len(p1),len(p2)), (3,3))
 
         p1,p2 = ppath(ks, None, 'Alice,Bob,*', email, nHops=6)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p1[0],p1[1],p2[-1]), (alice, bob, joe))
         eq((len(p1),len(p2)), (3,3))
 
         p1,p2 = ppath(ks, None, '*,Bob,Joe', email) #default nHops=6
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p2[-2],p2[-1]), (bob, joe))
         eq((len(p1),len(p2)), (3,3))
 
-        p1,p2 = ppath(ks, None, 'Bob,*,Alice', mboxWithServer) #default nHops=6
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+        p1,p2 = ppath(ks, None, 'Bob,*,Alice', mboxWithServer, nHops=5)
         pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
         eq((len(p1),len(p2)), (3,3))
 
         p1,p2 = ppath(ks, None, 'Bob,*,Alice,Lola', mboxWithoutServer)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
         eq((len(p1),len(p2)), (3,3))
 
         # 1d. Star and colon
-        p1,p2 = ppath(ks, None, 'Bob:*,Alice', mboxWithServer)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+        p1,p2 = ppath(ks, None, 'Bob:*,Alice', mboxWithServer, nHops=5)
         pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
         eq((len(p1),len(p2)), (1,5))
 
-        p1,p2 = ppath(ks, None, 'Bob,*:Alice', mboxWithServer)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+        p1,p2 = ppath(ks, None, 'Bob,*:Alice', mboxWithServer, nHops=5)
         pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
         eq((len(p1),len(p2)), (4,2))
 
-        p1,p2 = ppath(ks, None, 'Bob,*,Joe:Alice', mboxWithServer)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+        p1,p2 = ppath(ks, None, 'Bob,*,Joe:Alice', mboxWithServer, nHops=5)
         pathIs((p1[0],p1[-1],p2[-2],p2[-1]), (bob, joe, alice, lola))
         eq((len(p1),len(p2)), (4,2))
 
         p1,p2 = ppath(ks, None, 'Bob,*,Lola:Alice,Joe', email)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p1[0],p1[-1],p2[-2],p2[-1]), (bob, lola, alice, joe))
         eq((len(p1),len(p2)), (4,2))
 
         p1,p2 = ppath(ks, None, '*,Lola:Alice,Joe', email)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p1[-1],p2[-2],p2[-1]), (lola, alice, joe))
         eq((len(p1),len(p2)), (4,2))
 
         p1,p2 = ppath(ks, None, 'Lola:Alice,*', email)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
         pathIs((p1[0],p2[0],p2[-1]), (lola, alice, joe))
         eq((len(p1),len(p2)), (1,5))
 
-        p1,p2 = ppath(ks, None, 'Bob:Alice,*', mboxWithServer)
-        self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+        p1,p2 = ppath(ks, None, 'Bob:Alice,*', mboxWithServer, nHops=5)
         pathIs((p1[0],p2[0],p2[-1]), (bob, alice, lola))
         eq((len(p1),len(p2)), (1,5))
 
+        # 1e. Complex.
+        try:
+            suspendLog()
+            p1,p2 = ppath(ks, None, '?,Bob,*:Joe,*2,Joe', email, nHops=9)
+        finally:
+            resumeLog()
+        pathIs((p1[1],p2[0],p2[-1]), (bob, joe, joe))
+        eq((len(p1),len(p2)), (5,4))
+
         # 2. Failing cases
         raises = self.assertRaises
         # Nonexistant server
@@ -5320,8 +5473,6 @@
         raises(MixError, ppath, ks, None, "Alice:Bob,Fred", mboxWithoutServer)
         # Two stars.
         raises(MixError, ppath, ks, None, "Alice,*,Bob,*,Joe", email)
-        # Swap point mismatch
-        raises(MixError, ppath, ks, None, "Alice:Bob,Joe", email, nSwap=1)
         # NHops mismatch
         raises(MixError, ppath, ks, None, "Alice:Bob,Joe", email, nHops=2)
         raises(MixError, ppath, ks, None, "Alice:Bob,Joe", email, nHops=4)
@@ -5567,8 +5718,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(MMTPTests))
-        #suite.addTest(tc(MiscTests))
+        suite.addTest(tc(ClientMainTests))
         return suite
 
     suite.addTest(tc(MiscTests))

Index: testSupport.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/testSupport.py,v
retrieving revision 1.14
retrieving revision 1.15
diff -u -d -r1.14 -r1.15
--- testSupport.py	9 Feb 2003 22:30:58 -0000	1.14
+++ testSupport.py	17 May 2003 00:08:44 -0000	1.15
@@ -15,7 +15,7 @@
 import mixminion.Crypto
 import mixminion.Common
 from mixminion.Common import waitForChildren, createPrivateDir, LOG
-from mixminion.Config import _parseBoolean, ConfigError
+from mixminion.Config import _parseBoolean, _parseIntervalList, ConfigError
 
 from mixminion.server.Modules import DELIVER_FAIL_NORETRY, DELIVER_FAIL_RETRY,\
      DELIVER_OK, DeliveryModule, ImmediateDeliveryQueue, \
@@ -35,6 +35,9 @@
        Otherwise, creates a file in the specified directory, containing
           the routing info, a newline, and the message contents.
     """
+    def __init__(self):
+        DeliveryModule.__init__(self)
+
     ## Fields:
     # loc -- The directory to store files in.  All filenames are numbers;
     #    we always put new messages in the smallest number greater than
@@ -43,18 +46,22 @@
     def getConfigSyntax(self):
         return { 'Testing/DirectoryDump':
                  { 'Location' : ('REQUIRE', None, None),
-                   'UseQueue': ('REQUIRE', _parseBoolean, None) } }
+                   'UseQueue': ('REQUIRE', _parseBoolean, None),
+                   'Retry' : ('ALLOW', _parseIntervalList,
+                              "every 1 min for 10 min") } }
 
-    def validateConfig(self, sections, entries, lines, contents):
+    def validateConfig(self, config, lines, contents):
         # loc = sections['Testing/DirectoryDump'].get('Location')
         pass
 
+    def getRetrySchedule(self):
+        return self.retry
+
     def configure(self, config, manager):
         self.loc = config['Testing/DirectoryDump'].get('Location')
         if not self.loc:
             return
         self.useQueue = config['Testing/DirectoryDump']['UseQueue']
-        manager.enableModule(self)
 
         if not os.path.exists(self.loc):
             createPrivateDir(self.loc)
@@ -65,6 +72,9 @@
                 max = int(f)
         self.next = max+1
 
+        self.retry = config['Testing/DirectoryDump']['Retry']
+        manager.enableModule(self)
+
     def getServerInfoBlock(self):
         return ""
 
@@ -76,7 +86,8 @@
 
     def createDeliveryQueue(self, queueDir):
         if self.useQueue:
-            return SimpleModuleDeliveryQueue(self, queueDir)
+            return SimpleModuleDeliveryQueue(self, queueDir,
+                                             retrySchedule=self.retry)
         else:
             return ImmediateDeliveryQueue(self)