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

[minion-cvs] Commit changes before moving on to paper.



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.seul.org:/tmp/cvs-serv6099/lib/mixminion

Modified Files:
	ClientMain.py Common.py Config.py ServerInfo.py ServerMain.py 
	test.py 
Log Message:
Commit changes before moving on to paper.

ClientMain: debug keystore

Common: remove dead code

Config,ServerInfo: Change date format from DD/MM/YYYY (which confuses many 
   USians and maybe others too) into YYYY/MM/DD (which seems to be unambiguous
   for everyone)

ServerInfo: Guess IP address; don't default to 0.0.0.0

ServerMain: Slight doc improvements

test:
	- fix suspendLog code
	- test new date format
	- Make all serverinfo objects explicitly give an IP.
	- Test client keyring code



Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- ClientMain.py	13 Oct 2002 01:34:44 -0000	1.4
+++ ClientMain.py	30 Oct 2002 02:19:39 -0000	1.5
@@ -22,7 +22,7 @@
 #           directories.  Each server can have any number of virtual or 
 #           official tags.  Users should use the CLI to add/remove entries from
 #           dir.)
-#      - Per-systemm directory location is a neat idea, but individual users
+#      - Per-system directory location is a neat idea, but individual users
 #        must check signature.  That's a way better idea for later.
 
 import os
@@ -31,12 +31,13 @@
 import time
 import bisect
 
+from mixminion.Common import getLog, floorDiv, createPrivateDir, MixError
 import mixminion.Crypto
-from mixminion.Common import getLog, floorDiv, createPrivateDir
-import mixminion.Config
 import mixminion.BuildMessage
 import mixminion.MMTPClient
 import mixminion.Modules
+from mixminion.ServerInfo import ServerInfo
+from mixminion.Config import ClientConfig
 
 class DirectoryCache:
     """Holds a set of directories and serverinfo objects persistently.
@@ -86,9 +87,9 @@
 	    if self.servers.has_key(nickname):
 		self.servers[nickname].append(info)
 	    else:
-		self.servers[nickname] = info
+		self.servers[nickname] = [ info ]
 
-    def getCurrentServer(nickname, when=None, until=None):
+    def getCurrentServer(self,nickname, when=None, until=None):
         """Return a server descriptor valid during the interval
            when...until.  If 'nickname' is a string, return only a
            server with the appropriate nickname.  If 'nickname' is a
@@ -100,7 +101,7 @@
 	    when = time.time()
 	if until is None:
 	    until = when+1
-	if type(nickname) == ServerInfo:
+	if isinstance(nickname, ServerInfo):
 	    serverList = [ nickname ]
 	else:
 	    try:
@@ -114,7 +115,7 @@
 		return info
 	raise MixError("No time-valid information for server %s"%nickname)
 
-    def getAllCurrentServers(when=None, until=None):
+    def getAllCurrentServers(self, when=None, until=None):
 	"""Return all ServerInfo objects valid during a given interval."""
         self.load()
 	if when is None:
@@ -129,13 +130,17 @@
 		    result.append(info)
 	return result
 
-    def importServerInfo(self, fname, force=1):
+    def importServerInfo(self, fname, force=1, string=None):
 	"""Import a server descriptor from an external file into the internal
 	   cache.  Return 1 on import; 0 on failure."""
 	self.load()
-	f = open(fname)
-	contents = f.read()
-	f.close()
+	if string is None:
+	    f = open(fname)
+	    contents = f.read()
+	    f.close()
+	else:
+	    assert fname is None
+	    contents = string
 	info = ServerInfo(string=contents, assumeValid=0)
 	now = time.time()
 	if info['Server']['Valid-Until'] < now:
@@ -154,17 +159,22 @@
 		else:
 		    getLog().error("... importing anyway.")
 	
+	    for other in self.servers[nickname]:
+		if other['Server']['Digest'] == info['Server']['Digest']:
+		    getLog().warn("Duplicate server info; skipping")
+		    return 0
+
 	    self.servers[nickname].append(info)
 	else:
-	    self.servers[nickname] = info
+	    self.servers[nickname] = [ info ]
 
 	self.allServers.append(info)
 
 	self.highest_num += 1
 	fname_new = "si%d" % self.highest_num
-	f = os.fdopen(os.open(os.path.join(self.dirname, fname_name),
-			      os.O_CREAT|os.O_EXCL, 0600),
-		      'w')
+	fd = os.open(os.path.join(self.dirname, fname_new), 
+		     os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0600)
+	f = os.fdopen(fd, 'w')
 	f.write(contents)
 	f.close()
 
@@ -210,7 +220,7 @@
 		if not os.path.exists(conf):
 		    installDefaultConfig(conf)
 	conf = os.path.expanduser(conf)
-	self.config = mixminion.Config.ClientConfig(fname=conf)
+	self.config = ClientConfig(fname=conf)
 
 	getLog().configure(self.config)
 	getLog().debug("Configuring client")
@@ -243,6 +253,9 @@
 	servers = self.dirCache.getAllCurrentServers(when=time.time(),
 					     until=time.time()+24*60*60)
 
+	# XXXX Pick only servers that relay to all other servers!
+	# XXXX Watch out for many servers with the same IP or nickname or...
+
 	if length > len(servers):
 	    getLog().warn("I only know about %s servers; That's not enough to use distinct servers on your path.", len(servers))
 	    result = []
@@ -382,7 +395,7 @@
 
 def readConfigFile(configFile):
     try:
-	return mixminion.Config.ClientConfig(fname=configFile)
+	return ClientConfig(fname=configFile)
     except (IOError, OSError), e:
 	print >>sys.stderr, "Error reading configuration file %r:"%configFile
 	print >>sys.stderr, "   ", str(e)
@@ -412,7 +425,7 @@
     mixminion.Crypto.init_crypto(config)
     if len(args) < 2:
 	print >> sys.stderr, "I need at least 2 servers"
-    servers = [ mixminion.ServerInfo.ServerInfo(fn) for fn in args ]
+    servers = [ ServerInfo(fn) for fn in args ]
     idx = floorDiv(len(servers),2)
 
     sendTestMessage(servers[:idx], servers[idx:])

Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.22
retrieving revision 1.23
diff -u -d -r1.22 -r1.23
--- Common.py	16 Sep 2002 15:30:02 -0000	1.22
+++ Common.py	30 Oct 2002 02:19:39 -0000	1.23
@@ -371,8 +371,6 @@
     
     # we set the DST flag to zero so that subtracting time.timezone always
     # gives us gmt.
-    #return time.mktime((yyyy,MM,dd,hh,mm,ss,0,0,0))-time.timezone
-
     return calendar.timegm((yyyy,MM,dd,hh,mm,ss,0,0,0))
 
 def previousMidnight(when):

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.17
retrieving revision 1.18
diff -u -d -r1.17 -r1.18
--- Config.py	16 Sep 2002 15:30:02 -0000	1.17
+++ Config.py	30 Oct 2002 02:19:39 -0000	1.18
@@ -250,10 +250,10 @@
 	raise ConfigError("Invalid exponent on public key")
     return key
 
-_date_re = re.compile(r"(\d\d)/(\d\d)/(\d\d\d\d)")
-_time_re = re.compile(r"(\d\d)/(\d\d)/(\d\d\d\d) (\d\d):(\d\d):(\d\d)")
+_date_re = re.compile(r"(\d\d\d\d)/(\d\d)/(\d\d)")
+_time_re = re.compile(r"(\d\d\d\d)/(\d\d)/(\d\d) (\d\d):(\d\d):(\d\d)")
 def _parseDate(s,_timeMode=0):
