[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))