[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [bridgedb/master] Split servers into separate files
commit 120c8e6d4b9d714e79722d83c19e110120977e79
Author: aagbsn <aagbsn@xxxxxxxx>
Date: Mon Mar 18 19:55:11 2013 +0000
Split servers into separate files
---
lib/bridgedb/EmailServer.py | 478 +++++++++++++++++++++++++
lib/bridgedb/HTTPServer.py | 351 +++++++++++++++++++
lib/bridgedb/Server.py | 809 -------------------------------------------
3 files changed, 829 insertions(+), 809 deletions(-)
diff --git a/lib/bridgedb/EmailServer.py b/lib/bridgedb/EmailServer.py
new file mode 100644
index 0000000..8b913a1
--- /dev/null
+++ b/lib/bridgedb/EmailServer.py
@@ -0,0 +1,478 @@
+# BridgeDB by Nick Mathewson.
+# Copyright (c) 2007-2013, The Tor Project, Inc.
+# See LICENSE for licensing information
+
+"""
+This module implements the email interface to the bridge database.
+"""
+
+from StringIO import StringIO
+
+import gettext
+import gpgme
+import logging
+import re
+import rfc822
+import time
+
+from ipaddr import IPv4Address, IPv6Address
+
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+from twisted.internet.task import LoopingCall
+import twisted.mail.smtp
+
+from zope.interface import implements
+
+import bridgedb.Dist
+from bridgedb.Dist import BadEmail, TooSoonEmail, IgnoreEmail
+from bridgedb.Filters import filterBridgesByIP6, filterBridgesByIP4
+from bridgedb.Filters import filterBridgesByTransport
+from bridgedb.Filters import filterBridgesByNotBlockedIn
+
+import bridgedb.I18n as I18n
+
+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
+ def readline(self):
+ try :
+ line = self.lines[self.idx]
+ self.idx += 1
+ return line
+ except IndexError:
+ return ""
+
+def getBridgeDBEmailAddrFromList(ctx, address_list):
+ """Loop through a list of (full name, email address) pairs and look up our
+ mail address. If our address isn't found (which can't happen), return
+ the default ctx from address so we can keep on working.
+ """
+ email = ctx.fromAddr
+ for _, address in address_list:
+ # Strip the @torproject.org part from the address
+ idx = address.find('@')
+ if idx != -1:
+ username = address[:idx]
+ # See if the user looks familiar. We do a 'find' instead
+ # of compare because we might have a '+' address here
+ if username.find(ctx.username) != -1:
+ email = address
+ return email
+
+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)
+ if not subject: subject = "[no subject]"
+ clientFromAddr = msg.getaddr("From")
+ clientSenderAddr = msg.getaddr("Sender")
+ # RFC822 requires at least one 'To' address
+ clientToList = msg.getaddrlist("To")
+ clientToaddr = getBridgeDBEmailAddrFromList(ctx, clientToList)
+ msgID = msg.getheader("Message-ID", None)
+ if clientSenderAddr and clientSenderAddr[1]:
+ clientAddr = clientSenderAddr[1]
+ elif clientFromAddr and clientFromAddr[1]:
+ clientAddr = clientFromAddr[1]
+ else:
+ logging.info("No From or Sender header on incoming mail.")
+ return None,None
+
+ # Look up the locale part in the 'To:' address, if there is one and get
+ # the appropriate Translation object
+ lang = getLocaleFromPlusAddr(clientToaddr)
+ t = I18n.getLang(lang)
+
+ try:
+ _, addrdomain = bridgedb.Dist.extractAddrSpec(clientAddr.lower())
+ except BadEmail:
+ logging.info("Ignoring bad address on incoming email.")
+ return None,None
+ if not addrdomain:
+ logging.info("Couldn't parse domain from %r", clientAddr)
+ if addrdomain and ctx.cfg.EMAIL_DOMAIN_MAP:
+ addrdomain = ctx.cfg.EMAIL_DOMAIN_MAP.get(addrdomain, addrdomain)
+ if addrdomain not in ctx.cfg.EMAIL_DOMAINS:
+ logging.info("Unrecognized email domain %r", addrdomain)
+ return None,None
+ rules = ctx.cfg.EMAIL_DOMAIN_RULES.get(addrdomain, [])
+ if 'dkim' in rules:
+ # getheader() returns the last of a given kind of header; we want
+ # to get the first, so we use getheaders() instead.
+ dkimHeaders = msg.getheaders("X-DKIM-Authentication-Results")
+ dkimHeader = "<no header>"
+ if dkimHeaders: dkimHeader = dkimHeaders[0]
+ if not dkimHeader.startswith("pass"):
+ logging.info("Got a bad dkim header (%r) on an incoming mail; "
+ "rejecting it.", dkimHeader)
+ return None, None
+
+ # Was the magic string included
+ #for ln in lines:
+ # if ln.strip().lower() in ("get bridges", "subject: get bridges"):
+ # break
+ #else:
+ # logging.info("Got a mail from %r with no bridge request; dropping",
+ # clientAddr)
+ # return None,None
+
+ # Figure out which bridges to send
+ unblocked = transport = ipv6 = skippedheaders = False
+ bridgeFilterRules = []
+ addressClass = None
+ for ln in lines:
+ # ignore all lines before the subject header
+ if "subject" in ln.strip().lower():
+ skippedheaders = True
+ if not skippedheaders: continue
+
+ if "ipv6" in ln.strip().lower():
+ ipv6 = True
+ if "transport" in ln.strip().lower():
+ try:
+ transport = re.search("transport ([_a-zA-Z][_a-zA-Z0-9]*)",
+ ln).group(1).strip()
+ except (TypeError, AttributeError):
+ transport = None
+ logging.debug("Got request for transport: %s" % transport)
+ if "unblocked" in ln.strip().lower():
+ try:
+ unblocked = re.search("unblocked ([a-zA-Z]{2,4})",
+ ln).group(1).strip()
+ except (TypeError, AttributeError):
+ transport = None
+
+ if ipv6:
+ bridgeFilterRules.append(filterBridgesByIP6)
+ addressClass = IPv6Address
+ else:
+ bridgeFilterRules.append(filterBridgesByIP4)
+ addressClass = IPv4Address
+
+ if transport:
+ bridgeFilterRules = [filterBridgesByTransport(transport, addressClass)]
+
+ if unblocked:
+ rules.append(filterBridgesByNotBlockedIn(unblocked,
+ addressClass, transport))
+
+ try:
+ interval = ctx.schedule.getInterval(time.time())
+ bridges = ctx.distributor.getBridgesForEmail(clientAddr,
+ interval, ctx.N,
+ countryCode=None,
+ bridgeFilterRules=bridgeFilterRules)
+
+ # Handle rate limited email
+ except TooSoonEmail, e:
+ logging.info("Got a mail too frequently; warning %r: %s.",
+ clientAddr, e)
+
+ # Compose a warning email
+ # MAX_EMAIL_RATE is in seconds, convert to hours
+ body = buildSpamWarningTemplate(t) % (bridgedb.Dist.MAX_EMAIL_RATE / 3600)
+ return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID,
+ gpgContext=ctx.gpgContext)
+
+ except IgnoreEmail, e:
+ logging.info("Got a mail too frequently; ignoring %r: %s.",
+ clientAddr, e)
+ return None, None
+
+ except BadEmail, e:
+ logging.info("Got a mail from a bad email address %r: %s.",
+ clientAddr, e)
+ return None, None
+
+ if bridges:
+ with_fp = ctx.cfg.EMAIL_INCLUDE_FINGERPRINTS
+ answer = "".join(" %s\n" %b.getConfigLine(
+ includeFingerprint=with_fp,
+ addressClass=addressClass,
+ transport=transport,
+ request=clientAddr
+ ) for b in bridges)
+ else:
+ answer = "(no bridges currently available)"
+
+ body = buildMessageTemplate(t) % answer
+ # Generate the message.
+ return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID,
+ gpgContext=ctx.gpgContext)
+
+
+def buildMessageTemplate(t):
+ msg_template = t.gettext(I18n.BRIDGEDB_TEXT[5]) + "\n\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[0]) + "\n\n" \
+ + "%s\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[1]) + "\n\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[2]) + "\n\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[3]) + "\n\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[17])+ "\n\n"
+ # list supported commands, e.g. ipv6, transport
+ msg_template = msg_template \
+ + " " + t.gettext(I18n.BRIDGEDB_TEXT[18])+ "\n" \
+ + " " + t.gettext(I18n.BRIDGEDB_TEXT[19])+ "\n\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[6]) + "\n\n"
+ return msg_template
+
+def buildSpamWarningTemplate(t):
+ msg_template = t.gettext(I18n.BRIDGEDB_TEXT[5]) + "\n\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[10]) + "\n\n" \
+ + "%s " \
+ + t.gettext(I18n.BRIDGEDB_TEXT[11]) + "\n\n" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[12]) + "\n\n"
+ return msg_template
+
+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; deciding whether to reply.")
+ sendToUser, response = getMailResponse(lines, ctx)
+ if response is None:
+ logging.debug("getMailResponse said not to reply, so I won't.")
+ return
+ response.seek(0)
+ d = Deferred()
+ factory = twisted.mail.smtp.SMTPSenderFactory(
+ ctx.smtpFromAddr,
+ sendToUser,
+ response,
+ d)
+ reactor.connectTCP(ctx.smtpServer, ctx.smtpPort, factory)
+ logging.info("Sending reply to %r", sendToUser)
+ return d
+
+def getLocaleFromPlusAddr(address):
+ """See whether the user sent his email to a 'plus' address, for
+ instance to bridgedb+fa@tpo. Plus addresses are the current
+ mechanism to set the reply language
+ """
+ replyLocale = "en"
+ r = '.*(<)?(\w+\+(\w+)@\w+(?:\.\w+)+)(?(1)>)'
+ match = re.match(r, address)
+ if match:
+ replyLocale = match.group(3)
+
+ return replyLocale
+
+def getLocaleFromRequest(request):
+ # See if we did get a request for a certain locale, otherwise fall back
+ # to 'en':
+ # Try evaluating the path /foo first, then check if we got a ?lang=foo
+ default_lang = lang = "en"
+ if len(request.path) > 1:
+ lang = request.path[1:]
+ if lang == default_lang:
+ lang = request.args.get("lang", [default_lang])
+ lang = lang[0]
+ return I18n.getLang(lang)
+
+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 = (cfg.EMAIL_USERNAME or
+ "bridges")
+ # Reject any mail longer than this.
+ self.maximumSize = 32*1024
+ # Use this server for outgoing mail.
+ self.smtpServer = (cfg.EMAIL_SMTP_HOST or "127.0.0.1")
+ self.smtpPort = (cfg.EMAIL_SMTP_PORT or 25)
+ # Use this address in the MAIL FROM line for outgoing mail.
+ self.smtpFromAddr = (cfg.EMAIL_SMTP_FROM_ADDR or
+ "bridges@xxxxxxxxxxxxxx")
+ # Use this address in the "From:" header for outgoing mail.
+ self.fromAddr = (cfg.EMAIL_FROM_ADDR or
+ "bridges@xxxxxxxxxxxxxx")
+ # 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
+
+ # Initialize a gpg context or set to None for backward compatibliity.
+ self.gpgContext = getGPGContext(cfg)
+
+ self.cfg = cfg
+
+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)
+ logging.debug("> %s", line.rstrip("\r\n"))
+ if self.nBytes > self.ctx.maximumSize:
+ self.ignoring = True
+ else:
+ 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
+ def receivedHeader(self, helo, origin, recipients):
+ #XXXX what is this for? what should it be?
+ return "Received: BridgeDB"
+ def validateFrom(self, helo, origin):
+ return origin
+ def validateTo(self, user):
+ """If the local user that was addressed isn't our configured local
+ user or doesn't contain a '+' with a prefix matching the local
+ configured user: Yell
+ """
+ u = user.dest.local
+ # Hasplus? If yes, strip '+foo'
+ idx = u.find('+')
+ if idx != -1:
+ u = u[:idx]
+ if u != self.ctx.username:
+ raise twisted.mail.smtp.SMTPBadRcpt(user)
+ 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()
+
+ def setBridgeDBContext(self, ctx):
+ self.ctx = ctx
+ self.delivery.setBridgeDBContext(ctx)
+
+ def buildProtocol(self, addr):
+ p = twisted.mail.smtp.SMTPFactory.buildProtocol(self, addr)
+ p.delivery = self.delivery
+ 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
+ EMAIL_DOMAIN_RULES
+ dist -- an EmailBasedDistributor object.
+ sched -- an IntervalSchedule object.
+ """
+ ctx = MailContext(cfg, dist, sched)
+ factory = MailFactory()
+ factory.setBridgeDBContext(ctx)
+ ip = cfg.EMAIL_BIND_IP or ""
+ reactor.listenTCP(cfg.EMAIL_PORT, factory, interface=ip)
+ # Set up a LoopingCall to run every 30 minutes and forget old email times.
+ lc = LoopingCall(dist.cleanDatabase)
+ lc.start(1800, now=False)
+ return factory
+
+def composeEmail(fromAddr, clientAddr, subject, body, msgID=False,
+ gpgContext=None):
+
+ f = StringIO()
+ w = MimeWriter.MimeWriter(f)
+ w.addheader("From", fromAddr)
+ w.addheader("To", clientAddr)
+ w.addheader("Message-ID", twisted.mail.smtp.messageid())
+ if not subject.startswith("Re:"): subject = "Re: %s"%subject
+ w.addheader("Subject", subject)
+ if msgID:
+ w.addheader("In-Reply-To", msgID)
+ w.addheader("Date", twisted.mail.smtp.rfc822date())
+ mailbody = w.startbody("text/plain")
+
+ # gpg-clearsign messages
+ if gpgContext:
+ signature = StringIO()
+ plaintext = StringIO(body)
+ sigs = gpgContext.sign(plaintext, signature, gpgme.SIG_MODE_CLEAR)
+ if (len(sigs) != 1):
+ logging.warn('Failed to sign message!')
+ signature.seek(0)
+ [mailbody.write(l) for l in signature]
+ else:
+ mailbody.write(body)
+
+ f.seek(0)
+ logging.debug("Email body:\n%s" % f.read())
+ f.seek(0)
+ return clientAddr, f
+
+def getGPGContext(cfg):
+ """ Returns a gpgme Context() with the signers initialized by the keyfile
+ specified by the option EMAIL_GPG_SIGNING_KEY in bridgedb.conf, or None
+ if the option was not enabled or unable to initialize.
+
+ The key should not be protected by a passphrase.
+ """
+ try:
+ # must have enabled signing and specified a key file
+ if not cfg.EMAIL_GPG_SIGNING_ENABLED or not cfg.EMAIL_GPG_SIGNING_KEY:
+ return None
+ except AttributeError:
+ return None
+
+ try:
+ # import the key
+ keyfile = open(cfg.EMAIL_GPG_SIGNING_KEY)
+ logging.debug("Opened GPG Keyfile %s" % cfg.EMAIL_GPG_SIGNING_KEY)
+ ctx = gpgme.Context()
+ result = ctx.import_(keyfile)
+
+ assert len(result.imports) == 1
+ fingerprint = result.imports[0][0]
+ keyfile.close()
+ logging.debug("GPG Key with fingerprint %s imported" % fingerprint)
+
+ ctx.armor = True
+ ctx.signers = [ctx.get_key(fingerprint)]
+ assert len(ctx.signers) == 1
+
+ # make sure we can sign
+ message = StringIO('Test')
+ signature = StringIO()
+ new_sigs = ctx.sign(message, signature, gpgme.SIG_MODE_CLEAR)
+ assert len(new_sigs) == 1
+
+ # return the ctx
+ return ctx
+
+ except IOError, e:
+ # exit noisily if keyfile not found
+ exit(e)
+ except AssertionError:
+ # exit noisily if key does not pass tests
+ exit('Invalid GPG Signing Key')
diff --git a/lib/bridgedb/HTTPServer.py b/lib/bridgedb/HTTPServer.py
new file mode 100644
index 0000000..df5a031
--- /dev/null
+++ b/lib/bridgedb/HTTPServer.py
@@ -0,0 +1,351 @@
+# BridgeDB by Nick Mathewson.
+# Copyright (c) 2007-2013, The Tor Project, Inc.
+# See LICENSE for licensing information
+
+"""
+This module implements the web (http, https) interfaces to the bridge database.
+"""
+
+import base64
+import gettext
+import logging
+import re
+import textwrap
+import time
+
+from twisted.internet import reactor
+import twisted.web.resource
+import twisted.web.server
+
+import bridgedb.Dist
+import bridgedb.I18n as I18n
+
+from recaptcha.client import captcha
+from bridgedb.Raptcha import Raptcha
+from bridgedb.Filters import filterBridgesByIP6, filterBridgesByIP4
+from bridgedb.Filters import filterBridgesByTransport
+from bridgedb.Filters import filterBridgesByNotBlockedIn
+from ipaddr import IPv4Address, IPv6Address
+from random import randint
+
+try:
+ import GeoIP
+ # GeoIP data object: choose database here
+ # This is the same geoip implementation that pytorctl uses
+ geoip = GeoIP.new(GeoIP.GEOIP_STANDARD)
+ logging.info("GeoIP database loaded")
+except:
+ geoip = None
+ logging.warn("GeoIP database not found")
+
+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, useForwardedHeader=False,
+ includeFingerprints=True,
+ useRecaptcha=False,recaptchaPrivKey='', recaptchaPubKey='',
+ domains=None):
+ """Create a new WebResource.
+ distributor -- an IPBasedDistributor object
+ schedule -- an IntervalSchedule object
+ N -- the number of bridges to hand out per query.
+ """
+ gettext.install("bridgedb", unicode=True)
+ twisted.web.resource.Resource.__init__(self)
+ self.distributor = distributor
+ self.schedule = schedule
+ self.nBridgesToGive = N
+ self.useForwardedHeader = useForwardedHeader
+ self.includeFingerprints = includeFingerprints
+
+ # do not use mutable types as __init__ defaults!
+ if not domains: domains = []
+ self.domains = domains
+
+ # recaptcha options
+ self.useRecaptcha = useRecaptcha
+ self.recaptchaPrivKey = recaptchaPrivKey
+ self.recaptchaPubKey = recaptchaPubKey
+
+ def render_GET(self, request):
+ if self.useRecaptcha:
+ # get a captcha
+ c = Raptcha(self.recaptchaPubKey, self.recaptchaPrivKey)
+ c.get()
+
+ # TODO: this does not work for versions of IE < 8.0
+ imgstr = 'data:image/jpeg;base64,%s' % base64.b64encode(c.image)
+ HTML_CAPTCHA_TEMPLATE = self.buildHTMLMessageTemplateWithCaptcha(
+ getLocaleFromRequest(request), c.challenge, imgstr)
+ return HTML_CAPTCHA_TEMPLATE
+ else:
+ return self.getBridgeRequestAnswer(request)
+
+
+ def render_POST(self, request):
+
+ # check captcha if recaptcha support is enabled
+ if self.useRecaptcha:
+ try:
+ challenge = request.args['recaptcha_challenge_field'][0]
+ response = request.args['recaptcha_response_field'][0]
+
+ except:
+ return self.render_GET(request)
+
+ # generate a random IP for the captcha submission
+ remote_ip = '%d.%d.%d.%d' % (randint(1,255),randint(1,255),
+ randint(1,255),randint(1,255))
+
+ recaptcha_response = captcha.submit(challenge, response,
+ self.recaptchaPrivKey, remote_ip)
+ if recaptcha_response.is_valid:
+ logging.info("Valid recaptcha from %s. Parameters were %r",
+ remote_ip, request.args)
+ else:
+ logging.info("Invalid recaptcha from %s. Parameters were %r",
+ remote_ip, request.args)
+ logging.info("Recaptcha error code: %s", recaptcha_response.error_code)
+ return self.render_GET(request) # redirect back to captcha
+
+ return self.getBridgeRequestAnswer(request)
+
+ def getBridgeRequestAnswer(self, request):
+ """ returns a response to a bridge request """
+
+ interval = self.schedule.getInterval(time.time())
+ bridges = ( )
+ ip = None
+ countryCode = None
+ if self.useForwardedHeader:
+ h = request.getHeader("X-Forwarded-For")
+ if h:
+ ip = h.split(",")[-1].strip()
+ if not bridgedb.Bridges.is_valid_ip(ip):
+ logging.warn("Got weird forwarded-for value %r",h)
+ ip = None
+ else:
+ ip = request.getClientIP()
+
+ if geoip:
+ countryCode = geoip.country_code_by_addr(ip)
+
+ # get locale
+ t = getLocaleFromRequest(request)
+
+ format = request.args.get("format", None)
+ if format and len(format): format = format[0] # choose the first arg
+
+ # do want any options?
+ transport = ipv6 = unblocked = False
+
+ ipv6 = request.args.get("ipv6", False)
+ if ipv6: ipv6 = True # if anything after ?ipv6=
+
+ try:
+ # validate method name
+ transport = re.match('[_a-zA-Z][_a-zA-Z0-9]*',
+ request.args.get("transport")[0]).group()
+ except (TypeError, IndexError, AttributeError):
+ transport = None
+
+ try:
+ unblocked = re.match('[a-zA-Z]{2,4}',
+ request.args.get("unblocked")[0]).group()
+ except (TypeError, IndexError, AttributeError):
+ unblocked = False
+
+ rules = []
+
+ if ip:
+ if ipv6:
+ rules.append(filterBridgesByIP6)
+ addressClass = IPv6Address
+ else:
+ rules.append(filterBridgesByIP4)
+ addressClass = IPv4Address
+
+ if transport:
+ #XXX: A cleaner solution would differentiate between
+ # addresses by protocol rather than have separate lists
+ # Tor to be a transport, and selecting between them.
+ rules = [filterBridgesByTransport(transport, addressClass)]
+
+ if unblocked:
+ rules.append(filterBridgesByNotBlockedIn(unblocked,
+ addressClass, transport))
+
+ bridges = self.distributor.getBridgesForIP(ip, interval,
+ self.nBridgesToGive,
+ countryCode,
+ bridgeFilterRules=rules)
+
+ if bridges:
+ answer = "".join(" %s\n" % b.getConfigLine(
+ includeFingerprint=self.includeFingerprints,
+ addressClass=addressClass,
+ transport=transport,
+ request=bridgedb.Dist.uniformMap(ip)
+ ) for b in bridges)
+ else:
+ answer = t.gettext(I18n.BRIDGEDB_TEXT[7])
+
+ logging.info("Replying to web request from %s. Parameters were %r", ip,
+ request.args)
+ if format == 'plain':
+ request.setHeader("Content-Type", "text/plain")
+ return answer
+ else:
+ HTML_MESSAGE_TEMPLATE = self.buildHTMLMessageTemplate(t)
+ return HTML_MESSAGE_TEMPLATE % answer
+
+ def buildHTMLMessageTemplate(self, t):
+ """DOCDOC"""
+ if self.domains:
+ email_domain_list = "<ul>" \
+ + "".join(("<li>%s</li>"%d for d in self.domains)) + "</ul>"
+ else:
+ email_domain_list = "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[8]) + "</p>"
+ html_msg = "<html><head>"\
+ + "<meta http-equiv=\"Content-Type\" content=\"text/html;"\
+ + " charset=UTF-8\"/>" \
+ + "</head><body>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[0]) \
+ + "<pre id=\"bridges\">" \
+ + "%s" \
+ + "</pre></p>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[1]) + "</p>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[2]) + "</p>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[3]) + "</p>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[4]) + "</p>" \
+ + email_domain_list \
+ + "<hr /><p><a href='?ipv6=true'>" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[20]) + "</a></p>" \
+ + "<p><a href='?transport=obfs2'>" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[21]) + "</a></p>" \
+ + "<form method='GET'>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[22]) + "</p>" \
+ + "<input name='transport'>" \
+ + "<input type='submit' value='" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[23]) +"'>" \
+ + "</form>" \
+ + "</body></html>"
+ return html_msg
+
+ def buildHTMLMessageTemplateWithCaptcha(self, t, challenge, img):
+ """Builds a translated html response with recaptcha"""
+ if self.domains:
+ email_domain_list = "<ul>" \
+ + "".join(("<li>%s</li>"%d for d in self.domains)) + "</ul>"
+ else:
+ email_domain_list = "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[8]) + "</p>"
+
+ recaptchaTemplate = textwrap.dedent("""\
+ <form action="" method="POST">
+ <input type="hidden" name="recaptcha_challenge_field"
+ id="recaptcha_challenge_field"\
+ value="{recaptchaChallengeField}">
+ <img width="300" height="57" alt="{bridgeDBText14}"\
+ src="{recaptchaImgSrc}">
+ <div class="recaptcha_input_area">
+ <label for="recaptcha_response_field">{bridgeDBText12}</label>
+ </div>
+ <div>
+ <input name="recaptcha_response_field"\
+ id="recaptcha_response_field"
+ type="text" autocomplete="off">
+ </div>
+ <div>
+ <input type="submit" name="submit" value="{bridgeDBText13}">
+ </div>
+ </form>
+ """).strip()
+
+ recaptchaTemplate = recaptchaTemplate.format(
+ recaptchaChallengeField=challenge,
+ recaptchaImgSrc=img,
+ bridgeDBText12=t.gettext(I18n.BRIDGEDB_TEXT[13]),
+ bridgeDBText13=t.gettext(I18n.BRIDGEDB_TEXT[14]),
+ bridgeDBText14=t.gettext(I18n.BRIDGEDB_TEXT[15]))
+
+ html_msg = "<html><body>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[1]) + "</p>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[9]) + "</p>" \
+ + "<p>" + recaptchaTemplate + "</p>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[4]) + "</p>" \
+ + email_domain_list \
+ + "<hr /><p><a href='?ipv6=true'>" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[20]) + "</a></p>" \
+ + "<p><a href='?transport=obfs2'>" \
+ + t.gettext(I18n.BRIDGEDB_TEXT[21]) + "</a></p>" \
+ + "<form method='GET'>" \
+ + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[22]) + "</p>" \
+ + "<input name='transport'>" \
+ + "<input name='submit' type='submit'>" \
+ + "</form>" \
+ + "</body></html>"
+ return html_msg
+
+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
+ HTTP_USE_IP_FROM_FORWARDED_HEADER
+ HTTPS_PORT
+ HTTPS_BIND_IP
+ HTTPS_USE_IP_FROM_FORWARDED_HEADER
+ RECAPTCHA_ENABLED
+ RECAPTCHA_PUB_KEY
+ RECAPTCHA_PRIV_KEY
+ dist -- an IPBasedDistributor object.
+ sched -- an IntervalSchedule object.
+ """
+ Site = twisted.web.server.Site
+ site = None
+ if cfg.HTTP_UNENCRYPTED_PORT:
+ ip = cfg.HTTP_UNENCRYPTED_BIND_IP or ""
+ resource = WebResource(dist, sched, cfg.HTTPS_N_BRIDGES_PER_ANSWER,
+ cfg.HTTP_USE_IP_FROM_FORWARDED_HEADER,
+ includeFingerprints=cfg.HTTPS_INCLUDE_FINGERPRINTS,
+ useRecaptcha=cfg.RECAPTCHA_ENABLED,
+ domains=cfg.EMAIL_DOMAINS,
+ recaptchaPrivKey=cfg.RECAPTCHA_PRIV_KEY,
+ recaptchaPubKey=cfg.RECAPTCHA_PUB_KEY)
+ site = Site(resource)
+ 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.HTTPS_BIND_IP or ""
+ factory = DefaultOpenSSLContextFactory(cfg.HTTPS_KEY_FILE,
+ cfg.HTTPS_CERT_FILE)
+ resource = WebResource(dist, sched, cfg.HTTPS_N_BRIDGES_PER_ANSWER,
+ cfg.HTTPS_USE_IP_FROM_FORWARDED_HEADER,
+ includeFingerprints=cfg.HTTPS_INCLUDE_FINGERPRINTS,
+ domains=cfg.EMAIL_DOMAINS,
+ useRecaptcha=cfg.RECAPTCHA_ENABLED,
+ recaptchaPrivKey=cfg.RECAPTCHA_PRIV_KEY,
+ recaptchaPubKey=cfg.RECAPTCHA_PUB_KEY)
+ site = Site(resource)
+ reactor.listenSSL(cfg.HTTPS_PORT, site, factory, interface=ip)
+ return site
+
+def runServers():
+ """Start all the servers that we've configured. Exits when they do."""
+ reactor.run()
+
+def getLocaleFromRequest(request):
+ # See if we did get a request for a certain locale, otherwise fall back
+ # to 'en':
+ # Try evaluating the path /foo first, then check if we got a ?lang=foo
+ default_lang = lang = "en"
+ if len(request.path) > 1:
+ lang = request.path[1:]
+ if lang == default_lang:
+ lang = request.args.get("lang", [default_lang])
+ lang = lang[0]
+ return I18n.getLang(lang)
diff --git a/lib/bridgedb/Server.py b/lib/bridgedb/Server.py
deleted file mode 100644
index 86a402e..0000000
--- a/lib/bridgedb/Server.py
+++ /dev/null
@@ -1,809 +0,0 @@
-# BridgeDB by Nick Mathewson.
-# Copyright (c) 2007-2009, The Tor Project, Inc.
-# See LICENSE for licensing information
-
-"""
-This module implements the web and email interfaces to the bridge database.
-"""
-
-from StringIO import StringIO
-import MimeWriter
-import rfc822
-import time
-import logging
-import gettext
-import re
-
-from zope.interface import implements
-
-from twisted.internet import reactor
-from twisted.internet.defer import Deferred
-from twisted.internet.task import LoopingCall
-import twisted.web.resource
-import twisted.web.server
-import twisted.mail.smtp
-
-import bridgedb.Dist
-import bridgedb.I18n as I18n
-
-import recaptcha.client.captcha as captcha
-from random import randint
-from bridgedb.Raptcha import Raptcha
-import base64
-import textwrap
-
-from ipaddr import IPv4Address, IPv6Address
-from bridgedb.Dist import BadEmail, TooSoonEmail, IgnoreEmail
-
-from bridgedb.Filters import filterBridgesByIP6
-from bridgedb.Filters import filterBridgesByIP4
-from bridgedb.Filters import filterBridgesByTransport
-from bridgedb.Filters import filterBridgesByNotBlockedIn
-
-import gpgme
-
-try:
- import GeoIP
- # GeoIP data object: choose database here
- # This is the same geoip implementation that pytorctl uses
- geoip = GeoIP.new(GeoIP.GEOIP_STANDARD)
- logging.info("GeoIP database loaded")
-except:
- geoip = None
- logging.warn("GeoIP database not found")
-
-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, useForwardedHeader=False,
- includeFingerprints=True,
- useRecaptcha=False,recaptchaPrivKey='', recaptchaPubKey='',
- domains=None):
- """Create a new WebResource.
- distributor -- an IPBasedDistributor object
- schedule -- an IntervalSchedule object
- N -- the number of bridges to hand out per query.
- """
- gettext.install("bridgedb", unicode=True)
- twisted.web.resource.Resource.__init__(self)
- self.distributor = distributor
- self.schedule = schedule
- self.nBridgesToGive = N
- self.useForwardedHeader = useForwardedHeader
- self.includeFingerprints = includeFingerprints
-
- # do not use mutable types as __init__ defaults!
- if not domains: domains = []
- self.domains = domains
-
- # recaptcha options
- self.useRecaptcha = useRecaptcha
- self.recaptchaPrivKey = recaptchaPrivKey
- self.recaptchaPubKey = recaptchaPubKey
-
- def render_GET(self, request):
- if self.useRecaptcha:
- # get a captcha
- c = Raptcha(self.recaptchaPubKey, self.recaptchaPrivKey)
- c.get()
-
- # TODO: this does not work for versions of IE < 8.0
- imgstr = 'data:image/jpeg;base64,%s' % base64.b64encode(c.image)
- HTML_CAPTCHA_TEMPLATE = self.buildHTMLMessageTemplateWithCaptcha(
- getLocaleFromRequest(request), c.challenge, imgstr)
- return HTML_CAPTCHA_TEMPLATE
- else:
- return self.getBridgeRequestAnswer(request)
-
-
- def render_POST(self, request):
-
- # check captcha if recaptcha support is enabled
- if self.useRecaptcha:
- try:
- challenge = request.args['recaptcha_challenge_field'][0]
- response = request.args['recaptcha_response_field'][0]
-
- except:
- return self.render_GET(request)
-
- # generate a random IP for the captcha submission
- remote_ip = '%d.%d.%d.%d' % (randint(1,255),randint(1,255),
- randint(1,255),randint(1,255))
-
- recaptcha_response = captcha.submit(challenge, response,
- self.recaptchaPrivKey, remote_ip)
- if recaptcha_response.is_valid:
- logging.info("Valid recaptcha from %s. Parameters were %r",
- remote_ip, request.args)
- else:
- logging.info("Invalid recaptcha from %s. Parameters were %r",
- remote_ip, request.args)
- logging.info("Recaptcha error code: %s", recaptcha_response.error_code)
- return self.render_GET(request) # redirect back to captcha
-
- return self.getBridgeRequestAnswer(request)
-
- def getBridgeRequestAnswer(self, request):
- """ returns a response to a bridge request """
-
- interval = self.schedule.getInterval(time.time())
- bridges = ( )
- ip = None
- countryCode = None
- if self.useForwardedHeader:
- h = request.getHeader("X-Forwarded-For")
- if h:
- ip = h.split(",")[-1].strip()
- if not bridgedb.Bridges.is_valid_ip(ip):
- logging.warn("Got weird forwarded-for value %r",h)
- ip = None
- else:
- ip = request.getClientIP()
-
- if geoip:
- countryCode = geoip.country_code_by_addr(ip)
-
- # get locale
- t = getLocaleFromRequest(request)
-
- format = request.args.get("format", None)
- if format and len(format): format = format[0] # choose the first arg
-
- # do want any options?
- transport = ipv6 = unblocked = False
-
- ipv6 = request.args.get("ipv6", False)
- if ipv6: ipv6 = True # if anything after ?ipv6=
-
- try:
- # validate method name
- transport = re.match('[_a-zA-Z][_a-zA-Z0-9]*',
- request.args.get("transport")[0]).group()
- except (TypeError, IndexError, AttributeError):
- transport = None
-
- try:
- unblocked = re.match('[a-zA-Z]{2,4}',
- request.args.get("unblocked")[0]).group()
- except (TypeError, IndexError, AttributeError):
- unblocked = False
-
- rules = []
-
- if ip:
- if ipv6:
- rules.append(filterBridgesByIP6)
- addressClass = IPv6Address
- else:
- rules.append(filterBridgesByIP4)
- addressClass = IPv4Address
-
- if transport:
- #XXX: A cleaner solution would differentiate between
- # addresses by protocol rather than have separate lists
- # Tor to be a transport, and selecting between them.
- rules = [filterBridgesByTransport(transport, addressClass)]
-
- if unblocked:
- rules.append(filterBridgesByNotBlockedIn(unblocked,
- addressClass, transport))
-
- bridges = self.distributor.getBridgesForIP(ip, interval,
- self.nBridgesToGive,
- countryCode,
- bridgeFilterRules=rules)
-
- if bridges:
- answer = "".join(" %s\n" % b.getConfigLine(
- includeFingerprint=self.includeFingerprints,
- addressClass=addressClass,
- transport=transport,
- request=bridgedb.Dist.uniformMap(ip)
- ) for b in bridges)
- else:
- answer = t.gettext(I18n.BRIDGEDB_TEXT[7])
-
- logging.info("Replying to web request from %s. Parameters were %r", ip,
- request.args)
- if format == 'plain':
- request.setHeader("Content-Type", "text/plain")
- return answer
- else:
- HTML_MESSAGE_TEMPLATE = self.buildHTMLMessageTemplate(t)
- return HTML_MESSAGE_TEMPLATE % answer
-
- def buildHTMLMessageTemplate(self, t):
- """DOCDOC"""
- if self.domains:
- email_domain_list = "<ul>" \
- + "".join(("<li>%s</li>"%d for d in self.domains)) + "</ul>"
- else:
- email_domain_list = "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[8]) + "</p>"
- html_msg = "<html><head>"\
- + "<meta http-equiv=\"Content-Type\" content=\"text/html;"\
- + " charset=UTF-8\"/>" \
- + "</head><body>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[0]) \
- + "<pre id=\"bridges\">" \
- + "%s" \
- + "</pre></p>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[1]) + "</p>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[2]) + "</p>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[3]) + "</p>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[4]) + "</p>" \
- + email_domain_list \
- + "<hr /><p><a href='?ipv6=true'>" \
- + t.gettext(I18n.BRIDGEDB_TEXT[20]) + "</a></p>" \
- + "<p><a href='?transport=obfs2'>" \
- + t.gettext(I18n.BRIDGEDB_TEXT[21]) + "</a></p>" \
- + "<form method='GET'>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[22]) + "</p>" \
- + "<input name='transport'>" \
- + "<input type='submit' value='" \
- + t.gettext(I18n.BRIDGEDB_TEXT[23]) +"'>" \
- + "</form>" \
- + "</body></html>"
- return html_msg
-
- def buildHTMLMessageTemplateWithCaptcha(self, t, challenge, img):
- """Builds a translated html response with recaptcha"""
- if self.domains:
- email_domain_list = "<ul>" \
- + "".join(("<li>%s</li>"%d for d in self.domains)) + "</ul>"
- else:
- email_domain_list = "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[8]) + "</p>"
-
- recaptchaTemplate = textwrap.dedent("""\
- <form action="" method="POST">
- <input type="hidden" name="recaptcha_challenge_field"
- id="recaptcha_challenge_field"\
- value="{recaptchaChallengeField}">
- <img width="300" height="57" alt="{bridgeDBText14}"\
- src="{recaptchaImgSrc}">
- <div class="recaptcha_input_area">
- <label for="recaptcha_response_field">{bridgeDBText12}</label>
- </div>
- <div>
- <input name="recaptcha_response_field"\
- id="recaptcha_response_field"
- type="text" autocomplete="off">
- </div>
- <div>
- <input type="submit" name="submit" value="{bridgeDBText13}">
- </div>
- </form>
- """).strip()
-
- recaptchaTemplate = recaptchaTemplate.format(
- recaptchaChallengeField=challenge,
- recaptchaImgSrc=img,
- bridgeDBText12=t.gettext(I18n.BRIDGEDB_TEXT[13]),
- bridgeDBText13=t.gettext(I18n.BRIDGEDB_TEXT[14]),
- bridgeDBText14=t.gettext(I18n.BRIDGEDB_TEXT[15]))
-
- html_msg = "<html><body>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[1]) + "</p>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[9]) + "</p>" \
- + "<p>" + recaptchaTemplate + "</p>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[4]) + "</p>" \
- + email_domain_list \
- + "<hr /><p><a href='?ipv6=true'>" \
- + t.gettext(I18n.BRIDGEDB_TEXT[20]) + "</a></p>" \
- + "<p><a href='?transport=obfs2'>" \
- + t.gettext(I18n.BRIDGEDB_TEXT[21]) + "</a></p>" \
- + "<form method='GET'>" \
- + "<p>" + t.gettext(I18n.BRIDGEDB_TEXT[22]) + "</p>" \
- + "<input name='transport'>" \
- + "<input name='submit' type='submit'>" \
- + "</form>" \
- + "</body></html>"
- return html_msg
-
-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
- HTTP_USE_IP_FROM_FORWARDED_HEADER
- HTTPS_PORT
- HTTPS_BIND_IP
- HTTPS_USE_IP_FROM_FORWARDED_HEADER
- RECAPTCHA_ENABLED
- RECAPTCHA_PUB_KEY
- RECAPTCHA_PRIV_KEY
- dist -- an IPBasedDistributor object.
- sched -- an IntervalSchedule object.
- """
- Site = twisted.web.server.Site
- site = None
- if cfg.HTTP_UNENCRYPTED_PORT:
- ip = cfg.HTTP_UNENCRYPTED_BIND_IP or ""
- resource = WebResource(dist, sched, cfg.HTTPS_N_BRIDGES_PER_ANSWER,
- cfg.HTTP_USE_IP_FROM_FORWARDED_HEADER,
- includeFingerprints=cfg.HTTPS_INCLUDE_FINGERPRINTS,
- useRecaptcha=cfg.RECAPTCHA_ENABLED,
- domains=cfg.EMAIL_DOMAINS,
- recaptchaPrivKey=cfg.RECAPTCHA_PRIV_KEY,
- recaptchaPubKey=cfg.RECAPTCHA_PUB_KEY)
- site = Site(resource)
- 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.HTTPS_BIND_IP or ""
- factory = DefaultOpenSSLContextFactory(cfg.HTTPS_KEY_FILE,
- cfg.HTTPS_CERT_FILE)
- resource = WebResource(dist, sched, cfg.HTTPS_N_BRIDGES_PER_ANSWER,
- cfg.HTTPS_USE_IP_FROM_FORWARDED_HEADER,
- includeFingerprints=cfg.HTTPS_INCLUDE_FINGERPRINTS,
- domains=cfg.EMAIL_DOMAINS,
- useRecaptcha=cfg.RECAPTCHA_ENABLED,
- recaptchaPrivKey=cfg.RECAPTCHA_PRIV_KEY,
- recaptchaPubKey=cfg.RECAPTCHA_PUB_KEY)
- site = Site(resource)
- 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
- def readline(self):
- try :
- line = self.lines[self.idx]
- self.idx += 1
- return line
- except IndexError:
- return ""
-
-def getBridgeDBEmailAddrFromList(ctx, address_list):
- """Loop through a list of (full name, email address) pairs and look up our
- mail address. If our address isn't found (which can't happen), return
- the default ctx from address so we can keep on working.
- """
- email = ctx.fromAddr
- for _, address in address_list:
- # Strip the @torproject.org part from the address
- idx = address.find('@')
- if idx != -1:
- username = address[:idx]
- # See if the user looks familiar. We do a 'find' instead
- # of compare because we might have a '+' address here
- if username.find(ctx.username) != -1:
- email = address
- return email
-
-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)
- if not subject: subject = "[no subject]"
- clientFromAddr = msg.getaddr("From")
- clientSenderAddr = msg.getaddr("Sender")
- # RFC822 requires at least one 'To' address
- clientToList = msg.getaddrlist("To")
- clientToaddr = getBridgeDBEmailAddrFromList(ctx, clientToList)
- msgID = msg.getheader("Message-ID", None)
- if clientSenderAddr and clientSenderAddr[1]:
- clientAddr = clientSenderAddr[1]
- elif clientFromAddr and clientFromAddr[1]:
- clientAddr = clientFromAddr[1]
- else:
- logging.info("No From or Sender header on incoming mail.")
- return None,None
-
- # Look up the locale part in the 'To:' address, if there is one and get
- # the appropriate Translation object
- lang = getLocaleFromPlusAddr(clientToaddr)
- t = I18n.getLang(lang)
-
- try:
- _, addrdomain = bridgedb.Dist.extractAddrSpec(clientAddr.lower())
- except BadEmail:
- logging.info("Ignoring bad address on incoming email.")
- return None,None
- if not addrdomain:
- logging.info("Couldn't parse domain from %r", clientAddr)
- if addrdomain and ctx.cfg.EMAIL_DOMAIN_MAP:
- addrdomain = ctx.cfg.EMAIL_DOMAIN_MAP.get(addrdomain, addrdomain)
- if addrdomain not in ctx.cfg.EMAIL_DOMAINS:
- logging.info("Unrecognized email domain %r", addrdomain)
- return None,None
- rules = ctx.cfg.EMAIL_DOMAIN_RULES.get(addrdomain, [])
- if 'dkim' in rules:
- # getheader() returns the last of a given kind of header; we want
- # to get the first, so we use getheaders() instead.
- dkimHeaders = msg.getheaders("X-DKIM-Authentication-Results")
- dkimHeader = "<no header>"
- if dkimHeaders: dkimHeader = dkimHeaders[0]
- if not dkimHeader.startswith("pass"):
- logging.info("Got a bad dkim header (%r) on an incoming mail; "
- "rejecting it.", dkimHeader)
- return None, None
-
- # Was the magic string included
- #for ln in lines:
- # if ln.strip().lower() in ("get bridges", "subject: get bridges"):
- # break
- #else:
- # logging.info("Got a mail from %r with no bridge request; dropping",
- # clientAddr)
- # return None,None
-
- # Figure out which bridges to send
- unblocked = transport = ipv6 = skippedheaders = False
- bridgeFilterRules = []
- addressClass = None
- for ln in lines:
- # ignore all lines before the subject header
- if "subject" in ln.strip().lower():
- skippedheaders = True
- if not skippedheaders: continue
-
- if "ipv6" in ln.strip().lower():
- ipv6 = True
- if "transport" in ln.strip().lower():
- try:
- transport = re.search("transport ([_a-zA-Z][_a-zA-Z0-9]*)",
- ln).group(1).strip()
- except (TypeError, AttributeError):
- transport = None
- logging.debug("Got request for transport: %s" % transport)
- if "unblocked" in ln.strip().lower():
- try:
- unblocked = re.search("unblocked ([a-zA-Z]{2,4})",
- ln).group(1).strip()
- except (TypeError, AttributeError):
- transport = None
-
- if ipv6:
- bridgeFilterRules.append(filterBridgesByIP6)
- addressClass = IPv6Address
- else:
- bridgeFilterRules.append(filterBridgesByIP4)
- addressClass = IPv4Address
-
- if transport:
- bridgeFilterRules = [filterBridgesByTransport(transport, addressClass)]
-
- if unblocked:
- rules.append(filterBridgesByNotBlockedIn(unblocked,
- addressClass, transport))
-
- try:
- interval = ctx.schedule.getInterval(time.time())
- bridges = ctx.distributor.getBridgesForEmail(clientAddr,
- interval, ctx.N,
- countryCode=None,
- bridgeFilterRules=bridgeFilterRules)
-
- # Handle rate limited email
- except TooSoonEmail, e:
- logging.info("Got a mail too frequently; warning %r: %s.",
- clientAddr, e)
-
- # Compose a warning email
- # MAX_EMAIL_RATE is in seconds, convert to hours
- body = buildSpamWarningTemplate(t) % (bridgedb.Dist.MAX_EMAIL_RATE / 3600)
- return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID,
- gpgContext=ctx.gpgContext)
-
- except IgnoreEmail, e:
- logging.info("Got a mail too frequently; ignoring %r: %s.",
- clientAddr, e)
- return None, None
-
- except BadEmail, e:
- logging.info("Got a mail from a bad email address %r: %s.",
- clientAddr, e)
- return None, None
-
- if bridges:
- with_fp = ctx.cfg.EMAIL_INCLUDE_FINGERPRINTS
- answer = "".join(" %s\n" %b.getConfigLine(
- includeFingerprint=with_fp,
- addressClass=addressClass,
- transport=transport,
- request=clientAddr
- ) for b in bridges)
- else:
- answer = "(no bridges currently available)"
-
- body = buildMessageTemplate(t) % answer
- # Generate the message.
- return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID,
- gpgContext=ctx.gpgContext)
-
-def buildMessageTemplate(t):
- msg_template = t.gettext(I18n.BRIDGEDB_TEXT[5]) + "\n\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[0]) + "\n\n" \
- + "%s\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[1]) + "\n\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[2]) + "\n\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[3]) + "\n\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[17])+ "\n\n"
- # list supported commands, e.g. ipv6, transport
- msg_template = msg_template \
- + " " + t.gettext(I18n.BRIDGEDB_TEXT[18])+ "\n" \
- + " " + t.gettext(I18n.BRIDGEDB_TEXT[19])+ "\n\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[6]) + "\n\n"
- return msg_template
-
-def buildSpamWarningTemplate(t):
- msg_template = t.gettext(I18n.BRIDGEDB_TEXT[5]) + "\n\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[10]) + "\n\n" \
- + "%s " \
- + t.gettext(I18n.BRIDGEDB_TEXT[11]) + "\n\n" \
- + t.gettext(I18n.BRIDGEDB_TEXT[12]) + "\n\n"
- return msg_template
-
-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; deciding whether to reply.")
- sendToUser, response = getMailResponse(lines, ctx)
- if response is None:
- logging.debug("getMailResponse said not to reply, so I won't.")
- return
- response.seek(0)
- d = Deferred()
- factory = twisted.mail.smtp.SMTPSenderFactory(
- ctx.smtpFromAddr,
- sendToUser,
- response,
- d)
- reactor.connectTCP(ctx.smtpServer, ctx.smtpPort, factory)
- logging.info("Sending reply to %r", sendToUser)
- return d
-
-def getLocaleFromPlusAddr(address):
- """See whether the user sent his email to a 'plus' address, for
- instance to bridgedb+fa@tpo. Plus addresses are the current
- mechanism to set the reply language
- """
- replyLocale = "en"
- r = '.*(<)?(\w+\+(\w+)@\w+(?:\.\w+)+)(?(1)>)'
- match = re.match(r, address)
- if match:
- replyLocale = match.group(3)
-
- return replyLocale
-
-def getLocaleFromRequest(request):
- # See if we did get a request for a certain locale, otherwise fall back
- # to 'en':
- # Try evaluating the path /foo first, then check if we got a ?lang=foo
- default_lang = lang = "en"
- if len(request.path) > 1:
- lang = request.path[1:]
- if lang == default_lang:
- lang = request.args.get("lang", [default_lang])
- lang = lang[0]
- return I18n.getLang(lang)
-
-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 = (cfg.EMAIL_USERNAME or
- "bridges")
- # Reject any mail longer than this.
- self.maximumSize = 32*1024
- # Use this server for outgoing mail.
- self.smtpServer = (cfg.EMAIL_SMTP_HOST or "127.0.0.1")
- self.smtpPort = (cfg.EMAIL_SMTP_PORT or 25)
- # Use this address in the MAIL FROM line for outgoing mail.
- self.smtpFromAddr = (cfg.EMAIL_SMTP_FROM_ADDR or
- "bridges@xxxxxxxxxxxxxx")
- # Use this address in the "From:" header for outgoing mail.
- self.fromAddr = (cfg.EMAIL_FROM_ADDR or
- "bridges@xxxxxxxxxxxxxx")
- # 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
-
- # Initialize a gpg context or set to None for backward compatibliity.
- self.gpgContext = getGPGContext(cfg)
-
- self.cfg = cfg
-
-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)
- logging.debug("> %s", line.rstrip("\r\n"))
- if self.nBytes > self.ctx.maximumSize:
- self.ignoring = True
- else:
- 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
- def receivedHeader(self, helo, origin, recipients):
- #XXXX what is this for? what should it be?
- return "Received: BridgeDB"
- def validateFrom(self, helo, origin):
- return origin
- def validateTo(self, user):
- """If the local user that was addressed isn't our configured local
- user or doesn't contain a '+' with a prefix matching the local
- configured user: Yell
- """
- u = user.dest.local
- # Hasplus? If yes, strip '+foo'
- idx = u.find('+')
- if idx != -1:
- u = u[:idx]
- if u != self.ctx.username:
- raise twisted.mail.smtp.SMTPBadRcpt(user)
- 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()
-
- def setBridgeDBContext(self, ctx):
- self.ctx = ctx
- self.delivery.setBridgeDBContext(ctx)
-
- def buildProtocol(self, addr):
- p = twisted.mail.smtp.SMTPFactory.buildProtocol(self, addr)
- p.delivery = self.delivery
- 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
- EMAIL_DOMAIN_RULES
- dist -- an EmailBasedDistributor object.
- sched -- an IntervalSchedule object.
- """
- ctx = MailContext(cfg, dist, sched)
- factory = MailFactory()
- factory.setBridgeDBContext(ctx)
- ip = cfg.EMAIL_BIND_IP or ""
- reactor.listenTCP(cfg.EMAIL_PORT, factory, interface=ip)
- # Set up a LoopingCall to run every 30 minutes and forget old email times.
- lc = LoopingCall(dist.cleanDatabase)
- lc.start(1800, now=False)
- return factory
-
-def runServers():
- """Start all the servers that we've configured. Exits when they do."""
- reactor.run()
-
-def getLocaleFromRequest(request):
- # See if we did get a request for a certain locale, otherwise fall back
- # to 'en':
- # Try evaluating the path /foo first, then check if we got a ?lang=foo
- default_lang = lang = "en"
- if len(request.path) > 1:
- lang = request.path[1:]
- if lang == default_lang:
- lang = request.args.get("lang", [default_lang])
- lang = lang[0]
- return I18n.getLang(lang)
-
-def composeEmail(fromAddr, clientAddr, subject, body, msgID=False,
- gpgContext=None):
-
- f = StringIO()
- w = MimeWriter.MimeWriter(f)
- w.addheader("From", fromAddr)
- w.addheader("To", clientAddr)
- w.addheader("Message-ID", twisted.mail.smtp.messageid())
- if not subject.startswith("Re:"): subject = "Re: %s"%subject
- w.addheader("Subject", subject)
- if msgID:
- w.addheader("In-Reply-To", msgID)
- w.addheader("Date", twisted.mail.smtp.rfc822date())
- mailbody = w.startbody("text/plain")
-
- # gpg-clearsign messages
- if gpgContext:
- signature = StringIO()
- plaintext = StringIO(body)
- sigs = gpgContext.sign(plaintext, signature, gpgme.SIG_MODE_CLEAR)
- if (len(sigs) != 1):
- logging.warn('Failed to sign message!')
- signature.seek(0)
- [mailbody.write(l) for l in signature]
- else:
- mailbody.write(body)
-
- f.seek(0)
- logging.debug("Email body:\n%s" % f.read())
- f.seek(0)
- return clientAddr, f
-
-def getGPGContext(cfg):
- """ Returns a gpgme Context() with the signers initialized by the keyfile
- specified by the option EMAIL_GPG_SIGNING_KEY in bridgedb.conf, or None
- if the option was not enabled or unable to initialize.
-
- The key should not be protected by a passphrase.
- """
- try:
- # must have enabled signing and specified a key file
- if not cfg.EMAIL_GPG_SIGNING_ENABLED or not cfg.EMAIL_GPG_SIGNING_KEY:
- return None
- except AttributeError:
- return None
-
- try:
- # import the key
- keyfile = open(cfg.EMAIL_GPG_SIGNING_KEY)
- logging.debug("Opened GPG Keyfile %s" % cfg.EMAIL_GPG_SIGNING_KEY)
- ctx = gpgme.Context()
- result = ctx.import_(keyfile)
-
- assert len(result.imports) == 1
- fingerprint = result.imports[0][0]
- keyfile.close()
- logging.debug("GPG Key with fingerprint %s imported" % fingerprint)
-
- ctx.armor = True
- ctx.signers = [ctx.get_key(fingerprint)]
- assert len(ctx.signers) == 1
-
- # make sure we can sign
- message = StringIO('Test')
- signature = StringIO()
- new_sigs = ctx.sign(message, signature, gpgme.SIG_MODE_CLEAR)
- assert len(new_sigs) == 1
-
- # return the ctx
- return ctx
-
- except IOError, e:
- # exit noisily if keyfile not found
- exit(e)
- except AssertionError:
- # exit noisily if key does not pass tests
- exit('Invalid GPG Signing Key')
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits