[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Big pile of changes -- 0.0.4rc1 draws ever closer.



Update of /home/minion/cvsroot/src/minion/lib/mixminion/server
In directory moria.mit.edu:/tmp/cvs-serv21169/src/minion/lib/mixminion/server

Modified Files:
	EventStats.py HashLog.py MMTPServer.py Modules.py 
	PacketHandler.py ServerConfig.py ServerKeys.py ServerMain.py 
Log Message:
Big pile of changes -- 0.0.4rc1 draws ever closer.  

Main:
- Add a big warning banner to say we aren't compatible with anything
  right now.

BuildMessage, Packet, PacketHandler:
- Change to new packet format (2048-bit RSA keys, overlapping
  encryption)

setup.py:
- Bump version to 0.0.4alpha2

ClientMain:
- Add a handy 'ping' command -- too handy to stay in the codebase, but
  useful for testing.

Common:
- Add a checkPrivateFile function to enforce file permissions.

Config, ServerConfig, ServerInfo, Modules:
- Refactor away a lot of useless code.  I once thought the
  configuration files would be self-reloading, and everybody would use
  some kind of publish/subscribe mechanism to update themselves -- but
  that's kinda silly in practise.
- Add a prevalidate function so that we can freak out sooner if the
  version number doesn't match.

ServerInfo:
- Move IP to Incoming/MMTP section
- Bump Descriptor-Version to 0.2

Crypto:
- Make sure that private keys are stored mode 0600.

MMTPClient:
- Make 'bad authentication' a separate exception
- Check to make sure our certs aren't expired.

EventStats:
- Begin testing, debugging, refactoring.
- Fancier rule for rotation: don't rotate until we've accumulated data
  for a sufficiently long time, even if a long time has passed.  (That
  is, if we've been offline for 23 hours, don't trigger a daily
  rotation.)

MMTPServer:
- Refactor client connection cache
- Use PeerCertificateCache to remember which certificates we've
  already verified.


Index: EventStats.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/EventStats.py,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -d -r1.1 -r1.2
--- EventStats.py	26 Mar 2003 16:36:46 -0000	1.1
+++ EventStats.py	26 Apr 2003 14:39:59 -0000	1.2
@@ -12,9 +12,11 @@
 from threading import RLock
 from time import time
 
-from mixminion.Common import formatTime, LOG
+from mixminion.Common import formatTime, LOG, previousMidnight, floorDiv, \
+     createPrivateDir
 
-EVENTS = [ 'ReceivedPacket',
+# _EVENTS: a list of all recognized event types. 
+_EVENTS = [ 'ReceivedPacket',
            'AttemptedRelay',
            'SuccessfulRelay', 'FailedRelay', 'UnretriableRelay',
            'AttemptedDelivery',
@@ -22,20 +24,98 @@
             ]
 
 class NilEventLog:
+    """Null implementation of EventLog interface: ignores all events and
+       logs nothing.  
+    """
     def __init__(self):
         pass
-    def save(self):
+    def save(self, now=None):
+        """Flushes this eventlog to disk."""
         pass
-    def log(self, event, arg=None):
+    def _log(self, event, arg=None):
+        """Notes that an event has occurred.
+           event -- the type of event to note
+           arg -- an optional topic of the event.
+        """
         pass
-
+    def receivedPacket(self, arg=None):
+        """Called whenever a packet is received via MMTP."""
+        self._log("ReceivedPacket", arg)
+    def attemptedRelay(self, arg=None):
+        """Called whenever we attempt to relay a packet via MMTP."""
+        self._log("AttemptedRelay", arg)
+    def successfulRelay(self, arg=None):
+        """Called whenever packet delivery via MMTP succeeds"""
+        self._log("SuccessfulRelay", arg)
+    def failedRelay(self, arg=None):
+        """Called whenever packet delivery via MMTP fails retriably"""
+        self._log("FailedRelay", arg)
+    def unretriableRelay(self, arg=None):
+        """Called whenever packet delivery via MMTP fails unretriably"""
+        self._log("UnretriableRelay", arg)
+    def attemptedDelivery(self, arg=None):
+        """Called whenever we attempt to deliver a message via an exit
+           module.
+        """
+        self._log("AttemptedDelivery", arg)
+    def successfulDelivery(self, arg=None):
+        """Called whenever we successfully deliver a message via an exit
+           module.
+        """
+        self._log("SuccessfulDelivery", arg)
+    def failedDelivery(self, arg=None):
+        """Called whenever an attempt to deliver a message via an exit
+           module fails retriably.
+        """
+        self._log("FailedDelivery", arg)
+    def unretriableDelivery(self, arg=None):
+        """Called whenever an attempt to deliver a message via an exit
+           module fails unretriably.
+        """
+        self._log("UnretriableDelivery", arg)
+        
 class EventLog(NilEventLog):
-    # Fields:
-    # count (event -> arg -> value)
-    # lastRotation
-    # filename, historyFile
-    # rotateInterval
+    """An EventLog records events, aggregates them according to some time
+       periods, and logs the totals to disk.
+
+       Currently we retain two log files: one holds an interval-by-interval
+       human-readable record of past intervals; the other holds a pickled
+       record of events in the current interval.
+
+       We take some pains to avoid flushing the statistics when too
+       little time has passed.  We only rotate an aggregated total to disk
+       when:
+           - An interval has passsed since the last rotation time
+         AND
+           - We have accumulated events for at least 75% of an interval's
+             worth of time.
+
+       The second requirement prevents the following unpleasant failure mode:
+           - We set the interval to '1 day'.  At midnight on monday,
+             we rotate.  At 00:05, we go down.  At 23:55 we come back
+             up.  At midnight at tuesday, we noticing that it's been one
+             day since the last rotation, and rotate again -- thus making
+             a permanent record that reflects 10 minutes worth of traffic,
+             potentially exposing more about individual users than we should.
+    """
+    ### Fields:
+    # count: a map from event name -> argument|None -> total events received.
+    # lastRotation: the time at which we last flushed the log to disk and
+    #     reset the log.
+    # filename, historyFile: Names of the pickled and long-term event logs.
+    # rotateInterval: Interval after which to flush the current statistics
+    #     to disk.
+    # _lock: a threading.RLock object that must be held when modifying this
+    #     object.
+    # accumulatedTime: number of seconds since last rotation that we have
+    #     been logging events.
+    # lastSave: last time we saved the file.
+    ### Pickled format:
+    # Map from {"count","lastRotation","accumulatedTime"} to the values
+    # for those fields.
     def __init__(self, filename, historyFile, interval):
+        """Intializes an EventLog that caches events in 'filename', and
+           periodically writes to 'historyFile' every 'interval' seconds."""
         NilEventLog.__init__(self)
         if os.path.exists(filename):
             # XXXX If this doesn't work, then we should 
@@ -44,46 +124,61 @@
             f.close()
             assert self.count is not None
             assert self.lastRotation is not None
+            assert self.accumulatedTime is not None
         else:
             self.count = {}
-            for e in EVENTS:
+            for e in _EVENTS:
                 self.count[e] = {}
             self.lastRotation = time()
+            self.accumulatedTime = 0
         self.filename = filename
         self.historyFilename = historyFile
+        for fn in filename, historyFile:
+            parent = os.path.split(fn)[0]
+            createPrivateDir(parent)
         self.rotateInterval = interval
+        self.lastSave = time()
+        self._setNextRotation()
         self._lock = RLock()
         self.save()
 
-    def save(self):
+    def save(self, now=None):
+        """Write the statistics in this log to disk, rotating if necessary."""
         try:
             self._lock.acquire()
-            if time() > self.lastRotation + self.rotateInterval:
-                self._rotate()
-            self._save()
+            self._save(now)
         finally:
             self._lock.release()
             
-    def _save(self):
-        # Must hold lock
+    def _save(self, now=None):
+        """Implements 'save' method.  For internal use.  Must hold self._lock
+           to invoke."""
         LOG.debug("Syncing statistics to disk")
+        if not now: now = time()
+        if now > self.nextRotation:
+            self._rotate()
         tmpfile = self.filename + "_tmp"
         try:
             os.unlink(tmpfile)
         except:
             pass
+        self.accumulatedTime += int(now-self.lastSave)
+        self.lastSave = now
         f = open(tmpfile, 'wb')
-        cPickle.dump({ 'count' : self.count, 'filename' : self.filename,
-                       'lastRotation' : self.lastRotation },
+        cPickle.dump({ 'count' : self.count,
+                       'lastRotation' : self.lastRotation,
+                       'accumulatedTime' : self.accumulatedTime,
+                       },
                      f, 1)
         f.close()
         os.rename(tmpfile, self.filename)
 
-    def log(self, event, arg=None):
+    def _log(self, event, arg=None):
         try:
             self._lock.acquire()
-            if time() > self.lastRotation + self.rotateInterval:
+            if time() > self.nextRotation:
                 self._rotate()
+                self._save()
             try:
                 self.count[event][arg] += 1
             except KeyError:
@@ -95,30 +190,33 @@
             self._lock.release()
 
     def _rotate(self, now=None):
-        # XXXX Change behavior: rotate indexed on midnight.
+        """Flush all events since the last rotation to the history file,
+           and clears the current event log."""
         
         # Must hold lock
         LOG.debug("Flushing statistics log")
-        if now is None:
-            now = time()
+        if now is None: now = time() #????
+            
         f = open(self.historyFilename, 'a')
         self.dump(f)
         f.close()
-        
+
+        self.accumulatedTime = 0
         self.count = {}
-        for e in EVENTS:
+        for e in _EVENTS:
             self.count[e] = {}
-        self.lastRotation = now
-        self._save()
+        self.lastRotation = self.nextRotation
+        self._setNextRotation()
 
     def dump(self, f):
+        """Write the current data to a file handle 'f'."""
         try:
             self._lock.acquire()
             startTime = self.lastRotation
             endTime = time()
             print >>f, "========== From %s to %s:" % (formatTime(startTime,1),
                                                       formatTime(endTime,1))
-            for event in EVENTS:
+            for event in _EVENTS:
                 count = self.count[event]
                 if len(count) == 0:
                     print >>f, "  %s: 0" % event
@@ -131,6 +229,7 @@
                 args = count.keys()
                 args.sort()
                 length = max([ len(str(arg)) for arg in args ])
+                length = max((length, 10))
                 fmt = "    %"+str(length)+"s: %s"
                 for arg in args:
                     v = count[arg]
@@ -141,25 +240,50 @@
         finally:
             self._lock.release()
 
-def setLog(eventLog):
-    global THE_EVENT_LOG
-    global log
-    global save
-    THE_EVENT_LOG = eventLog
-    log = THE_EVENT_LOG.log
-    save = THE_EVENT_LOG.save
+
+    def _setNextRotation(self, now=None):
+        # ???? Lock to 24-hour cycle
+
+        # This is a little weird.  We won't save *until*:
+        #       - .75 * rotateInterval seconds are accumulated.
+        #  AND  - rotateInterval seconds have elapsed since the last
+        #         rotation.
+        #
+        # IF the rotation interval is divisible by one hour, we also
+        #  round to the hour, up to 5 minutes down and 55 up.
+        if not now: now = time()
+
+        secToGo = max(0, self.rotateInterval * 0.75 - self.accumulatedTime)
+        self.nextRotation = max(self.lastRotation + self.rotateInterval,
+                                now + secToGo)
+
+        if not self.lastRotation: self.lastRotation = now
+        if now - self.lastRotation <= self.rotateInterval * 1.2:
+            base = self.lastRotation
+        else:
+            base = now
+
+        self.nextRotation = base + self.rotateInterval
+        
+        if (self.rotateInterval % 3600) == 0:
+            mid = previousMidnight(self.nextRotation)
+            rest = self.nextRotation - mid
+            self.nextRotation = mid + 3600 * floorDiv(rest+55*60, 3600)
 
 def configureLog(config):
+    """DOCDOC"""
+    global log
     if config['Server']['LogStats']:
         LOG.info("Enabling statistics logging")
-        statsfile = config['Server']['StatsFile']
+        homedir = config['Server']['Homedir']
+        statsfile = config['Server'].get('StatsFile')
         if not statsfile:
-            homedir = config['Server']['Homedir']
             statsfile = os.path.join(homedir, "stats")
-        workfile = statsfile + ".work"
-        setLog(EventLog(
-            workfile, statsfile, config['Server']['StatsInterval'][2]))
+        workfile = os.path.join(homedir, "work", "stats.tmp")
+        log = EventLog(
+            workfile, statsfile, config['Server']['StatsInterval'][2])
     else:
+        log = NilEventLog()
         LOG.info("Statistics logging disabled")
 
-setLog(NilEventLog())
+log = NilEventLog()

Index: HashLog.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/HashLog.py,v
retrieving revision 1.8
retrieving revision 1.9
diff -u -d -r1.8 -r1.9
--- HashLog.py	13 Apr 2003 15:54:42 -0000	1.8
+++ HashLog.py	26 Apr 2003 14:39:59 -0000	1.9
@@ -63,6 +63,7 @@
            'keyid'."""
         parent = os.path.split(filename)[0]
         createPrivateDir(parent)
+        # XXXX004 catch empty hashlog.
         self.log = anydbm.open(filename, 'c')
         LOG.debug("Opening database %s for packet digests", filename)
         if isinstance(self.log, dumbdbm._Database):

Index: MMTPServer.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/MMTPServer.py,v
retrieving revision 1.24
retrieving revision 1.25
diff -u -d -r1.24 -r1.25
--- MMTPServer.py	22 Apr 2003 01:45:22 -0000	1.24
+++ MMTPServer.py	26 Apr 2003 14:39:59 -0000	1.25
@@ -664,7 +664,8 @@
            sentCallback -- None, or a function of (msg, handle) to be called
               whenever a message is successfully sent.
            failCallback -- None, or a function of (msg, handle, retriable)
-              to be called when messages can't be sent."""
+              to be called when messages can't be sent.
+              DOCDOC certcache,finishedCallback"""
 
         # Generate junk before connecting to avoid timing attacks
         self.junk = [] #XXXX doc this field.
@@ -855,6 +856,8 @@
        callbacks for message success and failure."""
     ##
     # clientConByAddr
+    # certificateCache
+    #DOCDOC fields
     def __init__(self, config, tls):
         AsyncServer.__init__(self)
 
@@ -879,6 +882,7 @@
         self.listener.register(self)
         self._timeout = config['Server']['Timeout'][2]
         self.clientConByAddr = {}
+        self.certificateCache = PeerCertificateCache()
 
     def setContext(self, context):
         """Change the TLS context used for newly received connections.
@@ -921,26 +925,30 @@
             pass
 
         try:
+            addr = (ip, port, keyID)
+            finished = lambda addr=addr, self=self: self.__clientFinished(addr)
             con = MMTPClientConnection(self.context,
-                                       ip, port, keyID, messages, handles,
-                                       self.onMessageSent,
-                                       self.onMessageUndeliverable)
-            #XXXX004
-            con.finishedCallback = (lambda con=con,self=self:
-                                       self.__clientFinished(con))
+                                     ip, port, keyID, messages, handles,
+                                     sentCallback=self.onMessageSent,
+                                     failCallback=self.onMessageUndeliverable,
+                                     finishedCallback=finished,
+                                     certCache=self.certificateCache)
             con.register(self)
-            self.clientConByAddr[con.getAddr()] = con
+            assert addr == con.getAddr()
+            self.clientConByAddr[addr] = con
         except socket.error, e:
             LOG.error("Unexpected socket error connecting to %s:%s: %s",
                       ip, port, e)
             for m,h in zip(messages, handles):
                 self.onMessageUndeliverable(m,h,1)
 
-    def __clientFinished(self, con):
+    def __clientFinished(self, addr):
+        """DOCDOC"""
         try:
-            del self.clientConByAddr[con.getAddr()]
+            del self.clientConByAddr[addr]
         except KeyError:
-            LOG.warn("Didn't find client connection in address map")
+            LOG.warn("Didn't find client connection to %s in address map",
+                     addr)
 
     def onMessageReceived(self, msg):
         pass

Index: Modules.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/Modules.py,v
retrieving revision 1.34
retrieving revision 1.35
diff -u -d -r1.34 -r1.35
--- Modules.py	26 Mar 2003 16:36:46 -0000	1.34
+++ Modules.py	26 Apr 2003 14:39:59 -0000	1.35
@@ -135,21 +135,21 @@
         """Instead of queueing our message, pass it directly to the underlying
            DeliveryModule."""
         try:
-            EventStats.log('AttemptedDelivery') #XXXX
+            EventStats.log.attemptedDelivery() #XXXX
             res = self.module.processMessage(packet)
             if res == DELIVER_OK:
-                EventStats.log('SuccessfulDelivery') #XXXXq
+                EventStats.log.successfulDelivery() #XXXX
                 return
             elif res == DELIVER_FAIL_RETRY:
                 LOG.error("Unable to retry delivery for message")
-                EventStats.log('UnretriableDelivery') #XXXX
+                EventStats.log.unretriableDelivery() #XXXX
             else:
                 LOG.error("Unable to deliver message")
-                EventStats.log('UnretriableDelivery') #XXXX
+                EventStats.log.unretriableDelivery() #XXXX
         except:
             LOG.error_exc(sys.exc_info(),
                                "Exception delivering message")
-            EventStats.log('UnretriableDelivery') #XXXX
+            EventStats.log.unretriableDeliery() #XXXX
 
     def sendReadyMessages(self):
         # We do nothing here; we already delivered the messages
@@ -172,23 +172,23 @@
     def _deliverMessages(self, msgList):
         for handle, packet, n_retries in msgList:
             try:
-                EventStats.log('AttemptedDelivery') #XXXX
+                EventStats.log.attemptedDelivery() #XXXX
                 result = self.module.processMessage(packet)
                 if result == DELIVER_OK:
                     self.deliverySucceeded(handle)
-                    EventStats.log('SuccessfulDelivery') #XXXX
+                    EventStats.log.successfulDelivery() #XXXX
                 elif result == DELIVER_FAIL_RETRY:
                     self.deliveryFailed(handle, 1)
-                    EventStats.log('FailedDelivery') #XXXX
+                    EventStats.log.failedDelivery() #XXXX
                 else:
                     LOG.error("Unable to deliver message")
                     self.deliveryFailed(handle, 0)
-                    EventStats.log('UnretriableDelivery') #XXXX
+                    EventStats.log.unretriableDelivery() #XXXX
             except:
                 LOG.error_exc(sys.exc_info(),
                                    "Exception delivering message")
                 self.deliveryFailed(handle, 0)
-                EventStats.log('UnretriableDelivery') #XXXX
+                EventStats.log.unretriableDelivery() #XXXX
 
 class DeliveryThread(threading.Thread):
     """A thread object used by ModuleManager to send messages in the

Index: PacketHandler.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/PacketHandler.py,v
retrieving revision 1.13
retrieving revision 1.14
diff -u -d -r1.13 -r1.14
--- PacketHandler.py	18 Apr 2003 18:32:36 -0000	1.13
+++ PacketHandler.py	26 Apr 2003 14:39:59 -0000	1.14
@@ -110,6 +110,8 @@
         # Break into headers and payload
         msg = Packet.parseMessage(msg)
         header1 = Packet.parseHeader(msg.header1)
+        encSubh = header1[:Packet.ENC_SUBHEADER_LEN]
+        header1 = header1[Packet.ENC_SUBHEADER_LEN:]
 
         # Try to decrypt the first subheader.  Try each private key in
         # order.  Only fail if all private keys fail.
@@ -119,7 +121,7 @@
         try:
             for pk, hashlog in zip(self.privatekey, self.hashlog):
                 try:
-                    subh = Crypto.pk_decrypt(header1[0], pk)
+                    subh = Crypto.pk_decrypt(encSubh, pk)
                     break
                 except Crypto.CryptoError, err:
                     e = err
@@ -137,7 +139,7 @@
             raise ContentError("Invalid protocol version")
 
         # Check the digest of all of header1 but the first subheader.
-        if subh.digest != Crypto.sha1(header1[1:]):
+        if subh.digest != Crypto.sha1(header1):
             raise ContentError("Invalid digest")
 
         # Get ready to generate message keys.
@@ -159,23 +161,26 @@
         # using this more than once.
         header_sec_key = Crypto.aes_key(keys.get(Crypto.HEADER_SECRET_MODE))
 
-        # If the subheader says that we have extra blocks of routing info,
-        # decrypt and parse them now.
-        if subh.isExtended():
-            nExtra = subh.getNExtraBlocks()
-            if (rt < Packet.MIN_EXIT_TYPE) or (nExtra > 15):
-                # None of the native methods allow multiple blocks; no
-                # size can be longer than the number of bytes in the rest
-                # of the header.
-                raise ContentError("Impossibly long routing info length")
+        # Prepare key to generate padding
+        junk_key = Crypto.aes_key(keys.get(Crypto.RANDOM_JUNK_MODE))
 
-            extra = Crypto.ctr_crypt(header1[1:1+nExtra], header_sec_key)
-            subh.appendExtraBlocks(extra)
-            remainingHeader = header1[1+nExtra:]
-        else:
-            nExtra = 0
-            remainingHeader = header1[1:]
+        # Pad the rest of header 1
+        header1 += Crypto.prng(junk_key,
+                               Packet.OAEP_OVERHEAD + Packet.MIN_SUBHEADER_LEN
+                               + subh.routinglen)
+
+        # Decrypt the rest of header 1, encrypting the padding.
+        header1 = Crypto.ctr_crypt(header1, header_sec_key)
 
+        # If the subheader says that we have extra routing info that didn't
+        # fit in the RSA-encrypted part, get it now.
+        overflowLength = subh.getOverflowLength()
+        if overflowLength:
+            subh.appendOverflow(header1[:overflowLength])
+            header1 = header1[overflowLength:]
+
+        header1 = subh.underflow + header1
+        
         # Decrypt the payload.
         payload = Crypto.lioness_decrypt(msg.payload,
                               keys.getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE))
@@ -191,14 +196,6 @@
         # routing type.
         if rt not in (Packet.SWAP_FWD_TYPE, Packet.FWD_TYPE):
             raise ContentError("Unrecognized Mixminion routing type")
-
-        # Pad the rest of header 1
-        remainingHeader = remainingHeader +\
-                          Crypto.prng(keys.get(Crypto.PRNG_MODE),
-                                      Packet.HEADER_LEN-len(remainingHeader))
-
-        # Decrypt the rest of header 1, encrypting the padding.
-        header1 = Crypto.ctr_crypt(remainingHeader, header_sec_key, nExtra*128)
 
         # Decrypt header 2.
         header2 = Crypto.lioness_decrypt(msg.header2,

Index: ServerConfig.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/ServerConfig.py,v
retrieving revision 1.21
retrieving revision 1.22
diff -u -d -r1.21 -r1.22
--- ServerConfig.py	18 Apr 2003 17:41:38 -0000	1.21
+++ ServerConfig.py	26 Apr 2003 14:39:59 -0000	1.22
@@ -33,20 +33,21 @@
 
         mixminion.Config._ConfigFile.__init__(self, fname, string)
 
-    def validate(self, sections, entries, lines, contents):
-        def _haveEntry(entries, section, ent):
+    def validate(self, lines, contents):
+        def _haveEntry(self, section, ent):
+            entries = self._sectionEntries
             return len([e for e in entries[section] if e[0] == ent]) != 0
 
         # Pre-emptively configure the log before validation, so we don't
         # write to the terminal if we've been asked not to.
-        if not sections['Server'].get("EchoMessages", 0):
+        if not self['Server'].get("EchoMessages", 0):
             LOG.handlers = []
             # ???? This can't be the best way to do this.
 
         # Now, validate the host section.
-        mixminion.Config._validateHostSection(sections.get('Host', {}))
+        mixminion.Config._validateHostSection(self['Host'])
         # Server section
-        server = sections['Server']
+        server = self['Server']
         bits = server['IdentityKeyBits']
         if not (2048 <= bits <= 4096):
             raise ConfigError("IdentityKeyBits must be between 2048 and 4096")
@@ -59,19 +60,19 @@
         if server['PublicKeySloppiness'][2] > 20*60:
             raise ConfigError("PublicKeySloppiness must be <= 20 minutes.")
 
-        if _haveEntry(entries, 'Server', 'NoDaemon'):
+        if _haveEntry(self, 'Server', 'NoDaemon'):
             LOG.warn("The NoDaemon option is obsolete.  Use Daemon instead.")
 
-        if _haveEntry(entries, 'Server', 'Mode'):
+        if _haveEntry(self, 'Server', 'Mode'):
             LOG.warn("Mode specification is not yet supported.")
 
         mixInterval = server['MixInterval'][2]
         if mixInterval < 30*60:
             LOG.warn("Dangerously low MixInterval")
         if server['MixAlgorithm'] == 'TimedMixPool':
-            if _haveEntry(entries, 'Server', 'MixPoolRate'):
+            if _haveEntry(self, 'Server', 'MixPoolRate'):
                 LOG.warn("Option MixPoolRate is not used for Timed mixing.")
-            if _haveEntry(entries, 'Server', 'MixPoolMinSize'):
+            if _haveEntry(self, 'Server', 'MixPoolMinSize'):
                 LOG.warn("Option MixPoolMinSize is not used for Timed mixing.")
         else:
             rate = server['MixPoolRate']
@@ -81,19 +82,23 @@
             if minSize < 0:
                 raise ConfigError("MixPoolMinSize %s must be nonnegative.")
 
-        if not sections['Incoming/MMTP'].get('Enabled'):
+        if not self['Incoming/MMTP'].get('Enabled'):
             LOG.warn("Disabling incoming MMTP is not yet supported.")
-        if [e for e in entries['Incoming/MMTP'] if e[0] in ('Allow', 'Deny')]:
+        if [e for e in self._sectionEntries['Incoming/MMTP']
+            if e[0] in ('Allow', 'Deny')]:
             LOG.warn("Allow/deny are not yet supported")
 
-        if not sections['Outgoing/MMTP'].get('Enabled'):
+        if not self['Outgoing/MMTP'].get('Enabled'):
             LOG.warn("Disabling incoming MMTP is not yet supported.")
-        if [e for e in entries['Outgoing/MMTP'] if e[0] in ('Allow', 'Deny')]:
+        if [e for e in self._sectionEntries['Outgoing/MMTP']
+            if e[0] in ('Allow', 'Deny')]:
             LOG.warn("Allow/deny are not yet supported")
 
-        _validateRetrySchedule(mixInterval, entries, "Outgoing/MMTP")
+        _validateRetrySchedule(mixInterval, self._sectionEntries,
+                               "Outgoing/MMTP")
 
-        self.moduleManager.validate(sections, entries, lines, contents)
+        self.moduleManager.validate(self._sections, self._sectionEntries,
+                                    lines, contents)
 
     def __loadModules(self, section, sectionEntries):
         """Callback from the [Server] section of a config file.  Parses
@@ -110,22 +115,37 @@
         "Return the module manager initialized by this server."
         return self.moduleManager
 
-    def isSecure(self):
-        "Return true iff this configuration is reasonably secure."
+    def getInsecurities(self):
+        """Return false iff this configuration is reasonably secure.
+           Otherwise, return a list of reasons why it isn't."""
         reasons = ["Software is alpha"]
-        
+
+        # SERVER
         server = self['Server']
         if server['LogLevel'] in ('TRACE', 'DEBUG'):
             reasons.append("Log is too verbose")
-        if server['LogStats'] and server['LogInterval'][2] < 24*60*60:
+        if server['LogStats'] and server['StatsInterval'][2] < 24*60*60:
             reasons.append("StatsInterval is too short")
         if not server["EncryptIdentityKey"]:
             reasons.append("Identity key is not encrypted")
+        # ???? Pkey lifetime, sloppiness? 
         if server["MixAlgorithm"] not in _SECURE_MIX_RULES:
             reasons.append("Mix algorithm is not secure")
-        #XXXX004 finish!
-
+        else:
+            if server["MixPoolMinSize"] < 5:
+                reasons.append("MixPoolMinSize is too small")
+            #MixPoolRate?
+        if server["MixInterval"][2] < 30*60:
+            reasons.append("Mix interval under 30 minutes")
         
+        # ???? DIRSERVERS?
+
+        # ???? Incoming/MMTP
+
+        # ???? Outgoing/MMTP
+
+        # ???? Modules?
+
 def _validateRetrySchedule(mixInterval, entries, sectionname,
                            entryname='Retry'):
     #XXXX writeme.

Index: ServerKeys.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/ServerKeys.py,v
retrieving revision 1.18
retrieving revision 1.19
diff -u -d -r1.18 -r1.19
--- ServerKeys.py	18 Apr 2003 18:32:36 -0000	1.18
+++ ServerKeys.py	26 Apr 2003 14:39:59 -0000	1.19
@@ -23,9 +23,11 @@
 import mixminion.server.PacketHandler
 import mixminion.server.MMTPServer
 
-from mixminion.ServerInfo import ServerInfo, PACKET_KEY_BYTES, signServerInfo
+from mixminion.ServerInfo import ServerInfo, PACKET_KEY_BYTES, MMTP_KEY_BYTES,\
+     signServerInfo
 from mixminion.Common import LOG, MixError, MixFatalError, createPrivateDir, \
-     formatBase64, formatDate, formatTime, previousMidnight, secureDelete
+     checkPrivateFile, formatBase64, formatDate, formatTime, previousMidnight,\
+     secureDelete
 
 #----------------------------------------------------------------------
 class ServerKeyring:
@@ -153,6 +155,7 @@
         fn = os.path.join(self.keyDir, "identity.key")
         bits = self.config['Server']['IdentityKeyBits']
         if os.path.exists(fn):
+            checkPrivateFile(fn)
             key = mixminion.Crypto.pk_PEM_load(fn, password)
             keylen = key.get_modulus_bytes()*8
             if keylen != bits:
@@ -326,7 +329,7 @@
         """Return out current ip/port/keyid tuple"""
         keys = self.getServerKeyset()
         desc = keys.getServerDescriptor()
-        return (desc['Server']['IP'],
+        return (desc['Incoming/MMTP']['IP'],
                 desc['Incoming/MMTP']['Port'],
                 desc['Incoming/MMTP']['Key-Digest'])
 
@@ -367,6 +370,8 @@
     def load(self, password=None):
         """Read the short-term keys from disk.  Must be called before
            getPacketKey or getMMTPKey."""
+        checkPrivateFile(self.packetKeyFile)
+        checkPrivateFile(self.mmtpKeyFile)
         self.packetKey = mixminion.Crypto.pk_PEM_load(self.packetKeyFile,
                                                       password)
         self.mmtpKey = mixminion.Crypto.pk_PEM_load(self.mmtpKeyFile,
@@ -432,7 +437,6 @@
     if config_s['Comments'] != info_s['Comments']:
         warn("Mismatched comments field.")
 
-    lifetime = info_s['Valid-Until'] - info_s['Valid-After'] 
     if (previousMidnight(info_s['Valid-Until']) !=
         previousMidnight(config_s['PublicKeyLifetime'][2] +
                          info_s['Valid-After'])):
@@ -448,7 +452,7 @@
         warn("Mismatched ports: %s configured; %s published.",
              config_im['Port'], info_im['Port'])
 
-    info_ip = info['Server']['IP']
+    info_ip = info['Incoming/MMTP']['IP']
     if config_im['IP'] == '0.0.0.0':
         guessed = _guessLocalIP()
         if guessed != config_im['IP']:
@@ -503,7 +507,7 @@
     if useServerKeys is None:
         # First, we generate both of our short-term keys...
         packetKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)
-        mmtpKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)
+        mmtpKey = mixminion.Crypto.pk_generate(MMTP_KEY_BYTES*8)
 
         # ...and save them to disk, setting up our directory structure while
         # we're at it.
@@ -535,6 +539,11 @@
     if not validAt:
         validAt = now
 
+    if config.getInsecurities():
+        secure = "no"
+    else:
+        secure = "yes"
+
     # Calculate descriptor and X509 certificate lifetimes.
     # (Round validAt to previous mignight.)
     validAt = mixminion.Common.previousMidnight(validAt+30)
@@ -577,7 +586,8 @@
         "MMTPProtocolsOut" : mmtpProtocolsOut,
         "PacketFormat" : "%s.%s"%(mixminion.Packet.MAJOR_NO,
                                   mixminion.Packet.MINOR_NO),
-        "mm_version" : mixminion.__version__
+        "mm_version" : mixminion.__version__,
+        "Secure" : secure
         }
 
     # If we don't know our IP address, try to guess
@@ -593,8 +603,7 @@
     # Signature: lines.
     info = """\
         [Server]
-        Descriptor-Version: 0.1
-        IP: %(IP)s
+        Descriptor-Version: 0.2
         Nickname: %(Nickname)s
         Identity: %(Identity)s
         Digest:
@@ -605,6 +614,7 @@
         Packet-Key: %(PacketKey)s
         Packet-Formats: %(PacketFormat)s
         Software: Mixminion %(mm_version)s
+        Secure-Configuration: %(Secure)s
         """ % fields
     if contact:
         info += "Contact: %s\n"%contact
@@ -616,6 +626,7 @@
         info += """\
             [Incoming/MMTP]
             Version: 0.1
+            IP: %(IP)s
             Port: %(Port)s
             Key-Digest: %(KeyID)s
             Protocols: %(MMTPProtocolsIn)s

Index: ServerMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/ServerMain.py,v
retrieving revision 1.49
retrieving revision 1.50
diff -u -d -r1.49 -r1.50
--- ServerMain.py	22 Apr 2003 02:07:34 -0000	1.49
+++ ServerMain.py	26 Apr 2003 14:39:59 -0000	1.50
@@ -274,20 +274,20 @@
 
     def onMessageReceived(self, msg):
         self.incomingQueue.queueMessage(msg)
-        EventStats.log("ReceivedPacket", None) # XXXX Replace with server.
+        EventStats.receivedPacket("ReceivedPacket", None) # XXXX Replace with server.
 
     def onMessageSent(self, msg, handle):
         self.outgoingQueue.deliverySucceeded(handle)
-        EventStats.log("AttemptedRelay", None) # XXXX replace with addr
-        EventStats.log("SuccessfulRelay", None) # XXXX replace with addr
+        EventStats.log.attemptedRelay() # XXXX replace with addr
+        EventStats.log.successfulRelay() # XXXX replace with addr
 
     def onMessageUndeliverable(self, msg, handle, retriable):
         self.outgoingQueue.deliveryFailed(handle, retriable)
-        EventStats.log("AttemptedRelay", None) # XXXX replace with addr
+        EventStats.log.attemptedRelay() # XXXX replace with addr
         if retriable:
-            EventStats.log("FailedRelay", None) # XXXX replace with addr
+            EventStats.log.failedRelay() # XXXX replace with addr
         else:
-            EventStats.log("UnretriableRelay", None) # XXXX replace with addr
+            EventStats.log.unretriableRelay() # XXXX replace with addr
         
 #----------------------------------------------------------------------
 class CleaningThread(threading.Thread):
@@ -444,6 +444,8 @@
         # The pid file.
         self.pidFile = os.path.join(homeDir, "pid")
 
+        #XXXX004 Catch ConfigError for bad serverinfo.
+        #XXXX004 Check whether config matches serverinfo
         self.keyring = mixminion.server.ServerKeys.ServerKeyring(config)
         if self.keyring._getLiveKey() is None:
             LOG.info("Generating a month's worth of keys.")
@@ -555,7 +557,7 @@
                     LOG.info("Caught sighup")
                     LOG.info("Resetting logs")
                     LOG.reset()
-                    EventStats.save()
+                    EventStats.log.save()
                     GOT_HUP = 0
                 # Make sure that our worker threads are still running.
                 if not (self.cleaningThread.isAlive() and
@@ -641,7 +643,7 @@
 
         self.packetHandler.close()
 
-        EventStats.save()
+        EventStats.log.save()
 
         try:
             self.lockFile.release()
@@ -821,7 +823,7 @@
     config = configFromServerArgs(cmd, args)
     _signalServer(config, 1)
     EventStats.configureLog(config)
-    EventStats.THE_EVENT_LOG.dump(sys.stdout)
+    EventStats.log.dump(sys.stdout)
 
 #----------------------------------------------------------------------
 _SIGNAL_SERVER_USAGE = """\