[or-cvs] r12862: Document the heck out of bridgedb and clean up the code a li (in bridgedb/trunk: . lib/bridgedb)
nickm at seul.org
nickm at seul.org
Tue Dec 18 23:04:49 UTC 2007
Author: nickm
Date: 2007-12-18 18:04:49 -0500 (Tue, 18 Dec 2007)
New Revision: 12862
Modified:
bridgedb/trunk/
bridgedb/trunk/lib/bridgedb/Bridges.py
bridgedb/trunk/lib/bridgedb/Dist.py
bridgedb/trunk/lib/bridgedb/Main.py
bridgedb/trunk/lib/bridgedb/Server.py
bridgedb/trunk/lib/bridgedb/Tests.py
bridgedb/trunk/lib/bridgedb/Time.py
bridgedb/trunk/setup.py
Log:
r17241 at catbus: nickm | 2007-12-18 18:04:43 -0500
Document the heck out of bridgedb and clean up the code a little.
Property changes on: bridgedb/trunk
___________________________________________________________________
svk:merge ticket from /bridgedb/trunk [r17241] on 8246c3cf-6607-4228-993b-4d95d33730f1
Modified: bridgedb/trunk/lib/bridgedb/Bridges.py
===================================================================
--- bridgedb/trunk/lib/bridgedb/Bridges.py 2007-12-18 23:04:47 UTC (rev 12861)
+++ bridgedb/trunk/lib/bridgedb/Bridges.py 2007-12-18 23:04:49 UTC (rev 12862)
@@ -1,7 +1,12 @@
# BridgeDB by Nick Mathewson.
# Copyright (c) 2007, The Tor Project, Inc.
-# See LICENSE for licensing informatino
+# See LICENSE for licensing information
+"""
+This module has low-level functionality for parsing bridges and arranging
+them in rings.
+"""
+
import binascii
import bisect
import hmac
@@ -47,6 +52,9 @@
return True
def is_valid_fingerprint(fp):
+ """Return true iff fp in the right format to be a hex fingerprint
+ of a Tor server.
+ """
if len(fp) != HEX_FP_LEN:
return False
try:
@@ -60,10 +68,13 @@
fromHex = binascii.a2b_hex
def get_hmac(k,v):
+ """Return the hmac of v using the key k."""
h = hmac.new(k, v, digestmod=DIGESTMOD)
return h.digest()
def get_hmac_fn(k, hex=True):
+ """Return a function that computes the hmac of its input using the key k.
+ If 'hex' is true, the output of the function will be hex-encoded."""
h = hmac.new(k, digestmod=DIGESTMOD)
def hmac_fn(v):
h_tmp = h.copy()
@@ -75,11 +86,23 @@
return hmac_fn
def chopString(s, size):
+ """Generator. Given a string and a length, divide the string into pieces
+ of no more than that length.
+ """
for pos in xrange(0, len(s), size):
yield s[pos:pos+size]
class Bridge:
+ """Holds information for a single bridge"""
+ ## Fields:
+ ## nickname -- The bridge's nickname. Not currently used.
+ ## ip -- The bridge's IP address, as a dotted quad.
+ ## orport -- The bridge's OR port.
+ ## fingerprint -- The bridge's identity digest, in lowercase hex, with
+ ## no spaces.
def __init__(self, nickname, ip, orport, fingerprint=None, id_digest=None):
+ """Create a new Bridge. One of fingerprint and id_digest must be
+ set."""
self.nickname = nickname
self.ip = ip
self.orport = orport
@@ -97,13 +120,16 @@
raise TypeError("Bridge with no ID")
def getID(self):
+ """Return the bridge's identity digest."""
return fromHex(self.fingerprint)
def __repr__(self):
+ """Return a piece of python that evaluates to this bridge."""
return "Bridge(%r,%r,%d,%r)"%(
self.nickname, self.ip, self.orport, self.fingerprint)
def getConfigLine(self):
+ """Return a line describing this bridge for inclusion in a torrc."""
return "bridge %s:%d %s" % (self.ip, self.orport, self.fingerprint)
def assertOK(self):
@@ -112,6 +138,9 @@
assert 1 <= self.orport <= 65535
def parseDescFile(f, bridge_purpose='bridge'):
+ """Generator. Parses a cached-descriptors file 'f', and yields a Bridge
+ object for every entry whose purpose matches bridge_purpose.
+ """
nickname = ip = orport = fingerprint = purpose = None
for line in f:
@@ -140,6 +169,7 @@
nickname = ip = orport = fingerprint = purpose = None
class BridgeHolder:
+ """Abstract base class for all classes that hold bridges."""
def insert(self, bridge):
raise NotImplemented()
@@ -147,7 +177,15 @@
return True
class BridgeRing(BridgeHolder):
+ """Arranges bridges in a ring based on an hmac function."""
+ ## Fields:
+ ## bridges: a map from hmac value to Bridge.
+ ## bridgesByID: a map from bridge ID Digest to Bridge.
+ ## isSorted: true iff sortedKeys is currently sorted.
+ ## sortedKeys: a list of all the hmacs, in order.
+ ## name: a string to represent this ring in the logs.
def __init__(self, key):
+ """Create a new BridgeRing, using key as its hmac key."""
self.bridges = {}
self.bridgesByID = {}
self.hmac = get_hmac_fn(key, hex=False)
@@ -159,6 +197,8 @@
return len(self.bridges)
def insert(self, bridge):
+ """Add a bridge to the ring. If the bridge is already there,
+ replace the old one."""
ident = bridge.getID()
pos = self.hmac(ident)
if not self.bridges.has_key(pos):
@@ -168,17 +208,20 @@
self.bridgesByID[id] = bridge
logging.debug("Adding %s to %s", bridge.getConfigLine(), self.name)
- def sort(self):
+ def _sort(self):
+ """Helper: put the keys in sorted order."""
if not self.isSorted:
self.sortedKeys.sort()
self.isSorted = True
def _getBridgeKeysAt(self, pos, N=1):
+ """Helper: return the N keys appearing in the ring after position
+ pos"""
assert len(pos) == DIGEST_LEN
if N >= len(self.sortedKeys):
return self.sortedKeys
if not self.isSorted:
- self.sort()
+ self._sort()
idx = bisect.bisect_left(self.sortedKeys, pos)
r = self.sortedKeys[idx:idx+N]
if len(r) < N:
@@ -188,17 +231,25 @@
return r
def getBridges(self, pos, N=1):
+ """Return the N bridges appearing in the ring after position pos"""
keys = self._getBridgeKeysAt(pos, N)
keys.sort()
return [ self.bridges[k] for k in keys ]
def getBridgeByID(self, fp):
+ """Return the bridge whose identity digest is fp, or None if no such
+ bridge exists."""
return self.bridgesByID.get(fp)
class LogDB:
+ """Wraps a database object and records all modifications to a
+ human-readable logfile."""
def __init__(self, kwd, db, logfile):
- self._kwd = kwd
+ if kwd:
+ self._kwd = "%s: "%kwd
+ else:
+ self._kwd = ""
self._db = db
self._logfile = logfile
def __delitem__(self, k):
@@ -211,7 +262,7 @@
try:
return self._db[k]
except KeyError:
- self._logfile.write("%s: [%r] = [%r]\n"%(self._kwd, k, v))
+ self._logfile.write("%s[%r] = [%r]\n"%(self._kwd, k, v))
self._db[k] = v
return v
def __len__(self):
@@ -227,6 +278,9 @@
class PrefixStore:
+ """Wraps a database object and prefixes the keys in all requests with
+ 'prefix'. This is used to multiplex several key->value mappings
+ onto a single database."""
def __init__(self, store, prefix):
self._d = store
self._p = prefix
@@ -247,6 +301,9 @@
return [ k[n:] for k in self._d.keys() if k.startswith(self._p) ]
class FixedBridgeSplitter(BridgeHolder):
+ """A bridgeholder that splits bridges up based on an hmac and assigns
+ them to several sub-bridgeholders with equal probability.
+ """
def __init__(self, key, rings):
self.hmac = get_hmac_fn(key, hex=True)
self.rings = rings[:]
@@ -268,6 +325,9 @@
class UnallocatedHolder(BridgeHolder):
+ """A pseudo-bridgeholder that ignores its bridges and leaves them
+ unassigned.
+ """
def insert(self, bridge):
logging.debug("Leaving %s unallocated", bridge.getConfigLine())
@@ -275,6 +335,9 @@
return False
class BridgeTracker:
+ """A stats tracker that records when we first saw and most recently
+ saw each bridge.
+ """
def __init__(self, firstSeenStore, lastSeenStore):
self.firstSeenStore = firstSeenStore
self.lastSeenStore = lastSeenStore
@@ -289,6 +352,10 @@
self.firstSeenStore.setdefault(bridgeID, now)
class BridgeSplitter(BridgeHolder):
+ """A BridgeHolder that splits incoming bridges up based on an hmac,
+ and assigns them to sub-bridgeholders with different probabilities.
+ Bridge-to-bridgeholder associations are recorded in a store.
+ """
def __init__(self, key, store):
self.hmac = get_hmac_fn(key, hex=True)
self.store = store
@@ -305,6 +372,13 @@
return n
def addRing(self, ring, ringname, p=1):
+ """Add a new bridgeholder.
+ ring -- the bridgeholder to add.
+ ringname -- a string representing the bridgeholder. This is used
+ to record which bridges have been assigned where in the store.
+ p -- the relative proportion of bridges to assign to this
+ bridgeholder.
+ """
assert isinstance(ring, BridgeHolder)
self.ringsByName[ringname] = ring
self.pValues.append(self.totalP)
@@ -312,6 +386,8 @@
self.totalP += p
def addTracker(self, t):
+ """Adds a statistics tracker that gets told about every bridge we see.
+ """
self.statsHolders.append(t)
def insert(self, bridge):
@@ -334,11 +410,3 @@
self.store[bridgeID] = ringname
ring.insert(bridge)
-if __name__ == '__main__':
- import sys
- br = BridgeRing("hello")
- for fname in sys.argv[1:]:
- f = open(fname)
- for bridge in parseDescFile(f):
- br.insert(bridge)
-
Modified: bridgedb/trunk/lib/bridgedb/Dist.py
===================================================================
--- bridgedb/trunk/lib/bridgedb/Dist.py 2007-12-18 23:04:47 UTC (rev 12861)
+++ bridgedb/trunk/lib/bridgedb/Dist.py 2007-12-18 23:04:49 UTC (rev 12862)
@@ -1,7 +1,11 @@
# BridgeDB by Nick Mathewson.
# Copyright (c) 2007, The Tor Project, Inc.
-# See LICENSE for licensing informatino
+# See LICENSE for licensing information
+"""
+This module has functions to decide which bridges to hand out to whom.
+"""
+
import bridgedb.Bridges
import logging
@@ -17,6 +21,18 @@
return ".".join( ip.split(".")[:3] )
class IPBasedDistributor(bridgedb.Bridges.BridgeHolder):
+ """Object that hands out bridges based on the IP address of an incoming
+ request and the current time period.
+ """
+ ## Fields:
+ ## areaMapper -- a function that maps an IP address to a string such
+ ## that addresses mapping to the same string are in the same "area".
+ ## rings -- a list of BridgeRing objects. Every bridge goes into one
+ ## of these rings, and every area is associated with one.
+ ## splitter -- a FixedBridgeSplitter to assign bridges into the
+ ## rings of this distributor.
+ ## areaOrderHmac -- an hmac function used to order areas within rings.
+ ## areaClusterHmac -- an hmac function used to assign areas to rings.
def __init__(self, areaMapper, nClusters, key):
self.areaMapper = areaMapper
@@ -36,9 +52,16 @@
self.areaClusterHmac = bridgedb.Bridges.get_hmac_fn(key4, hex=True)
def insert(self, bridge):
+ """Assign a bridge to this distributor."""
self.splitter.insert(bridge)
def getBridgesForIP(self, ip, epoch, N=1):
+ """Return a list of bridges to give to a user.
+ ip -- the user's IP address, as a dotted quad.
+ epoch -- the time period when we got this request. This can
+ be any string, so long as it changes with every period.
+ N -- the number of bridges to try to give back.
+ """
if not len(self.splitter):
return []
@@ -61,9 +84,9 @@
# These characters are the ones that RFC2822 allows.
#ASPECIAL = '!#$%&*+-/=?^_`{|}~'
#ASPECIAL += "\\\'"
-
# These are the ones we're pretty sure we can handle right.
ASPECIAL = '-_+/=_~'
+
ACHAR = r'[\w%s]' % "".join("\\%s"%c for c in ASPECIAL)
DOTATOM = r'%s+(?:\.%s+)*'%(ACHAR,ACHAR)
DOMAIN = r'\w+(?:\.\w+)*'
@@ -73,14 +96,21 @@
ADDRSPEC_PAT = re.compile(ADDRSPEC)
class BadEmail(Exception):
+ """Exception raised when we get a bad email address."""
def __init__(self, msg, email):
Exception.__init__(self, msg)
self.email = email
class UnsupportedDomain(BadEmail):
+ """Exception raised when we get an email address from a domain we
+ don't know."""
pass
def extractAddrSpec(addr):
+ """Given an email From line, try to extract and parse the addrspec
+ portion. Returns localpart,domain on success; raises BadEmail
+ on failure.
+ """
orig_addr = addr
addr = SPACE_PAT.sub(' ', addr)
addr = addr.strip()
@@ -116,6 +146,10 @@
return localpart, domain
def normalizeEmail(addr, domainmap):
+ """Given the contents of a from line, and a map of supported email
+ domains (in lowercase), raise BadEmail or return a normalized
+ email address.
+ """
addr = addr.lower()
localpart, domain = extractAddrSpec(addr)
if domainmap is not None:
@@ -128,21 +162,38 @@
return "%s@%s"%(localpart, domain)
class EmailBasedDistributor(bridgedb.Bridges.BridgeHolder):
+ """Object that hands out bridges based on the email address of an incoming
+ request and the current time period.
+ """
+ ## Fields:
+ ## emailHmac -- an hmac function used to order email addresses within
+ ## a ring.
+ ## ring -- a BridgeRing object to hold all the bridges we hand out.
+ ## store -- a database object to remember what we've given to whom.
+ ## domainmap -- a map from lowercase domains that we support mail from
+ ## to their canonical forms.
def __init__(self, key, store, domainmap):
-
key1 = bridgedb.Bridges.get_hmac(key, "Map-Addresses-To-Ring")
self.emailHmac = bridgedb.Bridges.get_hmac_fn(key1, hex=False)
key2 = bridgedb.Bridges.get_hmac(key, "Order-Bridges-In-Ring")
self.ring = bridgedb.Bridges.BridgeRing(key2)
self.ring.name = "email ring"
+ # XXXX clear the store when the period rolls over!
self.store = store
self.domainmap = domainmap
def insert(self, bridge):
+ """Assign a bridge to this distributor."""
self.ring.insert(bridge)
def getBridgesForEmail(self, emailaddress, epoch, N=1):
+ """Return a list of bridges to give to a user.
+ emailaddress -- the user's email address, as given in a from line.
+ epoch -- the time period when we got this request. This can
+ be any string, so long as it changes with every period.
+ N -- the number of bridges to try to give back.
+ """
emailaddress = normalizeEmail(emailaddress, self.domainmap)
if emailaddress is None:
return [] #XXXX raise an exception.
@@ -163,15 +214,3 @@
memo = "".join(b.getID() for b in result)
self.store[emailaddress] = memo
return result
-
-if __name__ == '__main__':
- import sys
- for line in sys.stdin:
- line = line.strip()
- if line.startswith("From: "):
- line = line[6:]
- try:
- normal = normalizeEmail(line, None)
- print normal
- except BadEmail, e:
- print line, e
Modified: bridgedb/trunk/lib/bridgedb/Main.py
===================================================================
--- bridgedb/trunk/lib/bridgedb/Main.py 2007-12-18 23:04:47 UTC (rev 12861)
+++ bridgedb/trunk/lib/bridgedb/Main.py 2007-12-18 23:04:49 UTC (rev 12862)
@@ -1,7 +1,11 @@
# BridgeDB by Nick Mathewson.
# Copyright (c) 2007, The Tor Project, Inc.
-# See LICENSE for licensing informatino
+# See LICENSE for licensing information
+"""
+This module sets up a bridgedb and starts the servers running.
+"""
+
import anydbm
import os
import signal
@@ -16,9 +20,13 @@
import bridgedb.Server as Server
class Conf:
+ """A configuration object. Holds unvalidated attributes.
+ """
def __init__(self, **attrs):
self.__dict__.update(attrs)
+# An example configuration. Used for testing. See sample
+# bridgedb.conf for documentation.
CONFIG = Conf(
RUN_IN_DIR = ".",
@@ -58,6 +66,8 @@
)
def configureLogging(cfg):
+ """Set up Python's logging subsystem based on the configuratino.
+ """
level = getattr(cfg, 'LOGLEVEL', 'WARNING')
level = getattr(logging, level)
extra = {}
@@ -100,17 +110,24 @@
return k
def load(cfg, splitter):
+ """Read all the bridge files from cfg, and pass them into a splitter
+ object.
+ """
for fname in cfg.BRIDGE_FILES:
f = open(fname, 'r')
for bridge in Bridges.parseDescFile(f, cfg.BRIDGE_PURPOSE):
splitter.insert(bridge)
f.close()
-_reloadFn = None
+_reloadFn = lambda: True
def _handleSIGHUP(*args):
+ """Called when we receive a SIGHUP; invokes _reloadFn."""
reactor.callLater(0, _reloadFn)
def startup(cfg):
+ """Parse bridges,
+ """
+ # Expand any ~ characters in paths in the configuration.
cfg.BRIDGE_FILES = [ os.path.expanduser(fn) for fn in cfg.BRIDGE_FILES ]
for key in ("RUN_IN_DIR", "DB_FILE", "DB_LOG_FILE", "MASTER_KEY_FILE",
"HTTPS_CERT_FILE", "HTTPS_KEY_FILE", "PIDFILE", "LOGFILE"):
@@ -118,28 +135,36 @@
if v:
setattr(cfg, key, os.path.expanduser(v))
+ # Change to the directory where we're supposed to run.
if cfg.RUN_IN_DIR:
os.chdir(cfg.RUN_IN_DIR)
+ # Write the pidfile.
if cfg.PIDFILE:
f = open(cfg.PIDFILE, 'w')
f.write("%s\n"%os.getpid())
f.close()
+ # Set up logging.
configureLogging(cfg)
+ # Load the master key, or create a new one.
key = getKey(cfg.MASTER_KEY_FILE)
+
+ # Initialize our DB file.
dblogfile = None
- emailDistributor = ipDistributor = None
-
baseStore = store = anydbm.open(cfg.DB_FILE, "c", 0600)
if cfg.DB_LOG_FILE:
dblogfile = open(cfg.DB_LOG_FILE, "a+", 0)
- store = Bridges.LogDB("db", store, dblogfile)
+ store = Bridges.LogDB(None, store, dblogfile)
+ # Create a BridgeSplitter to assign the bridges to the different
+ # distributors.
splitter = Bridges.BridgeSplitter(Bridges.get_hmac(key, "Splitter-Key"),
Bridges.PrefixStore(store, "sp|"))
+ emailDistributor = ipDistributor = None
+ # As appropriate, create an IP-based distributor.
if cfg.HTTPS_DIST and cfg.HTTPS_SHARE:
ipDistributor = Dist.IPBasedDistributor(
Dist.uniformMap,
@@ -148,6 +173,7 @@
splitter.addRing(ipDistributor, "https", cfg.HTTPS_SHARE)
webSchedule = Time.IntervalSchedule("day", 2)
+ # As appropriate, create an email-based distributor.
if cfg.EMAIL_DIST and cfg.EMAIL_SHARE:
for d in cfg.EMAIL_DOMAINS:
cfg.EMAIL_DOMAIN_MAP[d] = d
@@ -158,15 +184,18 @@
splitter.addRing(emailDistributor, "email", cfg.EMAIL_SHARE)
emailSchedule = Time.IntervalSchedule("day", 1)
+ # As appropriate, tell the splitter to leave some bridges unallocated.
if cfg.RESERVED_SHARE:
splitter.addRing(Bridges.UnallocatedHolder(),
"unallocated",
cfg.RESERVED_SHARE)
+ # Add a tracker to tell us how often we've seen various bridges.
stats = Bridges.BridgeTracker(Bridges.PrefixStore(store, "fs|"),
Bridges.PrefixStore(store, "ls|"))
splitter.addTracker(stats)
+ # Parse the bridges and log how many we put where.
logging.info("Loading bridges")
load(cfg, splitter)
logging.info("%d bridges loaded", len(splitter))
@@ -177,19 +206,22 @@
logging.info(" by location set: %s",
" ".join(str(len(r)) for r in ipDistributor.rings))
+ # Configure HTTP and/or HTTPS servers.
if cfg.HTTPS_DIST and cfg.HTTPS_SHARE:
Server.addWebServer(cfg, ipDistributor, webSchedule)
+ # Configure Email servers.
if cfg.EMAIL_DIST and cfg.EMAIL_SHARE:
Server.addSMTPServer(cfg, emailDistributor, emailSchedule)
+ # Make the parse-bridges function get re-called on SIGHUP.
def reload():
load(cfg, splitter)
-
global _reloadFn
_reloadFn = reload
signal.signal(signal.SIGHUP, _handleSIGHUP)
+ # Actually run the servers.
try:
logging.info("Starting reactors.")
Server.runServers()
@@ -201,6 +233,9 @@
os.unlink(cfg.PIDFILE)
def run():
+ """Parse the command line to determine where the configuration is.
+ Parse the configuration, and start the servers.
+ """
if len(sys.argv) != 2:
print "Syntax: %s [config file]" % sys.argv[0]
sys.exit(1)
Modified: bridgedb/trunk/lib/bridgedb/Server.py
===================================================================
--- bridgedb/trunk/lib/bridgedb/Server.py 2007-12-18 23:04:47 UTC (rev 12861)
+++ bridgedb/trunk/lib/bridgedb/Server.py 2007-12-18 23:04:49 UTC (rev 12862)
@@ -1,7 +1,11 @@
# BridgeDB by Nick Mathewson.
# Copyright (c) 2007, The Tor Project, Inc.
-# See LICENSE for licensing informatino
+# See LICENSE for licensing information
+"""
+This module implements the web and email interfaces to the bridge database.
+"""
+
from cStringIO import StringIO
import MimeWriter
import rfc822
@@ -68,9 +72,16 @@
"""
class WebResource(twisted.web.resource.Resource):
+ """This resource is used by Twisted Web to give a web page with some
+ bridges in response to a request."""
isLeaf = True
def __init__(self, distributor, schedule, N=1):
+ """Create a new WebResource.
+ distributor -- an IPBasedDistributor object
+ schedule -- an IntervalSchedule object
+ N -- the number of bridges to hand out per query.
+ """
twisted.web.resource.Resource.__init__(self)
self.distributor = distributor
self.schedule = schedule
@@ -90,22 +101,34 @@
return HTML_MESSAGE_TEMPLATE % answer
def addWebServer(cfg, dist, sched):
+ """Set up a web server.
+ cfg -- a configuration object from Main. We use these options:
+ HTTPS_N_BRIDGES_PER_ANSWER
+ HTTP_UNENCRYPTED_PORT
+ HTTP_UNENCRYPTED_BIND_IP
+ HTTPS_PORT
+ HTTPS_BIND_IP
+ dist -- an IPBasedDistributor object.
+ sched -- an IntervalSchedule object.
+ """
Site = twisted.web.server.Site
resource = WebResource(dist, sched, cfg.HTTPS_N_BRIDGES_PER_ANSWER)
site = Site(resource)
if cfg.HTTP_UNENCRYPTED_PORT:
- ip = cfg.HTTPS_BIND_IP or ""
+ ip = cfg.HTTP_UNENCRYPTED_BIND_IP or ""
reactor.listenTCP(cfg.HTTP_UNENCRYPTED_PORT, site, interface=ip)
if cfg.HTTPS_PORT:
from twisted.internet.ssl import DefaultOpenSSLContextFactory
#from OpenSSL.SSL import SSLv3_METHOD
- ip = cfg.HTTP_UNENCRYPTED_BIND_IP or ""
+ ip = cfg.HTTPS_BIND_IP or ""
factory = DefaultOpenSSLContextFactory(cfg.HTTPS_KEY_FILE,
cfg.HTTPS_CERT_FILE)
reactor.listenSSL(cfg.HTTPS_PORT, site, factory, interface=ip)
return site
class MailFile:
+ """A file-like object used to hand rfc822.Message a list of lines
+ as though it were reading them from a file."""
def __init__(self, lines):
self.lines = lines
self.idx = 0
@@ -113,11 +136,17 @@
try :
line = self.lines[self.idx]
self.idx += 1
- return line #Append a \n? XXXX
+ return line
except IndexError:
return ""
def getMailResponse(lines, ctx):
+ """Given a list of lines from an incoming email message, and a
+ MailContext object, parse the email and decide what to do in response.
+ If we want to answer, return a 2-tuple containing the address that
+ will receive the response, and a readable filelike object containing
+ the response. Return None,None if we shouldn't answer.
+ """
# Extract data from the headers.
msg = rfc822.Message(MailFile(lines))
subject = msg.getheader("Subject", None)
@@ -132,6 +161,8 @@
else:
logging.info("No From or Sender header on incoming mail.")
return None,None
+
+ # Was the magic string included?
for ln in lines:
if ln.strip().lower() in ("get bridges", "subject: get bridges"):
break
@@ -140,6 +171,7 @@
clientAddr)
return None,None
+ # Figure out which bridges to send
try:
interval = ctx.schedule.getInterval(time.time())
bridges = ctx.distributor.getBridgesForEmail(clientAddr,
@@ -160,7 +192,8 @@
w.addheader("Message-ID", twisted.mail.smtp.messageid())
if not subject.startswith("Re:"): subject = "Re: %s"%subject
w.addheader("Subject", subject)
- w.addheader("In-Reply-To", msgID)
+ if msgID:
+ w.addheader("In-Reply-To", msgID)
w.addheader("Date", twisted.mail.smtp.rfc822date())
body = w.startbody("text/plain")
@@ -171,6 +204,9 @@
return clientAddr, f
def replyToMail(lines, ctx):
+ """Given a list of lines from an incoming email message, and a
+ MailContext object, possibly send a reply.
+ """
logging.info("Got a completed email; attempting to reply.")
sendToUser, response = getMailResponse(lines, ctx)
if response is None:
@@ -187,26 +223,38 @@
return d
class MailContext:
+ """Helper object that holds information used by email subsystem."""
def __init__(self, cfg, dist, sched):
+ # Reject any RCPT TO lines that aren't to this user.
self.username = "bridges"
+ # Reject any mail longer than this.
self.maximumSize = 32*1024
+ # Use this server for outgoing mail.
self.smtpServer = "127.0.0.1"
self.smtpPort = 25
+ # Use this address as the from line for outgoing mail.
self.fromAddr = "bridges at torproject.org"
+ # An EmailBasedDistributor object
self.distributor = dist
+ # An IntervalSchedule object
self.schedule = sched
+ # The number of bridges to send for each email.
self.N = cfg.EMAIL_N_BRIDGES_PER_ANSWER
class MailMessage:
+ """Plugs into the Twisted Mail and receives an incoming message.
+ Once the message is in, we reply or we don't. """
implements(twisted.mail.smtp.IMessage)
def __init__(self, ctx):
+ """Create a new MailMessage from a MailContext."""
self.ctx = ctx
self.lines = []
self.nBytes = 0
self.ignoring = False
def lineReceived(self, line):
+ """Called when we get another line of an incoming message."""
self.nBytes += len(line)
if self.nBytes > self.ctx.maximumSize:
self.ignoring = True
@@ -214,14 +262,17 @@
self.lines.append(line)
def eomReceived(self):
+ """Called when we receive the end of a message."""
if not self.ignoring:
replyToMail(self.lines, self.ctx)
return twisted.internet.defer.succeed(None)
def connectionLost(self):
+ """Called if we die partway through reading a message."""
pass
class MailDelivery:
+ """Plugs into Twisted Mail and handles SMTP commands."""
implements(twisted.mail.smtp.IMessageDelivery)
def setBridgeDBContext(self, ctx):
self.ctx = ctx
@@ -236,6 +287,8 @@
return lambda: MailMessage(self.ctx)
class MailFactory(twisted.mail.smtp.SMTPFactory):
+ """Plugs into Twisted Mail; creates a new MailDelivery whenever we get
+ a connection on the SMTP port."""
def __init__(self, *a, **kw):
twisted.mail.smtp.SMTPFactory.__init__(self, *a, **kw)
self.delivery = MailDelivery()
@@ -250,6 +303,14 @@
return p
def addSMTPServer(cfg, dist, sched):
+ """Set up a smtp server.
+ cfg -- a configuration object from Main. We use these options:
+ EMAIL_BIND_IP
+ EMAIL_PORT
+ EMAIL_N_BRIDGES_PER_ANSWER
+ dist -- an EmailBasedDistributor object.
+ sched -- an IntervalSchedule object.
+ """
ctx = MailContext(cfg, dist, sched)
factory = MailFactory()
factory.setBridgeDBContext(ctx)
@@ -258,4 +319,5 @@
return factory
def runServers():
+ """Start all the servers that we've configured. Exits when they do."""
reactor.run()
Modified: bridgedb/trunk/lib/bridgedb/Tests.py
===================================================================
--- bridgedb/trunk/lib/bridgedb/Tests.py 2007-12-18 23:04:47 UTC (rev 12861)
+++ bridgedb/trunk/lib/bridgedb/Tests.py 2007-12-18 23:04:49 UTC (rev 12862)
@@ -1,6 +1,6 @@
# BridgeDB by Nick Mathewson.
# Copyright (c) 2007, The Tor Project, Inc.
-# See LICENSE for licensing informatino
+# See LICENSE for licensing information
import doctest
import unittest
Modified: bridgedb/trunk/lib/bridgedb/Time.py
===================================================================
--- bridgedb/trunk/lib/bridgedb/Time.py 2007-12-18 23:04:47 UTC (rev 12861)
+++ bridgedb/trunk/lib/bridgedb/Time.py 2007-12-18 23:04:49 UTC (rev 12862)
@@ -1,17 +1,29 @@
# BridgeDB by Nick Mathewson.
# Copyright (c) 2007, The Tor Project, Inc.
-# See LICENSE for licensing informatino
+# See LICENSE for licensing information
+"""
+This module implements functions for dividing time into chunks.
+"""
+
import calendar
import time
KNOWN_INTERVALS = [ "hour", "day", "week", "month" ]
-N_ELEMENTS = { 'month' : 2,
- 'day' : 3,
- 'hour' : 4 }
class IntervalSchedule:
+ """An IntervalSchedule splits time into somewhat natural periods,
+ based on hours, days, weeks, or months.
+ """
+ ## Fields:
+ ## itype -- one of "month", "day", "hour".
+ ## count -- how many of the units in itype belong to each period.
def __init__(self, intervaltype, count):
+ """Create a new IntervalSchedule.
+ intervaltype -- one of month, week, day, hour.
+ count -- how many of the units in intervaltype belong to each
+ period.
+ """
it = intervaltype.lower()
if it.endswith("s"): it = it[:-1]
if it not in KNOWN_INTERVALS:
@@ -22,26 +34,31 @@
count *= 7
self.itype = it
self.count = count
- self.n_elements = N_ELEMENTS[it]
def _intervalStart(self, when):
+ """Return the time (as an int) of the start of the interval containing
+ 'when'."""
if self.itype == 'month':
+ # For months, we always start at the beginning of the month.
tm = time.gmtime(when)
n = tm.tm_year * 12 + tm.tm_mon - 1
n -= (n % self.count)
month = n%12 + 1
return calendar.timegm((n//12, month, 1, 0, 0, 0))
elif self.itype == 'day':
+ # For days, we start at the beginning of a day.
when -= when % (86400 * self.count)
return when
elif self.itype == 'hour':
+ # For hours, we start at the beginning of an hour.
when -= when % (3600 * self.count)
return when
else:
assert False
def getInterval(self, when):
- """
+ """Return a string representing the interval that contains
+ the time 'when'.
>>> t = calendar.timegm((2007, 12, 12, 0, 0, 0))
>>> I = IntervalSchedule('month', 1)
@@ -67,6 +84,7 @@
assert False
def nextIntervalStarts(self, when):
+ """Return the start time of the interval starting _after_ when."""
if self.itype == 'month':
tm = time.gmtime(when)
n = tm.tm_year * 12 + tm.tm_mon - 1
Modified: bridgedb/trunk/setup.py
===================================================================
--- bridgedb/trunk/setup.py 2007-12-18 23:04:47 UTC (rev 12861)
+++ bridgedb/trunk/setup.py 2007-12-18 23:04:49 UTC (rev 12862)
@@ -1,3 +1,4 @@
+#!/usr/bin/python
# BridgeDB by Nick Mathewson.
# Copyright (c) 2007, The Tor Project, Inc.
# See LICENSE for licensing information
More information about the tor-commits
mailing list