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

[minion-cvs] Mixminion now has a working (but not bulletproof) reply...



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

Modified Files:
	ClientMain.py Config.py Crypto.py Main.py Packet.py test.py 
Log Message:
Mixminion now has a working (but not bulletproof) reply block CLI.

ClientMain:
	- Implement first pass of message decoding
	- Implement first pass of CLI for SURB generation
	- Implement CLI for sending messages to SURBs
	- Minor bugfixes to new code

Config:
	- Lower default surb path length

Crypto:
	- Suppress pychecker warning

Main:
	- Add new 'decode' and 'generate-surb' commands

Packet:
	- Debug text-encoded message parsing

test:
	- Test text-encoded message parsing

Modules, *:
	 - Rename ascii-encoded messages to text-encoded messages


Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.45
retrieving revision 1.46
diff -u -d -r1.45 -r1.46
--- ClientMain.py	4 Feb 2003 02:38:23 -0000	1.45
+++ ClientMain.py	5 Feb 2003 05:34:55 -0000	1.46
@@ -9,7 +9,8 @@
          support replies and end-to-end encryption.
    """
 
-__all__ = []
+__all__ = [ 'Address', 'ClientKeyring', 'ClientKeystore', 'MixminionClient',
+    'parsePath', ]
 
 # (NOTE: The stuff in the next comment isn't implemented yet.)
 # The client needs to store:
@@ -35,13 +36,14 @@
 from mixminion.Common import IntervalSet, LOG, floorDiv, MixError, \
      MixFatalError, ceilDiv, createPrivateDir, isSMTPMailbox, formatDate, \
      formatFnameTime, formatTime, openUnique, previousMidnight, \
-     readPossiblyGzippedFile
+     readPossiblyGzippedFile, stringContains
 from mixminion.Crypto import sha1, ctr_crypt, trng
 
 from mixminion.Config import ClientConfig, ConfigError
 from mixminion.ServerInfo import ServerInfo, ServerDirectory
-from mixminion.Packet import ParseError, parseMBOXInfo, parseSMTPInfo, \
-     MBOX_TYPE, SMTP_TYPE, DROP_TYPE
+from mixminion.Packet import ParseError, parseMBOXInfo, parseReplyBlock, \
+     parseSMTPInfo, parseTextEncodedMessage, parseTextReplyBlocks, MBOX_TYPE, \
+     SMTP_TYPE, DROP_TYPE
 
 # FFFF This should be made configurable and adjustable.
 MIXMINION_DIRECTORY_URL = "http://www.mixminion.net/directory/latest.gz";
@@ -599,7 +601,7 @@
         return startServers + midServers + endServers
 
 def resolvePath(keystore, address, enterPath, exitPath,
-                nHops, nSwap, startAt=None, endAt=None):
+                nHops, nSwap, startAt=None, endAt=None, halfPath=0):
     """Compute a two-leg validated path from options as entered on
        the command line.
 
@@ -613,7 +615,12 @@
        we raise MixError.
        """
     # First, find out what the exit node needs to be (or support).
-    routingType, _, exitNode = address.getRouting()
+    if address is None:
+        routingType = None
+        exitNode = None
+    else:
+        routingType, _, exitNode = address.getRouting()
+        
     if exitNode:
         exitNode = keystore.getServerInfo(exitNode, startAt, endAt)
     if routingType == MBOX_TYPE:
@@ -646,12 +653,12 @@
         nSwap = ceilDiv(len(path),2)-1
 
     path1, path2 = path[:nSwap+1], path[nSwap+1:]
-    if not path1 or not path2:
+    if not halfPath and (not path1 or not path2):
         raise MixError("Each leg of the path must have at least 1 hop")
     return path1, path2
 
 def parsePath(keystore, config, path, address, nHops=None,
-              nSwap=None, startAt=None, endAt=None):
+              nSwap=None, startAt=None, endAt=None, halfPath=0):
     """Resolve a path as specified on the command line.  Returns a
        (path-leg-1, path-leg-2) tuple.
 
@@ -766,7 +773,7 @@
 
     # Finally, resolve the path.
     return resolvePath(keystore, address, enterPath, exitPath,
-                       myNHops, myNSwap, startAt, endAt)
+                       myNHops, myNSwap, startAt, endAt, halfPath=halfPath)
 
 class ClientKeyring:
     "DOCDOC"
@@ -775,14 +782,15 @@
         createPrivateDir(self.keyDir)
         self.surbKey = None
 
-    def getSURBKey(self):
+    def getSURBKey(self, create=0):
         if self.surbKey is not None:
             return self.surbKey
         fn = os.path.join(self.keyDir, "SURBKey")
-        self.surbKey = self._getKey(fn, magic="SURBKEY0", which="reply block")
+        self.surbKey = self._getKey(fn, magic="SURBKEY0", which="reply block",
+                                    create=create)
         return self.surbKey
 
