Ejemplo n.º 1
0
def getPassword_term(prompt):
    """Read a password from the console, then return it.  Use the string
       'message' as a prompt."""
    # getpass.getpass uses stdout by default .... but stdout may have
    # been redirected.  If stdout is not a terminal, write the message
    # to stderr instead.
    if os.isatty(sys.stdout.fileno()):
        f = sys.stdout
        nl = 0
    else:
        f = sys.stderr
        nl = 1
    if os.isatty(sys.stdin.fileno()):
        # If stdin is a tty, then we use the magic from getpass.getpass to
        # disable echoing and read a line.
        f.write(prompt)
        f.flush()
        try:
            p = getpass.getpass("")
        except KeyboardInterrupt:
            if nl: print >> f
            raise UIError("Interrupted")
        if nl: print >> f
    else:
        # If stdin is _not_ a tty, however, then the getpass magic can
        # raise exceptions.
        print >> f, "Reading password from stdin."
        p = sys.stdin.readline()
        if not p: raise UIError("No password received")
        if p[-1] == '\n': p = p[:-1]
    return p
Ejemplo n.º 2
0
    def importServerInfo(self, contents, knownOnly=0, server=None):
        """Insert a ServerInfo into the list.  If the server is expired, or
           superseded, or inconsistent, raise a MixError.

           contents -- a string containing the descriptor, or the name of a
               file containing the descriptor (possibly gzip'd)
           knownOnly -- if true, raise MixError is we don't already have
               a descriptor with this nickname.
           server -- If provided, a parsed ServerInfo corresponding to
               'contents'.
        """
        # Raises ConfigError, MixError,

        if not server:
            contents, server = _readServer(contents)
        try:
            self._lock()

            nickname = server.getNickname()
            lcnickname = nickname.lower()

            known = self.isServerKnown(server)
            if knownOnly and not known:
                raise UIError("Unknown server %s: use import-new." % nickname)

            # Is the server already invalid?
            if server.isExpiredAt(time.time()):
                raise UIError("Descriptor has already expired")

            # Is there already a server with the same nickname?
            if self.serversByNickname.has_key(lcnickname):
                # Okay -- make sure we don't have this same descriptor.
                for fn in self.serversByNickname[lcnickname]:
                    oldServer = self.servers[fn]
                    if oldServer['Server']['Digest'] == \
                           server['Server']['Digest']:
                        raise UIError("Server descriptor already inserted.")
                # Okay -- make sure that this server isn't superseded.
                if server.isSupersededBy([
                        self.servers[fn]
                        for fn in self.serversByNickname[lcnickname]
                ]):
                    raise UIError("Server descriptor is superseded")

            if not known:
                # Is the identity new to us?
                self.learnServerID(server)

            newFile = _writeServer(self.serverDir, contents, nickname)

            # Now update the internal structure
            self.servers[newFile] = server
            self.serversByNickname.setdefault(lcnickname, []).append(newFile)
        finally:
            self._unlock()
Ejemplo n.º 3
0
def resolveFeatureName(name, klass):
    """Given a feature name and a subclass of _ConfigFile, check whether
       the feature exists, and return a sec/name tuple that, when passed to
       _ConfigFile.getFeature, gives the value of the appropriate feature.
       Raises a UIError if the feature name is invalid.

       A feature is either: a special string handled by the class (like
       'caps' for ServerInfo), a special string handled outside the class
       (like 'status' for ClientDirectory), a Section:Entry string, or an
       Entry string.  (If the Entry string is not unique within a section,
       raises UIError.)  All features are case-insensitive.

       Example features are: 'caps', 'status', 'Incoming/MMTP:Version',
         'hostname'.
       """
    syn = klass._syntax
    name = name.lower()
    if name in klass._features:
        return "-", name
    elif ':' in name:
        idx = name.index(':')
        sec, ent = name[:idx], name[idx + 1:]
        goodSection = None
        for section, entries in syn.items():
            if section.lower() == sec:
                goodSection = section
                for entry in entries.keys():
                    if entry.lower() == ent:
                        return section, entry
        if goodSection:
            raise UIError("Section %s has no entry %r" % (goodSection, ent))
        else:
            raise UIError("No such section as %s" % sec)
    else:
        result = []
        for secname, secitems in syn.items():
            if secname.lower() == name:
                raise UIError("No key given for section %s" % secname)
            for entname in secitems.keys():
                if entname.lower() == name:
                    result.append((secname, entname))
        if len(result) == 0:
            raise UIError("No key named %r found" % name)
        elif len(result) > 1:
            secs = [
                "%s:%s" % (secname, entname) for secname, entname in result
            ]
            raise UIError("%r is ambiguous.  Did you mean %s?" %
                          (name, englishSequence(secs, compound="or")))
        else:
            return result[0]