-    """Validation function.  Converts from DD/MM/YYYY format to a (long)
+    """Validation function.  Converts from YYYY/MM/DD format to a (long)
        time value for midnight on that date."""
     s = s.strip()
     r = (_date_re, _time_re)[_timeMode]
@@ -261,9 +261,9 @@
     if not m:
 	raise ConfigError("Invalid %s %r" % (("date", "time")[_timeMode],s))
     if _timeMode:
-	dd, MM, yyyy, hh, mm, ss = map(int, m.groups())
+	yyyy, MM, dd, hh, mm, ss = map(int, m.groups())
     else:
-	dd, MM, yyyy = map(int, m.groups())
+	yyyy, MM, dd = map(int, m.groups())
 	hh, mm, ss = 0, 0, 0	
 
     if not ((1 <= dd <= 31) and (1 <= MM <= 12) and
@@ -274,7 +274,7 @@
     return mixminion.Common.mkgmtime(yyyy, MM, dd, hh, mm, ss)
 
 def _parseTime(s):
-    """Validation function.  Converts from DD/MM/YYYY HH:MM:SS format
+    """Validation function.  Converts from YYYY/MM/DD HH:MM:SS format
        to a (float) time value for GMT."""
     return _parseDate(s,1)
 
@@ -647,9 +647,6 @@
 	if p < 4:
 	    getLog().warn("Your default path length is frighteningly low."
 			  "  I'll trust that you know what you're doing.")
-	
-	    
-	    
 
 SERVER_SYNTAX =  {
         'Host' : ClientConfig._syntax['Host'],
@@ -678,7 +675,7 @@
                                             "10 minutes",) },
 	# FFFF Generic multi-port listen/publish options.
         'Incoming/MMTP' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
-			    'IP' : ('ALLOW', _parseIP, None),
+			    'IP' : ('ALLOW', _parseIP, "0.0.0.0"),
                             'Port' : ('ALLOW', _parseInt, "48099"),
                             'Allow' : ('ALLOW*', _parseAddressSet_allow, None),
                             'Deny' : ('ALLOW*', _parseAddressSet_deny, None) },

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.14
retrieving revision 1.15
diff -u -d -r1.14 -r1.15
--- ServerInfo.py	10 Sep 2002 14:45:31 -0000	1.14
+++ ServerInfo.py	30 Oct 2002 02:19:39 -0000	1.15
@@ -15,7 +15,7 @@
 import base64
 import socket
 
-from mixminion.Common import createPrivateDir, getLog
+from mixminion.Common import createPrivateDir, getLog, MixError
 from mixminion.Modules import SWAP_FWD_TYPE, FWD_TYPE
 from mixminion.Packet import IPV4Info
 import mixminion.Config
@@ -156,7 +156,7 @@
         self.descFile = os.path.join(keydir, "ServerDesc")
         if not os.path.exists(keydir):
 	    createPrivateDir(keydir)
-        
+
     def load(self, password=None):
         "Read this set of keys from disk."
         self.packetKey = mixminion.Crypto.pk_PEM_load(self.packetKeyFile,
@@ -186,14 +186,14 @@
     """Helper function: turns a time (in seconds) into the format used by
        Server descriptors"""
     gmt = time.gmtime(t)
-    return "%02d/%02d/%04d %02d:%02d:%02d" % (
-	gmt[2],gmt[1],gmt[0],  gmt[3],gmt[4],gmt[5])
+    return "%04d/%02d/%02d %02d:%02d:%02d" % (
+	gmt[0],gmt[1],gmt[2],  gmt[3],gmt[4],gmt[5])
 
 def _date(t):
     """Helper function: turns a time (in seconds) into a date in the format
        used by server descriptors"""
     gmt = time.gmtime(t+1) # Add 1 to make sure we round down.
-    return "%02d/%02d/%04d" % (gmt[2],gmt[1],gmt[0])
+    return "%04d/%02d/%02d" % (gmt[0],gmt[1],gmt[2])
 
 def _rule(allow, (ip, mask, portmin, portmax)):
     if mask == '0.0.0.0':
@@ -279,6 +279,14 @@
 	"KeyID":
 	   _base64(serverKeys.getMMTPKeyID()),
 	}
+
+    if fields['IP'] == '0.0.0.0':
+	try:
+	    fields['IP'] = _guessLocalIP()
+	    getLog().warn("No IP configured; guessing %s",fields['IP'])
+	except IPGuessError, e:
+	    getLog().error("Can't guess IP: %s", str(e))
+	    raise MixError()
 	
     info = """\
         [Server]
@@ -353,7 +361,7 @@
 def signServerInfo(info, rsa):
     """Sign a server descriptor.  <info> should be a well-formed server
        descriptor, with Digest: and Signature: lines present but with
-       no values."""       
+       no values."""
     return _getServerInfoDigestImpl(info, rsa)
 
 def _getServerInfoDigestImpl(info, rsa=None):
@@ -395,3 +403,53 @@
     infoLines[signatureLine] = 'Signature: '+signature
 
     return "\n".join(infoLines)
+
+
+class IPGuessError(MixError):
+    pass
+
+_GUESSED_IP = None
+
+def _guessLocalIP():
+    "Try to find a reasonable IP for this host."
+    global _GUESSED_IP
+    if _GUESSED_IP is not None:
+	return _GUESSED_IP
+
+    # First, let's see what our name resolving subsystem says our
+    # name is.
+    ip_set = {}
+    try:
+	ip_set[ socket.gethostbyname(socket.gethostname()) ] = 1
+    except socket.error, host_error:
+	try:
+	    ip_by_host = socket.gethostbyname(socket.getfqdn())
+	except socket.error, _:
+	    pass
+
+    # And in case that doesn't work, let's see what other addresses we might
+    # think we have by using 'getsockname'.
+    for target_addr in ('18.0.0.1', '10.0.0.1', '192.168.0.1',
+			'172.16.0.1')+tuple(ip_set.keys()):
+	# open a datagram socket so that we don't actually send any packets
+	# by connecting.
+	try:
+	    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+	    s.connect((target_addr, 9)) #discard port
+	    ip_set[ s.getsockname()[0] ] = 1
+	except socket.error, _:
+	    pass
+
+    if len(ip_set) == 0:
+	raise IPGuessError("No address found")
+
+    for ip in ip_set.keys():
+	if ip.startswith("127.") or ip.startswith("0."):
+	    del ip_set[ip]
+
+    if len(ip_set) > 1:
+	raise IPGuessError("Multiple addresses found: %s" % (
+	            ", ".join(ip_set)))
+
+    return ip_set.keys()[0]
+

Index: ServerMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerMain.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- ServerMain.py	16 Sep 2002 15:30:02 -0000	1.10
+++ ServerMain.py	30 Oct 2002 02:19:39 -0000	1.11
@@ -3,7 +3,9 @@
 
 """mixminion.ServerMain
 
-   The main loop and related functionality for a Mixminion server
+   The main loop and related functionality for a Mixminion server.
+   See the "MixminionServer" class for more information about how it
+   all works.
 
    BUG: No support for encrypting private keys."""
 
@@ -436,19 +438,32 @@
 	nextRotate = self.keyring.getNextKeyRotation() # FFFF use this.
 	while 1:
 	    while time.time() < nextMix:
+		# Handle pending network events
 		self.mmtpServer.process(1)
+		# Process any new messages that have come in, placing them
+		# into the mix pool.
 		self.incomingQueue.sendReadyMessages()
 	    
+	    # Before we mix, we need to log the hashes to avoid replays.
+	    # FFFF We need to recover on server failure.
 	    self.packetHandler.syncLogs()
+	    
 	    getLog().trace("Mix interval elapsed")
+	    # Choose a set of outgoing messages; put them in outgoingqueue and
+	    # modulemanger
 	    self.mixPool.mix()
+	    # Send outgoing messages
 	    self.outgoingQueue.sendReadyMessages()
+	    # Send exit messages
 	    self.moduleManager.sendReadyMessages()
 
+	    # Choose next mix interval
 	    now = time.time()
 	    nextMix = now + 60
+
 	    if now > nextShred:
-		# Configurable shred interval
+		# FFFF Configurable shred interval
+		getLog().trace("Expunging queues")
 		self.incomingQueue.cleanQueue()
 		self.mixPool.queue.cleanQueue()
 		self.outgoingQueue.cleanQueue()
@@ -516,6 +531,7 @@
 	getLog().fatal_exc(sys.exc_info(),"Exception while running server")
     getLog().info("Server shutting down")
     server.close()
