[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[minion-cvs] Last round of localized refactoring before 0.0.1.



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

Modified Files:
	BuildMessage.py ClientMain.py Common.py Config.py Crypto.py 
	Modules.py Packet.py PacketHandler.py Queue.py ServerInfo.py 
	ServerMain.py benchmark.py test.py 
Log Message:
Last round of localized refactoring before 0.0.1.

*:
        - Remove 'stateful' reply blocks.
        - Make 'end-to-end encrypt' into a constant encryption mode.
	- Move exit types from Modules into Packet
	- Move RFC822 address-checking functionality from Packet to Common
	- Move 'printable?' checks from Config and ServerInfo into 
	  Common.
	- Move _time and _date into Common; give them real names.

Modules:
	- Add more validation for mbox module.

ServerInfo:
	- Validate a little more.

Queue:
	- Fix possible race by judicious application of O_EXCL.


test:
	- Add test for ModuleManager decoding of messages.


Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.20
retrieving revision 1.21
diff -u -d -r1.20 -r1.21
--- BuildMessage.py	9 Dec 2002 04:47:39 -0000	1.20
+++ BuildMessage.py	11 Dec 2002 05:53:32 -0000	1.21
@@ -11,10 +11,9 @@
 from mixminion.Packet import *
 from mixminion.Common import MixError, MixFatalError, LOG
 import mixminion.Crypto as Crypto
-import mixminion.Modules as Modules
 
 __all__ = ['buildForwardMessage', 'buildEncryptedMessage', 'buildReplyMessage',
-           'buildStatelessReplyBlock', 'buildReplyBlock', 'decodePayload' ]
+           'buildReplyBlock', 'decodePayload' ]
 
 def buildForwardMessage(payload, exitType, exitInfo, path1, path2,
 			paddingPRNG=None):
@@ -94,8 +93,7 @@
 	if not (ord(encrypted[0]) & 0x80):
 	    break
     # Lioness encryption.
-    # DOCDOC doc mode 'End-to-end encrypt' XXXX001
-    k = Crypto.Keyset(sessionKey).getLionessKeys("End-to-end encrypt")
+    k= Crypto.Keyset(sessionKey).getLionessKeys(Crypto.END_TO_END_ENCRYPT_MODE)
     lionessPart = Crypto.lioness_encrypt(lionessPart, k)
 
     # Now we re-divide the payload into the part that goes into the tag, and
@@ -132,10 +130,12 @@
     return _buildMessage(payload, None, None,
                          path1=path1, path2=replyBlock)
 
-def buildReplyBlock(path, exitType, exitInfo, expiryTime=0, secretPRNG=None,
-                    tag=None):
-    """Return a 3-tuple containing (1) a newly-constructed reply block, (2)
-       a list of secrets used to make it, (3) a tag.
+def _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime=0, 
+			 secretPRNG=None, tag=None):
+    """Helper function: makes a reply block, given a tag and a PRNG to
+       generate secrets. Returns a 3-tuple containing (1) a
+       newly-constructed reply block, (2) a list of secrets used to
+       make it, (3) a tag.
 
               path: A list of ServerInfo
               exitType: Routing type to use for the final node
@@ -147,9 +147,6 @@
                  will be used to encrypt the message in reverse order.
               tag: If provided, a 159-bit tag.  If not provided, a new one
                  is generated.
-
-        (This will go away when we disable 'stateful' (non-state-carrying)
-	 reply blocks.)
        """
     if secretPRNG is None:
         secretPRNG = Crypto.AESCounterPRNG()
@@ -174,13 +171,13 @@
                           paddingPRNG=Crypto.AESCounterPRNG())
 
     return ReplyBlock(header, expiryTime,
-                      Modules.SWAP_FWD_TYPE,
+                      SWAP_FWD_TYPE,
                       path[0].getRoutingInfo().pack(), sharedKey), secrets, tag
 
 # Maybe we shouldn't even allow this to be called with userKey==None.
-def buildStatelessReplyBlock(path, exitType, exitInfo, userKey,
-			     expiryTime=0, secretRNG=None):
-    """Construct a 'stateless' reply block that does not require the
+def buildReplyBlock(path, exitType, exitInfo, userKey,
+		    expiryTime=0, secretRNG=None):
+    """Construct a 'state-carrying' reply block that does not require the
        reply-message recipient to remember a list of secrets.
        Instead, all secrets are generated from an AES counter-mode
        stream, and the seed for the stream is stored in the 'tag'
@@ -190,6 +187,10 @@
                path: a list of ServerInfo objects
 	       exitType,exitInfo: The address to deliver the final message.
                userKey: a string used to encrypt the seed.
+
+       NOTE: We used to allow another kind of 'non-state-carrying' reply
+       block that stored its secrets on disk, and used an arbitrary tag to
+       determine 
        """
     if secretRNG is None: secretRNG = Crypto.AESCounterPRNG()
 
@@ -199,6 +200,10 @@
     # message with 99.6% probability.  (Otherwise, we'd need to repeatedly
     # lioness-decrypt the payload in order to see whether the message was 
     # a reply.)
+    
+    # XXXX D'oh!  This enables an offline password guessing attack for
+    # XXXX anybody who sees multiple tags.  We need to make sure that userKey
+    # XXXX is stored on disk, and isn't a password.  This needs more thought.
     while 1:
 	seed = _getRandomTag(secretRNG)
 	if Crypto.sha1(seed+userKey+"Validate")[-1] == '\x00':
@@ -206,13 +211,14 @@
 
     prng = Crypto.AESCounterPRNG(Crypto.sha1(seed+userKey+"Generate")[:16])
 
-    return buildReplyBlock(path, exitType, exitInfo, expiryTime, prng, seed)[0]
+    return _buildReplyBlockImpl(path, exitType, exitInfo, expiryTime, prng, 
+				seed)[0]
 
 #----------------------------------------------------------------------
 # MESSAGE DECODING
 
 def decodePayload(payload, tag, key=None, 
-		  storedKeys=None, #XXXX001 disable storedKeys
+		  #storedKeys=None, # 'Stateful' reply blocks are disabled.
 		  userKey=None):
     """Given a 28K payload and a 20-byte decoding tag, attempt to decode and
        decompress the original message.  
@@ -235,14 +241,16 @@
     if _checkPayload(payload):
 	return _decodeForwardPayload(payload)
 
-    # If we have a list of keys associated with the tag, it's a reply message
-    # using those keys.
-    #XXXX001 'Non-state-carrying' reply blocks are supposed to be disabled
-    if storedKeys is not None:
-	secrets = storedKeys.get(tag)
- 	if secrets is not None:
- 	    del storedKeys[tag]
- 	    return _decodeReplyPayload(payload, secrets)
+    # ('Stateful' reply blocks are disabled.)
+
+##    # If we have a list of keys associated with the tag, it's a reply message
+##    # using those keys.
+
+##     if storedKeys is not None:
+## 	secrets = storedKeys.get(tag)
+##  	if secrets is not None:
+##  	    del storedKeys[tag]
+##  	    return _decodeReplyPayload(payload, secrets)
 
     # If H(tag|userKey|"Validate") ends with 0, then the message _might_
     # be a reply message using H(tag|userKey|"Generate") as the seed for
@@ -287,8 +295,9 @@
     except Crypto.CryptoError:
 	return None
     rest = msg[key.get_modulus_bytes():]
-    # XXXX001 magic string
-    k =Crypto.Keyset(rsaPart[:SECRET_LEN]).getLionessKeys("End-to-end encrypt")
+
+    k = Crypto.Keyset(rsaPart[:SECRET_LEN]).getLionessKeys(
+	Crypto.END_TO_END_ENCRYPT_MODE)
     rest = rsaPart[SECRET_LEN:] + Crypto.lioness_decrypt(rest, k)
     
     # ... and then, check the checksum and continue.
@@ -357,7 +366,7 @@
     else:
 	if len(exitInfo) < TAG_LEN:
 	    raise MixError("Implausibly short exit info: %r"%exitInfo)
-	if exitType < Modules.MIN_EXIT_TYPE and exitType != Modules.DROP_TYPE:
+	if exitType < MIN_EXIT_TYPE and exitType != DROP_TYPE:
 	    raise MixError("Invalid exit type: %4x"%exitType)
 
     ### SETUP CODE: let's handle all the variant cases.
@@ -377,7 +386,7 @@
         path1exittype = reply.routingType
         path1exitinfo = reply.routingInfo
     else:
-        path1exittype = Modules.SWAP_FWD_TYPE
+        path1exittype = SWAP_FWD_TYPE
         path1exitinfo = path2[0].getRoutingInfo().pack()
 
     # Generate secrets for path1.
@@ -413,7 +422,7 @@
         raise MixError("Too many nodes in path")
 
     # Construct a list 'routing' of exitType, exitInfo.
-    routing = [ (Modules.FWD_TYPE, node.getRoutingInfo().pack()) for
+    routing = [ (FWD_TYPE, node.getRoutingInfo().pack()) for
                 node in path[1:] ]
     routing.append((exitType, exitInfo))
 

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.12
retrieving revision 1.13
diff -u -d -r1.12 -r1.13
--- ClientMain.py	9 Dec 2002 06:11:01 -0000	1.12
+++ ClientMain.py	11 Dec 2002 05:53:33 -0000	1.13
@@ -32,15 +32,15 @@
 import types
 
 from mixminion.Common import LOG, floorDiv, createPrivateDir, MixError, \
-     MixFatalError
+     MixFatalError, isSMTPMailbox
 import mixminion.Crypto
 import mixminion.BuildMessage
 import mixminion.MMTPClient
 import mixminion.Modules
 from mixminion.ServerInfo import ServerInfo
 from mixminion.Config import ClientConfig, ConfigError
-from mixminion.Packet import ParseError, parseMBOXInfo, parseSMTPInfo
-from mixminion.Modules import MBOX_TYPE, SMTP_TYPE, DROP_TYPE
+from mixminion.Packet import ParseError, parseMBOXInfo, parseSMTPInfo, \
+     MBOX_TYPE, SMTP_TYPE, DROP_TYPE
 
 class TrivialKeystore:
     """This is a temporary keystore implementation until we get a working
@@ -300,9 +300,9 @@
     elif s.lower() == 'test':
 	return Address(0xFFFE, "", None)
     elif ':' not in s:
-	try:
-	    return Address(SMTP_TYPE, parseSMTPInfo(s).pack(), None)
-	except ParseError:
+	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()

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.30
retrieving revision 1.31
diff -u -d -r1.30 -r1.31
--- Common.py	9 Dec 2002 04:47:39 -0000	1.30
+++ Common.py	11 Dec 2002 05:53:33 -0000	1.31
@@ -7,13 +7,15 @@
 
 __all__ = [ 'MixError', 'MixFatalError', 'onReset', 'onTerminate',
             'installSignalHandlers', 'secureDelete', 'secureRename',
-            'ceilDiv', 'floorDiv', 'LOG', 'stringContains' ]
+            'ceilDiv', 'floorDiv', 'LOG', 'stringContains',
+	    'formatDate', 'formatTime', 'isSMTPMailbox']
 
 import os
 import signal
 import sys
 import time
 import stat
+import re
 import statvfs
 import traceback
 import calendar
@@ -54,6 +56,7 @@
     return divmod(a-1,b)[0]+1
 
 #----------------------------------------------------------------------
+# String handling
 
 # We create an alias to make the intent of substring-checking code
 # more explicit.  It's a bit easier to remember "stringContains(s1,
@@ -66,6 +69,27 @@
        s1[i:i+len(s2)] == s2"""
     return s1.find(s2) != -1
 
+# String containing characters from "\x00" to "\xFF"; used by 'isPrintingAscii'
+_ALLCHARS = "".join(map(chr, range(256)))
+# String containing all printing ascii characters; used by 'isPrintingAscii'
+_P_ASCII_CHARS = "\t\n\v\r"+"".join(map(chr, range(0x20, 0x7F)))
+# String containing all printing ascii characters, and all characters that
+# may be used in an extended charset.
+_P_ASCII_CHARS_HIGH = "\t\n\v\r"+"".join(map(chr, range(0x20, 0x7F)+
+					          range(0x80, 0xFF)))
+
+def isPrintingAscii(s,allowISO=0):
+    """Return true iff every character in s is a printing ascii character.
+       If allowISO is true, also permit characters between 0x80 and 0xFF."""
+    if allowISO:
+	return len(s.translate(_ALLCHARS, _P_ASCII_CHARS_HIGH)) == 0
+    else:
+	return len(s.translate(_ALLCHARS, _P_ASCII_CHARS)) == 0
+
+def stripSpace(s, space=" \t\v\n"):
+    """Remove all whitespace from s."""
+    return s.translate(_ALLCHARS, space)
+
 #----------------------------------------------------------------------
 def createPrivateDir(d, nocreate=0):
     """Create a directory, and all parent directories, checking permissions
@@ -478,6 +502,44 @@
     yyyy,MM,dd = time.gmtime(when)[0:3]
     return calendar.timegm((yyyy,MM,dd,0,0,0,0,0,0))
 
+def formatTime(when,localtime=0):
+    """Given a time in seconds since the epoch, returns a time value in the
+       format used by server descriptors (YYYY/MM/DD HH:MM:SS) in GMT"""
+    if localtime:
+	gmt = time.localtime(when)
+    else:
+	gmt = time.gmtime(when)
+    return "%04d/%02d/%02d %02d:%02d:%02d" % (
+	gmt[0],gmt[1],gmt[2],  gmt[3],gmt[4],gmt[5])
+
+def formatDate(when):
+    """Given a time in seconds since the epoch, returns a date value in the
+       format used by server descriptors (YYYY/MM/DD) in GMT"""
+    gmt = time.gmtime(when+1) # Add 1 to make sure we round down.
+    return "%04d/%02d/%02d" % (gmt[0],gmt[1],gmt[2])
+
+#----------------------------------------------------------------------
+# SMTP address functionality
+
+# Regular expressions to valide RFC822 addresses.
+# (This is more strict than RFC822, actually.  RFC822 allows tricky stuff to
+#  quote special characters, and I don't trust every MTA or delivery command
+#  to support addresses like <bob@bob."; rm -rf /; echo".com>)
+ 
+# An 'Atom' is a non-escape, non-null, non-space, non-punctuation character.
+_ATOM_PAT = r'[^\x00-\x20()\[\]()<>@,;:\\".\x7f-\xff]+'
+# The 'Local part' (and, for us, the domain portion too) is a sequence of
+# dot-separated atoms.
+_LOCAL_PART_PAT = r"(?:%s)(?:\.(?:%s))*" % (_ATOM_PAT, _ATOM_PAT)
+# A mailbox is two 'local parts' separated by an @ sign.
+_RFC822_PAT = r"\A%s@%s\Z" % (_LOCAL_PART_PAT, _LOCAL_PART_PAT)
+RFC822_RE = re.compile(_RFC822_PAT)
+
+def isSMTPMailbox(s):
+    """Return true iff s is a valid SMTP address"""
+    m = RFC822_RE.match(s)
+    return m is not None
+     
 #----------------------------------------------------------------------
 # Signal handling
 

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.23
retrieving revision 1.24
diff -u -d -r1.23 -r1.24
--- Config.py	9 Dec 2002 06:11:01 -0000	1.23
+++ Config.py	11 Dec 2002 05:53:33 -0000	1.24
@@ -55,15 +55,10 @@
 from cStringIO import StringIO
 
 import mixminion.Common
-from mixminion.Common import MixError, LOG
+from mixminion.Common import MixError, LOG, isPrintingAscii, stripSpace
 import mixminion.Packet
 import mixminion.Crypto
 
-# String with all characters 0..255; used for str.translate
-_ALLCHARS = "".join(map(chr, range(256)))
-# String with all printing ascii characters; used for str.translate
-_GOODCHARS = "".join(map(chr, range(0x07,0x0e)+range(0x20,0x80)))
-
 class ConfigError(MixError):
     """Thrown when an error is found in a configuration file."""
     pass
@@ -228,7 +223,7 @@
 def _parseBase64(s,_hexmode=0):
     """Validation function.  Converts a base-64 encoded config value into
        its original. Raises ConfigError on failure."""
-    s = s.translate(_ALLCHARS, " \t\v\n")
+    s = stripSpace(s)
     try:
 	if _hexmode:
 	    return binascii.a2b_hex(s)
@@ -352,10 +347,10 @@
     lineno = 0
 
     # Make sure all characters in the file are ASCII.
-    badchars = contents.translate(_ALLCHARS, _GOODCHARS)
-    if badchars:
+    if not isPrintingAscii(contents):
 	raise ConfigError("Invalid characters in file: %r", badchars)
 
+    #FFFF We should really use xreadlines or something if we have a file.
     fileLines = contents.split("\n")
     if fileLines[-1] == '':
 	del fileLines[-1]
@@ -777,6 +772,10 @@
 	return self.moduleManager
 
 def _validateHostSection(sec):
-    # FFFF001
+    """Helper function: Makes sure that the shared [Host] section is correct;
+       raise ConfigError if it isn't"""
+    # For now, we do nothing here.  EntropySource and ShredCommand are checked
+    # in configure_trng and configureShredCommand, respectively.
     pass
+
 

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.25
retrieving revision 1.26
diff -u -d -r1.25 -r1.26
--- Crypto.py	9 Dec 2002 04:47:40 -0000	1.25
+++ Crypto.py	11 Dec 2002 05:53:33 -0000	1.26
@@ -403,6 +403,10 @@
 # Passed to the delivery module
 APPLICATION_KEY_MODE = "APPLICATION KEY"
 
+# Used by the sender to encrypt the payload when sending an encrypted forward
+#  message
+END_TO_END_ENCRYPT_MODE = "END-TO-END ENCRYPT"
+
 #----------------------------------------------------------------------
 # Key generation
 

Index: Modules.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Modules.py,v
retrieving revision 1.23
retrieving revision 1.24
diff -u -d -r1.23 -r1.24
--- Modules.py	9 Dec 2002 04:47:40 -0000	1.23
+++ Modules.py	11 Dec 2002 05:53:33 -0000	1.24
@@ -6,12 +6,12 @@
    Code to support pluggable exit module functionality; implementation
    for built-in modules.
    """
-# FFFF We may, someday, want to support non-exit modules.
+# FFFF We may, someday, want to support non-exit modules here.
+# FFFF Maybe we should refactor MMTP delivery here too.
 
 __all__ = [ 'ModuleManager', 'DeliveryModule',
-	    'DROP_TYPE', 'FWD_TYPE', 'SWAP_FWD_TYPE',
-	    'DELIVER_OK', 'DELIVER_FAIL_RETRY', 'DELIVER_FAIL_NORETRY',
-	    'SMTP_TYPE', 'MBOX_TYPE' ]
+	    'DELIVER_OK', 'DELIVER_FAIL_RETRY', 'DELIVER_FAIL_NORETRY'
+	    ]
 
 import os
 import re
@@ -25,27 +25,14 @@
 import mixminion.Queue
 import mixminion.BuildMessage
 from mixminion.Config import ConfigError, _parseBoolean, _parseCommand
-from mixminion.Common import LOG, createPrivateDir, MixError
+from mixminion.Common import LOG, createPrivateDir, MixError, isSMTPMailbox, \
+     isPrintingAscii
 
 # Return values for processMessage
 DELIVER_OK = 1
 DELIVER_FAIL_RETRY = 2
 DELIVER_FAIL_NORETRY = 3
 
-# Numerically first exit type.
-MIN_EXIT_TYPE  = 0x0100
-
-# XXXX001 move these into Packet.py ===================================START
-# 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
-
-# Exit types
-SMTP_TYPE      = 0x0100  # Mail the message
-MBOX_TYPE      = 0x0101  # Send the message to one of a fixed list of addresses
-# XXXX001 move these into Packet.py =====================================END
-
 class DeliveryModule:
     """Abstract base for modules; delivery modules should implement
        the methods in this class.
@@ -361,7 +348,7 @@
     def getName(self):
         return "DROP"
     def getExitTypes(self):
-        return [ DROP_TYPE ]
+        return [ mixminion.Packet.DROP_TYPE ]
     def createDeliveryQueue(self, directory):
 	return ImmediateDeliveryQueue(self)
     def processMessage(self, message, tag, exitType, exitInfo):
@@ -403,25 +390,35 @@
                  }
 
     def validateConfig(self, sections, entries, lines, contents):
-        # XXXX001 write this.  Parse address file.
-        pass
+	sec = sections['Delivery/MBOX']
+	if not sec.get('Enabled'):
+	    return
+	for field in ['AddressFile', 'ReturnAddress', 'RemoveContact',
+		      'SMTPServer']:
+	    if not sec.get(field):
+		raise ConfigError("Missing field %s in [Delivery/MBOX]"%field)
+	if not os.path.exists(sec['AddressFile']):
+	    raise ConfigError("Address file %s seems not to exist."%
+			      sec['AddresFile'])
+	for field in ['ReturnAddress', 'RemoveContact']:
+	    if not isSMTPMailbox(sec[field]):
+		LOG.warn("Value of %s (%s) doesn't look like an email address",
+			 field, sec[field])
+	    
 
     def configure(self, config, moduleManager):
-        # XXXX001 Check this.  Conside error handling
 	if not config['Delivery/MBOX'].get("Enabled", 0):
 	    moduleManager.disableModule(self)
 	    return
 
-	self.server = config['Delivery/MBOX']['SMTPServer']
-	self.addressFile = config['Delivery/MBOX']['AddressFile']
-	self.returnAddress = config['Delivery/MBOX']['ReturnAddress']
-	self.contact = config['Delivery/MBOX']['RemoveContact']
-	if not self.addressFile:
-	    raise ConfigError("Missing AddressFile field in Delivery/MBOX")
-	if not self.returnAddress:
-	    raise ConfigError("Missing ReturnAddress field in Delivery/MBOX")
-	if not self.contact:
-	    raise ConfigError("Missing RemoveContact field in Delivery/MBOX")
+	sec = config['Delivery/MBOX']
+	self.server = sec['SMTPServer']
+	self.addressFile = sec['AddressFile']
+	self.returnAddress = sec['ReturnAddress']
+	self.contact = sec['RemoveContact']
+	# validate should have caught these.
+	assert (self.server and self.addressFile and self.returnAddress
+		and self.contact)
 
         self.nickname = config['Server']['Nickname']
         if not self.nickname:
