commit 55f36f523dae951e2c0e57cdfdc84e3a78ba7cbe Author: Isis Lovecruft isis@torproject.org Date: Tue Apr 21 21:41:20 2015 +0000
Move bridgedb.Dist.EmailBasedDistributor → bridgedb.email.distributor. --- doc/sphinx/source/bridgedb.Dist.rst | 8 - doc/sphinx/source/bridgedb.email.rst | 1 + doc/sphinx/source/bridgedb.rst | 1 - doc/sphinx/source/conf.py | 2 +- lib/bridgedb/Dist.py | 186 ---------------------- lib/bridgedb/Main.py | 9 +- lib/bridgedb/email/autoresponder.py | 8 +- lib/bridgedb/email/distributor.py | 209 +++++++++++++++++++++++++ lib/bridgedb/email/request.py | 6 +- lib/bridgedb/email/server.py | 4 +- lib/bridgedb/email/templates.py | 2 +- lib/bridgedb/parse/addr.py | 7 +- lib/bridgedb/persistent.py | 4 +- lib/bridgedb/test/email_helpers.py | 14 +- lib/bridgedb/test/legacy_Tests.py | 43 +---- lib/bridgedb/test/test_email_autoresponder.py | 2 +- lib/bridgedb/test/test_email_distributor.py | 93 +++++++++++ lib/bridgedb/test/test_email_server.py | 6 +- 18 files changed, 340 insertions(+), 265 deletions(-)
diff --git a/doc/sphinx/source/bridgedb.Dist.rst b/doc/sphinx/source/bridgedb.Dist.rst deleted file mode 100644 index 995a7ac..0000000 --- a/doc/sphinx/source/bridgedb.Dist.rst +++ /dev/null @@ -1,8 +0,0 @@ -bridgedb.Dist ----------------- - -.. automodule:: bridgedb.Dist - :members: - :undoc-members: - :private-members: - :show-inheritance: diff --git a/doc/sphinx/source/bridgedb.email.rst b/doc/sphinx/source/bridgedb.email.rst index ba9b4a9..020b614 100644 --- a/doc/sphinx/source/bridgedb.email.rst +++ b/doc/sphinx/source/bridgedb.email.rst @@ -8,6 +8,7 @@ bridgedb.email
.. automodule:: bridgedb.email.__init__ .. automodule:: bridgedb.email.autoresponder +.. automodule:: bridgedb.email.distributor .. automodule:: bridgedb.email.dkim .. automodule:: bridgedb.email.request .. automodule:: bridgedb.email.server diff --git a/doc/sphinx/source/bridgedb.rst b/doc/sphinx/source/bridgedb.rst index 7b1fef7..207313c 100644 --- a/doc/sphinx/source/bridgedb.rst +++ b/doc/sphinx/source/bridgedb.rst @@ -13,7 +13,6 @@ BridgeDB Package and Module Documentation bridgedb.captcha bridgedb.configure bridgedb.crypto - bridgedb.Dist bridgedb.email bridgedb.filters bridgedb.geo diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py index 5038abb..cab614f 100644 --- a/doc/sphinx/source/conf.py +++ b/doc/sphinx/source/conf.py @@ -33,9 +33,9 @@ import bridgedb.captcha import bridgedb.Bridges import bridgedb.Bucket import bridgedb.crypto -import bridgedb.Dist import bridgedb.email import bridgedb.email.autoresponder +import bridgedb.email.distributor import bridgedb.email.dkim import bridgedb.email.request import bridgedb.email.server diff --git a/lib/bridgedb/Dist.py b/lib/bridgedb/Dist.py deleted file mode 100644 index 1be705f..0000000 --- a/lib/bridgedb/Dist.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_Dist -*- -# -# This file is part of BridgeDB, a Tor bridge distribution system. -# -# :authors: Nick Mathewson -# Isis Lovecruft 0xA3ADB67A2CDB8B35 isis@torproject.org -# Matthew Finkel 0x017DD169EA793BE2 sysrqb@torproject.org -# :copyright: (c) 2013-2015, Isis Lovecruft -# (c) 2013-2015, Matthew Finkel -# (c) 2007-2015, The Tor Project, Inc. -# :license: 3-Clause BSD, see LICENSE for licensing information - -"""This module has functions to decide which bridges to hand out to whom.""" - -import logging -import time - -import bridgedb.Storage - -from bridgedb.Bridges import BridgeRing -from bridgedb.Bridges import FilteredBridgeSplitter -from bridgedb.crypto import getHMAC -from bridgedb.crypto import getHMACFunc -from bridgedb.distribute import Distributor -from bridgedb.filters import byFilters -from bridgedb.filters import byIPv4 -from bridgedb.filters import byIPv6 -from bridgedb.filters import bySubring -from bridgedb.parse import addr - - -MAX_EMAIL_RATE = 3*3600 - -class IgnoreEmail(addr.BadEmail): - """Raised when we get requests from this address after rate warning.""" - -class TooSoonEmail(addr.BadEmail): - """Raised when we got a request from this address too recently.""" - -class EmailRequestedHelp(Exception): - """Raised when a client has emailed requesting help.""" - -class EmailRequestedKey(Exception): - """Raised when an incoming email requested a copy of our GnuPG keys.""" - - -class EmailBasedDistributor(Distributor): - """Object that hands out bridges based on the email address of an incoming - request and the current time period. - - :type hashring: :class:`~bridgedb.Bridges.BridgeRing` - :ivar hashring: A hashring to hold all the bridges we hand out. - """ - - def __init__(self, key, domainmap, domainrules, - answerParameters=None, whitelist=None): - """Create a bridge distributor which uses email. - - :type emailHmac: callable - :param emailHmac: An hmac function used to order email addresses - within a ring. See :func:`~bridgedb.crypto.getHMACFunc`. - :param dict domainmap: A map from lowercase domains that we support - mail from to their canonical forms. See `EMAIL_DOMAIN_MAP` option - in `bridgedb.conf`. - :param domainrules: DOCDOC - :param answerParameters: DOCDOC - :type whitelist: dict or ``None`` - :param whitelist: A dictionary that maps whitelisted email addresses - to GnuPG fingerprints. - """ - super(EmailBasedDistributor, self).__init__(key) - - key1 = getHMAC(key, "Map-Addresses-To-Ring") - self.emailHmac = getHMACFunc(key1, hex=False) - - key2 = getHMAC(key, "Order-Bridges-In-Ring") - # XXXX clear the store when the period rolls over! - self.domainmap = domainmap - self.domainrules = domainrules - self.whitelist = whitelist or dict() - self.answerParameters = answerParameters - - #XXX cache options not implemented - self.hashring = FilteredBridgeSplitter(key2, max_cached_rings=5) - self.name = "Email" - - def bridgesPerResponse(self, hashring=None): - return super(EmailBasedDistributor, self).bridgesPerResponse(hashring) - - def insert(self, bridge): - """Assign a bridge to this distributor.""" - self.hashring.insert(bridge) - - def getBridges(self, bridgeRequest, interval): - """Return a list of bridges to give to a user. - - :type bridgeRequest: :class:`~bridgedb.email.request.EmailBridgeRequest` - :param bridgeRequest: A :class:`~bridgedb.bridgerequest.BridgeRequestBase` - with the :data:`~bridgedb.bridgerequest.BridgeRequestBase.client` - attribute set to a string containing the client's full, canonicalized - email address. - :param interval: The time period when we got this request. This can be - any string, so long as it changes with every period. - """ - # All checks on the email address, such as checks for whitelisting and - # canonicalization of domain name, are done in - # :meth:`bridgedb.email.autoresponder.getMailTo` and - # :meth:`bridgedb.email.autoresponder.SMTPAutoresponder.runChecks`. - if (not bridgeRequest.client) or (bridgeRequest.client == 'default'): - raise addr.BadEmail( - ("%s distributor can't get bridges for invalid email email " - " address: %s") % (self.name, bridgeRequest.client)) - - logging.info("Attempting to get bridges for %s..." % bridgeRequest.client) - - now = time.time() - - with bridgedb.Storage.getDB() as db: - wasWarned = db.getWarnedEmail(bridgeRequest.client) - lastSaw = db.getEmailTime(bridgeRequest.client) - if lastSaw is not None: - if bridgeRequest.client in self.whitelist.keys(): - logging.info(("Whitelisted email address %s was last seen " - "%d seconds ago.") - % (bridgeRequest.client, now - lastSaw)) - elif (lastSaw + MAX_EMAIL_RATE) >= now: - wait = (lastSaw + MAX_EMAIL_RATE) - now - logging.info("Client %s must wait another %d seconds." - % (bridgeRequest.client, wait)) - if wasWarned: - raise IgnoreEmail("Client was warned.", - bridgeRequest.client) - else: - logging.info("Sending duplicate request warning.") - db.setWarnedEmail(bridgeRequest.client, True, now) - db.commit() - raise TooSoonEmail("Must wait %d seconds" % wait, - bridgeRequest.client) - # warning period is over - elif wasWarned: - db.setWarnedEmail(bridgeRequest.client, False) - - pos = self.emailHmac("<%s>%s" % (interval, bridgeRequest.client)) - - ring = None - ruleset = frozenset(bridgeRequest.filters) - if ruleset in self.hashring.filterRings.keys(): - logging.debug("Cache hit %s" % ruleset) - _, ring = self.hashring.filterRings[ruleset] - else: - # cache miss, add new ring - logging.debug("Cache miss %s" % ruleset) - - # add new ring - key1 = getHMAC(self.key, "Order-Bridges-In-Ring") - ring = BridgeRing(key1, self.answerParameters) - self.hashring.addRing(ring, ruleset, byFilters(ruleset), - populate_from=self.hashring.bridges) - - returnNum = self.bridgesPerResponse(ring) - result = ring.getBridges(pos, returnNum) - - db.setEmailTime(bridgeRequest.client, now) - db.commit() - - return result - - def cleanDatabase(self): - with bridgedb.Storage.getDB() as db: - try: - db.cleanEmailedBridges(time.time() - MAX_EMAIL_RATE) - db.cleanWarnedEmails(time.time() - MAX_EMAIL_RATE) - except: - db.rollback() - raise - else: - db.commit() - - def prepopulateRings(self): - # populate all rings (for dumping assignments and testing) - for filterFn in [byIPv4, byIPv6]: - ruleset = frozenset([filterFn]) - key1 = getHMAC(self.key, "Order-Bridges-In-Ring") - ring = BridgeRing(key1, self.answerParameters) - self.hashring.addRing(ring, ruleset, byFilters([filterFn]), - populate_from=self.hashring.bridges) diff --git a/lib/bridgedb/Main.py b/lib/bridgedb/Main.py index 3a15145..b281d21 100644 --- a/lib/bridgedb/Main.py +++ b/lib/bridgedb/Main.py @@ -30,13 +30,13 @@ from bridgedb.bridges import ServerDescriptorDigestMismatch from bridgedb.bridges import ServerDescriptorWithoutNetworkstatus from bridgedb.bridges import Bridge from bridgedb.configure import loadConfig +from bridgedb.email.distributor import EmailDistributor from bridgedb.https.distributor import HTTPSDistributor from bridgedb.parse import descriptors
import bridgedb.Storage
from bridgedb import Bridges -from bridgedb import Dist from bridgedb.Stability import updateBridgeHistory
@@ -197,7 +197,7 @@ def createBridgeRings(cfg, proxyList, key): :rtype: tuple :returns: A BridgeSplitter hashring, an :class:`~bridgedb.https.distributor.HTTPSDistributor` or None, and an - EmailBasedDistributor or None. + :class:`~bridgedb.email.distributor.EmailDistributor` or None. """ # Create a BridgeSplitter to assign the bridges to the different # distributors. @@ -222,7 +222,7 @@ def createBridgeRings(cfg, proxyList, key): # As appropriate, create an email-based distributor. if cfg.EMAIL_DIST and cfg.EMAIL_SHARE: logging.debug("Setting up Email Distributor...") - emailDistributor = Dist.EmailBasedDistributor( + emailDistributor = EmailDistributor( crypto.getHMAC(key, "Email-Dist-Key"), cfg.EMAIL_DOMAIN_MAP.copy(), cfg.EMAIL_DOMAIN_RULES.copy(), @@ -333,7 +333,8 @@ def run(options, reactor=reactor): known open proxies. :ivar ipDistributor: A :class:`~bridgedb.https.distributor.HTTPSDistributor`. - :ivar emailDistributor: A :class:`Dist.EmailBasedDistributor`. + :ivar emailDistributor: A + :class:`~bridgedb.email.distributor.EmailDistributor`. :ivar dict tasks: A dictionary of ``{name: task}``, where name is a string to associate with the ``task``, and ``task`` is some scheduled event, repetitive or otherwise, for the :class:`reactor diff --git a/lib/bridgedb/email/autoresponder.py b/lib/bridgedb/email/autoresponder.py index b98717f..ad63bfd 100644 --- a/lib/bridgedb/email/autoresponder.py +++ b/lib/bridgedb/email/autoresponder.py @@ -47,13 +47,13 @@ from twisted.python import failure
from bridgedb import safelog from bridgedb.crypto import NEW_BUFFER_INTERFACE -from bridgedb.Dist import EmailRequestedHelp -from bridgedb.Dist import EmailRequestedKey -from bridgedb.Dist import TooSoonEmail -from bridgedb.Dist import IgnoreEmail from bridgedb.email import dkim from bridgedb.email import request from bridgedb.email import templates +from bridgedb.email.distributor import EmailRequestedHelp +from bridgedb.email.distributor import EmailRequestedKey +from bridgedb.email.distributor import TooSoonEmail +from bridgedb.email.distributor import IgnoreEmail from bridgedb.parse import addr from bridgedb.parse.addr import canonicalizeEmailDomain from bridgedb.util import levenshteinDistance diff --git a/lib/bridgedb/email/distributor.py b/lib/bridgedb/email/distributor.py new file mode 100644 index 0000000..b73c082 --- /dev/null +++ b/lib/bridgedb/email/distributor.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_email_distributor -*- +# +# This file is part of BridgeDB, a Tor bridge distribution system. +# +# :authors: Nick Mathewson +# Isis Lovecruft 0xA3ADB67A2CDB8B35 isis@torproject.org +# Matthew Finkel 0x017DD169EA793BE2 sysrqb@torproject.org +# :copyright: (c) 2013-2015, Isis Lovecruft +# (c) 2013-2015, Matthew Finkel +# (c) 2007-2015, The Tor Project, Inc. +# :license: see LICENSE for licensing information + +"""A :class:`~bridgedb.distribute.Distributor` which hands out +:class:`bridges <bridgedb.bridges.Bridge>` to clients via an email interface. +""" + +import logging +import time + +import bridgedb.Storage + +from bridgedb.Bridges import BridgeRing +from bridgedb.Bridges import FilteredBridgeSplitter +from bridgedb.crypto import getHMAC +from bridgedb.crypto import getHMACFunc +from bridgedb.distribute import Distributor +from bridgedb.filters import byFilters +from bridgedb.filters import byIPv4 +from bridgedb.filters import byIPv6 +from bridgedb.filters import bySubring +from bridgedb.parse import addr + + +#: The minimum amount of time (in seconds) which must pass before a client who +#: has previously been given an email response must wait before being eligible +#: to receive another response. +MAX_EMAIL_RATE = 3 * 3600 + + +class IgnoreEmail(addr.BadEmail): + """Raised when we get requests from this address after rate warning.""" + + +class TooSoonEmail(addr.BadEmail): + """Raised when we got a request from this address too recently.""" + + +class EmailRequestedHelp(Exception): + """Raised when a client has emailed requesting help.""" + + +class EmailRequestedKey(Exception): + """Raised when an incoming email requested a copy of our GnuPG keys.""" + + +class EmailDistributor(Distributor): + """Object that hands out bridges based on the email address of an incoming + request and the current time period. + + :type hashring: :class:`~bridgedb.Bridges.BridgeRing` + :ivar hashring: A hashring to hold all the bridges we hand out. + """ + + #: The minimum amount of time (in seconds) which must pass before a client + #: who has previously been given an email response must wait before being + #: eligible to receive another response. + emailRateMax = MAX_EMAIL_RATE + + def __init__(self, key, domainmap, domainrules, + answerParameters=None, whitelist=None): + """Create a bridge distributor which uses email. + + :type emailHmac: callable + :param emailHmac: An hmac function used to order email addresses + within a ring. See :func:`~bridgedb.crypto.getHMACFunc`. + :param dict domainmap: A map from lowercase domains that we support + mail from to their canonical forms. See `EMAIL_DOMAIN_MAP` option + in `bridgedb.conf`. + :param domainrules: DOCDOC + :param answerParameters: DOCDOC + :type whitelist: dict or ``None`` + :param whitelist: A dictionary that maps whitelisted email addresses + to GnuPG fingerprints. + """ + super(EmailDistributor, self).__init__(key) + + self.domainmap = domainmap + self.domainrules = domainrules + self.whitelist = whitelist or dict() + self.answerParameters = answerParameters + + key1 = getHMAC(key, "Map-Addresses-To-Ring") + key2 = getHMAC(key, "Order-Bridges-In-Ring") + + self.emailHmac = getHMACFunc(key1, hex=False) + #XXX cache options not implemented + self.hashring = FilteredBridgeSplitter(key2, max_cached_rings=5) + + self.name = "Email" + + def bridgesPerResponse(self, hashring=None): + return super(EmailDistributor, self).bridgesPerResponse(hashring) + + def getBridges(self, bridgeRequest, interval): + """Return a list of bridges to give to a user. + + .. hint:: All checks on the email address (which should be stored in + the ``bridgeRequest.client`` attribute), such as checks for + whitelisting and canonicalization of domain name, are done in + :meth:`bridgedb.email.autoresponder.getMailTo` and + :meth:`bridgedb.email.autoresponder.SMTPAutoresponder.runChecks`. + + :type bridgeRequest: + :class:`~bridgedb.email.request.EmailBridgeRequest` + :param bridgeRequest: A + :class:`~bridgedb.bridgerequest.BridgeRequestBase` with the + :data:`~bridgedb.bridgerequest.BridgeRequestBase.client` attribute + set to a string containing the client's full, canonicalized email + address. + :param interval: The time period when we got this request. This can be + any string, so long as it changes with every period. + """ + if (not bridgeRequest.client) or (bridgeRequest.client == 'default'): + raise addr.BadEmail( + ("%s distributor can't get bridges for invalid email address: " + "%s") % (self.name, bridgeRequest.client), bridgeRequest.client) + + logging.info("Attempting to get bridges for %s..." % bridgeRequest.client) + + now = time.time() + + with bridgedb.Storage.getDB() as db: + wasWarned = db.getWarnedEmail(bridgeRequest.client) + lastSaw = db.getEmailTime(bridgeRequest.client) + if lastSaw is not None: + if bridgeRequest.client in self.whitelist: + logging.info( + "Whitelisted address %s was last seen %d seconds ago." + % (bridgeRequest.client, now - lastSaw)) + elif (lastSaw + self.emailRateMax) >= now: + wait = (lastSaw + self.emailRateMax) - now + logging.info("Client %s must wait another %d seconds." + % (bridgeRequest.client, wait)) + if wasWarned: + raise IgnoreEmail( + "Client %s was warned." % bridgeRequest.client, + bridgeRequest.client) + else: + logging.info("Sending duplicate request warning.") + db.setWarnedEmail(bridgeRequest.client, True, now) + db.commit() + raise TooSoonEmail("Must wait %d seconds" % wait, + bridgeRequest.client) + # warning period is over + elif wasWarned: + db.setWarnedEmail(bridgeRequest.client, False) + + pos = self.emailHmac("<%s>%s" % (interval, bridgeRequest.client)) + + ring = None + filtres = frozenset(bridgeRequest.filters) + if filtres in self.hashring.filterRings: + logging.debug("Cache hit %s" % filtres) + _, ring = self.hashring.filterRings[filtres] + else: + logging.debug("Cache miss %s" % filtres) + key = getHMAC(self.key, "Order-Bridges-In-Ring") + ring = BridgeRing(key, self.answerParameters) + self.hashring.addRing(ring, filtres, byFilters(filtres), + populate_from=self.hashring.bridges) + + returnNum = self.bridgesPerResponse(ring) + result = ring.getBridges(pos, returnNum) + + db.setEmailTime(bridgeRequest.client, now) + db.commit() + + return result + + def cleanDatabase(self): + """Clear all emailed response and warning times from the database.""" + logging.info(("Cleaning all response and warning times for the %s " + "distributor from the database...") % self.name) + with bridgedb.Storage.getDB() as db: + try: + db.cleanEmailedBridges(time.time() - self.emailRateMax) + db.cleanWarnedEmails(time.time() - self.emailRateMax) + except: + db.rollback() + raise + else: + db.commit() + + def prepopulateRings(self): + """Prepopulate this distributor's hashrings and subhashrings with + bridges. + """ + logging.info("Prepopulating %s distributor hashrings..." % self.name) + + for filterFn in [byIPv4, byIPv6]: + ruleset = frozenset([filterFn]) + key = getHMAC(self.key, "Order-Bridges-In-Ring") + ring = BridgeRing(key, self.answerParameters) + self.hashring.addRing(ring, ruleset, byFilters([filterFn]), + populate_from=self.hashring.bridges) + + # Since prepopulateRings is called every half hour when the bridge + # descriptors are re-parsed, we should clean the database then. + self.cleanDatabase() diff --git a/lib/bridgedb/email/request.py b/lib/bridgedb/email/request.py index 32446ac..50fd32c 100644 --- a/lib/bridgedb/email/request.py +++ b/lib/bridgedb/email/request.py @@ -40,8 +40,8 @@ import logging import re
from bridgedb import bridgerequest -from bridgedb.Dist import EmailRequestedHelp -from bridgedb.Dist import EmailRequestedKey +from bridgedb.email.distributor import EmailRequestedHelp +from bridgedb.email.distributor import EmailRequestedKey
#: A regular expression for matching the Pluggable Transport method TYPE in @@ -105,7 +105,7 @@ class EmailBridgeRequest(bridgerequest.BridgeRequestBase):
def __init__(self): """Process a new bridge request received through the - :class:`~bridgedb.Dist.EmailBasedDistributor`. + :class:`~bridgedb.email.distributor.EmailDistributor`. """ super(EmailBridgeRequest, self).__init__() self._wantsKey = False diff --git a/lib/bridgedb/email/server.py b/lib/bridgedb/email/server.py index 8034d35..736c3f6 100644 --- a/lib/bridgedb/email/server.py +++ b/lib/bridgedb/email/server.py @@ -103,7 +103,7 @@ class MailServerContext(object): """Create a context for storing configs for email bridge distribution.
:type config: :class:`bridgedb.persistent.Conf` - :type distributor: :class:`bridgedb.Dist.EmailBasedDistributor` + :type distributor: :class:`~bridgedb.email.distributor.EmailDistributor` :param distributor: The distributor will handle getting the correct bridges (or none) for a client for us. :type schedule: :class:`bridgedb.schedule.ScheduledInterval` @@ -466,7 +466,7 @@ def addServer(config, distributor):
:type config: :class:`bridgedb.configure.Conf` :param config: A configuration object. - :type distributor: :class:`bridgedb.Dist.EmailBasedDistributor` + :type distributor: :class:`bridgedb.email.distributor.EmailDistributor` :param dist: A distributor which will handle database interactions, and will decide which bridges to give to who and when. """ diff --git a/lib/bridgedb/email/templates.py b/lib/bridgedb/email/templates.py index 4db4e3c..6998716 100644 --- a/lib/bridgedb/email/templates.py +++ b/lib/bridgedb/email/templates.py @@ -30,7 +30,7 @@ import os from datetime import datetime
from bridgedb import strings -from bridgedb.Dist import MAX_EMAIL_RATE +from bridgedb.email.distributor import MAX_EMAIL_RATE
def addCommands(template): diff --git a/lib/bridgedb/parse/addr.py b/lib/bridgedb/parse/addr.py index b3ad680..cd512d1 100644 --- a/lib/bridgedb/parse/addr.py +++ b/lib/bridgedb/parse/addr.py @@ -217,7 +217,7 @@ def canonicalizeEmailDomain(domain, domainmap): :param str domain: The domain portion of an email address to validate. It will be checked that it is one of the domains allowed to email requests for bridges to the - :class:`~bridgedb.Dist.EmailBasedDistributor`. + :class:`~bridgedb.email.distributor.EmailDistributor`. :param dict domainmap: A map of permitted alternate domains (in lowercase) to their canonical domain names (in lowercase). This can be configured with the ``EMAIL_DOMAIN_MAP`` option in ``bridgedb.conf``, for @@ -441,8 +441,9 @@ def normalizeEmail(emailaddr, domainmap, domainrules, ignorePlus=True):
The email address, **emailaddr**, will be parsed and validated, and then checked that it originated from one of the domains allowed to email - requests for bridges to the :class:`~bridgedb.Dist.EmailBasedDistributor` - via the :func:`canonicaliseEmailDomain` function. + requests for bridges to the + :class:`~bridgedb.email.distributor.EmailDistributor` via the + :func:`canonicaliseEmailDomain` function.
:param str emailaddr: An email address to normalise. :param dict domainmap: A map of permitted alternate domains (in lowercase) diff --git a/lib/bridgedb/persistent.py b/lib/bridgedb/persistent.py index 0853c1e..22673dd 100644 --- a/lib/bridgedb/persistent.py +++ b/lib/bridgedb/persistent.py @@ -24,8 +24,8 @@ from twisted.python.reflect import safe_repr from twisted.spread import jelly
from bridgedb import Bridges -from bridgedb import Dist from bridgedb import filters +from bridgedb.email import distributor as emailDistributor from bridgedb.https import distributor as httpsDistributor from bridgedb.configure import Conf #from bridgedb.proxy import ProxySet @@ -35,7 +35,7 @@ _state = None #: Types and classes which are allowed to be jellied: _security = jelly.SecurityOptions() #_security.allowInstancesOf(ProxySet) -_security.allowModules(filters, Bridges, Dist, httpsDistributor) +_security.allowModules(filters, Bridges, emailDistributor, httpsDistributor)
class MissingState(Exception): diff --git a/lib/bridgedb/test/email_helpers.py b/lib/bridgedb/test/email_helpers.py index 20aee57..14c86f4 100644 --- a/lib/bridgedb/test/email_helpers.py +++ b/lib/bridgedb/test/email_helpers.py @@ -13,9 +13,9 @@
import io
-from bridgedb.Dist import IgnoreEmail -from bridgedb.Dist import TooSoonEmail from bridgedb.persistent import Conf +from bridgedb.email.distributor import IgnoreEmail +from bridgedb.email.distributor import TooSoonEmail from bridgedb.email.server import MailServerContext from bridgedb.schedule import Unscheduled from bridgedb.test import util @@ -120,8 +120,8 @@ def _createMailServerContext(config=None, distributor=None):
class DummyEmailDistributor(object): - """A mocked :class:`bridgedb.Dist.EmailBasedDistributor` which is used to - test :class:`bridgedb.EmailServer`. + """A mocked :class:`bridgedb.email.distributor.EmailDistributor` which is used + to test :class:`bridgedb.EmailServer`. """
_bridgesPerResponseMin = 3 @@ -144,9 +144,9 @@ class DummyEmailDistributor(object):
class DummyEmailDistributorWithState(DummyEmailDistributor): - """A mocked :class:`bridgedb.Dist.EmailBasedDistributor` which raises - :exc:`bridgedb.Dist.TooSoonEmail` on the second email and - :exc:`bridgedb.Dist.IgnoreEmail` on the third. + """A mocked :class:`bridgedb.email.distributor.EmailDistributor` which raises + :exc:`bridgedb.email.distributor.TooSoonEmail` on the second email and + :exc:`bridgedb.email.distributor.IgnoreEmail` on the third.
Note that the state tracking is done in a really dumb way. For example, we currently don't consider requests for help text or GnuPG keys to be a diff --git a/lib/bridgedb/test/legacy_Tests.py b/lib/bridgedb/test/legacy_Tests.py index c0a9ff0..40f7ae4 100644 --- a/lib/bridgedb/test/legacy_Tests.py +++ b/lib/bridgedb/test/legacy_Tests.py @@ -19,7 +19,6 @@ from datetime import datetime
import bridgedb.Bridges import bridgedb.Main -import bridgedb.Dist import bridgedb.schedule import bridgedb.Storage import re @@ -27,6 +26,9 @@ import ipaddr
from bridgedb.Stability import BridgeHistory
+from bridgedb.email.distributor import EmailDistributor +from bridgedb.email.distributor import IgnoreEmail +from bridgedb.email.distributor import TooSoonEmail from bridgedb.parse import addr from bridgedb.test.util import bracketIPv6 from bridgedb.test.util import randomIP @@ -94,42 +96,6 @@ def fakeBridge6(orport=8080, running=True, stable=True, or_addresses=False, return b
-class EmailBridgeDistTests(unittest.TestCase): - def setUp(self): - self.fd, self.fname = tempfile.mkstemp() - self.db = bridgedb.Storage.Database(self.fname) - bridgedb.Storage.setDB(self.db) - self.cur = self.db._conn.cursor() - - def tearDown(self): - self.db.close() - os.close(self.fd) - os.unlink(self.fname) - - def testEmailRateLimit(self): - db = self.db - EMAIL_DOMAIN_MAP = {'example.com':'example.com'} - d = bridgedb.Dist.EmailBasedDistributor( - "Foo", - {'example.com': 'example.com', - 'dkim.example.com': 'dkim.example.com'}, - {'example.com': [], 'dkim.example.com': ['dkim']}) - for _ in xrange(256): - d.insert(fakeBridge()) - d.getBridges('abc@example.com', 1) - self.assertRaises(bridgedb.Dist.TooSoonEmail, - d.getBridges, 'abc@example.com', 1) - self.assertRaises(bridgedb.Dist.IgnoreEmail, - d.getBridges, 'abc@example.com', 1) - - def testUnsupportedDomain(self): - db = self.db - self.assertRaises(bridgedb.parse.addr.UnsupportedDomain, - bridgedb.parse.addr.normalizeEmail, - 'bad@email.com', - {'example.com':'example.com'}, - {'example.com':[]}) -
class SQLStorageTests(unittest.TestCase): def setUp(self): @@ -358,8 +324,7 @@ class BridgeStabilityTests(unittest.TestCase): def testSuite(): suite = unittest.TestSuite() loader = unittest.TestLoader() - - for klass in [SQLStorageTests, EmailBridgeDistTests, BridgeStabilityTests]: + for klass in [SQLStorageTests, BridgeStabilityTests]: suite.addTest(loader.loadTestsFromTestCase(klass)) return suite
diff --git a/lib/bridgedb/test/test_email_autoresponder.py b/lib/bridgedb/test/test_email_autoresponder.py index 2895802..98302ca 100644 --- a/lib/bridgedb/test/test_email_autoresponder.py +++ b/lib/bridgedb/test/test_email_autoresponder.py @@ -25,7 +25,7 @@ from twisted.test import proto_helpers
from bridgedb.email import autoresponder from bridgedb.email.server import SMTPMessage -from bridgedb.Dist import TooSoonEmail +from bridgedb.email.distributor import TooSoonEmail from bridgedb.test.email_helpers import _createConfig from bridgedb.test.email_helpers import _createMailServerContext from bridgedb.test.email_helpers import DummyEmailDistributorWithState diff --git a/lib/bridgedb/test/test_email_distributor.py b/lib/bridgedb/test/test_email_distributor.py new file mode 100644 index 0000000..96eb306 --- /dev/null +++ b/lib/bridgedb/test/test_email_distributor.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# This file is part of BridgeDB, a Tor bridge distribution system. +# +# :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 isis@torproject.org +# :copyright: (c) 2013-2015 Isis Lovecruft +# (c) 2007-2015, The Tor Project, Inc. +# :license: see included LICENSE for information + +"""Tests for :mod:`bridgedb.email.distributor`.""" + +from __future__ import print_function + +import logging +import tempfile +import os + +from twisted.trial import unittest + +import bridgedb.Storage + +from bridgedb.bridges import Bridge +from bridgedb.email.distributor import EmailDistributor +from bridgedb.email.distributor import IgnoreEmail +from bridgedb.email.distributor import TooSoonEmail +from bridgedb.email.request import EmailBridgeRequest +from bridgedb.parse.addr import UnsupportedDomain +from bridgedb.parse.addr import normalizeEmail +from bridgedb.test.util import generateFakeBridges + +logging.disable(50) + + +BRIDGES = generateFakeBridges() + + +class EmailDistributorTests(unittest.TestCase): + """Tests for :class:`bridgedb.email.distributor.EmailDistributor`.""" + + def setUp(self): + self.fd, self.fname = tempfile.mkstemp() + bridgedb.Storage.initializeDBLock() + self.db = bridgedb.Storage.openDatabase(self.fname) + bridgedb.Storage.setDBFilename(self.fname) + self.cur = self.db.cursor() + self.db.close() + + self.bridges = BRIDGES + self.key = 'aQpeOFIj8q20s98awfoiq23rpOIjFaqpEWFoij1X' + self.domainmap = { + 'example.com': 'example.com', + 'dkim.example.com': 'dkim.example.com', + } + self.domainrules = { + 'example.com': ['ignore_dots'], + 'dkim.example.com': ['dkim', 'ignore_dots'] + } + + def tearDown(self): + self.db.close() + os.close(self.fd) + os.unlink(self.fname) + + def makeClientRequest(self, clientEmailAddress): + bridgeRequest = EmailBridgeRequest() + bridgeRequest.client = clientEmailAddress + bridgeRequest.isValid(True) + bridgeRequest.generateFilters() + return bridgeRequest + + def test_EmailDistributor_rate_limit(self): + """A client's first email should return bridges. The second should + return a warning, and the third should receive no response. + """ + dist = EmailDistributor(self.key, self.domainmap, self.domainrules) + [dist.hashring.insert(bridge) for bridge in self.bridges] + + bridgeRequest = self.makeClientRequest('abc@example.com') + + # The first request should get a response with bridges + bridges = dist.getBridges(bridgeRequest, 1) + self.assertGreater(len(bridges), 0) + [self.assertIsInstance(b, Bridge) for b in bridges] + self.assertEqual(len(bridges), 3) + + # The second gets a warning, and the third is ignored + self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest, 1) + self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest, 1) + + def test_EmailDistributor_unsupported_domain(self): + """An unsupported domain should raise an UnsupportedDomain exception.""" + self.assertRaises(UnsupportedDomain, normalizeEmail, + 'bad@email.com', self.domainmap, self.domainrules) diff --git a/lib/bridgedb/test/test_email_server.py b/lib/bridgedb/test/test_email_server.py index 561c14c..9c4fabb 100644 --- a/lib/bridgedb/test/test_email_server.py +++ b/lib/bridgedb/test/test_email_server.py @@ -31,9 +31,9 @@ from twisted.trial import unittest
from zope.interface import implementedBy
-from bridgedb.Dist import EmailBasedDistributor -from bridgedb.Dist import TooSoonEmail from bridgedb.email import server +from bridgedb.email.distributor import EmailDistributor +from bridgedb.email.distributor import TooSoonEmail from bridgedb.parse.addr import BadEmail from bridgedb.schedule import Unscheduled from bridgedb.test import util @@ -504,7 +504,7 @@ class EmailServerServiceTests(SMTPTestCaseMixin, unittest.TestCase): """Unittests for :func:`bridgedb.email.server.addServer`."""
def setUp(self): - """Create a server.MailServerContext and EmailBasedDistributor.""" + """Create a MailServerContext and EmailDistributor.""" self.config = _createConfig() self.context = _createMailServerContext(self.config) self.smtpFromAddr = self.context.smtpFromAddr # 'bridges@localhost'
tor-commits@lists.torproject.org