-    def _getKey(self, fn, magic, which, bytes=20):
+    def _getKey(self, fn, magic, which, bytes=20, create=0):
         if os.path.exists(fn):
             self._checkMagic(fn, magic)
             while 1:
@@ -790,16 +798,18 @@
                 try:
                     return self._load(fn, magic, p)
                 except MixError, e:
-                    LOG.error("Cannot load key", e)
-        else:
+                    LOG.error("Cannot load key: %s", e)
+        elif create:
             LOG.warn("No %s key found; generating.", which)
             key = trng(bytes)
             p = self._getNewPassword(which)
             self._save(fn, key, magic, p)
             return key
+        else:
+            return None
 
-    def _checkMagic(fn, magic):
-        f = open(rn, 'rb')
+    def _checkMagic(self, fn, magic):
+        f = open(fn, 'rb')
         s = f.read()
         f.close()
         if not s.startswith(magic):
@@ -812,7 +822,7 @@
         f.write(magic)
         f.write(ctr_crypt(data+sha1(data+magic), sha1(password)[:16]))
         f.close()
-    
+
     def _load(self, fn, magic, password):
         f = open(fn, 'rb')
         s = f.read()
@@ -825,7 +835,7 @@
         if hash != sha1(data+magic):
             raise MixError("Incorrect password")
         return data
-        
+
     def _getPassword(self, which):
         s = "Enter password for %s:"%which
         p = getpass.getpass(s)
@@ -864,6 +874,9 @@
 
 [Security]
 PathLength: 4
+#SURBAddress: XXXX003
+#SURBPathLength: 3 DOCDOC
+#SURBLifetime: 7 days DOCDOC
 
 [Network]
 ConnectionTimeout: 20 seconds
@@ -905,11 +918,21 @@
 
         self.sendMessages([message], firstHop)
 
+    def sendReplyMessage(self, payload, servers, surb):
+        """
+        DOCDOC
+        """
+        message, firstHop = \
+                 self.generateReplyMessage(payload, servers, surb)
+        self.sendMessages([message], firstHop)
+
     def generateReplyBlock(self, address, servers, expiryTime=0):
-        #DOCDOC
-        key = self.keys.getSURBKey()
+        """
+        DOCDOC
+        """
+        key = self.keys.getSURBKey(create=1)
         exitType, exitInfo, _ = address.getRouting()
-        
+
         block = mixminion.BuildMessage.buildReplyBlock(
             servers, exitType, exitInfo, key, expiryTime)
 
@@ -932,6 +955,15 @@
             self.prng)
         return msg, servers1[0]
 
+    def generateReplyMessage(self, payload, servers, surb):
+        """
+        DOCDOC
+        """
+        LOG.info("Generating payload...")
+        msg = mixminion.BuildMessage.buildReplyMessage(
+            payload, servers, surb, self.prng)
+        return msg, servers[0]
+
     def sendMessages(self, msgList, server):
         """Given a list of packets and a ServerInfo object, sends the
            packets to the server via MMTP"""
@@ -950,6 +982,27 @@
         except socket.error, e:
             raise MixError("Error sending packets: %s" % e)
 