@@ -464,11 +461,11 @@
         return "MBOX"
 
     def getExitTypes(self):
-        return [ MBOX_TYPE ]
+        return [ mixminion.Packet.MBOX_TYPE ]
 
     def processMessage(self, message, tag, exitType, address):
 	# Determine that message's address;
-        assert exitType == MBOX_TYPE
+        assert exitType == mixminion.Packet.MBOX_TYPE
         LOG.trace("Received MBOX message")
         info = mixminion.Packet.parseMBOXInfo(address)
 	try:
@@ -513,7 +510,7 @@
     def getName(self):
         return "SMTP"
     def getExitTypes(self):
-        return (SMTP_TYPE,)
+        return [ mixminion.Packet.SMTP_TYPE ]
 
 class MixmasterSMTPModule(SMTPModule):
     """Implements SMTP by relaying messages via Mixmaster nodes.  This
@@ -546,8 +543,9 @@
                  }
                    
     def validateConfig(self, sections, entries, lines, contents):
-        #FFFF001 implement
+	# Currently, we accept any configuration options that the config allows
         pass
+
     def configure(self, config, manager):
         sec = config['Delivery/SMTP-Via-Mixmaster']
 	if not sec.get("Enabled", 0):
@@ -573,7 +571,7 @@
 
     def processMessage(self, message, tag, exitType, smtpAddress):
 	"""Insert a message into the Mixmaster queue"""
-        assert exitType == SMTP_TYPE
+        assert exitType == mixminion.Packet.SMTP_TYPE
 	# parseSMTPInfo will raise a parse error if the mailbox is invalid.
         info = mixminion.Packet.parseSMTPInfo(smtpAddress)
 
@@ -627,18 +625,6 @@
 
 #----------------------------------------------------------------------
 
-# XXXX001 There's another function like this in config.  
-# DOCDOC
-_allChars = "".join(map(chr, range(256)))
-# DOCDOC
-# ????001 Are there any nonprinting chars >= 0x7f to worry about now?
-_nonprinting = "".join(map(chr, range(0x00, 0x07)+range(0x0E, 0x20)))
-
-def isPrintable(s):
-    """Return true iff s consists only of printable characters."""
-    printable = s.translate(_allChars, _nonprinting)
-    return len(printable) == len(s)
-
 def _escapeMessageForEmail(msg, tag):
     """Helper function: Given a message and tag, escape the message if
        it is not plaintext ascii, and wrap it in some standard
@@ -694,7 +680,7 @@
 	code = "ENC"
     else:
 	assert tag is None
-	if isPrintable(message):
+	if isPrintingAscii(message, allowISO=1):
 	    code = "TXT"
 	else:
 	    code = "BIN"

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.19
retrieving revision 1.20
diff -u -d -r1.19 -r1.20
--- Packet.py	9 Dec 2002 04:47:40 -0000	1.19
+++ Packet.py	11 Dec 2002 05:53:33 -0000	1.20
@@ -18,13 +18,14 @@
             'parseReplyBlock', 'ENC_SUBHEADER_LEN', 'HEADER_LEN',
             'PAYLOAD_LEN', 'MAJOR_NO', 'MINOR_NO', 'SECRET_LEN', 'TAG_LEN',
 	    'SINGLETON_PAYLOAD_OVERHEAD', 'OAEP_OVERHEAD',
-	    'FRAGMENT_PAYLOAD_OVERHEAD', 'ENC_FWD_OVERHEAD']
+	    'FRAGMENT_PAYLOAD_OVERHEAD', 'ENC_FWD_OVERHEAD',
+	    'DROP_TYPE', 'FWD_TYPE', 'SWAP_FWD_TYPE',
+	    'SMTP_TYPE', 'MBOX_TYPE', 'MIN_EXIT_TYPE'
+]
 
-import re
 import struct
 from socket import inet_ntoa, inet_aton
-from mixminion.Common import MixError, floorDiv
-import mixminion.Modules
+from mixminion.Common import MixError, floorDiv, isSMTPMailbox
 
 # Major and minor number for the understood packet format.
 MAJOR_NO, MINOR_NO = 0,1
@@ -58,6 +59,19 @@
 # Most info that fits in a single extened subheader
 ROUTING_INFO_PER_EXTENDED_SUBHEADER = ENC_SUBHEADER_LEN
 
+#----------------------------------------------------------------------
+# 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
+
+# Exit types
+MIN_EXIT_TYPE  = 0x0100  # The numerically first exit type.
+SMTP_TYPE      = 0x0100  # Mail the message
+MBOX_TYPE      = 0x0101  # Send the message to one of a fixed list of addresses
+MAX_EXIT_TYPE  = 0xFFFF
+
 class ParseError(MixError):
     """Thrown when a message or portion thereof is incorrectly formatted."""
     pass
@@ -144,7 +158,7 @@
     ri = s[MIN_SUBHEADER_LEN:]
     if rlen < len(ri):
         ri = ri[:rlen]
-    if rt >= mixminion.Modules.MIN_EXIT_TYPE and rlen < 20:
+    if rt >= MIN_EXIT_TYPE and rlen < 20:
 	raise ParseError("Subheader missing tag")
     return Subheader(major,minor,secret,digest,rt,ri,rlen)
 
@@ -191,16 +205,14 @@
     def getExitAddress(self):
 	"""Return the part of the routingInfo that contains the delivery
 	   address.  (Requires that routingType is an exit type.)"""
-	# XXXX001 SPEC This is not explicit in the spec.
-	assert self.routingtype >= mixminion.Modules.MIN_EXIT_TYPE
+	assert self.routingtype >= MIN_EXIT_TYPE
 	assert len(self.routinginfo) >= TAG_LEN
 	return self.routinginfo[TAG_LEN:]
     
     def getTag(self):
 	"""Return the part of the routingInfo that contains the decoding
 	   tag. (Requires that routingType is an exit type.)"""
-	# XXXX001 SPEC This is not explicit in the spec.
-	assert self.routingtype >= mixminion.Modules.MIN_EXIT_TYPE
+	assert self.routingtype >= MIN_EXIT_TYPE
 	assert len(self.routinginfo) >= TAG_LEN
 	return self.routinginfo[:TAG_LEN]
 
@@ -276,7 +288,6 @@
 # Number of bytes taken up from OAEP padding in an encrypted forward
 # payload, minus bytes saved by spilling the RSA-encrypted block into the
 # tag, minus the bytes taken by the session key.
-# XXXX001 (The e2e note is off by 4.)
 ENC_FWD_OVERHEAD = OAEP_OVERHEAD - TAG_LEN + SECRET_LEN
 
 def parsePayload(payload):
@@ -469,24 +480,9 @@
 	return (type(self) == type(other) and self.ip == other.ip and
 		self.port == other.port and self.keyinfo == other.keyinfo)
 
-# Regular expressions to valide RFC822 addresses.
-# (This is more strict than RFC822, actually.  RFC822 allows tricky
-#  stuff to quote special characters, and I don't trust every MTA or
-#  delivery command to support addresses like <bob@bob."; rm -rf /; echo".com>)
- 
-# An 'Atom' is a non-escape, non-null, non-space, non-punctuation character.
-_ATOM_PAT = r'[^\x00-\x20()\[\]()<>@,;:\\".\x7f-\xff]+'
-# The 'Local part' (and, for us, the domain portion too) is a sequence of
-# dot-separated atoms.
-_LOCAL_PART_PAT = r"(?:%s)(?:\.(?:%s))*" % (_ATOM_PAT, _ATOM_PAT)
-# A mailbox is two 'local parts' separated by an @ sign.
-_RFC822_PAT = r"\A%s@%s\Z" % (_LOCAL_PART_PAT, _LOCAL_PART_PAT)
-RFC822_RE = re.compile(_RFC822_PAT)
-
 def parseSMTPInfo(s):
     """Convert the encoding of an SMTP exitinfo into an SMTPInfo object."""