Ejemplo n.º 4
0
    def rescan(self):
        """Reconstruct this ServerList object's internal state."""
        try:
            self._lock()
            # First, build self.servers
            self.servers = {}
            for filename in os.listdir(self.serverDir):
                path = os.path.join(self.serverDir, filename)
                try:
                    self.servers[filename] = ServerInfo(fname=path)
                except ConfigError, e:
                    LOG.warn("Somehow, a bad server named %s got in our store",
                             filename)
                    LOG.warn(" (Error was: %s)", str(e))
                    _moveServer(self.serverDir, self.rejectDir, filename)

            # Next, rebuild self.serverIDs:
            self.serverIDs = {}
            for filename in os.listdir(self.serverIDDir):
                path = os.path.join(self.serverIDDir, filename)
                t = readPickled(path)
                if t[0] != 'V0':
                    LOG.warn("Skipping confusing stored key in file %s",
                             filename)
                    continue
                nickname, key = t[1]
                key = pk_decode_public_key(key)
                if self.serverIDs.has_key(nickname.lower()):
                    LOG.warn("Eeek! Multiple entries for %s", nickname)
                    if not pk_same_public_key(self.serverIDs[nickname.lower()],
                                              key):
                        raise MixFatalError(
                            "Multiple conflicting entries for %s" % nickname)
                self.serverIDs[nickname.lower()] = key

            # (check for consistency)
            for s in self.servers.values():
                lcn = s.getNickname().lower()
                try:
                    ident = self.serverIDs[lcn]
                except KeyError:
                    raise UIError("No stored key for server %s" %
                                  s.getNickname())

                if not pk_same_public_key(ident, s.getIdentity()):
                    raise UIError("Inconsistent stored key for server %s" %
                                  s.getNickname())

            # Then, rebuild self.serversByNickname
            self.__buildNicknameMap()
Ejemplo n.º 5
0
    def addFragment(self, fragment, nym=None):
        """Add a fragment to the pool, logging appropriate messages.  Return
           the messageID which was updated, if any.

             fragment -- an instance of FragmentPayload or a string payload.
             nym -- the identity which received this message.
        """
        pool = self.__getPool()
        if isinstance(fragment, types.StringType):
            try:
                fragment = mixminion.Packet.parsePayload(fragment)
            except mixminion.Packet.ParseError, s:
                raise UIError("Corrupted fragment payload: %s" % s)
            if fragment.isSingleton():
                raise UIError("Non-fragment payload marked as a fragment.")
Ejemplo n.º 6
0
class ServerInbox:
    """A ServerInbox holds server descriptors received from the outside
       world that are not yet ready to be included in the directory.
       """
    ## Fields:
    # newQueue: IncomingQueue object to hold descriptors for previously
    #      unknown servers.
    # updateQueue:  IncomingQueue object to hold descriptors for currently
    #      known servers.
    def __init__(self, base, idCache):
        """Initialize a ServerInbox to store its files in 'base', and
           check server descriptors against the IDCache 'idCache'."""
        self.newQueue = IncomingQueue(os.path.join(base, "new"),
                                      os.path.join(base, "reject"))
        self.updateQueue = IncomingQueue(os.path.join(base, "updates"),
                                         os.path.join(base, "reject"))
        self.idCache = idCache

    def receiveServer(self, text, source):
        """Process a new server descriptor and store it for later action.
           (To be run by the CGI user.)

           If the server will be automatically inserted, return true.
           If the server will be inserted (given administrator intervention),
              raise ServerQueuedException.
           If there is a problem, log it, and raise UIError.

           text -- a string containing a new server descriptor.
           source -- a (human readable) string describing the source
               of the descriptor, used in error messages.

           """
        try:
            server = ServerInfo(string=text,assumeValid=0)
        except MixError, e:
            LOG.warn("Rejected invalid server from %s: %s", source,e)
            raise UIError("Server descriptor was not valid: %s"%e)

        nickname = server.getNickname()

        try:
            known = self.idCache.containsServer(server)
        except MismatchedID:
            LOG.warn("Rejected server with mismatched identity from %s",
                     source)
            self.updateQueue.queueRejectedServer(text,server)
            raise UIError(("I already know a server named "
                           "%s with a different key.")%nickname)

        if not known:
            LOG.info("Received previously unknown server %s from %s",
                     nickname, source)
            self.newQueue.queueIncomingServer(text,server)
            raise ServerQueuedException(
                "Server queued pending manual checking")
        else:
            LOG.info("Received update for server %s from %s",
                     nickname, source)
            self.updateQueue.queueIncomingServer(text,server)
            return 1
Ejemplo n.º 7
0
    def acceptNewServer(self, serverList, nickname):
        """Move the descriptors for a new server with a given nickname
           into the directory.  (To be run by a the directory user.)

           If the nickname is of the format name:FINGERPRINT, then
           only insert servers with the nickname/fingerprint pair.
        """
        if ':' in nickname:
            nickname, fingerprint = nickname.split(":")
        else:
            fingerprint = None

        lcnickname = nickname.lower()
        incoming = self.newQueue.readPendingServers()
        # Do we have any pending servers of the desired name?
        incoming = [ (fname,server,text,fp)
                     for fname,server,text,fp in incoming
                     if server.getNickname().lower() == lcnickname ]
        if not incoming:
            raise UIError("No incoming servers named %s"%nickname)

        if not fingerprint:
            fps = [fp for f,s,t,fp in incoming]
            for f in fps:
                if f != fps[0]:
                    raise UIError("Multiple KeyIDs for servers named %s"%
                                  nickname)
            reject = []
        else:
            reject = [ (f,s,t,fp) for f,s,t,fp in incoming
                       if fp != fingerprint ]
            incoming = [ (f,s,t,fp) for f,s,t,fp in incoming
                        if fp == fingerprint ]
            if not incoming:
                raise UIError("No servers named %s with matching KeyID"%
                              nickname)
            if reject:
                LOG.warn("Rejecting %s servers named %s with unmatched KeyIDs",
                         len(reject), nickname)

        try:
            serverList._lock()
            serverList.learnServerID(incoming[0][1])
            self._doAccept(serverList, self.newQueue, incoming, reject,
                           knownOnly=1)
        finally:
            serverList._unlock()
Ejemplo n.º 8
0
def cmd_fingerprint(args):
    """[Entry point] Print the fingerprint for this directory's key."""

    if args:
        raise UIError("mixminion dir fingerprint takes no arguments")
    d = getDirectory()
    key = d.getIdentity()
    print pk_fingerprint(key)
Ejemplo n.º 9
0
def cmd_list(args):
    """[Entry point] List descriptors waiting to be imported."""
    if args:
        raise UIError("mixminion dir list takes no arguments")

    d = getDirectory()
    inbox = d.getInbox()
    inbox.listNewPendingServers(sys.stdout)
Ejemplo n.º 10
0
def cmd_init(args):
    """[Entry point] Set up a new set of directory files."""
    if args:
        raise UIError("mixminion dir initialize takes no arguments")

    d = getDirectory()
    d.setupDirectories()
    d.getServerList()
    d.getInbox()