+    def decodeMessage(self, s, force=0):
+        "DOCDOC"
+        #XXXX003 DOCDOC Exceptions
+        results = []
+        idx = 0
+        while idx < len(s):
+            msg, idx = parseTextEncodedMessage(s, idx=idx, force=force)
+            if msg is None:
+                return results
+            if msg.isOvercompressed() and not force:
+                LOG.warn("Message is a possible zlib bomb; not uncompressing")
+            if not msg.isEncrypted():
+                results.append(msg.getContents())
+            else:
+                surbKey = self.keys.getSURBKey(create=0)
+                results.append(
+                    mixminion.BuildMessage.decodePayload(msg.getContents(),
+                                                         tag=msg.getTag(),
+                                                         userKey=surbKey))
+        return results
+
 def parseAddress(s):
     """Parse and validate an address; takes a string, and returns an Address
        object.
@@ -1089,16 +1142,16 @@
     print _SEND_USAGE % { 'cmd' : "mixminion send" }
     sys.exit(0)
 
-# NOTE: This isn't anything LIKE the final client interface.  Many or all
-#       options will change between now and 1.0.0
+# NOTE: This isn't the final client interface.  Many or all options will
+#     change between now and 1.0.0
 def runClient(cmd, args):
     if cmd.endswith(" client"):
         print "The 'client' command is deprecated.  Use 'send' instead."
 
-    options, args = getopt.getopt(args, "hvf:i:t:H:P:D:",
+    options, args = getopt.getopt(args, "hvf:i:t:H:P:D:R:",
                                   ["help", "verbose", "config=", "input=",
-                                   "to=", "hops=", "swap-at=", "path",
-                                   "download-directory=",
+                                   "to=", "hops=", "swap-at=", "path=",
+                                   "download-directory=", "reply-block=",
                                   ])
     if not options:
         usageAndExit(cmd)
@@ -1110,6 +1163,8 @@
     nSwap = None
     address = None
     download = None
+    replyBlock = None
+    surb = None
     for opt,val in options:
         if opt in ('-h', '--help'):
             usageAndExit(cmd)
@@ -1148,7 +1203,8 @@
             else:
                 usageAndExit(cmd,
                       "Unrecognized value for %s. Expected 'yes' or 'no'"%opt)
-
+        elif opt in ('-R', '--reply-block'):
+            replyBlock = val
     if args:
         usageAndExit(cmd,"Unexpected arguments")
 
@@ -1168,15 +1224,37 @@
     if download != 0:
         keystore.updateDirectory(forceDownload=download)
 
-    if address is None:
+    if address is None and replyBlock is None:
         print >>sys.stderr, "No recipients specified; exiting."
         sys.exit(0)
+    elif address is not None and replyBlock is not None:
+        print >>sys.stderr, "Cannot specify both a recipient and a reply block"
+        sys.exit(0)
+    elif address is not None:
+        useRB = 0
+    else:
+        useRB = 1
+        f = open(replyBlock, 'rb')
+        s = f.read()
+        f.close()
+        if stringContains(s, "== BEGIN TYPE III REPLY BLOCK =="):
+            surb = parseTextReplyBlocks(s)[0] #????003
+        else:
+            surb = parseReplyBlock(s)
 
     try:
-        path1, path2 = parsePath(keystore, config, path, address, nHops, nSwap)
-        LOG.info("Selected path is %s:%s",
-                 ",".join([ s.getNickname() for s in path1 ]),
-                 ",".join([ s.getNickname() for s in path2 ]))
+        if useRB:
+            e, path1 = parsePath(keystore, config, path, address, nHops,
+                                 nSwap=-1, halfPath=1)
+            assert e == []
+            LOG.info("Selected path is %s:<reply block>",
+                     ",".join([ s.getNickname() for s in path1 ]))
+        else:
+            path1, path2 = parsePath(keystore, config, path, address, nHops,
+                                     nSwap)
+            LOG.info("Selected path is %s:%s",
+                     ",".join([ s.getNickname() for s in path1 ]),
+                     ",".join([ s.getNickname() for s in path2 ]))
     except MixError, e:
         print >>sys.stderr, e
         sys.exit(1)
@@ -1184,11 +1262,11 @@
     client = MixminionClient(config)
 
     # XXXX Clean up this ugly control structure.
-    if inFile is None and address.getRouting()[0] == DROP_TYPE:
+    if address and inFile is None and address.getRouting()[0] == DROP_TYPE:
         payload = None
         LOG.info("Sending dummy message")
     else:
-        if address.getRouting()[0] == DROP_TYPE:
+        if address and address.getRouting()[0] == DROP_TYPE:
             LOG.error("Cannot send a payload with a DROP message.")
             sys.exit(0)
 
@@ -1208,7 +1286,10 @@
             print "Interrupted.  Message not sent."
             sys.exit(1)
 
-    client.sendForwardMessage(address, payload, path1, path2)
+    if useRB:
+        client.sendReplyMessage(payload, path1, surb)
+    else:
+        client.sendForwardMessage(address, payload, path1, path2)
 
     LOG.info("Message sent")
 
@@ -1277,7 +1358,7 @@
     config = readConfigFile(configFile)
     LOG.configure(config)
     LOG.setMinSeverity("INFO")
-    
+
     userdir = os.path.expanduser(config['User']['UserDir'])
     keystore = ClientKeystore(userdir)
     keystore.updateDirectory(forceDownload=download)
@@ -1306,9 +1387,195 @@
     config = readConfigFile(configFile)
     LOG.configure(config)
     LOG.setMinSeverity("INFO")
-    
+
     userdir = os.path.expanduser(config['User']['UserDir'])
     keystore = ClientKeystore(userdir)
 
     keystore.updateDirectory(forceDownload=1)
     print "Directory updated"
+
+_CLIENT_DECODE_USAGE = """\
+Usage: %s [options] <files>
+Options:
+  -h, --help:                Print this usage message and exit.
+  -f <file>, --config=<file> Use a configuration file other than ~/.mixminionrc
+                             (You can also use MIXMINIONRC=FILE)
+  -F, --force:               Decode the input files, even if they seem
+                             overcompressed.
+  -o <file>, --output=<file> Write the results to <file> rather than stdout.
+""".strip()
+
+def clientDecode(cmd, args):
+    options, args = getopt.getopt(args, "hf:o:Fi:",
+                                  ['help', 'config=', 'output=', 'force',
+                                   'input='])    
+    configFile = None
+    outputFile = '-'
+    inputFile = None
+    force = 0
+    for o,v in options:
+        if o in ('-h', '--help'):
+            print _CLIENT_DECODE_USAGE % cmd
+            sys.exit(1)
+        elif o in ('-f', '--config'):
+            configFile = v
+        elif o in ('-o', '--output'):
+            outputFile = v
+        elif o in ('-F', '--force'):
+            force = 1
+        elif o in ('-i', '--input'):
+            inputFile = v
+
+    if not inputFile:
+        print >> sys.stderr, "Error: No input file specified"
+        sys.exit(1)
+        
+    if outputFile == '-':
+        out = sys.stdout
+    else:
+        # ????003 Should we sometimes open this in text mode?
+        out = open(outputFile, 'wb')
+
+    config = readConfigFile(configFile)
+    LOG.configure(config)
+    LOG.setMinSeverity("INFO")
+    client = MixminionClient(config)
+
+    mixminion.Crypto.init_crypto(config)
+    
+    if inputFile == '-':
+        s = sys.stdin.read()
+    else:
+        try:
+            f = open(inputFile, 'r')
+            s = f.read()
+            f.close()
+        except OSError, e:
+            LOG.error("Could not read file %s: %s", fn, e)
+    # XXXX003 catch exceptions
+    res = client.decodeMessage(s, force=force)
+    for r in res:
+        out.write(r)
+    out.close()
+
+def generateSURB(cmd, args):
+    options, args = getopt.getopt(args, "hf:o:t:H:P:D:vb",
+                                  ['help', 'config=', 'output=', 'to=',
+                                  'hops=', 'path=', 'download-directory=',
+                                  'verbose', 'days=', 'binary'])
+    configFile = None
+    outputFile = '-'
+    address = None
+    nHops = None
+    path = None
+    download = None
+    verbose = 0
+    days = None
+    binary = 0
+    for o,v in options:
+        if o in ('-h', '--help'):
+            print _GENERATE_SURB_USAGE % cmd
+            sys.exit(1)
+        elif o in ('-f', '--config'):
+            configFile = v
+        elif o in ('-o', '--output'):
+            outputFile = v
+        elif o in ('-v', '--verbose'):
+            verbose = 1
+        elif o in ('-t', '--address'):
+            try:
+                address = parseAddress(v)
+            except ParseError, e:
+                print >>sys.stderr, e
+                sys.exit(1)
+        elif o in ('-H', '--hops'):
+            try:
+                nHops = int(v)
+                if nHops < 2:
+                    usageAndExit(cmd, "Must have at least 2 hops")
+            except ValueError:
+                usageAndExit(cmd, "%s expects an integer"%o)
+        elif o in ('-P', '--path'):
+            path = v
+        elif o in ('-D', '--download-directory'):
+            download = v.lower()
+            if download in ('0','no','false','n','f'):
+                download = 0
+            elif download in ('1','yes','true','y','t','force'):
+                download = 1
+            else:
+                print >>sys.stderr, (
+                    "Unrecognized value for %s. Expected 'yes' or 'no'"%o)
+        elif o == '--days':
+            try:
+                days = int(v)
+            except ValueError:
+                usageAndExit(cmd, "%s expects an integer"%o)
+        elif o in ('-b', '--binary'):
+            binary = 1
+    if args:
+        print >>sys.stderr, "Unexpected arguments"
+        sys.exit(1)
+
+    config = readConfigFile(configFile)
+    LOG.configure(config)
+    if verbose:
+        LOG.setMinSeverity("TRACE")
+    else:
+        LOG.setMinSeverity("INFO")
+
+    LOG.debug("Configuring client")
+    mixminion.Common.configureShredCommand(config)
+    mixminion.Crypto.init_crypto(config)
+    client = MixminionClient(config)
+    userdir = os.path.expanduser(config['User']['UserDir'])
+    keystore = ClientKeystore(userdir)
+    if download != 0:
+        keystore.updateDirectory(forceDownload=download)
+
+    if address is None:
+        address = config['Security'].get('SURBAddress')
+        if address is None:
+            print >>sys.stderr, "No recipient specified; exiting."
+            sys.exit(1)
+        try:
+            address = parseAddress(address)
+        except ParseError, e:
+            print >>sys.stderr, \
+                  "Recipient in configuration file is invalid: %s"%e
+            sys.exit(1)
+
+    if days is not None:
+        endTime = previousMidnight(time.time() + days * 24*60*60 - 60)
+    else:
+        endTime = previousMidnight(time.time() +
+                      config['Security']['SURBLifetime'][2] + 24*60*60 - 60)
+
+    if nHops is None and not path:
+        nHops = config['Security']['SURBPathLength']
+        
+    try:
+        e,path1 = parsePath(keystore, config, path, address, nHops, nSwap=-1,
+                           startAt=time.time(), endAt=endTime,
+                           halfPath=1)
+        assert e == []
+        LOG.info("Selected path is %s",
+                 ",".join([ s.getNickname() for s in path1 ]))
+    except MixError, e:
+        print >>sys.stderr, e
+        sys.exit(1)
+
+    if outputFile == '-':
+        out = sys.stdout
+    elif binary:
+        out = open(outputFile, 'wb')
+    else:
+        #XXXX003 handle exception
+        out = open(outputFile, 'w')
+
+    surb = client.generateReplyBlock(address, path1, endTime)
+    if binary:
+        out.write(surb.pack())
+    else:
+        out.write(surb.packAsText())
+    out.close()

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.37
retrieving revision 1.38
diff -u -d -r1.37 -r1.38
--- Config.py	4 Feb 2003 02:03:35 -0000	1.37
+++ Config.py	5 Feb 2003 05:34:55 -0000	1.38
@@ -717,7 +717,7 @@
         'User' : { 'UserDir' : ('ALLOW', None, "~/.mixminion" ) },
         'Security' : { 'PathLength' : ('ALLOW', _parseInt, "8"),
                        'SURBAddress' : ('ALLOW', None, None),
-                       'SURBPathLength' : ('ALLOW', _parseInt, "8"),
+                       'SURBPathLength' : ('ALLOW', _parseInt, "4"),
                        'SURBLifetime' : ('ALLOW', _parseInterval, "7 days") },
         'Network' : { 'ConnectionTimeout' : ('ALLOW', _parseInterval, None) }
         }

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.38
retrieving revision 1.39
diff -u -d -r1.38 -r1.39
--- Crypto.py	4 Feb 2003 02:08:37 -0000	1.38
+++ Crypto.py	5 Feb 2003 05:34:55 -0000	1.39
@@ -587,7 +587,7 @@
                 return os.fdopen(fd, mode), base
             except OSError, e:
                 if e.errno != errno.EEXIST:
-                    raise
+                    raise e
                 # If the file exists (a rare event!) we pass through, and
                 # try again.  This paranoia is brought to you by user
                 # request. :)

Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.25
retrieving revision 1.26
diff -u -d -r1.25 -r1.26
--- Main.py	10 Jan 2003 20:12:05 -0000	1.25
+++ Main.py	5 Feb 2003 05:34:55 -0000	1.26
@@ -120,6 +120,8 @@
     "import-server" :  ( 'mixminion.ClientMain', 'importServer' ),
     "list-servers" :   ( 'mixminion.ClientMain', 'listServers' ),
     "update-servers" : ( 'mixminion.ClientMain', 'updateServers' ),
+    "decode" :         ( 'mixminion.ClientMain', 'clientDecode' ),
+    "generate-surb" :  ( 'mixminion.ClientMain', 'generateSURB' ),
     "server" :         ( 'mixminion.server.ServerMain', 'runServer' ),
     "server-keygen" :  ( 'mixminion.server.ServerMain', 'runKeygen'),
     "server-DELKEYS" : ( 'mixminion.server.ServerMain', 'removeKeys'),
@@ -135,6 +137,8 @@
   "       import-server  [Tell the client about a new server]\n"+
   "       list-servers   [Print a list of currently known servers]\n"+
   "       update-servers [Download a fresh server directory]\n"+
+  "       decode         [Decode or decrypt a received message]\n"+
+  "       generate-surb  [Generate a single-use reply block]\n"+
   "                               (For Servers)\n"+
   "       server         [Begin running a Mixminon server]\n"+
   "       server-keygen  [Generate keys for a Mixminion server]\n"+

Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.24
retrieving revision 1.25
diff -u -d -r1.24 -r1.25
--- Packet.py	4 Feb 2003 02:38:23 -0000	1.24
+++ Packet.py	5 Feb 2003 05:34:55 -0000	1.25
@@ -9,18 +9,18 @@
    packets, see BuildMessage.py.  For functions that handle
    server-side processing of packets, see PacketHandler.py."""
 