-    m = RFC822_RE.match(s)
-    if not m:
+    if not isSMTPMailbox(s):
 	raise ParseError("Invalid rfc822 mailbox %r" % s)
     return SMTPInfo(s)
 

Index: PacketHandler.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/PacketHandler.py,v
retrieving revision 1.11
retrieving revision 1.12
diff -u -d -r1.11 -r1.12
--- PacketHandler.py	9 Dec 2002 04:47:40 -0000	1.11
+++ PacketHandler.py	11 Dec 2002 05:53:33 -0000	1.12
@@ -5,7 +5,6 @@
 
 import mixminion.Crypto as Crypto
 import mixminion.Packet as Packet
-import mixminion.Modules as Modules
 import mixminion.Common as Common
 
 __all__ = [ 'PacketHandler', 'ContentError' ]
@@ -115,7 +114,7 @@
 
         # If we're meant to drop, drop now.
         rt = subh.routingtype
-        if rt == Modules.DROP_TYPE:
+        if rt == Packet.DROP_TYPE:
             return None
 
         # Prepare the key to decrypt the header in counter mode.  We'll be
@@ -126,7 +125,7 @@
         # decrypt and parse them now.
         if subh.isExtended():
             nExtra = subh.getNExtraBlocks() 
-            if (rt < Modules.MIN_EXIT_TYPE) or (nExtra > 15):
+            if (rt < Packet.MIN_EXIT_TYPE) or (nExtra > 15):
                 # None of the native methods allow multiple blocks; no
                 # size can be longer than the number of bytes in the rest
                 # of the header.
@@ -145,7 +144,7 @@
 
         # If we're an exit node, there's no need to process the headers
         # further.
-        if rt >= Modules.MIN_EXIT_TYPE:
+        if rt >= Packet.MIN_EXIT_TYPE:
             return ("EXIT",
                     (rt, subh.getExitAddress(),
                      keys.get(Crypto.APPLICATION_KEY_MODE),
@@ -154,7 +153,7 @@
 
         # If we're not an exit node, make sure that what we recognize our
         # routing type.
-        if rt not in (Modules.SWAP_FWD_TYPE, Modules.FWD_TYPE):
+        if rt not in (Packet.SWAP_FWD_TYPE, Packet.FWD_TYPE):
             raise ContentError("Unrecognized Mixminion routing type")
 
         # Pad the rest of header 1
@@ -172,7 +171,7 @@
         # If we're the swap node, (1) decrypt the payload with a hash of 
 	# header2... (2) decrypt header2 with a hash of the payload...
 	# (3) and swap the headers.
-        if rt == Modules.SWAP_FWD_TYPE:
+        if rt == Packet.SWAP_FWD_TYPE:
 	    hkey = Crypto.lioness_keys_from_header(header2)
 	    payload = Crypto.lioness_decrypt(payload, hkey)
 

Index: Queue.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Queue.py,v
retrieving revision 1.23
retrieving revision 1.24
diff -u -d -r1.23 -r1.24
--- Queue.py	9 Dec 2002 04:47:40 -0000	1.23
+++ Queue.py	11 Dec 2002 05:53:33 -0000	1.24
@@ -204,25 +204,36 @@
            Returns 1 if a clean is already in progress; otherwise
            returns 0.
         """
-	# XXXX001 This is race-prone if multiple processes sometimes try to 
-	# XXXX001   clean the same queue.  Use O_EXCL, Luke.
-        now = time.time()
 
+        now = time.time()
         cleanFile = os.path.join(self.dir,".cleaning")
-        try:
-            s = os.stat(cleanFile)
-            if now - s[stat.ST_MTIME] > CLEAN_TIMEOUT:
-                cleaning = 0
-            cleaning = 1
-        except OSError:
-            cleaning = 0
 
-        if cleaning:
-            return 1
-
-        f = open(cleanFile, 'w')
-        f.write(str(now))
-        f.close()
+	cleaning = 1
+	while cleaning:
+	    try:
+		# Try to get the .cleaning lock file.  If we can create it,
+		# we're the only cleaner around.
+		fd = os.open(cleanFile, os.O_WRONLY+os.O_CREAT+os.O_EXCL, 0600)
+		os.write(fd, str(now))
+		os.close(fd)
+		cleaning = 0
+	    except OSError:
+		try:
+		    # If we can't create the file, see if it's too old.  If it
+		    # is too old, delete it and try again.  If it isn't, there
+		    # may be a live clean in progress.
+		    s = os.stat(cleanFile)
+		    if now - s[stat.ST_MTIME] > CLEAN_TIMEOUT:
+			os.unlink(cleanFile)
+		    else:
+			return 1
+		except OSError:
+		    # If the 'stat' or 'unlink' calls above fail, then 
+		    # .cleaning must not exist, or must not be readable
+		    # by us.
+		    if os.path.exists(cleanFile):
+			# In the latter case, bail out.
+			return 1
 
         rmv = []
         allowedTime = int(time.time()) - INPUT_TIMEOUT

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.23
retrieving revision 1.24
diff -u -d -r1.23 -r1.24
--- ServerInfo.py	9 Dec 2002 04:47:40 -0000	1.23
+++ ServerInfo.py	11 Dec 2002 05:53:33 -0000	1.24
@@ -15,11 +15,12 @@
 import base64
 import socket
 
-from mixminion.Common import createPrivateDir, LOG, MixError
-from mixminion.Modules import SWAP_FWD_TYPE, FWD_TYPE
+from mixminion.Common import createPrivateDir, LOG, MixError, formatTime, \
+     formatDate
 from mixminion.Packet import IPV4Info
 import mixminion.Config
 import mixminion.Crypto
+from mixminion.Crypto import DIGEST_LEN
 
 ConfigError = mixminion.Config.ConfigError
 
@@ -112,13 +113,32 @@
 	digest = getServerInfoDigest(contents)
 	if digest != server['Digest']:
 	    raise ConfigError("Invalid digest")
-	
+
+	# Check signature
 	if digest != mixminion.Crypto.pk_check_signature(server['Signature'],
 							 identityKey):
 	    raise ConfigError("Invalid signature")
 
-	#### XXXX001 CHECK OTHER SECTIONS
+	## Incoming/MMTP section
+	inMMTP = sections['Incoming/MMTP']
+	if inMMTP:
+	    if inMMTP['Version'] != '0.1':
+		raise ConfigError("Unrecognized MMTP descriptor version %s"%
+				  inMMTP['Version'])
+	    if len(inMMTP['Key-Digest']) != DIGEST_LEN:
+		raise ConfigError("Invalid key digest %s"%
+				  base64.endodestring(inMMTP['Key-Digest']))
+	
+	## Outgoing/MMTP section
+	outMMTP = sections['Outgoing/MMTP']
+	if outMMTP:
+	    if outMMTP['Version'] != '0.1':
+		raise ConfigError("Unrecognized MMTP descriptor version %s"%
+				  inMMTP['Version'])
 
+	# FFFF When a better client module system exists, check the 
+	# FFFF module descriptors.
+	
     def getNickname(self):
 	"""Returns this server's nickname"""
 	return self['Server']['Nickname']
@@ -204,21 +224,6 @@
     "Helper function: returns a one-line base64 encoding of a given string."
     return base64.encodestring(s).replace("\n", "")
 
-def _time(t):
-    #XXXX001 move this to common.
-    """Helper function: turns a time (in seconds) into the format used by
-       Server descriptors"""
-    gmt = time.gmtime(t)
-    return "%04d/%02d/%02d %02d:%02d:%02d" % (
-	gmt[0],gmt[1],gmt[2],  gmt[3],gmt[4],gmt[5])
-
-def _date(t):
-    #XXXX001 move this to common.
-    """Helper function: turns a time (in seconds) into a date in the format
-       used by server descriptors"""
-    gmt = time.gmtime(t+1) # Add 1 to make sure we round down.
-    return "%04d/%02d/%02d" % (gmt[0],gmt[1],gmt[2])
-
 def _rule(allow, (ip, mask, portmin, portmax)):
     """Return an external represenntation of an IP allow/deny rule."""
     if mask == '0.0.0.0':
@@ -304,9 +309,9 @@
 	"Nickname": nickname,
 	"Identity":
 	   _base64(mixminion.Crypto.pk_encode_public_key(identityKey)),
-	"Published": _time(time.time()),
-	"ValidAfter": _date(validAt),
-	"ValidUntil": _date(validUntil),
+	"Published": formatTime(time.time()),
+	"ValidAfter": formatDate(validAt),
+	"ValidUntil": formatDate(validUntil),
 	"PacketKey":
   	   _base64(mixminion.Crypto.pk_encode_public_key(packetKey)),
 	"KeyID":

Index: ServerMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerMain.py,v
retrieving revision 1.18
retrieving revision 1.19
diff -u -d -r1.18 -r1.19
--- ServerMain.py	9 Dec 2002 04:47:40 -0000	1.18
+++ ServerMain.py	11 Dec 2002 05:53:33 -0000	1.19
@@ -19,10 +19,10 @@
 import mixminion.Crypto
 import mixminion.Queue
 import mixminion.MMTPServer
-from mixminion.ServerInfo import ServerKeyset, ServerInfo, _date, _time, \
+from mixminion.ServerInfo import ServerKeyset, ServerInfo, \
      generateServerDescriptorAndKeys
 from mixminion.Common import LOG, MixFatalError, MixError, secureDelete, \
-     createPrivateDir, previousMidnight, ceilDiv
+     createPrivateDir, previousMidnight, ceilDiv, formatDate, formatTime
 
 
 class ServerKeyring:
@@ -119,7 +119,7 @@
                 t2 = inf['Server']['Valid-Until']
                 self.keyIntervals.append( (t1, t2, keysetname) )
 		LOG.debug("Found key %s (valid from %s to %s)",
-			       dirname, _date(t1), _date(t2))
+			       dirname, formatDate(t1), formatDate(t2))
 	    else:
 		LOG.warn("No server descriptor found for key %s"%dirname)
 
@@ -134,10 +134,10 @@
 	    start = self.keyIntervals[idx+1][0]
 	    if start < end:
 		LOG.warn("Multiple keys for %s.  That's unsupported.",
-			      _date(end))
+			      formatDate(end))
 	    elif start > end:
 		LOG.warn("Gap in key schedule: no key from %s to %s",
-			      _date(end), _date(start))
+			      formatDate(end), formatDate(start))
 
 	self.nextKeyRotation = 0 # Make sure that now > nextKeyRotation before
 	                         # we call _getLiveKey()