Ejemplo n.º 11
0
def cmd_generate(args):
    """[Entry point] generate a fresh directory.  Can be run from a cron
       job."""
    if args:
        raise UIError("mixminion dir generate takes no arguments")

    d = getDirectory()
    serverList = d.getServerList()
    key = d.getIdentity()
    serverList.clean()

    config = d.getConfig()

    badServers = config['Directory'].get('BadServer', [])[:]
    badServerFiles = config['Directory'].get('BadServerFile', [])
    for fn in badServerFiles:
        if not os.path.exists(fn):
            print "No such file %r; skipping" % fn
            continue
        f = open(fn, 'r')
        for ln in iterFileLines(f):
            ln = ln.strip()
            if ln and ln[0] != '#':
                badServers.append(ln)
        f.close()

    excludeServers = config['Directory'].get("ExcludeServer", [])[:]
    excludeServers = [nn.strip().lower() for nn in excludeServers]

    location = config['Publishing']['Location']
    print "(Bad servers==%r)" % badServers

    now = time.time()
    tomorrow = now + 60 * 60 * 24
    twoWeeks = 60 * 60 * 24 * 14

    serverList.generateDirectory(startAt=now,
                                 endAt=tomorrow,
                                 extraTime=twoWeeks,
                                 identityKey=key,
                                 badServers=badServers,
                                 excludeServers=excludeServers)
    print "Directory generated; publishing."

    fname = serverList.getDirectoryFilename()

    if location.endswith(".gz"):
        fIn = open(fname)
        fOut = gzip.GzipFile(location, 'wb')
        fOut.write(fIn.read())
        fIn.close()
        fOut.close()
    else:
        shutil.copy(fname, location)

    print "Published."
Ejemplo n.º 12
0
def cmd_rebuildcache(args):
    """[Entry point] Reconstruct the ID cache from the contents of the
       'servers' directory.
    """
    if args:
        raise UIError("mixminion dir rebuildcache takes no arguments")
    d = getDirectory()
    serverList = d.getServerList()
    serverList.rebuildIDCache()
    d.getIDCache().save()
Ejemplo n.º 13
0
def cmd_update(args):
    """[Entry point] Process updates for currently known servers: copies
       descriptors from the Inbox to ServerList.  This can be run automatically
       as part of a cron job."""
    if args:
        raise UIError("mixminion dir update takes no arguments")

    d = getDirectory()
    serverList = d.getServerList()
    inbox = d.getInbox()
    inbox.acceptUpdates(serverList)
Ejemplo n.º 14
0
def getDirectory():
    """Return the Directory object for this directory.  Looks for a
       configuration file first in $MINION_DIR_CONF, then in
       ~/.mixminion_dir.cf, then in /etc/mixminion_dir.cf.
    """
    fn = os.environ.get('MINION_DIR_CONF')
    if not fn:
        fn = os.path.expanduser("~/.mixminion_dir.cf")
        if not os.path.exists(fn):
            fn = None
    if not fn:
        fn = "/etc/mixminion_dir.cf"
        if not os.path.exists(fn):
            fn = None
    if not fn:
        raise UIError("No configuration file found")

    try:
        config = DirectoryConfig(filename=fn)
    except ConfigError, e:
        raise UIError("Error in %s: %s" % (fn, e))
Ejemplo n.º 15
0
 def removeMessages(self, msgids):
     """Remove all the messages whose IDs are in the list 'msgIDs'.  If the
        messages were reassembled, mark them as 'COMPLETED'; else mark them
        as 'REJECTED'."""
     pool = self.__getPool()
     idSet = {}
     for i in msgids:
         state = pool.getStateByMsgID(i)
         if state is None:
             raise UIError("No such message as %s")
         idSet[state.messageid] = 1
     pool._deleteMessageIDs(idSet, "?")
     pool.cleanQueue()
