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
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()
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]
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()
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.")
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
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()
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)
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)
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()
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."
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()
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)
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))
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()
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()
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])
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))
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)
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))
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))
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:
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")