@@ -213,7 +213,8 @@
 	    nextStart = startAt + self.config['Server']['PublicKeyLifetime'][2]
 
 	    LOG.info("Generating key %s to run from %s through %s (GMT)",
-			  keyname, _date(startAt), _date(nextStart-3600))
+		     keyname, formatDate(startAt), 
+		     formatDate(nextStart-3600))
  	    generateServerDescriptorAndKeys(config=self.config,
 					    identityKey=self.getIdentityKey(),
 					    keyname=keyname,
@@ -240,7 +241,7 @@
 
 	for dirname, (va, vu, name) in zip(dirs, self.keyIntervals):
             LOG.info("Removing%s key %s (valid from %s through %s)",
-                        expiryStr, name, _date(va), _date(vu-3600))
+                        expiryStr, name, formatDate(va), formatDate(vu-3600))
 	    files = [ os.path.join(dirname,f)
                                  for f in os.listdir(dirname) ]
 	    secureDelete(files, blocking=1)
@@ -536,7 +537,7 @@
 	#FFFF Unused
 	#nextRotate = self.keyring.getNextKeyRotation()
 	while 1:
-	    LOG.trace("Next mix at %s", _time(nextMix))
+	    LOG.trace("Next mix at %s", formatTime(nextMix,1))
 	    while time.time() < nextMix:
 		# Handle pending network events
 		self.mmtpServer.process(1)

Index: benchmark.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/benchmark.py,v
retrieving revision 1.15
retrieving revision 1.16
diff -u -d -r1.15 -r1.16
--- benchmark.py	9 Dec 2002 04:47:40 -0000	1.15
+++ benchmark.py	11 Dec 2002 05:53:33 -0000	1.16
@@ -382,7 +382,7 @@
     def logHash(self,h): pass
 
 from mixminion.PacketHandler import PacketHandler
-from mixminion.Modules import SMTP_TYPE
+from mixminion.Packet import SMTP_TYPE
 
 def serverProcessTiming():
     print "#================= SERVER PROCESS ====================="

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.44
retrieving revision 1.45
diff -u -d -r1.44 -r1.45
--- test.py	9 Dec 2002 06:11:01 -0000	1.44
+++ test.py	11 Dec 2002 05:53:33 -0000	1.45
@@ -183,6 +183,19 @@
 	    # 4. That previousMidnight is idempotent
 	    self.assertEquals(previousMidnight(pm), pm)
 
+    def test_isSMTPMailbox(self):
+	from mixminion.Common import isSMTPMailbox
+	# Do we accept good addresses?
+	for addr in "Foo@bar.com", "a@b", "a@b.c.d.e", "a!b.c@d", "z@z":
+	    self.assert_(isSMTPMailbox(addr))
+
+	# Do we reject bad addresses?
+	for addr in ("(foo)@bar.com", "z.d" "z@", "@z", "@foo.com", "aaa",
+		     "foo.bar@", "foo\177@bar.com", "foo@bar\177.com",
+		     "foo@bar;cat /etc/shadow;echo ","foo bar@baz.com",
+		     "a@b@c"):
+	    self.assert_(not isSMTPMailbox(addr))
+
 #----------------------------------------------------------------------
 import mixminion._minionlib as _ml
 
@@ -729,18 +742,6 @@
         self.failUnlessRaises(ParseError, parseIPV4Info, ri[:-1])
         self.failUnlessRaises(ParseError, parseIPV4Info, ri+"x")
 
-    def test_smtpinfo(self):
-	# Do we accept good addresses?
-	for addr in "Foo@bar.com", "a@b", "a@b.c.d.e", "a!b.c@d", "z@z":
-	    self.assertEquals(parseSMTPInfo(addr).pack(), addr)
-
-	# Do we reject bad addresses?
-	for addr in ("(foo)@bar.com", "z.d" "z@", "@z", "@foo.com", "aaa",
-		     "foo.bar@", "foo\177@bar.com", "foo@bar\177.com",
-		     "foo@bar;cat /etc/shadow;echo ","foo bar@baz.com",
-		     "a@b@c"):
-	    self.failUnlessRaises(ParseError, parseSMTPInfo, addr)
-
     def test_replyblock(self):
 	# Try parsing an example 'reply block' object
 	key = "\x99"*16