Ejemplo n.º 16
0
 def isServerKnown(self, server):
     """Return true iff the current server descriptor is known.  Raises
        MixError if we have a server descriptor with this name, but
        a different key."""
     try:
         self._lock()
         try:
             return self.idCache.containsServer(server)
         except mixminion.directory.Directory.MismatchedID:
             raise UIError(
                 ("Already know a server named "
                  "%r with different identity key.") % server.getNickname())
     finally:
         self._unlock()
Ejemplo n.º 17
0
def checkPathLength(path1, path2, exitType, exitInfo, explicitSwap=0,
                    suppressTag=0):
    """Given two path legs (lists of servers), an exit type and an
       exitInfo, raise an error if we can't build a header with the
       provided legs.  If suppressTag is true, no decoding handle will
       be included.

       The leg "path1" may be null.
    """
    err = 0 # 0: no error. 1: 1st leg too big. 2: 1st leg okay, 2nd too big.
    if path1 is not None and path2 is not None:
        try:
            rt,ri = path1[-1].getRoutingFor(path2[0],swap=1)
            _getRouting(path1, rt, ri)
        except MixError:
            err = 1
    # Add a dummy tag as needed to last exitinfo.
    if (not suppressTag
        and exitInfo is not None):
        exitInfo += "X"*20
    else:
        exitInfo = ""
    if err == 0:
        try:
            if path2 and not isinstance(path2, ReplyBlock):
                try:
                    _getRouting(path2, exitType, exitInfo)
                except:
                    print ">>>>>",path2
                    raise
        except MixError:
            err = 2
    if err and not explicitSwap:
        raise UIError("Address and path will not fit in one header")
    elif err:
        raise UIError("Address and %s leg of path will not fit in one header"
                      % ["first", "second"][err-1])
Ejemplo n.º 18
0
 def __init__(self, family, ip, port, backlog, connectionFactory):
     """Create a new ListenConnection"""
     self.ip = ip
     self.port = port
     self.sock = socket.socket(family, socket.SOCK_STREAM)
     self.sock.setblocking(0)
     self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     try:
         self.sock.bind((self.ip, self.port))
     except socket.error, (err, msg):
         extra = ""
         code = errno.errorcode.get(err)
         if code in ["EADDRNOTAVAIL", "WSAEADDRNOTAVAIL"]:
             extra = " (Is that really your IP address?)"
         elif code == "EACCES":
             extra = " (Remember, only root can bind low ports)"
         raise UIError("Error while trying to bind to %s:%s: %s%s" %
                       (self.ip, self.port, msg, extra))
Ejemplo n.º 19
0
    def receiveServer(self, text, source):
        """Process a new server descriptor and store it for later action.
           (To be run by the CGI user.)

           If the server will be automatically inserted, return true.
           If the server will be inserted (given administrator intervention),
              raise ServerQueuedException.
           If there is a problem, log it, and raise UIError.

           text -- a string containing a new server descriptor.
           source -- a (human readable) string describing the source
               of the descriptor, used in error messages.

           """
        try:
            server = ServerInfo(string=text,assumeValid=0)
        except MixError, e:
            LOG.warn("Rejected invalid server from %s: %s", source,e)
            raise UIError("Server descriptor was not valid: %s"%e)
Ejemplo n.º 20
0
    def getMessage(self, msgid, force=0):
        """Return the string value of the (compressed) reassembled
           message with ID 'msgid', or raise an error explaining why
           we can't.

           If 'force' is true, return the message even if it seems
           overcompressed.  Otherwise raise a CompressedDataTooLong
           exception.
        """
        pool = self.__getPool()
        state = pool.getStateByMsgID(msgid)
        if state is not None:
            msg = pool.getReadyMessage(state.messageid)
            if msg is not None:
                try:
                    if force:
                        maxSize = None
                    else:
                        maxSize = len(msg) * 20
                    return mixminion.Packet.uncompressData(msg, maxSize)
                except mixminion.Packet.ParseError, e:
                    raise UIError("Invalid message %s: %s" % (msgid, e))
