[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[minion-cvs] Finish implementation and tests for directories and pat...
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv16537/src/minion/lib/mixminion
Modified Files:
BuildMessage.py ClientMain.py Common.py ServerInfo.py test.py
Log Message:
Finish implementation and tests for directories and path selection.
BuildMessage:
- Raise an error if either leg of the path is empty
ClientMain:
- Debug client-side directory and path code.
- Add a magic string to cache files so we can update later.
- Add code to actually *call* downloadDirectory.
- Remove last vestiges of 'TrivialKeystore'.
Common:
- Add 'start' and 'end' to IntervalSet
Common, ServerList:
- Move openUnique from ServerList to Common
ClientMain, ServerInfo, ServerDirectory:
- Keep a dict of the digests we've already validated, so we
don't need to do N RSA-checks every time we download a directory.
test:
- Tests for client path selection logic
- Tests for client keystore
- Tests for formatFnameTime
- Tests for openUnique
- Adapt tests for MixminionClient to use new interface.
Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.28
retrieving revision 1.29
diff -u -d -r1.28 -r1.29
--- BuildMessage.py 3 Jan 2003 08:47:27 -0000 1.28
+++ BuildMessage.py 4 Jan 2003 04:12:50 -0000 1.29
@@ -31,11 +31,14 @@
paddingPRNG: random number generator used to generate padding.
If None, a new PRNG is initialized.
- Neither path1 nor path2 may be empty.
+ Neither path1 nor path2 may be empty. If one is, MixError is raised.
"""
if paddingPRNG is None:
paddingPRNG = Crypto.getCommonPRNG()
- assert path1 and path2
+ if not path1:
+ raise MixError("First leg of path is empty")
+ if not path2:
+ raise MixError("Second leg of path is empty")
LOG.debug("Encoding forward message for %s-byte payload",len(payload))
LOG.debug(" Using path %s/%s",
Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.20
retrieving revision 1.21
diff -u -d -r1.20 -r1.21
--- ClientMain.py 3 Jan 2003 08:47:27 -0000 1.20
+++ ClientMain.py 4 Jan 2003 04:12:51 -0000 1.21
@@ -32,7 +32,9 @@
import mixminion.MMTPClient
from mixminion.Common import IntervalSet, LOG, floorDiv, MixError, \
MixFatalError, ceilDiv, createPrivateDir, isSMTPMailbox, formatDate, \
- formatFnameTime, readPossiblyGzippedFile
+ formatFnameTime, formatTime, openUnique, previousMidnight, \
+ readPossiblyGzippedFile
+
from mixminion.Config import ClientConfig, ConfigError
from mixminion.ServerInfo import ServerInfo, ServerDirectory
from mixminion.Packet import ParseError, parseMBOXInfo, parseSMTPInfo, \
@@ -42,33 +44,49 @@
MIXMINION_DIRECTORY_URL = "http://www.mixminion.net/directory/latest.gz"
# FFFF This should be made configurable.
MIXMINION_DIRECTORY_FINGERPRINT = ""
-
+
class ClientKeystore:
- """A ClientKeystore manages a list of server descriptors, either
+ """A ClientKeystore manages a list of server descriptors, either
imported from the command line or from a directory."""
##Fields:
# dir: directory where we store everything.
# lastModified: time when we last modified this keystore
# lastDownload: time when we last downloaded a directory
- # serverList: List of (ServerInfo, 'D'|'I:filename') tuples. The second
- # element indicates whether the ServerInfo comes from a directory or
- # a file.
+ # serverList: List of (ServerInfo, 'D'|'I:filename') tuples. The
+ # second element indicates whether the ServerInfo comes from a
+ # directory or a file.
+ # digestMap: Map of (Digest -> 'D'|'I:filename').
# byNickname: Map from nickname to list of (ServerInfo, source) tuples.
# byCapability: Map from capability ('mbox'/'smtp'/'relay'/None) to
# list of (ServerInfo, source) tuples.
# allServers: Same as byCapability[None]
# __scanning: Flag to prevent recursive invocation of self.rescan().
## Layout:
- # DIR/cache: A cPickled tuple of (lastModified, lastDownload, serverlist)
+ # DIR/cache: A cPickled tuple of ("ClientKeystore-0",
+ # lastModified, lastDownload, serverlist, digestMap)
# DIR/dir.gz *or* DIR/dir: A (possibly gzipped) directory file.
# DIR/servers/: A directory of server descriptors.
+
+ MAGIC = "ClientKeystore-0"
def __init__(self, directory):
"""Create a new ClientKeystore to keep directories and descriptors
under <directory>."""
self.dir = directory
createPrivateDir(self.dir)
+ self.digestMap = {}
self.__scanning = 0
self.__load()
+
+ def updateDirectory(self, forceDownload=0, now=None):
+ """Download a directory from the network as needed."""
+ if now is None:
+ now = time.time()
+
+ if forceDownload or self.lastDownload < previousMidnight(now):
+ self.downloadDirectory()
+ else:
+ LOG.debug("Directory is up to date.")
+
def downloadDirectory(self):
"""Download a new directory from the network, validate it, and
rescan its servers."""
@@ -96,7 +114,8 @@
# Open and validate the directory
LOG.info("Validating directory")
try:
- directory = ServerDirectory(fname=fname)
+ directory = ServerDirectory(fname=fname,
+ validatedDigests=self.digestMap)
except ConfigError, e:
raise MixFatalError("Downloaded invalid directory: %s" % e)
@@ -119,14 +138,15 @@
# And regenerate the cache.
self.rescan()
- # FFFF Actually, we could be a bit more clever here, and more clever
- # FFFF about not revalidating already-known-to-be-correct descriptors.
- # FFFF But that's for later.
+ # FFFF Actually, we could be a bit more clever here, and same some
+ # FFFF time. But that's for later.
- def rescan(self, now=None):
+ def rescan(self, force=None, now=None):
"""Regenerate the cache based on files on the disk."""
self.lastModified = self.lastDownload = -1
self.serverList = []
+ if force:
+ self.digestMap = {}
# Read the servers from the directory.
gzipFile = os.path.join(self.dir, "dir.gz")
@@ -136,13 +156,15 @@
self.lastDownload = self.lastModified = \
os.stat(fname)[stat.ST_MTIME]
try:
- directory = ServerDirectory(fname=fname)
+ directory = ServerDirectory(fname=fname,
+ validatedDigests=self.digestMap)
except ConfigError:
LOG.warn("Ignoring invalid directory (!)")
continue
for s in directory.getServers():
self.serverList.append((s, 'D'))
+ self.digestMap[s.getDigest()] = 'D'
break
# Now check the server in DIR/servers.
@@ -152,7 +174,9 @@
# Try to read a file: is it a server descriptor?
p = os.path.join(serverDir, fn)
try:
- info = ServerInfo(fname=p, assumeValid=0)
+ # Use validatedDigests *only* when not explicitly forced.
+ info = ServerInfo(fname=p, assumeValid=0,
+ validatedDigests=self.digestMap)
except ConfigError:
LOG.warn("Invalid server descriptor %s", p)
continue
@@ -160,6 +184,7 @@
if mtime > self.lastModified:
self.lastModifed = mtime
self.serverList.append((info, "I:%s"%fn))
+ self.digestMap[info.getDigest()] = "I:%s"%fn
# Regenerate the cache
self.__save()
@@ -169,15 +194,22 @@
def __load(self):
"""Helper method. Read the cached parsed descriptors from disk."""
+ #self.lastModified = self.lastDownload = -1
+ #self.serverList = []
+ #self.digestMap = {}
try:
f = open(os.path.join(self.dir, "cache"), 'rb')
cached = cPickle.load(f)
- self.lastModified, self.lastDowload, self.serverList = cached
+ magic, self.lastModified, self.lastDownload, self.serverList, \
+ self.digestMap = cached
f.close()
- self.__rebuildTables()
- return
- except OSError, e:
- LOG.info("Couldn't create server cache: %s", e)
+ if magic == self.MAGIC:
+ self.__rebuildTables()
+ return
+ else:
+ LOG.warn("Bad magic on keystore cache; rebuilding...")
+ except (OSError, IOError):
+ LOG.info("Couldn't read server cache; rebuilding")
except (cPickle.UnpicklingError, ValueError), e:
LOG.info("Couldn't unpickle server cache: %s", e)
if self.__scanning:
@@ -187,18 +219,23 @@
def __save(self):
"""Helper method. Recreate the cache on disk."""
fname = os.path.join(self.dir, "cache.new")
- os.unlink(fname)
+ try:
+ os.unlink(fname)
+ except OSError:
+ pass
f = open(fname, 'wb')
- cPickle.dump((self.lastModified, self.lastDownload, self.serverList),
+ cPickle.dump((self.MAGIC,
+ self.lastModified, self.lastDownload, self.serverList,
+ self.digestMap),
f, 1)
f.close()
os.rename(fname, os.path.join(self.dir, "cache"))
def importFromFile(self, filename):
- """Import a new server descriptor storred in 'filename'"""
+ """Import a new server descriptor stored in 'filename'"""
contents = readPossiblyGzippedFile(filename)
- info = ServerInfo(string=contents)
+ info = ServerInfo(string=contents, validatedDigests=self.digestMap)
nickname = info.getNickname()
identity = info.getIdentity()
@@ -209,15 +246,20 @@
s.getIdentity()):
raise MixError("Identity key changed for server %s in %s",
nickname, filename)
+
+ # Have we already imported this server?
+ if self.digestMap.get(info.getDigest(), "X").startswith("I:"):
+ raise MixError("Server descriptor is already imported")
# Copy the server into DIR/servers.
fnshort = "%s-%s"%(nickname, formatFnameTime())
fname = os.path.join(self.dir, "servers", fnshort)
- f = open(fname, 'w')
+ f = openUnique(fname)[0]
f.write(contents)
f.close()
# Now store into the cache.
- self.serverList.append((info, 'I:%s', fnshort))
+ self.serverList.append((info, 'I:%s'%fnshort))
+ self.digestMap[info.getDigest()] = 'I:%s'%fnshort
self.lastModified = time.time()
self.__save()
self.__rebuildTables()
@@ -244,6 +286,7 @@
if n:
self.lastModifed = time.time()
self.__save()
+ self.__rebuildTables()
return n
def __rebuildTables(self):
@@ -267,8 +310,8 @@
def listServers(self):
"""Returns a linewise listing of the current servers and their caps.
- stdout. This will go away or get refactored in future versions
- once we have client-level modules."""
+ This will go away or get refactored in future versions once we
+ have client-level modules."""
lines = []
nicknames = self.byNickname.keys()
nicknames.sort()
@@ -338,7 +381,7 @@
pass
else:
valid = info.getIntervalSet()
- for s in self.byNickname[info.getNickname()]:
+ for s, _ in self.byNickname[info.getNickname()]:
if s.isNewerThan(info):
valid -= s.getIntervalSet()
if not valid.isEmpty():
@@ -376,31 +419,32 @@
else:
LOG.error("Server is not currently valid")
elif self.byNickname.has_key(name):
- s = self.__find(self.byNickname[name], startAt, endAt)
+ s = self.__findOne(self.byNickname[name], startAt, endAt)
if not s:
- raise MixError("Couldn't find valid descriptor %s")
+ raise MixError("Couldn't find valid descriptor %s" % name)
+ return s
elif os.path.exists(name):
try:
return ServerInfo(fname=name, assumeValid=0)
except OSError, e:
- raise MixError("Couldn't read descriptor %s: %s" %
+ raise MixError("Couldn't read descriptor %r: %s" %
(name, e))
except ConfigError, e:
- raise MixError("Couldn't parse descriptor %s: %s" %
+ raise MixError("Couldn't parse descriptor %r: %s" %
(name, e))
elif strict:
- raise MixError("Couldn't find descriptor %s")
+ raise MixError("Couldn't find descriptor %r" % name)
else:
return None
def getPath(self, midCap=None, endCap=None, length=None,
startServers=(), endServers=(),
startAt=None, endAt=None, prng=None):
- """Workhorse method for path selection. Constructs a 'length'-hop
- path, beginning with startServers and ending with endServers. If
- more servers are required to make up 'length' hops, they are
- selected at random.
-
+ """Workhorse method for path selection. Constructs a path of length
+ >= 'length' hops, path, beginning with startServers and ending with
+ endServers. If more servers are required to make up 'length' hops,
+ they are selected at random.
+
All servers are chosen to be valid continuously from startAt to
endAt. All newly-selected servers except the last are required to
have 'midCap' (typically 'relay'); the last server (if endServers
@@ -436,12 +480,16 @@
# If so, find all candidates...
endList = self.__find(self.byCapability[endCap],startAt,endAt)
if not endList:
- raise MixError("No %s servers known"% endCap)
- # ... and pick one.
- LOG.info("Choosing from among %s %s servers",
- len(endList), endCap)
- endServers = [ prng.pick(endList) ]
- LOG.debug("Chose %s", endServers[0].getNickname())
+ raise MixError("No %s servers known" % endCap)
+ # ... and pick one that hasn't been used, if possible.
+ used = [ info.getNickname() for info in startServers ]
+ unusedEndList = [ info for info in endList
+ if info.getNickname() not in used ]
+ if unusedEndList:
+ endServers = [ prng.pick(unusedEndList) ]
+ else:
+ endServers = [ prng.pick(endList) ]
+ LOG.debug("Chose %s at exit server", endServers[0].getNickname())
nNeeded -= 1
# Now are we done?
@@ -466,14 +514,15 @@
# Which are left?
unusedMidList = [ info for info in midList
if info.getNickname() not in used ]
- if len(unusedMidList) >= length:
+ if len(unusedMidList) >= nNeeded:
# We have enough enough servers to choose without replacement.
midServers = prng.shuffle(unusedMidList, nNeeded)
elif len(midList) >= 3:
# We have enough servers to choose without two hops in a row to
# the same server.
- LOG.warn("Not enough servers for distinct path (only %s unused)",
- len(unusedMidList))
+ LOG.warn("Not enough servers for distinct path (%s unused, %s known)",
+ len(unusedMidList), len(midList))
+
midServers = []
if startServers:
prevNickname = startServers[-1].getNickname()
@@ -491,13 +540,13 @@
midServers.append(info)
prevNickname = n
nNeeded -= 1
- elif midList == 2:
+ elif len(midList) == 2:
# We have enough servers to concoct a path that at least
# _sometimes_ doesn't go to the same server twice in a row.
LOG.warn("Not enough relays to avoid same-server hops")
midList = prng.shuffle(midList)
- midServers = (midList * ceilDiv(nNeeded, 2))[-nNeeded]
- elif midList == 1:
+ midServers = (midList * ceilDiv(nNeeded, 2))[:nNeeded]
+ elif len(midList) == 1:
# There's no point in choosing a long path here: it can only
# have one server in it.
LOG.warn("Only one relay known")
@@ -505,11 +554,11 @@
else:
# We don't know any servers at all.
raise MixError("No relays known")
-
+
LOG.info("Chose path: [%s][%s][%s]",
- " ".join([s.getNickname() for s in startServers]),
- " ".join([s.getNickname() for s in midServers]),
- " ".join([s.getNickname() for s in endServers]))
+ " ".join([ s.getNickname() for s in startServers ]),
+ " ".join([ s.getNickname() for s in midServers ]),
+ " ".join([ s.getNickname() for s in endServers ]))
return startServers + midServers + endServers
@@ -530,7 +579,7 @@
if startAt is None:
startAt = time.time()
if endAt is None:
- endAt = time.time()+3*60*60 # FFFF Configurable
+ endAt = startAt+3*60*60 # FFFF Configurable
# First, find out what the exit node needs to be (or support).
routingType, _, exitNode = address.getRouting()
@@ -545,8 +594,9 @@
# We have a normally-specified path.
if exitNode is not None:
+ exitPath = exitPath[:]
exitPath.append(exitNode)
- nHops -= 1
+
path = keystore.getPath(length=nHops,
startServers=enterPath,
endServers=exitPath,
@@ -562,7 +612,7 @@
% (server.getNickname(), exitCap))
if nSwap is None:
- nSwap = ceilDiv(len(path),2)
+ nSwap = ceilDiv(len(path),2)-1
return path[:nSwap+1], path[nSwap+1:]
def parsePath(keystore, config, path, address, nHops=None,
@@ -571,6 +621,7 @@
(path-leg-1, path-leg-2) tuple.
keystore -- the keystore to use.
+ config -- unused for now.
path -- the path, in a format described below. If the path is
None, all servers are chosen as if the path were '*'.
address -- the address to deliver the message to; if it specifies
@@ -643,7 +694,17 @@
myNSwap = swapPos - 1
else:
# a,*,b:c,d
- myNSwap = myNHops - (len(path)-swapPos-1)
+ # There are len(path)-swapPos-1 servers after the swap point.
+ # There are a total of myNHops servers.
+ # Thus, there are myNHops-(len(path)-swapPos-1) servers up to and
+ # including the swap server.
+ # So, the swap server is at index myNHops - (len(path)-swapPos-1) -1,
+ # which is the same as...
+ myNSwap = myNHops - len(path) + swapPos
+ # But we need to adjust for the last node that we may have to
+ # add because of the address
+ if address.getRouting()[2]:
+ myNSwap -= 1
# Check myNSwap for consistency
if nSwap is not None:
@@ -664,156 +725,6 @@
return resolvePath(keystore, address, enterPath, exitPath,
myNHops, myNSwap, startAt, endAt)
-## class TrivialKeystore:
-## """This is a temporary keystore implementation until we get a working
-## directory server implementation.
-
-## The idea couldn't be simpler: we just keep a directory of files, each
-## containing a single server descriptor. We cache nothing; we validate
-## everything; we have no automatic path generation. Servers can be
-## accessed by nickname, by filename within our directory, or by filename
-## from elsewhere on the filesystem.
-
-## We skip all server descriptors that have expired, that will
-## soon expire, or which aren't yet in effect.
-## """
-## ## Fields:
-## # directory: path to the directory we scan for server descriptors.
-## # byNickname: a map from nickname to valid ServerInfo object.
-## # byFilename: a map from filename within self.directory to valid
-## # ServerInfo object.
-## def __init__(self, directory, now=None):
-## """Create a new TrivialKeystore to access the descriptors stored in
-## directory. Selects descriptors that are valid at the time 'now',
-## or at the current time if 'now' is None."""
-## self.directory = directory
-## createPrivateDir(directory)
-## self.byNickname = {}
-## self.byFilename = {}
-
-## if now is None:
-## now = time.time()
-
-## for f in os.listdir(self.directory):
-## # Try to read a file: is it a server descriptor?
-## p = os.path.join(self.directory, f)
-## try:
-## info = ServerInfo(fname=p, assumeValid=0)
-## except ConfigError:
-## LOG.warn("Invalid server descriptor %s", p)
-## continue
-
-## # Find its nickname and normalized filename
-## serverSection = info['Server']
-## nickname = serverSection['Nickname']
-
-## if '.' in f:
-## f = f[:f.rindex('.')]
-
-## # Skip the descriptor if it isn't valid yet...
-## if now < serverSection['Valid-After']:
-## LOG.info("Ignoring future decriptor %s", p)
-## continue
-## # ... or if it's expired ...
-## if now >= serverSection['Valid-Until']:
-## LOG.info("Ignoring expired decriptor %s", p)
-## continue
-## # ... or if it's going to expire within 3 hours (HACK!).
-## if now + 3*60*60 >= serverSection['Valid-Until']:
-## LOG.info("Ignoring soon-to-expire decriptor %s", p)
-## continue
-## # Only allow one server per nickname ...
-## if self.byNickname.has_key(nickname):
-## LOG.warn(
-## "Ignoring descriptor %s with duplicate nickname %s",
-## p, nickname)
-## continue
-## # ... and per normalized filename.
-## if self.byFilename.has_key(f):
-## LOG.warn(
-## "Ignoring descriptor %s with duplicate prefix %s",
-## p, f)
-## continue
-## LOG.info("Loaded server %s from %s", nickname, f)
-## # Okay, it's good. Cache it.
-## self.byNickname[nickname] = info
-## self.byFilename[f] = info
-
-## def getServerInfo(self, name):
-## """Return a ServerInfo object corresponding to 'name'. If 'name' is
-## a ServerInfo object, returns 'name'. Otherwise, checks server by
-## nickname, then by filename within the keystore, then by filename
-## on the file system. If no server is found, returns None."""
-## if isinstance(name, ServerInfo):
-## return name
-## elif self.byNickname.has_key(name):
-## return self.byNickname[name]
-## elif self.byFilename.has_key(name):
-## return self.byFilename[name]
-## elif os.path.exists(name):
-## try:
-## return ServerInfo(fname=name, assumeValid=0)
-## except OSError, e:
-## raise MixError("Couldn't read descriptor %s: %s" %
-## (name, e))
-## except ConfigError, e:
-## raise MixError("Couldn't parse descriptor %s: %s" %
-## (name, e))
-## else:
-## return None
-
-## def getPath(self, serverList):
-## """Given a sequence of strings of ServerInfo objects, resolves each
-## one according to the rule of getServerInfo, and returns a list of
-## ServerInfos. Raises MixError if any server can't be resolved."""
-## path = []
-## for s in serverList:
-## if isinstance(s, ServerInfo):
-## path.append(s)
-## elif isinstance(s, types.StringType):
-## server = self.getServerInfo(s)
-## if server is not None:
-## path.append(server)
-## else:
-## raise MixError("Couldn't find descriptor %s" % s)
-## return path
-
-## def listServers(self):
-## """Returns a linewise listing of the current servers and their caps.
-## stdout. This will go away or get refactored in future versions
-## once we have real path selection and client-level modules."""
-## lines = []
-## nicknames = self.byNickname.keys()
-## nicknames.sort()
-## longestnamelen = max(map(len, nicknames))
-## fmtlen = min(longestnamelen, 20)
-## format = "%"+str(fmtlen)+"s (expires %s): %s"
-## for n in nicknames:
-## caps = []
-## si = self.byNickname[n]
-## if si['Delivery/MBOX'].get('Version',None):
-## caps.append("mbox")
-## if si['Delivery/SMTP'].get('Version',None):
-## caps.append("smtp")
-## # XXXX This next check is highly bogus.
-## if (si['Incoming/MMTP'].get('Version',None) and
-## si['Outgoing/MMTP'].get('Version',None)):
-## caps.append("relay")
-## until = formatDate(si['Server']['Valid-Until'])
-## line = format % (n, until, " ".join(caps))
-## lines.append(line)
-## return lines
-
-## def getRandomServers(self, prng, n):
-## """Returns a list of n different servers, in random order, according
-## to prng. Raises MixError if not enough exist.
-
-## (This isn't actually used.)"""
-## vals = self.byNickname.values()
-## if len(vals) < n:
-## raise MixError("Not enough servers (%s requested)", n)
-## return prng.shuffle(vals, n)
-
def installDefaultConfig(fname):
"""Create a default, 'fail-safe' configuration in a given file"""
LOG.warn("No configuration file found. Installing default file in %s",
@@ -1008,9 +919,10 @@
# options will change between now and 1.0.0
def runClient(cmd, args):
# DOCDOC
- options, args = getopt.getopt(args, "hvf:i:t:H:P:",
+ options, args = getopt.getopt(args, "hvf:i:t:H:P:D:",
["help", "verbose", "config=", "input=",
"to=", "hops=", "swap-at=", "path",
+ "download-directory=",
])
configFile = '~/.mixminionrc'
inFile = "-"
@@ -1019,6 +931,7 @@
nHops = None
nSwap = None
address = None
+ download = None
for opt,val in options:
if opt in ('-h', '--help'):
usageAndExit(cmd)
@@ -1042,6 +955,14 @@
usageAndExit(cmd, "%s expects an integer"%opt)
elif opt in ('-t', '--to'):
address = parseAddress(val)
+ elif opt in ('-D', '--download-directory'):
+ download = val.lower()
+ if download in ('0','no','false','n','f'):
+ download = 0
+ elif download in ('1','yes','true','y','t','force'):
+ download = 1
+ else:
+ usageAndExit(cmd, "Unrecognized value for %s"%opt)
if args:
usageAndExit(cmd,"Unexpected options")
if address is None:
@@ -1057,6 +978,9 @@
mixminion.Crypto.init_crypto(config)
keystore = ClientKeystore(os.path.expanduser(config['User']['UserDir']))
+ if download != 0:
+ keystore.updateDirectory(forceDownload=download)
+
#try:
if 1:
path1, path2 = parsePath(keystore, config, path, address, nHops, nSwap)
Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- Common.py 3 Jan 2003 08:25:47 -0000 1.40
+++ Common.py 4 Jan 2003 04:12:51 -0000 1.41
@@ -9,9 +9,9 @@
'MixProtocolError', 'ceilDiv', 'checkPrivateDir',
'createPrivateDir', 'floorDiv', 'formatBase64', 'formatDate',
'formatFnameTime', 'formatTime', 'installSignalHandlers',
- 'isSMTPMailbox', 'onReset', 'onTerminate', 'previousMidnight',
- 'readPossiblyGzippedFile',
- 'secureDelete', 'stringContains', 'waitForChildren' ]
+ 'isSMTPMailbox', 'onReset', 'onTerminate', 'openUnique',
+ 'previousMidnight', 'readPossiblyGzippedFile', 'secureDelete',
+ 'stringContains', 'waitForChildren' ]
import base64
import bisect
@@ -542,7 +542,6 @@
def formatFnameTime(when=None):
"""Given a time in seconds since the epoch, returns a date value suitable
for use as part of a fileame. Defaults to the current time."""
- # XXXX002 test
if when is None:
when = time.time()
return time.strftime("%Y%m%d%H%M%S", time.localtime(when))
@@ -690,6 +689,14 @@
def __cmp__(self, other):
"""A == B iff A and B contain exactly the same intervals."""
return cmp(self.edges, other.edges)
+
+ def start(self):
+ """Return the first point contained in this inverval."""
+ return self.edges[0][0]
+
+ def end(self):
+ """Return the last point contained in this interval."""
+ return self.edges[-1][0]
#----------------------------------------------------------------------
# SMTP address functionality
@@ -786,7 +793,7 @@
signal.signal(signal.SIGTERM, _sigHandler)
#----------------------------------------------------------------------
-# Gzip helpers
+# File helpers.
def readPossiblyGzippedFile(fname, mode='r'):
"""Read the contents of the file <fname>. If <fname> ends with ".gz",
@@ -802,4 +809,17 @@
if f is not None:
f.close()
-
+def openUnique(fname, mode='w'):
+ """Helper function. Returns a file open for writing into the file named
+ 'fname'. If fname already exists, opens 'fname.1' or 'fname.2' or
+ 'fname.3' or so on."""
+ base, rest = os.path.split(fname)
+ idx = 0
+ while 1:
+ try:
+ fd = os.open(fname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0600)
+ return os.fdopen(fd, mode), fname
+ except OSError:
+ pass
+ idx += 1
+ fname = os.path.join(base, "%s.%s"%(rest,idx))
Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.30
retrieving revision 1.31
diff -u -d -r1.30 -r1.31
--- ServerInfo.py 3 Jan 2003 08:25:47 -0000 1.30
+++ ServerInfo.py 4 Jan 2003 04:12:51 -0000 1.31
@@ -38,6 +38,9 @@
class ServerInfo(mixminion.Config._ConfigFile):
## Fields: (as in ConfigFile, plus)
# _isValidated: flag. Has this serverInfo been fully validated?
+ # _validatedDigests: a dict whose keys are already-validated server
+ # digests. Optional. Only valid while 'validate' is being called.
+
"""A ServerInfo object holds a parsed server descriptor."""
_restrictFormat = 1
_syntax = {
@@ -77,20 +80,46 @@
}
}
- def __init__(self, fname=None, string=None, assumeValid=0):
+ def __init__(self, fname=None, string=None, assumeValid=0,
+ validatedDigests=None):
+ """Read a server descriptor from a file named <fname>, or from
+ <string>.
+
+ If assumeValid is true, don't bother to validate it.
+
+ If the (computed) digest of this descriptor is a key of the dict
+ validatedDigests, assume we have already validated it, and
+ pass it along.
+ """
self._isValidated = 0
+ self._validatedDigests = validatedDigests
mixminion.Config._ConfigFile.__init__(self, fname, string, assumeValid)
+ del self._validatedDigests
LOG.trace("Reading server descriptor %s from %s",
self['Server']['Nickname'],
fname or "<string>")
def validate(self, sections, entries, lines, contents):
####
- # Check 'Server' section.
+ # Check 'Server' section.
server = sections['Server']
if server['Descriptor-Version'] != '0.1':
raise ConfigError("Unrecognized descriptor version %r",
server['Descriptor-Version'])
+
+ ####
+ # Check the igest of file
+ digest = getServerInfoDigest(contents)
+ if digest != server['Digest']:
+ raise ConfigError("Invalid digest")
+
+ # Have we already validated this particular ServerInfo?
+ if (self._validatedDigests and
+ self._validatedDigests.has_key(digest)):
+ self._isValidated = 1
+ return
+
+ # Validate the rest of the server section.
identityKey = server['Identity']
identityBytes = identityKey.get_modulus_bytes()
if not (MIN_IDENTITY_BYTES <= identityBytes <= MAX_IDENTITY_BYTES):
@@ -107,12 +136,8 @@
if packetKeyBytes != PACKET_KEY_BYTES:
raise ConfigError("Invalid length on packet key")
- ####
- # Check Digest of file
- digest = getServerInfoDigest(contents)
- if digest != server['Digest']:
- raise ConfigError("Invalid digest")
+ ####
# Check signature
try:
signedDigest = pk_check_signature(server['Signature'], identityKey)
@@ -148,6 +173,11 @@
"""Returns this server's nickname"""
return self['Server']['Nickname']
+ def getDigest(self):
+ """Returns the declared (not computed) digest of this server
+ descriptor."""
+ return self['Server']['Digest']
+
def getAddr(self):
"""Returns this server's IP address"""
return self['Server']['IP']
@@ -256,9 +286,14 @@
# servers: list of validated ServerInfo objects, in no particular order.
# header: a _DirectoryHeader object for the non-serverinfo part of this
# directory.
- def __init__(self, string=None, fname=None):
+ def __init__(self, string=None, fname=None, validatedDigests=None):
"""Create a new ServerDirectory object, either from a literal <string>
(if specified) or a filename [possibly gzipped].
+
+ If validatedDigests is provided, it must be a dict whose keys
+ are the digests of already-validated descriptors. Any descriptor
+ whose (calculated) digest matches doesn't need to be validated
+ again.
"""
if string:
contents = string
@@ -277,7 +312,9 @@
servercontents = [ "[Server]\n%s"%s for s in sections[1:] ]
self.header = _DirectoryHeader(headercontents, digest)
- self.servers = [ ServerInfo(string=s) for s in servercontents ]
+ self.servers = [ ServerInfo(string=s,
+ validatedDigests=validatedDigests)
+ for s in servercontents ]
def getServers(self):
"""Return a list of ServerInfo objects in this directory"""
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.55
retrieving revision 1.56
diff -u -d -r1.55 -r1.56
--- test.py 3 Jan 2003 08:47:27 -0000 1.55
+++ test.py 4 Jan 2003 04:12:51 -0000 1.56
@@ -15,6 +15,7 @@
import base64
import cPickle
import cStringIO
+import gzip
import os
import re
import stat
@@ -204,6 +205,11 @@
# 3. That previousMidnight is idempotent
self.assertEquals(previousMidnight(pm), pm)
+ now = time.time()
+ ft = formatFnameTime(now)
+ tm = time.localtime(now)
+ self.assertEquals(ft, "%04d%02d%02d%02d%02d%02d" % tm[:6])
+
def test_isSMTPMailbox(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":
@@ -388,6 +394,25 @@
t(36 not in fromSquareToSquare)
t(100 not in fromSquareToSquare)
+ def test_openUnique(self):
+ d = mix_mktemp()
+ os.mkdir(d)
+ dX = os.path.join(d,"X")
+ f, fn = openUnique(dX)
+ f.write("X")
+ f.close()
+ self.assertEquals(fn, dX)
+
+ f, fn = openUnique(dX)
+ f.write("X")
+ f.close()
+ self.assertEquals(fn, dX+".1")
+
+ f, fn = openUnique(dX)
+ f.write("X")
+ f.close()
+ self.assertEquals(fn, dX+".2")
+
def _intervalEq(self, a, *others):
eq = self.assertEquals
for b in others:
@@ -4124,81 +4149,462 @@
sys.stdout.flush()
return _EXAMPLE_DESCRIPTORS
+def getDirectory(servers, identity):
+ """Return the filename of a newly created server directory, containing
+ the server descriptors provided as literal strings in <servers>,
+ signed with the RSA key <identity>"""
+ SL = mixminion.directory.ServerList.ServerList(mix_mktemp())
+ active = IntervalSet()
+ for s in servers:
+ SL.importServerInfo(s)
+ s = mixminion.ServerInfo.ServerInfo(fname=s, assumeValid=1)
+ active += s.getIntervalSet()
+ start, end = active.start(), active.end()
+ SL.generateDirectory(start, end, 0, identity)
+ return SL.getDirectoryFilename()
+
# variable to hold the latest instance of FakeBCC.
BCC_INSTANCE = None
class ClientMainTests(unittest.TestCase):
- def testTrivialKeystore(self):
+ def testClientKeystore(self):
"""Check out ClientMain's keystore implementation"""
eq = self.assertEquals
- raises = self.failUnlessRaises
-
+ neq = self.assertNotEquals
ServerInfo = mixminion.ServerInfo.ServerInfo
dirname = mix_mktemp()
- ks = mixminion.ClientMain.TrivialKeystore(dirname)
+ ks = mixminion.ClientMain.ClientKeystore(dirname)
+ ## Write the descriptors to disk.
edesc = getExampleServerDescriptors()
+ impdirname = mix_mktemp()
+ createPrivateDir(impdirname)
+ for server, descriptors in edesc.items():
+ for idx in xrange(len(descriptors)):
+ fname = os.path.join(impdirname, "%s%s" % (server,idx))
+ writeFile(fname, descriptors[idx])
+ f = gzip.GzipFile(fname+".gz", 'w')
+ f.write(descriptors[idx])
+ f.close()
- # Test empty keystore
+ ## Test empty keystore
eq(None, ks.getServerInfo("Fred"))
+ self.assertRaises(MixError, ks.getServerInfo, "Fred", strict=1)
+ fred = ks.getServerInfo(os.path.join(impdirname, "fred2"))
+ self.assertEquals("Fred", fred.getNickname())
+ self.assertSameSD(edesc["Fred"][2],fred)
+
+ ## Test importing.
+ ks.importFromFile(os.path.join(impdirname, "Joe0"))
+ ks.importFromFile(os.path.join(impdirname, "Joe1.gz"))
+ ks.importFromFile(os.path.join(impdirname, "Lola1"))
+
+ now = time.time()
+ oneDay=24*60*60
+ for i in 0,1,2,3:
+ self.assertSameSD(edesc["Joe"][0], ks.getServerInfo("Joe"))
+ self.assertSameSD(edesc["Lola"][1], ks.getServerInfo("Lola"))
+ self.assertSameSD(edesc["Joe"][1],
+ ks.getServerInfo("Joe", startAt=now+10*oneDay))
+ self.assertRaises(MixError, ks.getServerInfo, "Joe",
+ startAt=now+30*oneDay)
+ self.assertRaises(MixError, ks.getServerInfo, "Joe", startAt=now,
+ endAt=now+6*oneDay)
+ if i in (0,1,2):
+ ks = mixminion.ClientMain.ClientKeystore(dirname)
+ if i == 1:
+ ks.rescan()
+ if i == 2:
+ ks.rescan(force=1)
+
+ # Refuse to import a server with a modified identity
+ writeFile(os.path.join(impdirname, "Notjoe"),
+ edesc["Lola"][0].replace("Nickname: Lola", "Nickname: Joe"))
+ self.assertRaises(MixError,
+ ks.importFromFile,
+ os.path.join(impdirname, "Notjoe"))
+
+ # Try getServerInfo(ServerInfo)
+ si = ServerInfo(string=edesc['Lisa'][1],assumeValid=1)
+ self.assert_(ks.getServerInfo(si) is si)
try:
suspendLog()
- raises(MixError, ks.getPath, ["Fred"])
+ si = ServerInfo(string=edesc['Lisa'][0],assumeValid=1)
+ self.assert_(ks.getServerInfo(si) is None)
finally:
- resumeLog()
- fred = ServerInfo(string=edesc["Fred"][0])
- eq(1, len(ks.getPath([fred])))
- self.failUnless(ks.getPath([fred])[0] is fred)
+ s = resumeLog()
+ self.assert_(stringContains(s, "Server is not currently"))
- ## Test importing.
- for sname, servers in edesc.items():
- for idx, sdesc in zip(range(len(servers)), servers):
- writeFile(os.path.join(dirname, "%s%02d"%(sname,idx)), sdesc)
+ ##
+ # Now try out the directory. This is tricky; we add the other
+ # descriptors here.
+ identity = getRSAKey(0,2048)
+ fingerprint = Crypto.pk_fingerprint(identity)
+ fname = getDirectory(
+ [os.path.join(impdirname, s) for s in
+ ("Fred1", "Fred2", "Lola2", "Alice0", "Alice1",
+ "Bob3", "Bob4", "Lisa1") ], identity)
+
+ # Replace the real URL and fingerprint with the ones we have; for
+ # unit testing purposes, we can't rely on an http server.
+ mixminion.ClientMain.MIXMINION_DIRECTORY_URL = "file://%s"%fname
+ mixminion.ClientMain.MIXMINION_DIRECTORY_FINGERPRINT = fingerprint
+
+ # Reload the directory.
+ ks.updateDirectory(now=now)
+
+ for i in 0,1,2,3:
+ self.assertSameSD(ks.getServerInfo("Alice"), edesc["Alice"][0])
+ self.assertSameSD(ks.getServerInfo("Bob"), edesc["Bob"][3])
+ self.assertSameSD(ks.getServerInfo("Bob", startAt=now+oneDay*5),
+ edesc["Bob"][4])
+
+ if i in (0,1,2):
+ ks = mixminion.ClientMain.ClientKeystore(dirname)
+ if i == 1:
+ ks.rescan()
+ if i == 2:
+ ks.rescan(force=1)
+
+ replaceFunction(ks, 'downloadDirectory')
+
+ # Now make sure that update is properly zealous.
+ ks.updateDirectory(now=now)
+ self.assertEquals([], getReplacedFunctionCallLog())
+ ks.updateDirectory(now=now, forceDownload=1)
+ self.assertEquals(1, len(getReplacedFunctionCallLog()))
+ ks.updateDirectory(now=now+oneDay+60)
+ self.assertEquals(2, len(getReplacedFunctionCallLog()))
+ undoReplacedAttributes()
+ clearReplacedFunctionCallLog()
+
+ ## Now make sure we can really update the directory.
+ # (this is the same as before, but with 'Lisa2'.)
+ fname = getDirectory(
+ [os.path.join(impdirname, s) for s in
+ ("Fred1", "Fred2", "Lola2", "Alice0", "Alice1",
+ "Bob3", "Bob4", "Lisa1", "Lisa2") ], identity)
+ mixminion.ClientMain.MIXMINION_DIRECTORY_URL = "file://%s"%fname
+ ks.updateDirectory(forceDownload=1)
+ # Previous entries.
+ self.assertSameSD(ks.getServerInfo("Alice"), edesc["Alice"][0])
+ self.assertSameSD(ks.getServerInfo("Bob"), edesc["Bob"][3])
+ # New entry
+ self.assertSameSD(ks.getServerInfo("Lisa",startAt=now+6*oneDay),
+ edesc["Lisa"][2])
+ # Entry from server info
+ self.assertSameSD(edesc["Joe"][0], ks.getServerInfo("Joe"))
+
+ def nRuns(lst):
+ n = 0
+ for idx in xrange(len(lst)-1):
+ if lst[idx] == lst[idx+1]:
+ n += 1
+ return n
+
+ def allUnique(lst):
+ d = {}
+ for item in lst:
+ d[item] = 1
+ return len(d) == len(lst)
suspendLog()
+ joe = edesc["Joe"]
+ lisa = edesc["Lisa"]
+ alice = edesc["Alice"]
+ lola = edesc["Lola"]
+ fred = edesc["Fred"]
+ bob = edesc["Bob"]
try:
- ks = mixminion.ClientMain.TrivialKeystore(dirname)
- finally:
- resumeLog()
+ ### Try out getPath.
+ # 1. Fully-specified paths.
+ p = ks.getPath(startServers=("Joe", "Lisa"),
+ endServers=("Alice", "Joe"))
+ p = ks.getPath(startServers=("Joe", "Lisa", "Alice", "Joe"))
+ p = ks.getPath(endServers=("Joe", "Lisa", "Alice", "Joe"))
- # Try getServerInfo(ServerInfo)
- si = ServerInfo(string=edesc['Lisa'][0])
- self.assert_(si is ks.getServerInfo(si))
+ # 2. Partly-specified paths...
+ # 2a. With plenty of servers
+ p = ks.getPath(length=2)
+ eq(2, len(p))
+ neq(p[0].getNickname(), p[1].getNickname())
- # 'Bob' and 'Fred' are dangerous with regspect to the 'almost-expired'
- # check; don't use them here
- for (s,i) in [("Lola",0), ("Joe",0),("Alice",0),("Lisa",1)]:
- self.assert_(self.isSameServerDesc(edesc[s][i],
- ks.getServerInfo(s)))
- self.assert_(self.isSameServerDesc(edesc[s][i],
- ks.getServerInfo("%s%02d"%(s,i))))
+ p = ks.getPath(startServers=("Joe",), length=3)
+ eq(3, len(p))
+ self.assertSameSD(p[0], joe[0])
+ self.assert_(allUnique([s.getNickname() for s in p]))
+ neq(p[1].getNickname(), "Joe")
+ neq(p[2].getNickname(), "Joe")
+ neq(p[1].getNickname(), p[2].getNickname())
- # Check a nonexistant server.
- self.assertEquals(None, ks.getServerInfo("Foob"))
+ p = ks.getPath(endServers=("Joe",), length=3)
+ eq(3, len(p))
+ self.assertSameSD(joe[0], p[2])
+ self.assert_(allUnique([s.getNickname() for s in p]))
- # Try getPath()
- x = mix_mktemp()
- writeFile(x, edesc["Fred"][1])
- p = ks.getPath(("Lola", "Joe", "Lola00", ks.getServerInfo("Lisa"), x))
- self.assert_(self.isSameServerDesc(ks.getServerInfo("Lola"), p[0]))
- self.assert_(self.isSameServerDesc(ks.getServerInfo("Joe"), p[1]))
- self.assert_(self.isSameServerDesc(ks.getServerInfo("Lola"), p[2]))
- self.assert_(self.isSameServerDesc(ks.getServerInfo("Lisa"), p[3]))
- self.assert_(self.isSameServerDesc(edesc["Fred"][1], p[4]))
+ p = ks.getPath(startServers=("Alice",),endServers=("Joe",),
+ length=4)
+ eq(4, len(p))
+ self.assertSameSD(alice[0], p[0])
+ self.assertSameSD(joe[0], p[3])
+ nicks = [ s.getNickname() for s in p ]
+ eq(1, nicks.count("Alice"))
+ eq(1, nicks.count("Joe"))
+ neq(nicks[1],nicks[2])
+ self.assert_(allUnique([s.getNickname() for s in p]))
- # We fail on nonexistant files.
- self.failUnlessRaises(MixError, ks.getPath, ["Lola", mix_mktemp()])
+ p = ks.getPath(startServers=("Joe",),endServers=("Alice","Joe"),
+ length=4)
+ eq(4, len(p))
+ self.assertSameSD(alice[0], p[2])
+ self.assertSameSD(joe[0], p[0])
+ self.assertSameSD(joe[0], p[3])
+ neq(p[1].getNickname(), "Alice")
+ neq(p[1].getNickname(), "Joe")
+ # 2b. With 3 <= servers < length
+ p = ks.getPath(length=9)
+ eq(9, len(p))
+ self.failIf(nRuns([s.getNickname() for s in p]))
- # We warn on unparseable files.
- fn = os.path.join(dirname, "xyzzy")
- writeFile(fn, "this file is not a server descriptor\n")
- try:
- suspendLog()
- _ = mixminion.ClientMain.TrivialKeystore(dirname)
+ p = ks.getPath(startServers=("Joe",),endServers=("Joe",),length=8)
+ self.failIf(nRuns([s.getNickname() for s in p]))
+ eq(8, len(p))
+ self.assertSameSD(joe[0], p[0])
+ self.assertSameSD(joe[0], p[-1])
+
+ p = ks.getPath(startServers=("Joe",),length=7)
+ self.failIf(nRuns([s.getNickname() for s in p]))
+ eq(7, len(p))
+ self.assertSameSD(joe[0], p[0])
+
+ p = ks.getPath(endServers=("Joe",),length=7)
+ self.failIf(nRuns([s.getNickname() for s in p]))
+ eq(7, len(p))
+ self.assertSameSD(joe[0], p[-1])
+
+ # 2c. With 2 servers
+ p = ks.getPath(length=4, startAt=now-9*oneDay)
+ self.failIf(nRuns([s.getNickname() for s in p]) > 1)
+
+ p = ks.getPath(length=4,startServers=("Joe",),startAt=now-9*oneDay)
+ self.failIf(nRuns([s.getNickname() for s in p]) > 2)
+
+ p = ks.getPath(length=4, endServers=("Joe",), startAt=now-9*oneDay)
+ self.failIf(nRuns([s.getNickname() for s in p]) > 1)
+
+ p = ks.getPath(length=6, endServers=("Joe",), startAt=now-9*oneDay)
+ self.failIf(nRuns([s.getNickname() for s in p]) > 1)
+
+ # 2d. With only 1.
+ p = ks.getPath(length=4, startAt=now-11*oneDay)
+ eq(len(p), 2)
+ p = ks.getPath(length=4,
+ startServers=("Joe",),startAt=now-11*oneDay)
+ eq(len(p), 3)
+ p = ks.getPath(length=4, endServers=("Joe",),startAt=now-11*oneDay)
+ eq(len(p), 2)
+
+ # 2e. With 0
+ self.assertRaises(MixError, ks.getPath,
+ length=4, startAt=now+100*oneDay)
finally:
- msg = resumeLog()
- self.assert_(stringContains(msg,"Invalid server descriptor %s"%fn))
+ s = resumeLog()
+ self.assertEquals(4, s.count("Not enough servers for distinct"))
+ self.assertEquals(4, s.count("to avoid same-server hops"))
+ self.assertEquals(3, s.count("Only one relay known"))
+
+ # 3. With capabilities.
+ p = ks.getPath(length=5, endCap="smtp", midCap="relay")
+ eq(5, len(p))
+ self.assertSameSD(p[-1], joe[0]) # Only Joe has SMTP
+
+ p = ks.getPath(length=4, endCap="mbox", midCap="relay")
+ eq(4, len(p))
+ self.assertSameSD(p[-1], lola[1]) # Only Lola has MBOX
+
+ p = ks.getPath(length=5, endCap="mbox", midCap="relay",
+ startServers=("Alice",))
+ eq(5, len(p))
+ self.assertSameSD(p[-1], lola[1]) # Only Lola has MBOX
+ self.assertSameSD(p[0], alice[0])
+
+ p = ks.getPath(length=5, endCap="mbox", midCap="relay",
+ endServers=("Alice",))
+ eq(5, len(p))
+ self.assertSameSD(p[-1], alice[0]) # We ignore endCap with endServers
+
+ ### Now try parsePath. This should exercise resolvePath as well.
+ ppath = mixminion.ClientMain.parsePath
+ paddr = mixminion.ClientMain.parseAddress
+ email = paddr("smtp:lloyd@dobler.com")
+ mboxWithServer = paddr("mbox:Granola@Lola")
+ mboxWithoutServer = paddr("mbox:Granola")
+
+ alice = ks.getServerInfo("Alice")
+ fred = ks.getServerInfo("Fred")
+ bob = ks.getServerInfo("Bob")
+ joe = ks.getServerInfo("Joe")
+ lola = ks.getServerInfo("Lola")
+ lisa = ks.getServerInfo("Lisa")
+
+ def pathIs(p, exp, self=self):
+ if isinstance(p[0],mixminion.ServerInfo.ServerInfo):
+ p1, p2 = p, ()
+ exp1, exp2 = exp, ()
+ else:
+ p1, p2 = p
+ exp1, exp2 = exp
+ try:
+ self.assertEquals(len(p1),len(exp1))
+ self.assertEquals(len(p2),len(exp2))
+ for a, b in zip(p1, exp1):
+ self.assertSameSD(a,b)
+ for a, b in zip(p2, exp2):
+ self.assertSameSD(a,b)
+ except:
+ print [s.getNickname() for s in p1], \
+ [s.getNickname() for s in p2]
+ raise
+
+ # 1. Successful cases
+ # 1a. No colon, no star
+ fredfile = os.path.join(impdirname, "Fred1")
+ p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Joe", email)
+ pathIs((p1,p2), ((alice,fred),(bob,joe)))
+ fredfile = os.path.join(impdirname, "Fred1")
+ p1,p2 = ppath(ks, None, "Alice,%s,Bob,Joe"%fredfile, email)
+ pathIs((p1,p2), ((alice,fred),(bob,joe)))
+ p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Joe", email, nHops=4, nSwap=1)
+ pathIs((p1,p2), ((alice,fred),(bob,joe)))
+ p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Lola,Joe", email, nHops=5,
+ nSwap=1)
+ pathIs((p1,p2), ((alice,fred),(bob,lola,joe)))
+ p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Lola,Joe", email, nHops=5)
+ pathIs((p1,p2), ((alice,fred,bob),(lola,joe)))
+ p1,p2 = ppath(ks, None, "Alice,Fred,Bob", mboxWithServer)
+ pathIs((p1,p2), ((alice,fred),(bob,lola)))
+ p1,p2 = ppath(ks, None, "Alice,Fred,Bob,Lola", mboxWithoutServer)
+ pathIs((p1,p2), ((alice,fred),(bob,lola)))
+ p1,p2 = ppath(ks, None, "Alice,Fred,Bob", mboxWithServer, nSwap=0)
+ pathIs((p1,p2), ((alice,),(fred,bob,lola)))
+
+ # 1b. Colon, no star
+ p1,p2 = ppath(ks, None, "Alice:Fred,Joe", email)
+ pathIs((p1,p2), ((alice,),(fred,joe)))
+ p1,p2 = ppath(ks, None, "Alice:Bob,Fred,Joe", email)
+ pathIs((p1,p2), ((alice,),(bob,fred,joe)))
+ p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", email)
+ pathIs((p1,p2), ((alice,bob,fred),(joe,)))
+ p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", email, nHops=4)
+ pathIs((p1,p2), ((alice,bob,fred),(joe,)))
+ p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", email, nSwap=2)
+ pathIs((p1,p2), ((alice,bob,fred),(joe,)))
+ p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Joe", mboxWithServer)
+ pathIs((p1,p2), ((alice,bob,fred),(joe,lola)))
+ p1,p2 = ppath(ks, None, "Alice,Bob,Fred:Lola", mboxWithoutServer)
+ pathIs((p1,p2), ((alice,bob,fred),(lola,)))
+
+ # 1c. Star, no colon
+ p1,p2 = ppath(ks, None, 'Alice,*,Joe', email, nHops=5)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p2[-1]), (alice, joe))
+ eq((len(p1),len(p2)), (3,2))
+
+ p1,p2 = ppath(ks, None, 'Alice,Bob,*,Joe', email, nHops=6)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p1[1],p2[-1]), (alice, bob, joe))
+ eq((len(p1),len(p2)), (3,3))
+
+ p1,p2 = ppath(ks, None, 'Alice,Bob,*', email, nHops=6)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p1[1],p2[-1]), (alice, bob, joe))
+ eq((len(p1),len(p2)), (3,3))
+
+ p1,p2 = ppath(ks, None, '*,Bob,Joe', email) #default nHops=6
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p2[-2],p2[-1]), (bob, joe))
+ eq((len(p1),len(p2)), (3,3))
+
+ p1,p2 = ppath(ks, None, 'Bob,*,Alice', mboxWithServer) #default nHops=6
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
+ eq((len(p1),len(p2)), (3,3))
+
+ p1,p2 = ppath(ks, None, 'Bob,*,Alice,Lola', mboxWithoutServer)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
+ eq((len(p1),len(p2)), (3,3))
+
+ # 1d. Star and colon
+ p1,p2 = ppath(ks, None, 'Bob:*,Alice', mboxWithServer)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
+ eq((len(p1),len(p2)), (1,5))
+
+ p1,p2 = ppath(ks, None, 'Bob,*:Alice', mboxWithServer)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p2[-2],p2[-1]), (bob, alice, lola))
+ eq((len(p1),len(p2)), (4,2))
+
+ p1,p2 = ppath(ks, None, 'Bob,*,Joe:Alice', mboxWithServer)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p1[-1],p2[-2],p2[-1]), (bob, joe, alice, lola))
+ eq((len(p1),len(p2)), (4,2))
+
+ p1,p2 = ppath(ks, None, 'Bob,*,Lola:Alice,Joe', email)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p1[-1],p2[-2],p2[-1]), (bob, lola, alice, joe))
+ eq((len(p1),len(p2)), (4,2))
+
+ p1,p2 = ppath(ks, None, '*,Lola:Alice,Joe', email)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[-1],p2[-2],p2[-1]), (lola, alice, joe))
+ eq((len(p1),len(p2)), (4,2))
+
+ p1,p2 = ppath(ks, None, 'Lola:Alice,*', email)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p2[0],p2[-1]), (lola, alice, joe))
+ eq((len(p1),len(p2)), (1,5))
+
+ p1,p2 = ppath(ks, None, 'Bob:Alice,*', mboxWithServer)
+ self.assert_(allUnique([s.getNickname() for s in p1+p2]))
+ pathIs((p1[0],p2[0],p2[-1]), (bob, alice, lola))
+ eq((len(p1),len(p2)), (1,5))
+
+ # 2. Failing cases
+ raises = self.assertRaises
+ # Nonexistant server
+ raises(MixError, ppath, ks, None, "Pierre:Alice,*", email)
+ # Two swap points
+ raises(MixError, ppath, ks, None, "Alice:Bob:Joe", email)
+ # Last hop doesn't support exit type
+ raises(MixError, ppath, ks, None, "Alice:Bob,Fred", email)
+ raises(MixError, ppath, ks, None, "Alice:Bob,Fred", mboxWithoutServer)
+ # Two stars.
+ raises(MixError, ppath, ks, None, "Alice,*,Bob,*,Joe", email)
+ # Swap point mismatch
+ raises(MixError, ppath, ks, None, "Alice:Bob,Joe", email, nSwap=1)
+ # NHops mismatch
+ raises(MixError, ppath, ks, None, "Alice:Bob,Joe", email, nHops=2)
+ raises(MixError, ppath, ks, None, "Alice:Bob,Joe", email, nHops=4)
+ # Nonexistant file
+ raises(MixError, ppath, ks, None, "./Pierre:Alice,*", email)
+
+ ## Try 'expungeByNickname'.
+ # Zapping 'Lisa' does nothing, since she's in the directory...
+ ks.expungeByNickname("Lisa")
+ self.assertSameSD(ks.getServerInfo("Lisa",startAt=now+6*oneDay),
+ edesc["Lisa"][2])
+ # But Joe can be removed.
+ ks.expungeByNickname("Joe")
+ eq(None, ks.getServerInfo("Joe"))
+
+ ## Now try clean()
+ ks.clean() # Should do nothing.
+ ks = mixminion.ClientMain.ClientKeystore(dirname)
+ ks.clean(now=now+oneDay*500) # Should zap all of imported servers.
+ raises(MixError, ks.getServerInfo, "Lola")
def testAddress(self):
def parseEq(s, tp, addr, server, eq=self.assertEquals):
@@ -4258,9 +4664,10 @@
# Now try with some servers...
edesc = getExampleServerDescriptors()
- writeFile(os.path.join(serverdir,"lola1"), edesc["Lola"][1])
- writeFile(os.path.join(serverdir,"joe1"), edesc["Joe"][0])
- writeFile(os.path.join(serverdir,"alice1"), edesc["Alice"][0])
+ ServerInfo = mixminion.ServerInfo.ServerInfo
+ Lola = ServerInfo(string=edesc["Lola"][1], assumeValid=1)
+ Joe = ServerInfo(string=edesc["Joe"][0], assumeValid=1)
+ Alice = ServerInfo(string=edesc["Alice"][1], assumeValid=1)
# ... and for now, we need to restart the client.
client = mixminion.ClientMain.MixminionClient(usercfg)
@@ -4278,11 +4685,11 @@
client.generateForwardMessage(
parseAddress("joe@cledonism.net"),
payload,
- path1=["Lola", "Joe"], path2=["Alice", "Joe"])
+ servers1=[Lola, Joe], servers2=[Alice, Joe])
client.generateForwardMessage(
parseAddress("smtp:joe@cledonism.net"),
"Hey Joe, where you goin' with that gun in your hand?",
- path1=["Lola", "Joe"], path2=["Alice", "Joe"])
+ servers1=[Lola, Joe], servers2=[Alice, Joe])
for fn, args, kwargs in getCalls():
self.assertEquals(fn, "buildForwardMessage")
@@ -4298,12 +4705,12 @@
client.generateForwardMessage(
parseAddress("mbox:granola"),
payload,
- path1=["Lola", "Joe"], path2=["Alice", "Lola"])
+ servers1=[Lola, Joe], servers2=[Alice, Lola])
# And an mbox message with a last hop implicit in the address
client.generateForwardMessage(
parseAddress("mbox:granola@Lola"),
payload,
- path1=["Lola", "Joe"], path2=["Alice"])
+ servers1=[Lola, Joe], servers2=[Alice, Lola])
for fn, args, kwargs in getCalls():
self.assertEquals(fn, "buildForwardMessage")
@@ -4311,7 +4718,7 @@
(payload, MBOX_TYPE, "granola"))
self.assert_(len(args[3]) == len(args[4]) == 2)
self.assertEquals(["Lola", "Joe", "Alice", "Lola"],
- [x['Server']['Nickname'] for x in args[3]+args[4]])
+ [x.getNickname() for x in args[3]+args[4]])
clearCalls()
finally:
undoReplacedAttributes()
@@ -4322,23 +4729,7 @@
self.assertRaises(MixError,
client.generateForwardMessage,
parseAddress("0xFFFF:zed"),
- "Z", [], ["Alice"])
- # Nonexistant servers...
- self.assertRaises(MixError,
- client.generateForwardMessage,
- parseAddress("0xFFFF:zed"),
- "Z", ["Marvin"], ["Fred"])
- # Lola doesn't support SMTP...
- self.assertRaises(MixError,
- client.generateForwardMessage,
- parseAddress("smtp:joe@cledonism.net"),
- "Z", ["Joe"], ["Lola"])
- # Joe doesn't support MBOX...
- self.assertRaises(MixError,
- client.generateForwardMessage,
- parseAddress("mbox:wahoo"),
- "Z", ["Lola"], ["Joe"])
-
+ "Z", [], [Alice])
# Temporarily replace BlockingClientConnection so we can try the client
# without hitting the network.
@@ -4365,7 +4756,7 @@
client.sendForwardMessage(
parseAddress("mbox:granola@Lola"),
"You only give me your information.",
- ["Alice", "Lola", "Joe", "Alice"], ["Joe", "Alice"])
+ [Alice, Lola, Joe, Alice], [Joe, Alice])
bcc = BCC_INSTANCE
# first hop is alice
self.assertEquals(bcc.addr, "10.0.0.9")
@@ -4378,6 +4769,9 @@
undoReplacedAttributes()
clearCalls()
+ def assertSameSD(self, s1, s2):
+ self.assert_(self.isSameServerDesc(s1,s2))
+
def isSameServerDesc(self, s1, s2):
"""s1 and s2 are either ServerInfo objects or strings containing server
descriptors. Returns 1 iff their digest fields match"""
@@ -4387,8 +4781,10 @@
m = re.search(r"^Digest: (\S+)\n", s, re.M)
assert m
ds.append(base64.decodestring(m.group(1)))
+ elif isinstance(s, mixminion.ServerInfo.ServerInfo):
+ ds.append(s.getDigest())
else:
- ds.append(s['Server']['Digest'])
+ return 0
return ds[0] == ds[1]
#----------------------------------------------------------------------