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