Ejemplo n.º 21
0
def generateServerDescriptorAndKeys(config,
                                    identityKey,
                                    keydir,
                                    keyname,
                                    hashdir,
                                    validAt=None,
                                    now=None,
                                    useServerKeys=0,
                                    validUntil=None):
    """Generate and sign a new server descriptor, and generate all the keys to
       go with it.

          config -- Our ServerConfig object.
          identityKey -- This server's private identity key
          keydir -- The root directory for storing key sets.
          keyname -- The name of this new key set within keydir
          hashdir -- The root directory for storing hash logs.
          validAt -- The starting time (in seconds) for this key's lifetime.
          useServerKeys -- If true, try to read an existing keyset from
               (keydir,keyname,hashdir) rather than generating a fresh one.
          validUntil -- Time at which the generated descriptor should
               expire.
    """
    if useServerKeys:
        serverKeys = ServerKeyset(keydir, keyname, hashdir)
        serverKeys.load()
        packetKey = serverKeys.packetKey
    else:
        # First, we generate both of our short-term keys...
        packetKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES * 8)

        # ...and save them to disk, setting up our directory structure while
        # we're at it.
        serverKeys = ServerKeyset(keydir, keyname, hashdir)
        serverKeys.packetKey = packetKey
        serverKeys.save()

    # FFFF unused
    # allowIncoming = config['Incoming/MMTP'].get('Enabled', 0)

    # Now, we pull all the information we need from our configuration.
    nickname = config['Server']['Nickname']
    contact = config['Server']['Contact-Email']
    fingerprint = config['Server']['Contact-Fingerprint']
    comments = config['Server']['Comments']
    if not now:
        now = time.time()
    if not validAt:
        validAt = now

    insecurities = config.getInsecurities()
    if insecurities:
        secure = "no"
    else:
        secure = "yes"

    # Calculate descriptor and X509 certificate lifetimes.
    # (Round validAt to previous midnight.)
    validAt = mixminion.Common.previousMidnight(validAt + 30)
    if not validUntil:
        keyLifetime = config['Server']['PublicKeyLifetime'].getSeconds()
        validUntil = previousMidnight(validAt + keyLifetime + 30)

    mmtpProtocolsIn = mixminion.server.MMTPServer.MMTPServerConnection \
                      .PROTOCOL_VERSIONS[:]
    mmtpProtocolsOut = mixminion.server.MMTPServer.MMTPClientConnection \
                       .PROTOCOL_VERSIONS[:]
    mmtpProtocolsIn.sort()
    mmtpProtocolsOut.sort()
    mmtpProtocolsIn = ",".join(mmtpProtocolsIn)
    mmtpProtocolsOut = ",".join(mmtpProtocolsOut)

    #XXXX009 remove: hasn't been checked since 007 or used since 005.
    identityKeyID = formatBase64(
        mixminion.Crypto.sha1(
            mixminion.Crypto.pk_encode_public_key(identityKey)))

    fields = {
        # XXXX009 remove: hasn't been checked since 007.
        "IP":
        config['Incoming/MMTP'].get('IP', "0.0.0.0"),
        "Hostname":
        config['Incoming/MMTP'].get('Hostname', None),
        "Port":
        config['Incoming/MMTP'].get('Port', 0),
        "Nickname":
        nickname,
        "Identity":
        formatBase64(mixminion.Crypto.pk_encode_public_key(identityKey)),
        "Published":
        formatTime(now),
        "ValidAfter":
        formatDate(validAt),
        "ValidUntil":
        formatDate(validUntil),
        "PacketKey":
        formatBase64(mixminion.Crypto.pk_encode_public_key(packetKey)),
        "KeyID":
        identityKeyID,
        "MMTPProtocolsIn":
        mmtpProtocolsIn,
        "MMTPProtocolsOut":
        mmtpProtocolsOut,
        "PacketVersion":
        mixminion.Packet.PACKET_VERSION,
        "mm_version":
        mixminion.__version__,
        "Secure":
        secure,
        "Contact":
        contact,
    }

    # If we don't know our IP address, try to guess
    if fields['IP'] == '0.0.0.0':  #XXXX008 remove; not needed since 005.
        try:
            fields['IP'] = _guessLocalIP()
            LOG.warn("No IP configured; guessing %s", fields['IP'])
        except IPGuessError, e:
            LOG.error("Can't guess IP: %s", str(e))
            raise UIError("Can't guess IP: %s" % str(e))
