[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[minion-cvs] Resolve all DOCDOCs and most XXXX006s.
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv16632/lib/mixminion
Modified Files:
BuildMessage.py ClientDirectory.py ClientMain.py Common.py
Config.py MMTPClient.py Main.py Packet.py ServerInfo.py
test.py
Log Message:
Resolve all DOCDOCs and most XXXX006s.
Additionally, tweak the list-servers interface a bit.
Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.62
retrieving revision 1.63
diff -u -d -r1.62 -r1.63
--- BuildMessage.py 20 Nov 2003 08:51:27 -0000 1.62
+++ BuildMessage.py 24 Nov 2003 19:59:03 -0000 1.63
@@ -83,7 +83,7 @@
return fragments
def buildRandomPayload(paddingPRNG=None):
- """DOCDOC"""
+ """Return a new random payload, suitable for use in a DROP packet."""
if not paddingPRNG:
paddingPRNG = Crypto.getCommonPRNG()
return paddingPRNG.getBytes(PAYLOAD_LEN)
@@ -110,7 +110,7 @@
raise MixError("Second leg of path is empty")
suppressTag = 0
- #XXXX006 refactor _TYPES_WITHOUT_TAGS
+ #XXXX refactor _TYPES_WITHOUT_TAGS
if exitType == DROP_TYPE or mixminion.Packet._TYPES_WITHOUT_TAGS.get(exitType):
suppressTag = 1
@@ -333,14 +333,16 @@
#----------------------------------------------------------------------
# MESSAGE DECODING
-def decodePayload(payload, tag, key=None, userKeys=None):
+def decodePayload(payload, tag, key=None, userKeys=()):
"""Given a 28K payload and a 20-byte decoding tag, attempt to decode the
original message. Returns either a SingletonPayload instance, a
FragmentPayload instance, or None.
key: an RSA key to decode encrypted forward messages, or None
- userKeys: a map from identity names to keys for reply blocks,
- or None. DOCDOC : prefer list of (name,key)
+ userKeys: a sequence of (name,key) tuples maping identity names to
+ SURB keys. For backward compatibility, 'userKeys' may also be
+ None (no SURBs known), a dict (from name to key), or a single
+ key (implied identity is "").
If we can successfully decrypt the payload, we return it. If we
might be able to decrypt the payload given more/different keys,
Index: ClientDirectory.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientDirectory.py,v
retrieving revision 1.17
retrieving revision 1.18
diff -u -d -r1.17 -r1.18
--- ClientDirectory.py 24 Nov 2003 02:53:39 -0000 1.17
+++ ClientDirectory.py 24 Nov 2003 19:59:03 -0000 1.18
@@ -5,8 +5,8 @@
dealing with mixminion directories. This includes:
- downloading and caching directories
- path generation
- DOCDOC
- """
+ - address parsing.
+"""
__all__ = [ 'ClientDirectory', 'parsePath', 'parseAddress' ]
@@ -64,9 +64,9 @@
# goodServerNicknames: A map from lowercased nicknames of recommended
# servers to 1.
## Layout:
- # DIR/cache: A cPickled tuple of ("ClientKeystore-0.2",
- # lastModified, lastDownload, clientVersions, serverlist,
- # fullServerList, digestMap) DOCDOC is this correct?
+ # DIR/cache: A cPickled tuple of ("ClientKeystore-0.3",
+ # lastModified, lastDownload, clientVersions, serverList,
+ # fullServerList, digestMap)
# DIR/dir.gz *or* DIR/dir: A (possibly gzipped) directory file.
# DIR/imported/: A directory of server descriptors.
MAGIC = "ClientKeystore-0.3"
@@ -104,7 +104,8 @@
def downloadDirectory(self, timeout=None):
"""Download a new directory from the network, validate it, and
- rescan its servers. DOCDOC timeout"""
+ rescan its servers. If the operation doesn't complete within
+ timeout seconds, raise an error."""
# Start downloading the directory.
url = MIXMINION_DIRECTORY_URL
LOG.info("Downloading directory from %s", url)
@@ -279,7 +280,8 @@
writePickled(os.path.join(self.dir, "cache"), data)
def _installAsKeyIDResolver(self):
- """DOCDOC"""
+ """Use this ClientDirectory to identify servers in calls to
+ ServerInfo.displayServer."""
mixminion.ServerInfo._keyIDToNicknameFn = self.getNicknameByKeyID
def importFromFile(self, filename):
@@ -743,12 +745,11 @@
if endAt is None: endAt = startAt+self.DEFAULT_REQUIRED_LIFETIME
p = pathSpec.path1+pathSpec.path2
+ assert p
# Make sure all elements are valid.
for e in p:
e.validate(self, startAt, endAt)
- #XXXX006 make sure p can never be empty!
-
# If there is a 1st element, make sure we can route to it.
fixed = p[0].getFixedServer(self, startAt, endAt)
if fixed and not fixed.canStartAt():
@@ -875,7 +876,8 @@
return result
-def formatFeatureMap(features, featureMap, showTime=0, cascade=0, sep=" "):
+def formatFeatureMap(features, featureMap, showTime=0, cascade=0, sep=" ",
+ just=0):
"""Given a list of features (by name; see Config.resolveFeatureName) and
a featureMap as returned by ClientDirectory.getFeatureMap or
compressFeatureMap, formats the map for display to an end users.
@@ -895,25 +897,42 @@
'sep' is used to concatenate feauture values when putting them on
the same line.
+
+ If 'just' is true, we left-justify features in columns.
"""
nicknames = [ (nn.lower(), nn) for nn in featureMap.keys() ]
nicknames.sort()
lines = []
if not nicknames: return lines
- maxnicklen = max([len(nn) for nn in nicknames])
- nnformat = "%-"+str(maxnicklen)+"s"
+
+ if just:
+ maxnicklen = max([len(nn) for nn in featureMap.keys()])
+ nnformat = "%-"+str(maxnicklen)+"s"
+ maxFeatureLength = {}
+ for f in features: maxFeatureLength[f] = 0
+ for byTime in featureMap.values():
+ for fMap in byTime.values():
+ for k, v in fMap.items():
+ if maxFeatureLength[k] < len(v):
+ maxFeatureLength[k] = len(v)
+ formatEntries = [ "%-"+str(maxFeatureLength[f])+"s" for
+ f in features ]
+ format = sep.join(formatEntries)
+ else:
+ nnformat = "%s"
+ format = sep.join(["%s"]*len(features))
+
for _, nickname in nicknames:
d = featureMap[nickname]
if not d: continue
items = d.items()
items.sort()
if cascade: lines.append("%s:"%nickname)
- justified_nickname = nnformat%nickname
for (va,vu),fmap in items:
ftime = "%s to %s"%(formatDate(va),formatDate(vu))
+ fvals = tuple([fmap[f] for f in features])
if cascade==1:
- lines.append(" [%s] %s"%(ftime,
- sep.join([fmap[f] for f in features])))
+ lines.append(" [%s] %s"%(ftime, format%fvals))
elif cascade==2:
if showTime:
lines.append(" [%s]"%ftime)
@@ -921,11 +940,10 @@
v = fmap[f]
lines.append(" %s:%s"%(f,v))
elif showTime:
- lines.append("%s:%s:%s" %(justified_nickname,ftime,
- sep.join([fmap[f] for f in features])))
+ lines.append("%s:%s:%s" %(nnformat%nickname,ftime,
+ format%fvals))
else:
- lines.append("%s:%s" %(justified_nickname,
- sep.join([fmap[f] for f in features])))
+ lines.append("%s:%s" %(nnformat%nickname,format%fvals))
return lines
#----------------------------------------------------------------------
@@ -1377,9 +1395,8 @@
if not path:
path = "*%d"%(nHops or defaultNHops or 6)
# Break path into a list of entries of the form:
- # Nickname
+ # string
# or "<swap>"
- # or "?"
p = []
while path:
if path[0] == "'":
@@ -1412,6 +1429,8 @@
path = path[1:]
p.append("<swap>")
+ # Convert each parsed entry into a PathElement, or the string
+ # '*', or the string '<swap>'.
pathEntries = []
for ent in p:
if re.match(r'\*(\d+)', ent):
@@ -1429,13 +1448,15 @@
# If there's a variable-length wildcard...
if "*" in pathEntries:
- # Find out how many hops we should have.
+ # Find out where it is...
starPos = pathEntries.index("*")
if "*" in pathEntries[starPos+1:]:
raise UIError("Only one '*' is permitted in a single path")
+ # Figure out how many hops we expect to have...
approxHops = reduce(operator.add,
[ ent.getAvgLength() for ent in pathEntries
if ent not in ("*", "<swap>") ], 0)
+ # Replace the '*' with the number of additional hops we want.
myNHops = nHops or defaultNHops or 6
extraHops = max(myNHops-approxHops, 0)
pathEntries[starPos:starPos+1] =[RandomServersPathElement(n=extraHops)]
@@ -1447,24 +1468,33 @@
# Figure out how long the first leg should be.
lateSplit = 0
if "<swap>" in pathEntries:
- # Calculate colon position
+ # We got a colon...
if halfPath:
+ # ...in a reply or SURB. That's an error.
raise UIError("Can't specify swap point with replies")
+ # Divide the path at the '<swap>'.
colonPos = pathEntries.index("<swap>")
if "<swap>" in pathEntries[colonPos+1:]:
raise UIError("Only one ':' is permitted in a single path")
firstLegLen = colonPos
del pathEntries[colonPos]
elif isReply:
+ # A reply message is all first leg.
firstLegLen = len(pathEntries)
elif isSURB:
+ # A SURB is all second-leg.
firstLegLen = 0
else:
+ # We have no explicit swap point, but we have a foward message. Thus,
+ # we set 'lateSplit' so that we'll know to divide the path into two
+ # legs later on.
firstLegLen = 0
lateSplit = 1
- # This is a kludge to convert paths of the form ~N to ?,~(N-1) when we've
- # got a full path.
+ # This is a kludge to convert paths of the form ~N to ?,~(N-1), when
+ # we're generating a two-legged path. Otherwise, there is a possibility
+ # that ~N could expand into only a single server, thus leaving one leg
+ # empty.
if (len(pathEntries) == 1
and not halfPath
and isinstance(pathEntries[0], RandomServersPathElement)
@@ -1472,13 +1502,11 @@
n_minus_1 = max(pathEntries[0].approx-1,0)
pathEntries = [ RandomServersPathElement(n=1),
RandomServersPathElement(approx=n_minus_1) ]
- lateSplit = 1 # XXXX Is this redundant?
+ assert lateSplit
- # Split the path into 2 legs.
path1, path2 = pathEntries[:firstLegLen], pathEntries[firstLegLen:]
- # XXXX006 when checking lengths, if the specifier is something like ~5,
- # XXXX006 we should convert it to something more like *2,~3.
+ # Die if the path is too short, or if either leg is empty in a full path.
if not lateSplit and not halfPath:
if len(path1)+len(path2) < 2:
raise UIError("The path must have at least 2 hops")
Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.131
retrieving revision 1.132
diff -u -d -r1.131 -r1.132
--- ClientMain.py 20 Nov 2003 08:51:27 -0000 1.131
+++ ClientMain.py 24 Nov 2003 19:59:04 -0000 1.132
@@ -21,10 +21,11 @@
import mixminion.Crypto
import mixminion.Filestore
import mixminion.MMTPClient
+import mixminion.ServerInfo
from mixminion.Common import LOG, Lockfile, LockfileLocked, MixError, \
MixFatalError, MixProtocolBadAuth, MixProtocolError, UIError, \
- UsageError, createPrivateDir, isPrintingAscii, isSMTPMailbox, readFile, \
+ UsageError, createPrivateDir, englishSequence, isPrintingAscii, isSMTPMailbox, readFile, \
stringContains, succeedingMidnight, writeFile, previousMidnight, floorDiv
from mixminion.Packet import encodeMailHeaders, ParseError, parseMBOXInfo, \
parseReplyBlocks, parseSMTPInfo, parseTextEncodedMessages, \
@@ -61,32 +62,42 @@
is limited to a single SURB decryption key. In the future, we may
include more SURB keys, as well as end-to-end encryption keys.
"""
- # XXXX Most of this class should go into ClientUtils?
- # XXXX006 Are the error messages here still reasonable?
- # DOCDOC -- very changed.
+ # XXXX Can any more of this class should go into ClientUtils?
+ ## Fields
+ # keyring: an instance of ClientUtils.Keyring
+
+ # We discard SURB keys after 3 months.
KEY_LIFETIME = 3*30*24*60*60
+ # We don't make new SURBs with any key that will expire in the next
+ # month.
MIN_KEY_LIFETIME_TO_USE = 30*24*60*60
def __init__(self, keyDir, passwordManager=None):
- """DOCDOC"""
+ """Create a new ClientKeyring, storing its keys in 'keyDir'"""
if passwordManager is None:
passwordManager = mixminion.ClientUtils.CLIPasswordManager()
createPrivateDir(keyDir)
+
+ # XXXX008 remove this.
+ # We used to store our keys in a different format. At this point,
+ # it's easier to change the filename.
obsoleteFn = os.path.join(keyDir, "keyring")
if os.path.exists(obsoleteFn):
LOG.warn("Ignoring obsolete keyring stored in %r",obsoleteFn)
fn = os.path.join(keyDir, "keyring.txt")
+
+ # Setup the keyring.
self.keyring = mixminion.ClientUtils.Keyring(fn, passwordManager)
def getSURBKey(self, name="", create=0, password=None):
- """Helper function. Return a key for a given keyid.
+ """Return a SURB key for a given identity, asking for passwords and
+ loading the keyring if necessary.. Return None on failure.
- keyid -- the name of the key.
+ name -- the SURB key identity
create -- If true, create a new key if none is found.
- createFn -- a callback to return a new key.
password -- Optionally, a password for the keyring.
- DOCDOC
"""
+ # If we haven't loaded the keyring yet, try to do so.
if not self.keyring.isLoaded():
try:
self.keyring.load(create=create,password=password)
@@ -95,7 +106,8 @@
return None
if not self.keyring.isLoaded():
return None
-
+
+
try:
key = self.keyring.getNewestSURBKey(
name,minLifetime=self.MIN_KEY_LIFETIME_TO_USE)
@@ -104,11 +116,14 @@
elif not create:
return None
else:
- # No key, we're allowed to create.
+ # No key, but we're allowed to create a new one.
LOG.info("Creating new key for identity %r", name)
return self.keyring.newSURBKey(name,
time.time()+self.KEY_LIFETIME)
finally:
+ # Check whether we changed the keyring, and save it if we did.
+ # The keyring may have changed even if we didn't generate any
+ # new keys, so this check is always necessary.
if self.keyring.isDirty():
self.keyring.save()
@@ -215,16 +230,22 @@
startAt, endAt, forceQueue=0, forceNoQueue=0,
forceNoServerSideFragments=0):
"""Generate and send a forward message.
- address -- the results of a parseAddress call
- payload -- the contents of the message to send
- servers1,servers2 -- lists of ServerInfos for the first and second
- legs the path, respectively.
+ directory -- an instance of ClientDirectory; used to generate
+ paths.
+ address -- an instance of ExitAddress, used to tell where to
+ deliver the message.
+ pathSpec -- an instance of PathSpec, describing the path to use.
+ message -- the contents of the message to send
+ startAt, endAt -- an interval over which all servers in the path
+ must be valid.
forceQueue -- if true, do not try to send the message; simply
queue it and exit.
forceNoQueue -- if true, do not queue the message even if delivery
fails.
-
- DOCDOC forceNoServerSideFragments
+ forceNoServerSideFragments -- if true, and the message is too
+ large to fit in a single packet, deliver fragment packets to
+ the eventual recipient rather than having the exit server
+ defragment them.
"""
assert not (forceQueue and forceNoQueue)
@@ -242,17 +263,22 @@
startAt, endAt, forceQueue=0,
forceNoQueue=0):
"""Generate and send a reply message.
- payload -- the contents of the message to send
- servers -- a list of ServerInfos for the first leg of the path.
- surbList -- a list of SURBs to consider for the second leg of
- the path. We use the first one that is neither expired nor
- used, and mark it used.
+ directory -- an instance of ClientDirectory; used to generate
+ paths.
+ address -- an instance of ExitAddress, used to tell where to
+ deliver the message.
+ pathSpec -- an instance of PathSpec, describing the path to use.
+ surbList -- a list of SURBs to consider using for the reply. We
+ use the first N that are neither expired nor used, and mark them
+ used.
+ message -- the contents of the message to send
+ startAt, endAt -- an interval over which all servers in the path
+ must be valid.
forceQueue -- if true, do not try to send the message; simply
queue it and exit.
forceNoQueue -- if true, do not queue the message even if delivery
fails.
-
- DOCDOC args are wrong."""
+ """
#XXXX write unit tests
allPackets = self.generateReplyPackets(
directory, address, pathSpec, message, surbList, startAt, endAt)
@@ -267,6 +293,7 @@
"""Generate an return a new ReplyBlock object.
address -- the results of a parseAddress call
servers -- lists of ServerInfos for the reply leg of the path.
+ name -- the name of the identity to use for the reply block.
expiryTime -- if provided, a time at which the replyBlock must
still be valid, and after which it should not be used.
"""
@@ -284,11 +311,21 @@
"""Generate packets for a forward message, but do not send
them. Return a list of tuples of (the packet body, a
ServerInfo for the first hop.)
-
- DOCDOC
+
+ directory -- an instance of ClientDirectory; used to generate
+ paths.
+ address -- an instance of ExitAddress, used to tell where to
+ deliver the message.
+ pathSpec -- an instance of PathSpec, describing the path to use.
+ message -- the contents of the message to send
+ noSSFragments -- if true, and the message is too large to fit in a
+ single packet, deliver fragment packets to the eventual
+ recipient rather than having the exit server defragment them.
+ startAt, endAt -- an interval over which all servers in the path
+ must be valid.
"""
- #XXXX006 we need to factor this long-message logic out to the
- #XXXX006 common code. For now, this is a temporary measure.
+ #XXXX we need to factor more of this long-message logic out to the
+ #XXXX common code. For now, this is a temporary measure.
if noSSFragments:
fragmentedMessagePrefix = ""
@@ -326,14 +363,18 @@
"""Generate a reply message, but do not send it. Returns
a tuple of (packet body, ServerInfo for the first hop.)
- address -- the results of a parseAddress call
- payload -- the contents of the message to send (None for DROP
- messages)
- servers -- list of ServerInfo for the first leg of the path.
- surbList -- a list of SURBs to consider for the second leg of
- the path. We use the first one that is neither expired nor
- used, and mark it used.
- DOCDOC: generates multiple packets
+
+ directory -- an instance of ClientDirectory; used to generate
+ paths.
+ address -- an instance of ExitAddress, used to tell where to
+ deliver the message.
+ pathSpec -- an instance of PathSpec, describing the path to use.
+ message -- the contents of the message to send
+ surbList -- a list of SURBs to consider using for the reply. We
+ use the first N that are neither expired nor used, and mark them
+ used.
+ startAt, endAt -- an interval over which all servers in the path
+ must be valid.
"""
#XXXX write unit tests
assert address.isReply
@@ -400,8 +441,6 @@
If warnIfLost is true, log a warning if we fail to deliver
the packets, and we don't queue them.
-
- DOCDOC never raises
"""
#XXXX write unit tests
timeout = self.config['Network'].get('ConnectionTimeout')
@@ -454,11 +493,11 @@
raise UIError(str(e))
def flushQueue(self, maxPackets=None):
- """Try to send all packets in the queue to their destinations.
- DOCDOC maxPackets
+ """Try to send packets in the queue to their destinations. Do not try
+ to send more than maxPackets packets. If not all packets will be
+ sent, choose the ones to try at random.
"""
#XXXX write unit tests
-
class PacketProxy:
def __init__(self,h,queue):
self.h = h
@@ -537,7 +576,6 @@
LOG.info("Packet queued")
return handles
- #XXXX006 rename to decodePacket ?
def decodeMessage(self, s, force=0, isatty=0):
"""Given a string 's' containing one or more text-encoded messages,
return a list containing the decoded messages.
@@ -1201,20 +1239,30 @@
Force the client to download/not to download a
fresh directory.
- DOCDOC Somebody needs to explain this. :)
+ -R, --recommended Only display recommended servers.
+ -T, --with-time Display validity intervals for server descriptors.
+ --no-collapse Don't combine descriptors with adjacent times.
+ -s <str>,--separator=<str> Separate features with <str> instead of tab.
+ -c, --cascade Pretty-print results, cascading by descriptors.
+ -C, --cascade-features Pretty-print results, cascading by features.
+ -F <name>,--feature=<name> Select which server features to list.
+ --list-features Display a list of all recognized features.
EXAMPLES:
List all currently known servers.
%(cmd)s
+ Same as above, but explicitly name the features to be listed.
+ %(cmd)s -F caps -F status
""".strip()
def listServers(cmd, args):
"""[Entry point] Print info about """
- options, args = getopt.getopt(args, "hf:D:vF:TVs:cC",
+ options, args = getopt.getopt(args, "hf:D:vF:JTRs:cC",
['help', 'config=', "download-directory=",
- 'verbose', 'feature=',
- 'with-time', "no-collapse", "valid",
- "separator=", "cascade","cascade-features"])
+ 'verbose', 'feature=', 'justify',
+ 'with-time', "no-collapse", "recommended",
+ "separator=", "cascade","cascade-features",
+ 'list-features' ])
try:
parser = CLIArgumentParser(options, wantConfig=1,
wantClientDirectory=1,
@@ -1226,37 +1274,61 @@
features = []
cascade = 0
showTime = 0
- validOnly = 0
- separator = "\t"
+ goodOnly = 0
+ separator = None
+ justify = 0
+ listFeatures = 0
for opt,val in options:
if opt in ('-F', '--feature'):
features.extend(val.split(","))
- elif opt == ('-T'):
- showTime += 1
- elif opt == ('--with-time'):
+ elif opt in ('-T', '--with-time'):
showTime = 1
elif opt == ('--no-collapse'):
showTime = 2
- elif opt in ('-V', '--valid'):
- validOnly = 1
+ elif opt in ('-R', '--recommended'):
+ goodOnly = 1
elif opt in ('-s', '--separator'):
separator = val
elif opt in ('-c', '--cascade'):
cascade = 1
elif opt in ('-C', '--cascade-features'):
cascade = 2
+ elif opt in ('-J', '--justify'):
+ justify = 1
+ elif opt == ('--list-features'):
+ listFeatures = 1
+
+ if listFeatures:
+ features = mixminion.Config.getFeatureList(
+ mixminion.ServerInfo.ServerInfo)
+ features.append(("caps",))
+ features.append(("status",))
+ for f in features:
+ fCanon = f[0]
+ if len(f)>1:
+ print "%-30s (abbreviate as %s)" % (
+ f[0], englishSequence(f[1:],compound="or"))
+ else:
+ print f[0]
+ return
if not features:
- if validOnly:
+ if goodOnly:
features = [ 'caps' ]
else:
features = [ 'caps', 'status' ]
+ if separator is None:
+ if justify:
+ separator = ' '
+ else:
+ separator = '\t'
+
parser.init()
directory = parser.directory
# Look up features in directory.
- featureMap = directory.getFeatureMap(features,goodOnly=validOnly)
+ featureMap = directory.getFeatureMap(features,goodOnly=goodOnly)
# If any servers are listed on the command line, restrict to those
# servers.
@@ -1270,7 +1342,7 @@
lcfound[nn.lower()] = 1
for arg in args:
if not lcfound.has_key(arg.lower()):
- if validOnly:
+ if goodOnly:
raise UIError("No recommended descriptors found for %s"%
arg)
else:
@@ -1284,7 +1356,7 @@
# Now display the result.
for line in mixminion.ClientDirectory.formatFeatureMap(
- features,featureMap,showTime,cascade,separator):
+ features,featureMap,showTime,cascade,separator,justify):
print line
Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.116
retrieving revision 1.117
diff -u -d -r1.116 -r1.117
--- Common.py 7 Nov 2003 08:02:23 -0000 1.116
+++ Common.py 24 Nov 2003 19:59:04 -0000 1.117
@@ -223,9 +223,10 @@
_HOST_CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
"abcdefghijklmnopqrstuvwxyz"+
- "0123456789.")
+ "0123456789.-")
def isPlausibleHostname(s):
- """DOCDOC"""
+ """Return true iff 's' is made up only of characters that sometimes
+ appear in hostnames, and has a plausible arrangement of dots."""
if not s:
return 0
if s.translate(_ALLCHARS, _HOST_CHARS):
@@ -1072,14 +1073,14 @@
gmt = time.localtime(when)
else:
gmt = time.gmtime(when)
- return "%04d/%02d/%02d %02d:%02d:%02d" % (
+ 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])
+ return "%04d-%02d-%02d" % (gmt[0],gmt[1],gmt[2])
def formatFnameTime(when=None):
"""Given a time in seconds since the epoch, returns a date value suitable
@@ -1272,7 +1273,7 @@
"""Returns a list of (start,end) tuples for a the intervals in this
set."""
s = []
- for i in range(0, len(self.edges), 2):
+ for i in xrange(0, len(self.edges), 2):
s.append((self.edges[i][0], self.edges[i+1][0]))
return s
@@ -1280,7 +1281,7 @@
"""Helper function: raises AssertionError if this set's data is
corrupted."""
assert (len(self.edges) % 2) == 0
- for i in range(0, len(self.edges), 2):
+ for i in xrange(0, len(self.edges), 2):
assert self.edges[i][0] < self.edges[i+1][0]
assert self.edges[i][1] == '+'
assert self.edges[i+1][1] == '-'
@@ -1578,12 +1579,19 @@
TimeoutQueue = ClearableQueue
else:
class TimeoutQueue(ClearableQueue):
- """DOCDOC -- for python 2.2 and earlier."""
+ """Helper class for Python 2.2. and earlier: extends the 'get'
+ functionality of Queue.Queue to support a 'timeout' argument.
+ If 'block' is true and timeout is provided, wait for no more
+ than 'timeout' seconds before raising QueueEmpty.
+
+ In Python 2.3 and later, this interface is standard.
+ """
def get(self, block=1, timeout=None):
- if timeout is None:
+ if timeout is None or not block:
return MessageQueue.get(self, block)
- # Adapted from 'Condition'.
+ # This logic is adapted from 'Condition' in the Python
+ # threading module.
_time = time.time
_sleep = time.sleep
deadline = timeout+_time()
Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.67
retrieving revision 1.68
diff -u -d -r1.67 -r1.68
--- Config.py 20 Nov 2003 08:48:33 -0000 1.67
+++ Config.py 24 Nov 2003 19:59:04 -0000 1.68
@@ -349,8 +349,7 @@
raise ConfigError("Invalid exponent on public key")
return key
-# FFFF006 begin generating YYYY-MM-DD
-# FFFF007 stop accepting YYYYY/MM/DD
+# FFFF007/8 stop accepting YYYY/MM/DD
# Regular expression to match YYYY/MM/DD or YYYY-MM-DD
_date_re = re.compile(r"^(\d\d\d\d)([/-])(\d\d)([/-])(\d\d)$")
def _parseDate(s):
@@ -370,9 +369,7 @@
raise ConfigError("Invalid date %r"%s)
return calendar.timegm((yyyy,MM,dd,0,0,0,0,0,0))
-
-# FFFF006 begin generating YYYY-MM-DD
-# FFFF007 stop accepting YYYYY/MM/DD
+# FFFF007/8 stop accepting YYYY/MM/DD
# Regular expression to match YYYY/MM/DD HH:MM:SS
_time_re = re.compile(r"^(\d\d\d\d)([/-])(\d\d)([/-])(\d\d)\s+"
r"(\d\d):(\d\d):(\d\d)((?:\.\d\d\d)?)$")
@@ -626,6 +623,28 @@
name, englishSequence(secs,compound="or")))
else:
return result[0]
+
+def getFeatureList(klass):
+ """Get a list of all feature names from the _ConfigFile subclass
+ 'klass'. Return a list of tuples, each of which contains all the
+ synonyms for a single feature."""
+ syn = klass._syntax
+ features = []
+ for secname, secitems in syn.items():
+ for entname in secitems.keys():
+ if entname.startswith("__"): continue
+ synonyms = []
+ synonyms.append("%s:%s"%(secname,entname))
+ unique = 1
+ for sn, si in syn.items():
+ if sn != secname and si.has_key(entname):
+ unique = 0
+ break
+ if unique:
+ synonyms.append(entname)
+ features.append(tuple(synonyms))
+ features.sort()
+ return features
class _ConfigFile:
"""Base class to parse, validate, and represent configuration files.
Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.42
retrieving revision 1.43
diff -u -d -r1.42 -r1.43
--- MMTPClient.py 19 Nov 2003 09:48:09 -0000 1.42
+++ MMTPClient.py 24 Nov 2003 19:59:04 -0000 1.43
@@ -35,6 +35,7 @@
# targetKeyID -- sha1 hash of the ASN1 encoding of the public key we
# expect the server to use, or None if we don't care.
# context: a TLSContext object; used to create connections.
+ # serverName: The name of the server to display in log messages.
# sock: a TCP socket, open to the server.
# tls: a TLS socket, wrapping sock.
# protocol: The MMTP protocol version we're currently using, or None
@@ -58,7 +59,6 @@
self.certCache = PeerCertificateCache()
if serverName:
self.serverName = serverName
- #DOCDOC
else:
self.serverName = mixminion.ServerInfo.displayServer(
mixminion.Packet.IPV4Info(targetAddr, targetPort, targetKeyID))
@@ -283,11 +283,11 @@
def __init__(self):
self.cache = {}
- #XXXX006 use displayName to
def check(self, tls, targetKeyID, serverName):
"""Check whether the certificate chain on the TLS connection 'tls'
is valid, current, and matches the keyID 'targetKeyID'. If so,
- return. If not, raise MixProtocolBadAuth.
+ return. If not, raise MixProtocolBadAuth. Display all messages
+ using the server 'serverName'.
"""
# First, make sure the certificate is neither premature nor expired.
@@ -304,6 +304,7 @@
# Get the KeyID for the peer (temporary) key.
hashed_peer_pk = sha1(tls.get_peer_cert_pk().encode_key(public=1))
+
# Before 0.0.4alpha, a server's keyID was a hash of its current
# TLS public key. In 0.0.4alpha, we allowed this for backward
# compatibility. As of 0.0.4alpha2, since we've dropped backward
Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.61
retrieving revision 1.62
diff -u -d -r1.61 -r1.62
--- Main.py 19 Nov 2003 09:48:09 -0000 1.61
+++ Main.py 24 Nov 2003 19:59:04 -0000 1.62
@@ -141,11 +141,6 @@
"dir": ( 'mixminion.directory.DirMain', 'main'),
"shell": ( 'mixminion.Main', 'commandShell' ),
-
- # XXXX006 Obsolete commands. Remove in 0.0.6
- "server" : ( 'mixminion.Main', 'rejectCommand' ),
- "inspect-pool" : ( 'mixminion.Main', 'rejectCommand' ),
- "pool" : ( 'mixminion.Main', 'rejectCommand' ),
}
_USAGE = (
@@ -191,11 +186,11 @@
print " to be anonymous, and the code is too alpha to be reliable."
def rejectCommand(cmd,args):
+ # This function gets called when we have an obsolete command 'cmd'.
+ # First, let's see whether we know an updated equivalent or not.
cmd = cmd.split()[-1]
- cmdDict = { "client" : "send",
- "pool" : "queue",
- "inspect-pool" : "inspect-queue",
- "server" : "server-start" }
+ # Map from obsolete commands to current versions.
+ cmdDict = { } # Right now, there aren't any obsolete commands still in use.
newCmd = cmdDict.get(cmd)
if newCmd:
print "The command %r is obsolete. Use %r instead."%(cmd,newCmd)
@@ -210,6 +205,8 @@
print " to be anonymous, and the code is too alpha to be reliable."
def commandShell(cmd,args):
+ # Used to implement a 'mixminion shell' on systems (like windows) with
+ # somewhat bogus CLI support.
import mixminion
import shlex
Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.65
retrieving revision 1.66
diff -u -d -r1.65 -r1.66
--- Packet.py 19 Nov 2003 09:48:09 -0000 1.65
+++ Packet.py 24 Nov 2003 19:59:04 -0000 1.66
@@ -94,9 +94,9 @@
MAX_EXIT_TYPE = 0xFFFF
# Set of exit types that don't get tag fields.
-# XXXX006 This interface is really brittle; it needs to change. I added it
-# XXXX006 in order to allow 'fragment' to be an exit type without adding a
-# XXXX006 needless tag field to every fragment routing info.
+# XXXX007 This interface is really brittle; it needs to change. I added it
+# XXXX007 in order to allow 'fragment' to be an exit type without adding a
+# XXXX007 needless tag field to every fragment routing info.
_TYPES_WITHOUT_TAGS = { FRAGMENT_TYPE : 1 }
def typeIsSwap(tp):
@@ -205,7 +205,7 @@
"""Return the part of the routingInfo that contains the delivery
address. (Requires that routingType is an exit type.)"""
assert self.routingtype >= MIN_EXIT_TYPE
- #XXXX006 This interface is completely insane. Change it.
+ #XXXX007 This interface is completely insane. Change it.
if _TYPES_WITHOUT_TAGS.get(self.routingtype):
return self.routinginfo
else:
@@ -216,7 +216,7 @@
"""Return the part of the routingInfo that contains the decoding
tag. (Requires that routingType is an exit type.)"""
assert self.routingtype >= MIN_EXIT_TYPE
- #XXXX006 This interface is completely insane. Change it.
+ #XXXX007 This interface is completely insane. Change it.
if _TYPES_WITHOUT_TAGS.get(self.routingtype):
return ""
else:
@@ -363,9 +363,6 @@
class FragmentPayload(_Payload):
"""Represents the fields of a decoded fragment payload.
-
- FFFF Fragments are not yet fully supported; there's no code to generate
- or decode them.
"""
def __init__(self, index, hash, msgID, msgLen, data):
self.index = index
Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.63
retrieving revision 1.64
diff -u -d -r1.63 -r1.64
--- ServerInfo.py 20 Nov 2003 08:49:20 -0000 1.63
+++ ServerInfo.py 24 Nov 2003 19:59:04 -0000 1.64
@@ -42,7 +42,10 @@
# ----------------------------------------------------------------------
def displayServer(s):
- """DOCDOC"""
+ """Return the best possible human-readable name for a server 's'.
+ 's' must be one of: None, IPV4Info, MMTPHostInfo, ServerInfo,
+ string.
+ """
#XXXX006 unit tests are needed
if isinstance(s, types.StringType):
return s
@@ -71,13 +74,22 @@
return "%s at %s" % (nickname, addr)
def getNicknameByKeyID(keyid):
- """DOCDOC"""
+ """Given a 20-byte keyid, look up the nickname of the corresponding
+ server. Return the nickname on success and None if we don't recognize
+ the server.
+
+ FFFF Right now, this is not supported for servers, since they don't
+ FFFF download directories.
+ """
+ #FFFF Be cleverer with all-zero keyids.
if _keyIDToNicknameFn:
return _keyIDToNicknameFn(keyid)
else:
return None
-#DOCDOC
+# This variable should hold None, or a function that maps from keyids to
+# nicknames, and returns None on failure. Currently set by
+# ClientDirectory.ClientDirectory._installAsKeyIDResolver().
_keyIDToNicknameFn = None
# ----------------------------------------------------------------------
@@ -107,8 +119,8 @@
"Comments": ("ALLOW", None, None),
"Packet-Key": ("REQUIRE", "publicKey", None),
"Contact-Fingerprint": ("ALLOW", None, None),
- # XXXX010 change these next few to "REQUIRE".
"Packet-Formats": ("ALLOW", None, None),#XXXX007 remove
+ # XXXX010 change these next few to "REQUIRE".
"Packet-Versions": ("ALLOW", None, None),
"Software": ("ALLOW", None, None),
"Secure-Configuration": ("ALLOW", "boolean", None),
@@ -116,10 +128,10 @@
},
"Incoming/MMTP" : {
"Version": ("REQUIRE", None, None),
- "IP": ("ALLOW", "IP", None),#XXXX007 remove
+ "IP": ("ALLOW", "IP", None),#XXXX007/8 remove
"Hostname": ("ALLOW", "host", None),#XXXX008 require
"Port": ("REQUIRE", "int", None),
- "Key-Digest": ("ALLOW", "base64", None),#XXXX007 rmv
+ "Key-Digest": ("ALLOW", "base64", None),#XXXX007/8 rmv
"Protocols": ("REQUIRE", None, None),
"Allow": ("ALLOW*", "addressSet_allow", None),
"Deny": ("ALLOW*", "addressSet_deny", None),
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.166
retrieving revision 1.167
diff -u -d -r1.166 -r1.167
--- test.py 20 Nov 2003 08:51:28 -0000 1.166
+++ test.py 24 Nov 2003 19:59:04 -0000 1.167
@@ -34,7 +34,9 @@
except ImportError:
import mixminion._unittest as unittest
-#DOCDOC
+# If the environment variable MM_COVERAGE is set, then start running the
+# coverage analysis tool. You'll need to install the file 'coverage.py':
+# it's included in the contrib directory.
if os.environ.get("MM_COVERAGE"):
import coverage
coverage.erase()
@@ -132,7 +134,10 @@
#----------------------------------------------------------------------
# DNS override
def overrideDNS(overrideDict,delay=0):
- """DOCDOC"""
+ """Helper function: temporarily replace the DNS functionality in
+ NetUtils to lookup entries in overrideDict instead of on the
+ network, and pause for 'delay' seconds before returning an answer.
+ """
def getIPs_replacement(addr,overrideDict=overrideDict,delay=delay):
v = overrideDict.get(addr)
if delay: time.sleep(delay)
@@ -5938,15 +5943,22 @@
(socket.AF_INET, '10.2.4.11'))
self.assertEquals(receiveDict['bar'][:2],
(mixminion.NetUtils.AF_INET6, '18:0FFF::4:1'))
- self.assert_(DELAY <= receiveDict['foo'][2]-start <= DELAY+LATENCY)
- self.assert_(DELAY <= receiveDict['bar'][2]-start <= DELAY+LATENCY)
+ # We allow a little wiggle on DELAY, since many OS's don't
+ # stricly guarantee that
+ # t1=time();sleep(x);t2=time();assert t2>=t1+x
+ # will pass.
+ self.assert_(DELAY*.8 <= receiveDict['foo'][2]-start
+ <= DELAY+LATENCY)
+ self.assert_(DELAY*.8 <= receiveDict['bar'][2]-start
+ <= DELAY+LATENCY)
self.assertEquals(receiveDict['nowhere.noplace'][0], "NOENT")
self.assertEquals(cache.getNonblocking("foo"),
receiveDict['foo'])
self.assertEquals(cache.getNonblocking("baz.com")[:2],
(socket.AF_INET, '10.99.22.8'))
- self.assert_(DELAY*1.25 <= receiveDict['baz.com'][2]-start <= DELAY*1.24 + LATENCY)
- cache.cleanCache(receiveDict['foo'][2]+
+ self.assert_(DELAY*1.20 <= receiveDict['baz.com'][2]-start
+ <= DELAY*1.25 + LATENCY)
+ cache.cleanCache(start+DELAY+
mixminion.server.DNSFarm.MAX_ENTRY_TTL+.001)
self.assertEquals(cache.getNonblocking('foo'), None)
self.assertEquals(cache.getNonblocking('nowhere.noplace'),
@@ -6751,6 +6763,8 @@
eq((len(p1),len(p2)), (1,5))
# 1d'. Tilde
+ p1,p2 = ppath(ks, None, '~2', email)
+ self.assert_(p1 and p2)
p1,p2 = ppath(ks, None, '?,~4,Bob,Joe', email) #default nHops=6
p = p1+p2
pathIs((p2[-1], p2[-2],), (joe, bob))
@@ -6835,38 +6849,42 @@
self.assertEquals(fm['Bob'].values()[0],
{ 'software' : 'Mixminion %s' %mixminion.__version__,
'caps' : 'relay',
- 'status' : '(not recommended)' })
+ 'status' : '(ok)' })
# Now let's try compressing.
- fm = { 'Alice' : { (100,200) : { "A" : "xx", "B" : "yy" },
- (200,300) : { "A" : "xx", "B" : "yy" },
- (350,400) : { "A" : "xx", "B" : "yy" } },
+ fm = { 'Alice' : { (100,200) : { "A" : "xxx", "B" : "yy" },
+ (200,300) : { "A" : "xxx", "B" : "yy" },
+ (350,400) : { "A" : "xxx", "B" : "yy" } },
'Bob' : { (100,200) : { "A" : "zz", "B" : "ww" },
(200,300) : { "A" : "zz", "B" : "kk" } } }
fm2 = compressFeatureMap(fm, ignoreGaps=0, terse=0)
self.assertEquals(fm2,
- { 'Alice' : { (100,300) : { "A" : "xx", "B" : "yy" },
- (350,400) : { "A" : "xx", "B" : "yy" } },
+ { 'Alice' : { (100,300) : { "A" : "xxx", "B" : "yy" },
+ (350,400) : { "A" : "xxx", "B" : "yy" } },
'Bob' : { (100,200) : { "A" : "zz", "B" : "ww" },
(200,300) : { "A" : "zz", "B" : "kk" } } })
fm3 = compressFeatureMap(fm, ignoreGaps=1, terse=0)
self.assertEquals(fm3,
- { 'Alice' : { (100,400) : { "A" : "xx", "B" : "yy" } },
+ { 'Alice' : { (100,400) : { "A" : "xxx", "B" : "yy" } },
'Bob' : { (100,200) : { "A" : "zz", "B" : "ww" },
(200,300) : { "A" : "zz", "B" : "kk" } } })
fm4 = compressFeatureMap(fm, terse=1)
self.assertEquals(fm4,
- { 'Alice' : { (100,400) : { "A" : "xx", "B" : "yy" } },
+ { 'Alice' : { (100,400) : { "A" : "xxx", "B" : "yy" } },
'Bob' : { (100,300) : { "A" : "zz", "B" : "ww / kk" } } })
# Test formatFeatureMap.
self.assertEquals(formatFeatureMap(["A","B"],fm4,showTime=0,cascade=0),
- [ "Alice:xx yy", "Bob:zz ww / kk" ])
+ [ "Alice:xxx yy", "Bob:zz ww / kk" ])
+ self.assertEquals(formatFeatureMap(["A","B"],fm4,showTime=0,cascade=0,
+ just=1,sep="!"),
+ [ "Alice:xxx!yy ",
+ "Bob :zz !ww / kk" ])
self.assertEquals(formatFeatureMap(["A","B"],fm3,showTime=1,cascade=0),
- [ "Alice:1970/01/01 to 1970/01/01:xx yy",
- "Bob:1970/01/01 to 1970/01/01:zz ww",
- "Bob:1970/01/01 to 1970/01/01:zz kk" ])
+ [ "Alice:1970-01-01 to 1970-01-01:xxx yy",
+ "Bob:1970-01-01 to 1970-01-01:zz ww",
+ "Bob:1970-01-01 to 1970-01-01:zz kk" ])
day1 = 24*60*60
day2 = 2*24*60*60
@@ -6875,16 +6893,16 @@
'Bob' : { (day1,day2) : { "A" : "a1", "B" : "b1" },
(day2,day3) : { "A" : "a2", "B" : "b2" } } }
self.assertEquals(formatFeatureMap(["A","B"],fmx,cascade=1,sep="##"),
- [ "Alice:", " [1970/01/02 to 1970/01/04] aa##bb",
+ [ "Alice:", " [1970-01-02 to 1970-01-04] aa##bb",
"Bob:",
- " [1970/01/02 to 1970/01/03] a1##b1",
- " [1970/01/03 to 1970/01/04] a2##b2" ])
+ " [1970-01-02 to 1970-01-03] a1##b1",
+ " [1970-01-03 to 1970-01-04] a2##b2" ])
self.assertEquals(formatFeatureMap(["A","B"],fmx,showTime=1,cascade=2),
- [ "Alice:", " [1970/01/02 to 1970/01/04]", " A:aa", " B:bb",
+ [ "Alice:", " [1970-01-02 to 1970-01-04]", " A:aa", " B:bb",
"Bob:",
- " [1970/01/02 to 1970/01/03]", " A:a1", " B:b1",
- " [1970/01/03 to 1970/01/04]", " A:a2", " B:b2" ])
+ " [1970-01-02 to 1970-01-03]", " A:a1", " B:b1",
+ " [1970-01-03 to 1970-01-04]", " A:a2", " B:b2" ])
def writeDescriptorsToDisk(self):
edesc = getExampleServerDescriptors()
@@ -7364,7 +7382,7 @@
tc = loader.loadTestsFromTestCase
if 0:
- suite.addTest(tc(ClientUtilTests))
+ suite.addTest(tc(DNSFarmTests))
return suite
testClasses = [MiscTests,
MinionlibCryptoTests,
@@ -7406,7 +7424,8 @@
initializeGlobals()
unittest.TextTestRunner(verbosity=1).run(testSuite())
- #DOCDOC
+ # If we're doing coverage analysis, then report on all the modules
+ # under the mixminion package.
if os.environ.get("MM_COVERAGE"):
allmods = [ mod for name, mod in sys.modules.items()
if (mod is not None and