+    getLog().info("Server is shut down")
     
     sys.exit(0)
 

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.33
retrieving revision 1.34
diff -u -d -r1.33 -r1.34
--- test.py	21 Oct 2002 02:52:03 -0000	1.33
+++ test.py	30 Oct 2002 02:19:39 -0000	1.34
@@ -25,7 +25,8 @@
 import cStringIO
 
 from mixminion.testSupport import mix_mktemp
-from mixminion.Common import MixError, MixFatalError, MixProtocolError, getLog
+from mixminion.Common import MixError, MixFatalError, MixProtocolError, \
+     getLog, previousMidnight
 import mixminion.Crypto as Crypto
 
 try:
@@ -58,7 +59,7 @@
     if hasattr(log, '_storedHandlers'):
 	resumeLog()
     buf = cStringIO.StringIO()
-    h = mixminion.Common._ConsoleLogHandler(cStringIO.StringIO())
+    h = mixminion.Common._ConsoleLogHandler(buf)
     log._storedHandlers = log.handlers
     log._testBuf = buf
     log.handlers = []
@@ -74,7 +75,7 @@
     del log._testBuf
     log.handlers = log._storedHandlers
     del log._storedHandlers
-    return str(buf)
+    return buf.getvalue()
 
 # RSA key caching functionality
 _generated_rsa_keys = {}
@@ -1417,7 +1418,7 @@
 	for k in s:
 	    key = Keyset(k).getLionessKeys(PAYLOAD_ENCRYPT_MODE)
 	    m = lioness_decrypt(m,key)