Ejemplo n.º 22
0
        pool = self.__getPool()
        state = pool.getStateByMsgID(msgid)
        if state is not None:
            msg = pool.getReadyMessage(state.messageid)
            if msg is not None:
                try:
                    if force:
                        maxSize = None
                    else:
                        maxSize = len(msg) * 20
                    return mixminion.Packet.uncompressData(msg, maxSize)
                except mixminion.Packet.ParseError, e:
                    raise UIError("Invalid message %s: %s" % (msgid, e))

        if state is None:
            raise UIError("No such message as '%s'" % msgid)
        elif not state.isDone():
            raise UIError("Message '%s' is still missing fragments." % msgid)
        else:
            raise MixFatalError("Can't decode message %s; I don't know why!" %
                                msgid)

    def removeMessages(self, msgids):
        """Remove all the messages whose IDs are in the list 'msgIDs'.  If the
           messages were reassembled, mark them as 'COMPLETED'; else mark them
           as 'REJECTED'."""
        pool = self.__getPool()
        idSet = {}
        for i in msgids:
            state = pool.getStateByMsgID(i)
            if state is None:
Ejemplo n.º 23
0
class ServerInbox:
    """A ServerInbox holds server descriptors received from the outside
       world that are not yet ready to be included in the directory.
       """

    ## Fields:
    # store: A ServerStore to hold server files.  Must be readable/writeable by
    #    directory server user and CGI user.
    # voteFile: A VoteFile obejct.  Must be readable by CGI user.
    def __init__(self, store, voteFile):
        """Create a new ServerInbox."""
        self.store = store
        self.voteFile = voteFile

    def receiveServer(self, text, source, now=None):
        """Process a new server descriptor and store it for later action.
           (To be run by the CGI user.)

           If the server will be automatically inserted, return true.
           If the server will be inserted (given administrator intervention),
              raise ServerQueuedException.
           If there is a problem, log it, and raise UIError.

           text -- a string containing a new server descriptor.
           source -- a (human readable) string describing the source
               of the descriptor, used in error messages.

        """
        if now is None:
            now = time.time()

        try:
            #XXXX digest cache??
            server = ServerInfo(string=text, assumeValid=0, _keepContents=1)
        except MixError, e:
            LOG.warn("Rejected invalid server from %s: %s", source, e)
            raise UIError("Server descriptor was not valid: %s" % e)

        status = self.voteFile.getServerStatus(server)
        if status == "mismatch":
            LOG.warn("Rejected server with mismatched identity for %r from %s",
                     nickname, source)
            self.store.addServer(server)
            raise UIError(("I already know a server named "
                           "%s with a different key.") % server.getNickname())
        elif status == "ignore":
            LOG.warn("Rejected descriptor for ignored server %r from %s",
                     nickname, source)
            return

        if server.isExpiredAt(time.time()):
            LOG.warn("Rejecting expired descriptor from %s", source)
            raise UIError("That descriptor is already expired; your clock"
                          " is probably skewed.")

        if status in ("yes", "no", "abstain"):
            LOG.info("Received update for server %r from %s (vote=%s)",
                     server.getNickname(), source, status)
            self.store.addServer(server)
            return 1
        else:
            assert status == "unknown"
            LOG.info("Received previously unknown server %s from %s", nickname,
                     source)
            self.store.addServer(server)
            raise ServerQueuedException(
                "Server queued pending manual checking")