@@ -1373,20 +1374,20 @@
 	    sessionkey, rsa_rest = mrsa[:16], mrsa[16:]
 	    ks = Keyset(sessionkey)
 	    msg = rsa_rest + lioness_decrypt(mrest,
-			      ks.getLionessKeys("End-to-end encrypt"))
+			      ks.getLionessKeys("END-TO-END ENCRYPT"))
 	    comp = BuildMessage.compressData(payload)
 	    self.assert_(len(comp), ord(msg[0])*256 + ord(msg[1]))
 	    self.assertEquals(sha1(msg[22:]), msg[2:22])
 	    self.assert_(msg[22:].startswith(comp))
 
     def test_buildreply(self):
+        brbi = BuildMessage._buildReplyBlockImpl
         brb = BuildMessage.buildReplyBlock
-        bsrb = BuildMessage.buildStatelessReplyBlock
         brm = BuildMessage.buildReplyMessage
 
         ## Stateful reply blocks.
         reply, secrets_1, tag_1 = \
-             brb([self.server3, self.server1, self.server2,
+             brbi([self.server3, self.server1, self.server2,
                   self.server1, self.server3],
                  SMTP_TYPE,
 		 "no-such-user@invalid", tag=("+"*20))
@@ -1421,7 +1422,7 @@
                              "Information???",
 			     decoder=decoder)
         ## Stateless replies
-        reply = bsrb([self.server3, self.server1, self.server2,
+        reply = brb([self.server3, self.server1, self.server2,
                       self.server1, self.server3], MBOX_TYPE,
                      "fred", "Tyrone Slothrop", 0)
 
@@ -1506,7 +1507,7 @@
 	efwd = (comp+"RWE/HGW"*4096)[:28*1024-22-38]
 	efwd = '\x00\x6D'+sha1(efwd)+efwd
 	rsa1 = self.pk1
-	key1 = Keyset("RWE "*4).getLionessKeys("End-to-end encrypt")
+	key1 = Keyset("RWE "*4).getLionessKeys("END-TO-END ENCRYPT")
 	efwd_rsa = pk_encrypt(("RWE "*4)+efwd[:70], rsa1)
 	efwd_lioness = lioness_encrypt(efwd[70:], key1)
 	efwd_t = efwd_rsa[:20]
@@ -1548,44 +1549,47 @@
 	decodePayload = BuildMessage.decodePayload
 	# fwd
 	for pk in (self.pk1, None):
-	    for d in (sdict, None):
+	    ##for d in (sdict, None): # stateful replies disabled.
 		for p in (passwd, None):
 		    for tag in ("zzzz"*5, "pzzz"*5):
 			self.assertEquals(payload,
-					  decodePayload(encoded1, tag,pk,d,p))
+					  decodePayload(encoded1, tag, pk, p))
 
 	# efwd
-	for d in (sdict, None):
+	##for d in (sdict, None): # stateful replies disabled
+	if 1:
 	    for p in (passwd, None):
 		self.assertEquals(payload,
-		        decodePayload(efwd_p, efwd_t, self.pk1, d,p))
+		        decodePayload(efwd_p, efwd_t, self.pk1, p))
 		self.assertEquals(None,
-		        decodePayload(efwd_p, efwd_t, None, d,p))
+		        decodePayload(efwd_p, efwd_t, None, p))
 		self.assertEquals(None,
-		        decodePayload(efwd_p, efwd_t, self.pk2, d,p))
+		        decodePayload(efwd_p, efwd_t, self.pk2, p))
 
-	# repl (stateful)
-	sdict2 = { 'tag2'*5 : [secrets] + [ '\x00\xFF'*8] }
-	for pk in (self.pk1, None):
-	    for p in (passwd, None):
-		sd = sdict.copy()
-		self.assertEquals(payload,
-		       decodePayload(repl1, "tag1"*5, pk, sd, p))
-		self.assert_(not sd)
-		self.assertEquals(None,
-		       decodePayload(repl1, "tag1"*5, pk, None, p))
-		self.assertEquals(None,
-		       decodePayload(repl1, "tag1"*5, pk, sdict2, p))
+	# Stateful replies are disabled.
+
+## 	# repl (stateful)
+## 	sdict2 = { 'tag2'*5 : [secrets] + [ '\x00\xFF'*8] }
+## 	for pk in (self.pk1, None):
+## 	    for p in (passwd, None):
+## 		sd = sdict.copy()
+## 		self.assertEquals(payload,
+## 		       decodePayload(repl1, "tag1"*5, pk, sd, p))
+## 		self.assert_(not sd)
+## 		self.assertEquals(None,
+## 		       decodePayload(repl1, "tag1"*5, pk, None, p))
+## 		self.assertEquals(None,
+## 		       decodePayload(repl1, "tag1"*5, pk, sdict2, p))
 
 	# repl (stateless)
 	for pk in (self.pk1, None):
-	    for sd in (sdict, None):
+	    #for sd in (sdict, None): #Stateful replies are disabled
 		self.assertEquals(payload,
-			    decodePayload(repl2, repl2tag, pk, sd, passwd))
+			    decodePayload(repl2, repl2tag, pk, passwd))
 		self.assertEquals(None,
-			    decodePayload(repl2, repl2tag, pk, sd,"Bliznerty"))
+			    decodePayload(repl2, repl2tag, pk, "Bliznerty"))
 		self.assertEquals(None,
-			    decodePayload(repl2, repl2tag, pk, sd,None))
+			    decodePayload(repl2, repl2tag, pk, None))
 
 	# And now the cases that fail hard.  This can only happen on:
 	#   1) *: Hash checks out, but zlib or size is wrong.  Already tested.
@@ -1598,28 +1602,29 @@
 	self.failUnlessRaises(MixError,
 			      BuildMessage._decodeEncryptedForwardPayload,
 			      efwd_pbad, efwd_t, self.pk1)
-	for d in (sdict, None):
+	#for d in (sdict, None):
+	if 1:
 	    for p in (passwd, None):
 		self.failUnlessRaises(MixError, decodePayload,
-				      efwd_pbad, efwd_t, self.pk1, d, p)
+				      efwd_pbad, efwd_t, self.pk1, p)
 		self.assertEquals(None,
-			  decodePayload(efwd_pbad, efwd_t, self.pk2, d,p))
+			  decodePayload(efwd_pbad, efwd_t, self.pk2, p))
 
-	# Bad repl
-	repl1_bad = repl1[:-1] + chr(ord(repl1[-1])^0xaa)
-	for pk in (self.pk1, None):
-	    for p in (passwd, None):
-		sd = sdict.copy()
-		self.failUnlessRaises(MixError,
-			 decodePayload, repl1_bad, "tag1"*5, pk, sd, p)
-		sd = sdict.copy()
-		self.failUnlessRaises(MixError,
-			 BuildMessage._decodeReplyPayload, repl1_bad,
-				      sd["tag1"*5])
+## 	# Bad repl
+## 	repl2_bad = repl2[:-1] + chr(ord(repl1[-1])^0xaa)
+## 	for pk in (self.pk1, None):
+## 	    for p in (passwd, None):
+## 		#sd = sdict.copy()
+## 		self.failUnlessRaises(MixError,
+## 			 decodePayload, repl1_bad, "tag1"*5, pk, p)
+## 		#sd = sdict.copy()
+## 		self.failUnlessRaises(MixError,
+## 			 BuildMessage._decodeReplyPayload, repl1_bad,
+## 				      sd["tag1"*5])
 	# Bad srepl
 	repl2_bad = repl2[:-1] + chr(ord(repl2[-1])^0xaa)
 	self.assertEquals(None,
-		  decodePayload(repl2_bad, repl2tag, None, None, passwd))
+		  decodePayload(repl2_bad, repl2tag, None, passwd))
 
 #----------------------------------------------------------------------
 # Having tested BuildMessage without using PacketHandler, we can now use