-__all__ = [ 'ParseError', 'Message', 'Header', 'Subheader', 'parseMessage',
-            'parseHeader', 'parseSubheader',
-            'getTotalBlocksForRoutingInfoLen', 'parsePayload',
-            'SingletonPayload', 'FragmentPayload', 'ReplyBlock', 'IPV4Info',
-            'SMTPInfo', 'MBOXInfo', 'parseIPV4Info', 'parseSMTPInfo',
-            'parseMBOXInfo', 'ReplyBlock', 'parseReplyBlock',
-            'parseTextReplyBlocks', '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', 'DROP_TYPE',
-            'FWD_TYPE', 'SWAP_FWD_TYPE', 'SMTP_TYPE', 'MBOX_TYPE',
-            'MIN_EXIT_TYPE'
+__all__ = [ 'DROP_TYPE', 'ENC_FWD_OVERHEAD', 'ENC_SUBHEADER_LEN',
+            'FRAGMENT_PAYLOAD_OVERHEAD', 'FWD_TYPE', 'FragmentPayload',
+            'HEADER_LEN', 'Header', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
+            'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE', 'Message',
+            'OAEP_OVERHEAD', 'PAYLOAD_LEN', 'ParseError', 'ReplyBlock',
+            'ReplyBlock', 'SECRET_LEN', 'SINGLETON_PAYLOAD_OVERHEAD',
+            'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_TYPE', 'SingletonPayload',
+            'Subheader', 'TAG_LEN', 'TextEncodedMessage',
+            'getTotalBlocksForRoutingInfoLen', 'parseHeader', 'parseIPV4Info',
+            'parseMBOXInfo', 'parseMessage', 'parsePayload',
+            'parseReplyBlock', 'parseSMTPInfo', 'parseSubheader',
+            'parseTextEncodedMessage', 'parseTextReplyBlocks'
           ]
 
 import base64
@@ -29,7 +29,8 @@
 import struct
 from socket import inet_ntoa, inet_aton
 import mixminion.BuildMessage
-from mixminion.Common import MixError, floorDiv, isSMTPMailbox, LOG
+from mixminion.Common import MixError, MixFatalError, floorDiv, isSMTPMailbox,\
+     LOG
 
 # Major and minor number for the understood packet format.
 MAJOR_NO, MINOR_NO = 0,1  #XXXX003 Bump minor_no for 0.0.3
@@ -396,8 +397,9 @@
 #   routingInfo for the last server.
 RB_UNPACK_PATTERN = "!4sBBL%dsHH%ss" % (HEADER_LEN, SECRET_LEN)
 MIN_RB_LEN = 30+HEADER_LEN
-RB_TEXT_START = "======= BEGIN TYPE III REPLY BLOCK ========"
-RB_TEXT_END   = "======== END TYPE III REPLY BLOCK ========="
+# XXXX003 handle input with differing number of ='s.
+RB_TEXT_START = "======= BEGIN TYPE III REPLY BLOCK ======="
+RB_TEXT_END   = "======== END TYPE III REPLY BLOCK ========"
 RB_TEXT_RE = re.compile(RB_TEXT_START+
                         r'[\r\n]+Version: (\d+.\d+)\s*[\r\n]+(.*)[\r\n]+'+
                         RB_TEXT_END, re.M) 
@@ -551,67 +553,73 @@
 #----------------------------------------------------------------------
 # Ascii-encoded packets
 
-MESSAGE_START_LINE = "======= TYPE III ANONYMOUS MESSAGE BEGINS ========"
-MESSAGE_END_LINE   = "======== TYPE III ANONYMOUS MESSAGE ENDS ========="
+#XXXX003 accept lines with different #'s of equal signs.
+MESSAGE_START_LINE = "======= TYPE III ANONYMOUS MESSAGE BEGINS ======="
+MESSAGE_END_LINE   = "======== TYPE III ANONYMOUS MESSAGE ENDS ========"
+_MESSAGE_START_RE  = re.compile(r"==+ TYPE III ANONYMOUS MESSAGE BEGINS ==+")
+_MESSAGE_END_RE    = re.compile(r"==+ TYPE III ANONYMOUS MESSAGE ENDS ==+")
 _FIRST_LINE_RE = re.compile(r'''^Decoding-handle:\s(.*)\r*\n|
                                  Message-type:\s(.*)\r*\n''', re.X+re.S)
-_LINE_RE = re.compile(r'[^\r\n]+\r*\n', re.S)
+_LINE_RE = re.compile(r'[^\r\n]*\r*\n', re.S+re.M)
 
 def _nextLine(s, idx):
-    m = _LINE_RE.match(s)
+    m = _LINE_RE.match(s[idx:])
     if m is None:
         return len(s)
     else:
-        return m.end()
+        return m.end()+idx
 
-def getMessageContents(msg,force=0,idx=0):
-    """ Returns
-            ( 'TXT'|'ENC'|'LONG'|'BIN', tag|None, message, end-idx )
+def parseTextEncodedMessage(msg,force=0,idx=0):
+    """ DOCDOC
     """
-    idx = msg.find(MESSAGE_START_LINE)
-    if idx < 0:
-        raise ParseError("No begin line found")
-    endIdx = msg.find(MESSAGE_END_LINE, idx)
-    if endIdx < 0:
+    #idx = msg.find(MESSAGE_START_PAT, idx)
+    m = _MESSAGE_START_RE.search(msg[idx:])
+    if m is None:
+        return None, None
+    idx += m.start()
+    m = _MESSAGE_END_RE.search(msg[idx:])
+    if m is None:
         raise ParseError("No end line found")
+    msgEndIdx = idx+m.start()
     idx = _nextLine(msg, idx)
     firstLine = msg[idx:_nextLine(msg, idx)]
     m = _FIRST_LINE_RE.match(firstLine)
     if m is None:
         msgType = 'TXT'
     elif m.group(1):
+        # XXXX003 enforce length
         ascTag = m.group(1)
-        msgType = "ENC" #XXXX003 refactor
-        idx = firstLine
+        msgType = "ENC" 
+        idx = _nextLine(msg, idx)
     elif m.group(2):
         if m.group(2) == 'overcompressed':
-            msgType = 'LONG' #XXXX003 refactor
+            msgType = 'LONG' 
         elif m.group(2) == 'binary':
             msgType = 'BIN' #XXXX003 refactor
         else:
             raise ParseError("Unknown message type: %r"%m.group(2))
-        idx = firstLine
+        idx = _nextLine(msg, idx)
 
-    msg = msg[idx:endIdx]
-    endIdx = _nextLine(endIdx)
+    endIdx = _nextLine(msg, msgEndIdx)
+    msg = msg[idx:msgEndIdx]
 
     if msgType == 'TXT':
-        return 'TXT', None, msg, endIdx
+        return TextEncodedMessage(msg, 'TXT'), endIdx
 
     msg = binascii.a2b_base64(msg) #XXXX May raise
     if msgType == 'BIN':
-        return 'BIN', None, msg, endIdx
+        return TextEncodedMessage(msg, 'BIN'), endIdx
     elif msgType == 'LONG':
         if force:
             msg = mixminion.BuildMessage.uncompressData(msg) #XXXX may raise
-        return 'LONG', None, msg, endIdx
+        return TextEncodedMessage(msg, 'LONG'), endIdx
     elif msgType == 'ENC':
         tag = binascii.a2b_base64(ascTag)
-        return 'ENC', tag, msg, endIdx
+        return TextEncodedMessage(msg, 'ENC', tag), endIdx
     else:
         raise MixFatalError("unreached")
 
-class AsciiEncodedMessage:
+class TextEncodedMessage:
     def __init__(self, contents, messageType, tag=None):
         assert messageType in ('TXT', 'ENC', 'LONG', 'BIN')
         assert tag is None or (messageType == 'ENC' and len(tag) == 20)
@@ -632,7 +640,7 @@
         return self.tag
     def pack(self):
         c = self.contents
-        preNL = ""
+        preNL = postNL = ""
 
         if self.messageType != 'TXT':
             c = base64.encodestring(c)
@@ -641,7 +649,6 @@
                 c.startswith("Message-type:")):
                 preNL = "\n"
                 
-        preNL = postNL = ""
         if self.messageType == 'TXT':
             tagLine = ""
         elif self.messageType == 'ENC':
@@ -652,7 +659,7 @@
         elif self.messageType == 'BIN':
             tagLine = "Message-type: binary\n"
 
-        if c[-1] != '\n':
+        if c and c[-1] != '\n':
             postNL = "\n"
 
         return "%s\n%s%s%s%s%s\n" % (

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.76
retrieving revision 1.77
diff -u -d -r1.76 -r1.77
--- test.py	4 Feb 2003 02:38:23 -0000	1.76
+++ test.py	5 Feb 2003 05:34:55 -0000	1.77
@@ -1092,6 +1092,63 @@
         self.failUnlessRaises(ParseError,parsePayload,bad_payload_1)
         self.failUnlessRaises(ParseError,parsePayload,bad_payload_2)
 
+    def testTextEncodedMessage(self):
+        tem = TextEncodedMessage
+        ptem = parseTextEncodedMessage
+        eq = self.assertEquals
+        start = "======= TYPE III ANONYMOUS MESSAGE BEGINS =======\n"
+        end =   "======== TYPE III ANONYMOUS MESSAGE ENDS ========\n"
+
+        # Test generation: text case
+        mt1 = tem("Hello, whirled","TXT")
+        eq(mt1.pack(), start+"Hello, whirled\n"+end)
+        mt2 = tem("Hello, whirled\n", "TXT")
+        eq(mt2.pack(), start+"Hello, whirled\n"+end)
+        mt3 = tem("Decoding-handle: gotcha!\nFoobar\n", "TXT")
+        eq(mt3.pack(), start+"\nDecoding-handle: gotcha!\nFoobar\n"+end)
+        # Text generation: binary case
+        v = hexread("00D1E50FED1F1CE5")*12
+        v64 = base64.encodestring(v)
+        mb1 = tem(v, "BIN")
+        eq(mb1.pack(), start+"""\
+Message-type: binary
+ANHlD+0fHOUA0eUP7R8c5QDR5Q/tHxzlANHlD+0fHOUA0eUP7R8c5QDR5Q/tHxzlANHlD+0fHOUA
+0eUP7R8c5QDR5Q/tHxzlANHlD+0fHOUA0eUP7R8c5QDR5Q/tHxzl
+"""+end)
+        eq(mb1.pack(), start+"Message-type: binary\n"+v64+end)
+        # Overcompressed
+        ml1 = tem(v, "LONG")
+        eq(ml1.pack(), start+"Message-type: overcompressed\n"+v64+end)
+        # Encoded
+        menc1 = tem(v, "ENC", "9"*20)
+        tag64 = base64.encodestring("9"*20).strip()
+        eq(menc1.pack(), start+"Decoding-handle: "+tag64+"\n"+v64+end)
+
+        # Test parsing: successful cases
+        p = ptem(mt1.pack())[0]
+        eq(p.pack(), mt1.pack())
+        eq(p.getContents(), "Hello, whirled\n")
+        eq(p.isText(), 1)
+        p = ptem("This message is a test of the emergent broadcast system?\n "
+                 +mt2.pack())[0]
+        eq(p.pack(), mt2.pack())
+        eq(p.getContents(), "Hello, whirled\n")
+        # Two concatenated message.
+        s = mb1.pack() + "\n\n" + ml1.pack()
+        p, i = ptem(s)
+        p2, _ = ptem(s, idx=i)
+        eq(p.pack(), mb1.pack())
+        eq(p.isBinary(), 1)
+        eq(p.getContents(), v)
+        eq(p2.pack(), ml1.pack())
+        eq(p2.isOvercompressed(), 1)
+        eq(p2.getContents(), v)
+        # An encoded message
+        p = ptem(menc1.pack())[0]
+        eq(p.pack(), menc1.pack())
+        eq(p.getContents(), v)
+        eq(p.isEncrypted(), 1)
+        eq(p.getTag(), "9"*20)
 
 #----------------------------------------------------------------------
 class HashLogTests(unittest.TestCase):
@@ -1710,9 +1767,9 @@
         self.assertEquals(reply.pack(), parseReplyBlock(reply.pack()).pack())
         txt = reply.packAsText()
         self.assert_(txt.startswith(
-            "======= BEGIN TYPE III REPLY BLOCK ========\nVersion: 0.1\n"))
+            "======= BEGIN TYPE III REPLY BLOCK =======\nVersion: 0.1\n"))
         self.assert_(txt.endswith(
-            "\n======== END TYPE III REPLY BLOCK =========\n"))
+            "\n======== END TYPE III REPLY BLOCK ========\n"))
         parsed = parseTextReplyBlocks(txt)
         self.assertEquals(1, len(parsed))
         self.assertEquals(reply.pack(), parsed[0].pack())
@@ -3886,10 +3943,10 @@
         ####
         # Tests escapeMessageForEmail
         self.assert_(stringContains(eme(FDPFast('plain',message)), message))
-        expect = "BEGINS ========\nMessage-type: binary\n"+\
+        expect = "BEGINS =======\nMessage-type: binary\n"+\
                  base64.encodestring(binmessage)+"====="
         self.assert_(stringContains(eme(FDPFast('plain',binmessage)), expect))
-        expect = "BEGINS ========\nDecoding-handle: "+\
+        expect = "BEGINS =======\nDecoding-handle: "+\
                  base64.encodestring(tag)+\
                  base64.encodestring(binmessage)+"====="
         self.assert_(stringContains(eme(FDPFast('enc',binmessage,tag)),
@@ -3918,12 +3975,12 @@
 This message is not in plaintext.  It's either 1) a reply; 2) a forward
 message encrypted to you; or 3) junk.
 
-======= TYPE III ANONYMOUS MESSAGE BEGINS ========
+======= TYPE III ANONYMOUS MESSAGE BEGINS =======
 Decoding-handle: eHh4eHh4eHh4eHh4eHh4eHh4eHg=
 7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v
 +s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3u/6
 zqse+sre7/rOqx76yt7v+s6rHvrK3u/6zqse+sre7/rOqx76yt7v+s6rHvrK3g==
-======== TYPE III ANONYMOUS MESSAGE ENDS =========
+======== TYPE III ANONYMOUS MESSAGE ENDS ========
 """
 
 EXAMPLE_ADDRESS_SET = """
@@ -4129,11 +4186,11 @@
 
 Avast ye mateys!  Prepare to be anonymized!
 
-======= TYPE III ANONYMOUS MESSAGE BEGINS ========
+======= TYPE III ANONYMOUS MESSAGE BEGINS =======
 Hidden, we are free
 Free to speak, to free ourselves
 Free to hide no more.
-======== TYPE III ANONYMOUS MESSAGE ENDS =========\n"""
+======== TYPE III ANONYMOUS MESSAGE ENDS ========\n"""
             d = findFirstDiff(EXPECTED_SMTP_PACKET, args[3])
             if d != -1:
                 print d, "near", repr(args[3][d-10:d+10])
@@ -5207,7 +5264,7 @@
     tc = loader.loadTestsFromTestCase
 
     if 0:
-        suite.addTest(tc(BuildMessageTests))
+        suite.addTest(tc(PacketTests))
         return suite
 
     suite.addTest(tc(MiscTests))