-	self.assertEquals(payload, 
+	self.assertEquals(payload,
 		     BuildMessage.decodeStatelessReplyPayload(m,tag,passwd))
 	repl2, repl2tag = m, tag
 	
@@ -1432,13 +1433,13 @@
 	    for d in (sdict, None):
 		for p in (passwd, None):
 		    for tag in ("zzzz"*5, "pzzz"*5):
-			self.assertEquals(payload, 
+			self.assertEquals(payload,
 					  decodePayload(encoded1, tag,pk,d,p))
 	
 	# efwd
 	for d in (sdict, None):
 	    for p in (passwd, None):
-		self.assertEquals(payload, 
+		self.assertEquals(payload,
 		        decodePayload(efwd_p, efwd_t, self.pk1, d,p))
 		self.assertEquals(None,
 		        decodePayload(efwd_p, efwd_t, None, d,p))
@@ -1476,12 +1477,12 @@
 	
 	# Bad efwd
 	efwd_pbad = efwd_p[:-1] + chr(ord(efwd_p[-1])^0xaa)
-	self.failUnlessRaises(MixError, 
-			      BuildMessage.decodeEncryptedForwardPayload, 
+	self.failUnlessRaises(MixError,
+			      BuildMessage.decodeEncryptedForwardPayload,
 			      efwd_pbad, efwd_t, self.pk1)
 	for d in (sdict, None):
 	    for p in (passwd, None):
-		self.failUnlessRaises(MixError, decodePayload, 
+		self.failUnlessRaises(MixError, decodePayload,
 				      efwd_pbad, efwd_t, self.pk1, d, p)
 		self.assertEquals(None,
 			  decodePayload(efwd_pbad, efwd_t, self.pk2, d,p))
@@ -1495,9 +1496,9 @@
 			 decodePayload, repl1_bad, "tag1"*5, pk, sd, p)
 		sd = sdict.copy()
 		self.failUnlessRaises(MixError,
-			 BuildMessage.decodeReplyPayload, repl1_bad, 
+			 BuildMessage.decodeReplyPayload, repl1_bad,
 				      sd["tag1"*5])
-	# Bad srepl 
+	# Bad srepl
 	repl2_bad = repl2[:-1] + chr(ord(repl2[-1])^0xaa)
 	self.assertEquals(None,
 		  decodePayload(repl2_bad, repl2tag, None, None, passwd))
@@ -2475,11 +2476,11 @@
 
 	self.assertEquals(C._parseBase64(" YW\nJj"), "abc")
 	self.assertEquals(C._parseHex(" C0D0"), "\xC0\xD0")
-	tm = C._parseDate("30/05/2002")
+	tm = C._parseDate("2002/05/30")
 	self.assertEquals(time.gmtime(tm)[:6], (2002,5,30,0,0,0))
-	tm = C._parseDate("01/01/2000")
+	tm = C._parseDate("2000/01/01")
 	self.assertEquals(time.gmtime(tm)[:6], (2000,1,1,0,0,0))
-	tm = C._parseTime("25/12/2001 06:15:10")
+	tm = C._parseTime("2001/12/25 06:15:10")
 	self.assertEquals(time.gmtime(tm)[:6], (2001,12,25,6,15,10))
 
         def fails(fn, val, self=self):
@@ -2505,11 +2506,11 @@
 	fails(C._parseBase64, "Y")
 	fails(C._parseHex, "Z")
 	fails(C._parseHex, "A")
-	fails(C._parseDate, "1/1/2000")
-	fails(C._parseDate, "01/50/2000")
-	fails(C._parseDate, "01/50/2000 12:12:12")
-	fails(C._parseTime, "01/50/2000 12:12:12")
-	fails(C._parseTime, "01/50/2000 12:12:99")
+	fails(C._parseDate, "2000/1/1")
+	fails(C._parseDate, "2000/50/01")
+	fails(C._parseDate, "2000/50/01 12:12:12")
+	fails(C._parseTime, "2000/50/01 12:12:12")
+	fails(C._parseTime, "2000/50/01 12:12:99")
 
         nonexistcmd = '/file/that/does/not/exist'
         if not os.path.exists(nonexistcmd):
@@ -2570,8 +2571,8 @@
 Nickname: fred-the-bunny
 """
 
-def _getIdentityKey():
-    return getRSAKey(0,2048)
+def _getIdentityKey(n=0):
+    return getRSAKey(n,2048)
 
 import mixminion.Config
 import mixminion.ServerInfo
@@ -2645,7 +2646,11 @@
         # Now with a shorter configuration
 	try:
 	    suspendLog()
-	    conf = mixminion.Config.ServerConfig(string=SERVER_CONFIG_SHORT)
+	    conf = mixminion.Config.ServerConfig(string=SERVER_CONFIG_SHORT+
+					   """[Incoming/MMTP]
+Enabled: yes
+IP: 192.168.0.99
+""")
 	finally:
 	    resumeLog()
 	mixminion.ServerInfo.generateServerDescriptorAndKeys(conf,
@@ -2653,7 +2658,6 @@
 							     d,
 							     "key2",
 							     d)
-
         # Now with a bad signature
         sig2 = mixminion.Crypto.pk_sign(sha1("Hello"), identity)
         sig2 = base64.encodestring(sig2).replace("\n", "")
@@ -2855,18 +2859,18 @@
 	# plaintext text message, bin mode.
 	self.assertEquals(dem(payload, tag, 0), ("TXT", message, None))
 	# plaintext bin message, text mode.	
-	self.assertEquals(dem(binpayload, tag, 1), 
+	self.assertEquals(dem(binpayload, tag, 1),
 			  ("BIN", base64.encodestring(binmessage), None))
 	# plaintext bin message, bin mode.
 	self.assertEquals(dem(binpayload, tag, 0), ("BIN", binmessage, None))
 
 	encoded = "baobob "*1024*4
 	# "Encoded" message, text mode
-	self.assertEquals(dem(encoded, tag, 1), 
-			  ("ENC", base64.encodestring(encoded), 
+	self.assertEquals(dem(encoded, tag, 1),
+			  ("ENC", base64.encodestring(encoded),
 			   base64.encodestring(tag)[:-1]))
 	# "Encoded" message, binary mode
-	self.assertEquals(dem(encoded, tag, 0), 
+	self.assertEquals(dem(encoded, tag, 0),
 			  ("ENC", encoded, tag))
 
 	####
@@ -2891,10 +2895,13 @@
 IdentityKeyBits: 2048
 EncryptPrivateKey: no
 Nickname: mac-the-knife
+[Incoming/MMTP]
+Enabled: yes
+IP: 10.0.0.1
 """
 
 _FAKE_HOME = None
-def _getKeyring():
+def _getServerKeyring():
     global _FAKE_HOME
     if _FAKE_HOME is None:
 	_FAKE_HOME = mix_mktemp()	
@@ -2908,7 +2915,7 @@
 
 class ServerMainTests(unittest.TestCase):
     def testServerKeyring(self):
-	keyring = _getKeyring()
+	keyring = _getServerKeyring()
 	home = _FAKE_HOME
 
 	# Test creating identity key
@@ -2987,12 +2994,172 @@
 	pass
 
 #----------------------------------------------------------------------
+
+_EXAMPLE_DESCRIPTORS = {} # name->list of str
+EX_SERVER_CONF_TEMPLATE = """
+[Server]
+Mode: relay
+EncryptIdentityKey: No
+PublicKeyLifetime: %(lifetime)s days
+IdentityKeyBits: 2048
+EncryptPrivateKey: no
+Nickname: %(nickname)s
+[Incoming/MMTP]
+Enabled: yes
+IP: %(ip)s
+[Outgoing/MMTP]
+Enabled: yes
+"""
+
+_EXAMPLE_DESCRIPTORS_INP = [
+    # name        days         ip?        validAt
+    [ "Fred",	  "10 days", "10.0.0.6", (-19,-9,1,11) ],
+    [ "Lola",	  "5 days",  "10.0.0.7", (-2,0,5) ],
+    [ "Joe",	  "20 days", "10.0.0.8", (-15,5,25) ],
+    [ "Alice",	  "8 days",  "10.0.0.9", (-3,5,13) ],
+    [ "Bob",	  "11 days", "10.0.0.10", (-10,-1,6) ],
+    [ "Lisa",	  "3 days",  "10.0.0.11", (-10,-1,5) ],
+]
+
+def getExampleServerDescriptors():
+    if _EXAMPLE_DESCRIPTORS:
+ 	return _EXAMPLE_DESCRIPTORS
+    global _EXAMPLE_DESCRIPTORS_TIME
+    gen = mixminion.ServerInfo.generateServerDescriptorAndKeys
+    tmpkeydir = mix_mktemp()
+    identity = _getIdentityKey()
+    _EXAMPLE_DESCRIPTORS_TIME = now = time.time()
+
+    sys.stdout.flush()
+
+    for (nickname, lifetime, ip, starting) in _EXAMPLE_DESCRIPTORS_INP:
+	conf = EX_SERVER_CONF_TEMPLATE % locals()
+	try:
+	    suspendLog()
+	    conf = mixminion.Config.ServerConfig(string=conf)
+	finally:
+	    resumeLog()
+	
+	_EXAMPLE_DESCRIPTORS[nickname] = []
+	for n in xrange(len(starting)):
+	    k = "tst%d"%n
+	    validAt = previousMidnight(now + 24*60*60*starting[n])
+	    gen(config=conf, identityKey=identity, keyname=k,
+		keydir=tmpkeydir, hashdir=tmpkeydir, validAt=validAt)
+
+	    sd = os.path.join(tmpkeydir,"key_"+k,"ServerDesc")
+	    f = open(sd,'r')
+	    _EXAMPLE_DESCRIPTORS[nickname].append(f.read())
+	    f.close()
+	    sys.stdout.write('.')
+	    sys.stdout.flush()
+    sys.stdout.flush()
+    return _EXAMPLE_DESCRIPTORS
+
 class ClientMainTests(unittest.TestCase):
     def testClientKeystore(self):
+	eq = self.assertEquals
+	raises = self.failUnlessRaises
+
 	import mixminion.ClientMain
 	dirname = mix_mktemp()
 	dc = mixminion.ClientMain.DirectoryCache(dirname)
-	dc.load()
+
+	# Test empty directorycache.
+	for _ in xrange(2):
+	    now = time.time()
+	    eq([], dc.getAllCurrentServers())
+	    eq([], dc.getAllCurrentServers(now-1000, now+36000))
+	    eq([], dc.getAllCurrentServers(now-1000))
+	    eq([], dc.getAllCurrentServers(None, now+36000))
+	    raises(MixError, dc.getCurrentServer, "Fred")
+	    raises(MixError, dc.getCurrentServer, "Fred", now-1000)
+	    raises(MixError, dc.getCurrentServer, "Fred", now-1000,now+36000)
+	    raises(MixError, dc.getCurrentServer, "Fred", None,now+36000)
+	    dc.load(1)
+
+	edesc = getExampleServerDescriptors()
+	
+	## Test importing.
+	# tell server about descriptors: "Lisa", "Fred". The first of 
+	# each is expired.
+	for sd in (edesc['Lisa'][0],edesc['Fred'][0]):
+	    try:
+		suspendLog()
+		dc.importServerInfo(fname=None,string=sd)
+	    finally:
+		s = resumeLog()
+		self.failUnless(s.find("expired")>=0)
+	for sd in edesc['Lisa'][1:]+edesc['Fred'][1:]:
+	    dc.importServerInfo(fname=None,string=sd)
+
+	# tests shouldn't fail at 11:55pm
+	now = previousMidnight(_EXAMPLE_DESCRIPTORS_TIME)+60*60
+
+	for _ in (0,1): # test once before; once after a reload
+	    self.assertEquals(5, len(dc.allServers))
+	    self.assertEquals(2, len(dc.servers))
+	    s = dc.getCurrentServer("Fred",when=now)
+	    s2 = dc.getCurrentServer("Fred",when=(now+25*60*60))
+	    s3 = dc.getCurrentServer("Fred",when=(now+6*24*60*60))
+	    self.assert_(self.isSameServerDesc(edesc['Fred'][1], s))
+	    self.assert_(self.isSameServerDesc(s2, s3))
+	    self.assert_(not self.isSameServerDesc(s, s3))
+	    s4 = dc.getCurrentServer("Lisa")
+	    s5 = dc.getCurrentServer("Lisa",when=(now+5*25*60*60))
+	    self.assert_(not self.isSameServerDesc(s4, s))
+	    self.assert_(not self.isSameServerDesc(s4, s5))
+	    raises(MixError, dc.getCurrentServer, "Lisa", when=now+3*25*60*60)
+	    s6 = dc.getCurrentServer("Lisa", 
+				     when=(now-12*60*60),
+				     until=(now+12*60*60))
+	    self.assert_(self.isSameServerDesc(s6, s4))
+	    raises(MixError, dc.getCurrentServer, "Lisa", when=(now-12*60*60),
+		   until=(now+3*24*60*60))
+	    
+	    # Test reloading.
+	    dc.load(forceReload=1)
+
+	# test duplicates.
+	try:
+	    suspendLog()
+	    for _ in xrange(10):
+		dc.importServerInfo(fname=None,string=edesc['Lisa'][2])
+	finally:
+	    resumeLog()
+	self.assertEquals(5, len(dc.allServers))
+
+	# import rest of servers
+	try:
+	    suspendLog()
+	    for _, sds in edesc.items():
+		for sd in sds:
+		    dc.importServerInfo(fname=None,string=sd)
+	finally:
+	    resumeLog()
+	    
+	s1 = dc.getAllCurrentServers(now)
+	self.assertEquals(len(s1), 8)
+	s2 = dc.getAllCurrentServers(0)
+	self.assertEquals([], s2)
+	s3 = dc.getAllCurrentServers(now, now+50*60*60)
+	self.assertEquals(len(s3), 5)
+	s4 = dc.getAllCurrentServers(now-2*25*60*60, now+2*25*60*60)
+	self.assertEquals(len(s4), 2)
+	
+    def isSameServerDesc(self, s1, s2):
+	"""s1 and s2 are either ServerInfo objects or strings containing server
+	   descriptors. Returns 1 iff their digest fields match"""
+	ds = []
+	for s in s1, s2:
+	    if type(s) == type(""):
+		m = re.search(r"^Digest: (\S+)\n", s, re.M)
+		assert m
+		ds.append(base64.decodestring(m.group(1)))
+	    else:
+		ds.append(s['Server']['Digest'])
+	return ds[0] == ds[1]
+
 	
 #----------------------------------------------------------------------
 def testSuite():