@@ -1732,7 +1737,7 @@
     def test_rejected(self):
         bfm = BuildMessage.buildForwardMessage
         brm = BuildMessage.buildReplyMessage
-        brb = BuildMessage.buildReplyBlock
+        brbi = BuildMessage._buildReplyBlockImpl
         from mixminion.PacketHandler import ContentError
 
         # A long intermediate header needs to fail.
@@ -1753,7 +1758,7 @@
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m)
 
         # Duplicate reply blocks need to fail
-        reply,s,tag = brb([self.server3], SMTP_TYPE, "fred@invalid")
+        reply,s,tag = brbi([self.server3], SMTP_TYPE, "fred@invalid")
         m = brm("Y", [self.server2], reply)
         m2 = brm("Y", [self.server1], reply)
         q, (a,m) = self.sp2.processMessage(m)
@@ -1763,9 +1768,9 @@
 
         # Even duplicate secrets need to go.
         prng = AESCounterPRNG(" "*16)
-        reply1,s,t = brb([self.server1], SMTP_TYPE, "fred@invalid",0,prng)
+        reply1,s,t = brbi([self.server1], SMTP_TYPE, "fred@invalid",0,prng)
         prng = AESCounterPRNG(" "*16)
-        reply2,s,t = brb([self.server2], MBOX_TYPE, "foo",0,prng)
+        reply2,s,t = brbi([self.server2], MBOX_TYPE, "foo",0,prng)
         m = brm("Y", [self.server3], reply1)
         m2 = brm("Y", [self.server3], reply2)
         q, (a,m) = self.sp3.processMessage(m)
@@ -1794,11 +1799,14 @@
 
         # Bad internal type
 	try:
-	    save = mixminion.Modules.SWAP_FWD_TYPE
-	    mixminion.Modules.SWAP_FWD_TYPE = 50
+	    # (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
 	    m_x = bfm("Z", 500, "", [self.server1], [self.server2])
 	finally:
-	    mixminion.Modules.SWAP_FWD_TYPE = save
+	    mixminion.BuildMessage.SWAP_FWD_TYPE = save
         self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
 
         # Subhead we can't parse
@@ -2270,7 +2278,9 @@
 import mixminion.MMTPServer
 import mixminion.MMTPClient
 
-TEST_PORT = 40102
+# Run on a different port so we don't conflict with any actual servers
+# running on this machine.
+TEST_PORT = 40199
 
 dhfile = pkfile = certfile = None
 
@@ -2857,6 +2867,7 @@
 class TestModule(mixminion.Modules.DeliveryModule):
     def __init__(self):
 	self.processedMessages = []
+	self.processedAll = []
     def getName(self):
 	return "TestModule"
     def getConfigSyntax(self):
@@ -2883,6 +2894,7 @@
 	return (1234,)
     def processMessage(self, message, tag, exitType, exitInfo):
 	self.processedMessages.append(message)
+	self.processedAll.append( (message, tag, exitType, exitInfo) )
 	if exitInfo == 'fail?':
 	    return mixminion.Modules.DELIVER_FAIL_RETRY
 	elif exitInfo == 'fail!':
@@ -2935,7 +2947,7 @@
 	manager.queueMessage("Hello 2", t, 1234, "fail?")
 	manager.queueMessage("Hello 3", t, 1234, "good")
 	manager.queueMessage("Drop very much", None,
-			     mixminion.Modules.DROP_TYPE, t)
+			     mixminion.Packet.DROP_TYPE, t)
 	queue = manager.queues['TestModule']
 	# Did the test module's delivery queue get the messages?
 	self.failUnless(isinstance(queue,
@@ -2959,6 +2971,39 @@
 	self.assertEquals(4, len(exampleMod.processedMessages))
 	self.assertEquals("Hello 2", exampleMod.processedMessages[-1])
 
+	# But, none of them was decodeable: all of them should have been
+	# tagged as 'err'
+	self.assertEquals('err', exampleMod.processedAll[0][1])
+
+	# Try a real message, to make sure that we really decode stuff properly
+	msg = mixminion.BuildMessage._encodePayload(
+	    "A man disguised as an ostrich, actually.",
+	    0, Crypto.getCommonPRNG())
+	manager.queueMessage(msg, "A"*20, 1234, "Hello")
+	exampleMod.processedAll = []
+	manager.sendReadyMessages()
+	# The retriable message got sent again; the other one, we care about.
+	pos = None
+	for i in xrange(len(exampleMod.processedAll)):
+	    if not exampleMod.processedAll[i][0].startswith('Hello'):
+		pos = i
+	self.assert_(pos is not None)
+	self.assertEquals(exampleMod.processedAll[i],
+			  ("A man disguised as an ostrich, actually.",
+			   None, 1234, "Hello" ))
+
+	# Now a non-decodeable message
+	manager.queueMessage("XYZZYZZY"*3584, "Z"*20, 1234, "Buenas noches")
+	exampleMod.processedAll = []
+	manager.sendReadyMessages()
+	pos = None
+	for i in xrange(len(exampleMod.processedAll)):
+	    if not exampleMod.processedAll[i][0].startswith('Hello'):
+		pos = i
+	self.assert_(pos is not None)
+	self.assertEquals(exampleMod.processedAll[i],
+			  ("XYZZYZZY"*3584, "Z"*20, 1234, "Buenas noches"))
+
 	# Check serverinfo generation.
 	try:
 	    suspendLog()
@@ -3317,10 +3362,11 @@
 	Crypto.pk_PEM_save(identity, fn)
 
 	# Now create a keyset
-	keyring.createKeys(1)
+	now = time.time()
+	keyring.createKeys(1, now)
 	# check internal state
 	ivals = keyring.keyIntervals
-	start = mixminion.Common.previousMidnight(time.time())
+	start = mixminion.Common.previousMidnight(now)
 	finish = mixminion.Common.previousMidnight(start+(10*24*60*60)+30)
 	self.assertEquals(1, len(ivals))
 	self.assertEquals((start,finish,"0001"), ivals[0])
@@ -3345,15 +3391,14 @@
 
 	# Make a key in the past, to see if it gets scrubbed.
 	keyring.createKeys(1, mixminion.Common.previousMidnight(
-	    start - 10*24*60*60 +60))
+	    start - 10*24*60*60 + 1))
 	self.assertEquals(4, len(keyring.keyIntervals))
         waitForChildren() # make sure keys are really gone before we remove
-	keyring.removeDeadKeys()
-	#XXXX001 Sometimes this fails, and says that len(keyring.keyIntervals)
-	#        is 4.  It may be time-related; it tends to fail once or
-	#        twice in rapid succession, then work for a while.  (Last time
-	#        it failed as around 7:00-7:06 Eastern -- just after midnight
-	#        GMT.  This bears thinking!
+
+        # In case we started very close to midnight, remove keys as if it
+	# were a little in the future; otherwise, we won't remove the 
+	# just-expired keys.
+	keyring.removeDeadKeys(now+360) 
 	self.assertEquals(3, len(keyring.keyIntervals))
 
 	if USE_SLOW_MODE: