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 setUp(self): self.bridges = copy.deepcopy(util.generateFakeBridges()) self.fd, self.fname = tempfile.mkstemp(suffix=".sqlite", dir=os.getcwd()) bridgedb.Storage.initializeDBLock() self.db = bridgedb.Storage.openDatabase(self.fname) bridgedb.Storage.setDBFilename(self.fname) key = 'fake-hmac-key' self.splitter = Bridges.BridgeSplitter(key) ringParams = Bridges.BridgeRingParameters(needPorts=[(443, 1)], needFlags=[("Stable", 1)]) self.https_distributor = HTTPSDistributor(4, crypto.getHMAC( key, "HTTPS-IP-Dist-Key"), None, answerParameters=ringParams) self.moat_distributor = MoatDistributor(4, crypto.getHMAC( key, "Moat-Dist-Key"), None, answerParameters=ringParams) self.unallocated_distributor = Bridges.UnallocatedHolder() self.splitter.addRing(self.https_distributor.hashring, "https", p=10) self.splitter.addRing(self.moat_distributor.hashring, "moat", p=10) self.splitter.addRing(self.unallocated_distributor, "unallocated", p=10) self.https_ring = self.splitter.ringsByName.get("https") self.moat_ring = self.splitter.ringsByName.get("moat") self.unallocated_ring = self.splitter.ringsByName.get("unallocated")
def __init__(self, key, domainmap, domainrules, answerParameters=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 """ 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.answerParameters = answerParameters #XXX cache options not implemented self.splitter = bridgedb.Bridges.FilteredBridgeSplitter( key2, max_cached_rings=5) self.setDistributorName('Email')
def __init__(self, areaMapper, nClusters, key, ipCategories=None, answerParameters=None): """Create a Distributor that decides which bridges to distribute based upon the client's IP address and the current time. :type areaMapper: callable :param areaMapper: A function that maps IP addresses arbitrarily to strings, such that addresses which map to identical strings are considered to be in the same "area" (for some arbitrary definition of "area"). See :func:`bridgedb.Dist.uniformMap` for an example. :param integer nClusters: The number of clusters to group IP addresses into. Note that if PROXY_LIST_FILES is set in bridgedb.conf, then the actual number of clusters is one higher than ``nClusters``, because the set of known open proxies constitutes its own category. DOCDOC What exactly does a cluster *do*? :param bytes key: The master HMAC key for this distributor. All added bridges are HMACed with this key in order to place them into the hashrings. :type ipCategories: iterable or None :param ipCategories: DOCDOC :type answerParameters: :class:`bridgedb.Bridges.BridgeRingParameters` :param answerParameters: A mechanism for ensuring that the set of bridges that this distributor answers a client with fit certain parameters, i.e. that an answer has "at least two obfsproxy bridges" or "at least one bridge on port 443", etc. """ self.areaMapper = areaMapper self.nClusters = nClusters self.answerParameters = answerParameters if not ipCategories: ipCategories = [] if not answerParameters: answerParameters = [] self.rings = [] self.categories = [] for c in ipCategories: self.categories.append(c) key2 = getHMAC(key, "Assign-Bridges-To-Rings") key3 = getHMAC(key, "Order-Areas-In-Rings") self.areaOrderHmac = getHMACFunc(key3, hex=False) key4 = getHMAC(key, "Assign-Areas-To-Rings") self.areaClusterHmac = getHMACFunc(key4, hex=True) # add splitter and cache the default rings # plus leave room for dynamic filters # # XXX Why is the "extra room" hardcoded to be 5? Shouldn't it be some # fraction of the number of clusters/categories? --isis ring_cache_size = self.nClusters + len(ipCategories) + 5 self.splitter = bridgedb.Bridges.FilteredBridgeSplitter( key2, max_cached_rings=ring_cache_size) logging.debug("Added splitter %s to IPBasedDistributor." % self.splitter.__class__) self.setDistributorName('HTTPS')
def createBridgeRings(cfg, proxyList, key): """Create the bridge distributors defined by the config file :type cfg: :class:`Conf` :param cfg: The current configuration, including any in-memory settings (i.e. settings whose values were not obtained from the config file, but were set via a function somewhere) :type proxyList: :class:`~bridgedb.proxy.ProxySet` :param proxyList: The container for the IP addresses of any currently known open proxies. :param bytes key: Hashring master key :rtype: tuple :returns: A BridgeSplitter hashring, an :class:`~bridgedb.https.distributor.HTTPSDistributor` or None, and an :class:`~bridgedb.email.distributor.EmailDistributor` or None. """ # Create a BridgeSplitter to assign the bridges to the different # distributors. hashring = Bridges.BridgeSplitter(crypto.getHMAC(key, "Hashring-Key")) logging.debug("Created hashring: %r" % hashring) # Create ring parameters. ringParams = Bridges.BridgeRingParameters(needPorts=cfg.FORCE_PORTS, needFlags=cfg.FORCE_FLAGS) emailDistributor = ipDistributor = None # As appropriate, create an IP-based distributor. if cfg.HTTPS_DIST and cfg.HTTPS_SHARE: logging.debug("Setting up HTTPS Distributor...") ipDistributor = HTTPSDistributor(cfg.N_IP_CLUSTERS, crypto.getHMAC( key, "HTTPS-IP-Dist-Key"), proxyList, answerParameters=ringParams) hashring.addRing(ipDistributor.hashring, "https", cfg.HTTPS_SHARE) # As appropriate, create an email-based distributor. if cfg.EMAIL_DIST and cfg.EMAIL_SHARE: logging.debug("Setting up Email Distributor...") emailDistributor = EmailDistributor( crypto.getHMAC(key, "Email-Dist-Key"), cfg.EMAIL_DOMAIN_MAP.copy(), cfg.EMAIL_DOMAIN_RULES.copy(), answerParameters=ringParams, whitelist=cfg.EMAIL_WHITELIST.copy()) hashring.addRing(emailDistributor.hashring, "email", cfg.EMAIL_SHARE) # As appropriate, tell the hashring to leave some bridges unallocated. if cfg.RESERVED_SHARE: hashring.addRing(Bridges.UnallocatedHolder(), "unallocated", cfg.RESERVED_SHARE) # Add pseudo distributors to hashring for pseudoRing in cfg.FILE_BUCKETS.keys(): hashring.addPseudoRing(pseudoRing) return hashring, emailDistributor, ipDistributor
def __init__(self, totalSubrings, key, proxies=None, answerParameters=None): """Create a Distributor that decides which bridges to distribute based upon the client's IP address and the current time. :param int totalSubrings: The number of subhashrings to group clients into. Note that if ``PROXY_LIST_FILES`` is set in bridgedb.conf, then the actual number of clusters is one higher than ``totalSubrings``, because the set of all known open proxies is given its own subhashring. :param bytes key: The master HMAC key for this distributor. All added bridges are HMACed with this key in order to place them into the hashrings. :type proxies: :class:`~bridgedb.proxy.ProxySet` :param proxies: A :class:`bridgedb.proxy.ProxySet` containing known Tor Exit relays and other known proxies. These will constitute the extra cluster, and any client requesting bridges from one of these **proxies** will be distributed bridges from a separate subhashring that is specific to Tor/proxy users. :type answerParameters: :class:`bridgedb.bridgerings.BridgeRingParameters` :param answerParameters: A mechanism for ensuring that the set of bridges that this distributor answers a client with fit certain parameters, i.e. that an answer has "at least two obfsproxy bridges" or "at least one bridge on port 443", etc. """ super(HTTPSDistributor, self).__init__(key) self.totalSubrings = totalSubrings self.answerParameters = answerParameters if proxies: logging.info("Added known proxies to HTTPS distributor...") self.proxies = proxies self.totalSubrings += 1 self.proxySubring = self.totalSubrings else: logging.warn("No known proxies were added to HTTPS distributor!") self.proxies = proxy.ProxySet() self.proxySubring = 0 self.ringCacheSize = self.totalSubrings * 3 key2 = getHMAC(key, "Assign-Bridges-To-Rings") key3 = getHMAC(key, "Order-Areas-In-Rings") key4 = getHMAC(key, "Assign-Areas-To-Rings") self._clientToPositionHMAC = getHMACFunc(key3, hex=False) self._subnetToSubringHMAC = getHMACFunc(key4, hex=True) self.hashring = FilteredBridgeSplitter(key2, self.ringCacheSize) self.name = 'HTTPS' logging.debug("Added %s to %s distributor." % (self.hashring.__class__.__name__, self.name))
def __init__(self, totalSubrings, key, proxies=None, answerParameters=None): """Create a Distributor that decides which bridges to distribute based upon the client's IP address and the current time. :param int totalSubrings: The number of subhashrings to group clients into. Note that if ``PROXY_LIST_FILES`` is set in bridgedb.conf, then the actual number of clusters is one higher than ``totalSubrings``, because the set of all known open proxies is given its own subhashring. :param bytes key: The master HMAC key for this distributor. All added bridges are HMACed with this key in order to place them into the hashrings. :type proxies: :class:`~bridgedb.proxy.ProxySet` :param proxies: A :class:`bridgedb.proxy.ProxySet` containing known Tor Exit relays and other known proxies. These will constitute the extra cluster, and any client requesting bridges from one of these **proxies** will be distributed bridges from a separate subhashring that is specific to Tor/proxy users. :type answerParameters: :class:`bridgedb.Bridges.BridgeRingParameters` :param answerParameters: A mechanism for ensuring that the set of bridges that this distributor answers a client with fit certain parameters, i.e. that an answer has "at least two obfsproxy bridges" or "at least one bridge on port 443", etc. """ super(HTTPSDistributor, self).__init__(key) self.totalSubrings = totalSubrings self.answerParameters = answerParameters if proxies: logging.info("Added known proxies to HTTPS distributor...") self.proxies = proxies self.totalSubrings += 1 self.proxySubring = self.totalSubrings else: logging.warn("No known proxies were added to HTTPS distributor!") self.proxies = proxy.ProxySet() self.proxySubring = 0 self.ringCacheSize = self.totalSubrings * 3 key2 = getHMAC(key, "Assign-Bridges-To-Rings") key3 = getHMAC(key, "Order-Areas-In-Rings") key4 = getHMAC(key, "Assign-Areas-To-Rings") self._clientToPositionHMAC = getHMACFunc(key3, hex=False) self._subnetToSubringHMAC = getHMACFunc(key4, hex=True) self.hashring = FilteredBridgeSplitter(key2, self.ringCacheSize) self.name = 'HTTPS' logging.debug("Added %s to %s distributor." % (self.hashring.__class__.__name__, self.name))
def check(cls, challenge, solution, secretKey, hmacKey): """Check a client's CAPTCHA **solution** against the **challenge**. :param str challenge: The contents of the ``'captcha_challenge_field'`` HTTP form field. :param str solution: The client's proposed solution to the CAPTCHA that they were presented with. :param str secretkey: A PKCS#1 OAEP-padded, private RSA key, used for verifying the client's solution to the CAPTCHA. :param bytes hmacKey: A private key for generating HMACs. :rtype: bool :returns: True if the CAPTCHA solution was correct. """ validHMAC = False if not solution: return validHMAC logging.debug("Checking CAPTCHA solution %r against challenge %r" % (solution, challenge)) try: decoded = urlsafe_b64decode(challenge) hmac, original = decoded.split(';', 1) verified = crypto.getHMAC(hmacKey, original) validHMAC = verified == hmac except Exception as error: logging.exception(error) finally: if validHMAC: decrypted = secretKey.decrypt(original) if solution == decrypted: return True return False
def getCaptchaImage(self, request): """Get a random CAPTCHA image from our **captchaDir**. Creates a :class:`~bridgedb.captcha.GimpCaptcha`, and calls its :meth:`~bridgedb.captcha.GimpCaptcha.get` method to return a random CAPTCHA and challenge string. :type request: :api:`twisted.web.http.Request` :param request: A client's initial request for some other resource which is protected by this one (i.e. protected by a CAPTCHA). :returns: A 2-tuple of ``(image, challenge)``, where:: - ``image`` is a string holding a binary, JPEG-encoded image. - ``challenge`` is a unique string associated with the request. """ # Create a new HMAC key, specific to requests from this client: clientIP = self.getClientIP(request) clientHMACKey = crypto.getHMAC(self.hmacKey, clientIP) capt = captcha.GimpCaptcha(self.publicKey, self.secretKey, clientHMACKey, self.captchaDir) try: capt.get() except captcha.GimpCaptchaError as error: logging.error(error) except Exception as error: # pragma: no cover logging.error("Unhandled error while retrieving Gimp captcha!") logging.exception(error) return (capt.image, capt.challenge)
def checkSolution(self, request): """Process a solved CAPTCHA via :meth:`bridgedb.captcha.GimpCaptcha.check`. :type request: :api:`twisted.web.http.Request` :param request: A ``Request`` object, including POST arguments which should include two key/value pairs: one key being ``'captcha_challenge_field'``, and the other, ``'captcha_response_field'``. These POST arguments should be obtained from :meth:`render_GET`. :rtupe: bool :returns: True, if the CAPTCHA solution was valid; False otherwise. """ valid = False challenge, solution = self.extractClientSolution(request) clientIP = self.getClientIP(request) clientHMACKey = crypto.getHMAC(self.hmacKey, clientIP) try: valid = captcha.GimpCaptcha.check(challenge, solution, self.secretKey, clientHMACKey) except captcha.CaptchaExpired as error: logging.warn(error) valid = False logging.debug("%sorrect captcha from %r: %r." % ("C" if valid else "Inc", clientIP, solution)) return valid
def checkSolution(self, request): """Process a solved CAPTCHA by sending rehashing the solution together with the client's IP address, and checking that the result matches the challenge. The client's IP address is not sent to the ReCaptcha server; instead, a completely random IP is generated and sent instead. :type request: :api:`twisted.web.http.Request` :param request: A ``Request`` object, including POST arguments which should include two key/value pairs: one key being ``'captcha_challenge_field'``, and the other, ``'captcha_response_field'``. These POST arguments should be obtained from :meth:`render_GET`. :rtupe: bool :returns: True, if the CAPTCHA solution was valid; False otherwise. """ challenge, solution = self.extractClientSolution(request) clientIP = self.getClientIP(request) clientHMACKey = crypto.getHMAC(self.hmacKey, clientIP) valid = captcha.GimpCaptcha.check(challenge, solution, self.secretKey, clientHMACKey) logging.debug("%sorrect captcha from %r: %r." % ( "C" if valid else "Inc", Util.logSafely(clientIP), solution)) return valid
def prepopulateRings(self): # populate all rings (for dumping assignments and testing) for filterFn in [filterBridgesByIP4, filterBridgesByIP6]: ruleset = frozenset([filterFn]) key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring") ring = bridgedb.Bridges.BridgeRing(key1, self.answerParameters) self.splitter.addRing(ring, ruleset, filterBridgesByRules([filterFn]), populate_from=self.splitter.bridges)
def test_createChallenge_hmacValid(self): """The HMAC in createChallenge() return value should be valid.""" c = captcha.GimpCaptcha(self.publik, self.sekrit, self.hmacKey, self.cacheDir) challenge = c.createChallenge("ShouldHaveAValidHMAC") decoded = urlsafe_b64decode(challenge) hmac = decoded[:20] orig = decoded[20:] correctHMAC = crypto.getHMAC(self.hmacKey, orig) self.assertEquals(hmac, correctHMAC)
def test_createChallenge_hmacValid(self): """The HMAC in createChallenge() return value should be valid.""" c = captcha.GimpCaptcha(self.publik, self.sekrit, self.hmacKey, self.cacheDir) challenge = c.createChallenge('ShouldHaveAValidHMAC') decoded = urlsafe_b64decode(challenge) hmac = decoded[:20] orig = decoded[20:] correctHMAC = crypto.getHMAC(self.hmacKey, orig) self.assertEquals(hmac, correctHMAC)
def prepopulateRings(self): logging.info("Prepopulating %s distributor hashrings..." % self.name) # populate all rings (for dumping assignments and testing) for filterFn in [None, filterBridgesByIP4, filterBridgesByIP6]: n = self.nClusters for category in self.categories: g = filterAssignBridgesToRing(self.splitter.hmac, self.nClusters + len(self.categories), n) bridgeFilterRules = [g] if filterFn: bridgeFilterRules.append(filterFn) ruleset = frozenset(bridgeFilterRules) key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring-%d" % n) n += 1 ring = bridgedb.Bridges.BridgeRing(key1, self.answerParameters) ring.setName('{0} Ring'.format(self.name)) self.splitter.addRing(ring, ruleset, filterBridgesByRules(bridgeFilterRules), populate_from=self.splitter.bridges) # populate all ip clusters for clusterNum in xrange(self.nClusters): g = filterAssignBridgesToRing(self.splitter.hmac, self.nClusters + len(self.categories), clusterNum) bridgeFilterRules = [g] if filterFn: bridgeFilterRules.append(filterFn) ruleset = frozenset(bridgeFilterRules) key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring-%d" % clusterNum) ring = bridgedb.Bridges.BridgeRing(key1, self.answerParameters) self.splitter.addRing(ring, ruleset, filterBridgesByRules(bridgeFilterRules), populate_from=self.splitter.bridges)
def check(cls, challenge, solution, secretKey, hmacKey): """Check a client's CAPTCHA **solution** against the **challenge**. :param str challenge: The contents of the ``'captcha_challenge_field'`` HTTP form field. :param str solution: The client's proposed solution to the CAPTCHA that they were presented with. :param str secretKey: A PKCS#1 OAEP-padded, private RSA key, used for verifying the client's solution to the CAPTCHA. :param bytes hmacKey: A private key for generating HMACs. :raises CaptchaExpired: if the **solution** was for a stale CAPTCHA. :rtype: bool :returns: ``True`` if the CAPTCHA solution was correct and not stale. ``False`` otherwise. """ hmacIsValid = False if not solution: return hmacIsValid logging.debug("Checking CAPTCHA solution %r against challenge %r" % (solution, challenge)) try: decoded = urlsafe_b64decode(challenge) hmacFromBlob = decoded[:20] encBlob = decoded[20:] hmacNew = crypto.getHMAC(hmacKey, encBlob) hmacIsValid = hmacNew == hmacFromBlob except Exception: return False finally: if hmacIsValid: try: answerBlob = secretKey.decrypt(encBlob) timestamp = answerBlob[:12].lstrip('0') then = cls.sched.nextIntervalStarts(int(timestamp)) now = int(time.time()) answer = answerBlob[12:] except Exception as error: logging.warn(error.message) else: # If the beginning of the 'next' interval (the interval # after the one when the CAPTCHA timestamp was created) # has already passed, then the CAPTCHA is stale. if now >= then: exp = schedule.fromUnixSeconds(then).isoformat(sep=' ') raise CaptchaExpired("Solution %r was for a CAPTCHA " "which already expired at %s." % (solution, exp)) if solution.lower() == answer.lower(): return True return False
def test_createChallenge_decryptedAnswerMatches(self): """The HMAC in createChallenge() return value should be valid.""" c = captcha.GimpCaptcha(self.sekrit, self.publik, self.hmacKey, self.cacheDir) answer = 'ThisAnswerShouldDecryptToThis' challenge = c.createChallenge(answer) decoded = urlsafe_b64decode(challenge) hmac, orig = decoded.split(';', 1) correctHMAC = crypto.getHMAC(self.hmacKey, orig) self.assertEqual(hmac, correctHMAC) decrypted = self.sekrit.decrypt(orig) self.assertEqual(answer, decrypted)
def test_createChallenge_decryptedAnswerMatches(self): """The HMAC in createChallenge() return value should be valid.""" c = captcha.GimpCaptcha(self.publik, self.sekrit, self.hmacKey, self.cacheDir) challenge = c.createChallenge("ThisAnswerShouldDecryptToThis") decoded = urlsafe_b64decode(challenge) hmac = decoded[:20] orig = decoded[20:] correctHMAC = crypto.getHMAC(self.hmacKey, orig) self.assertEqual(hmac, correctHMAC) decrypted = self.sekrit.decrypt(orig) timestamp = int(decrypted[:12].lstrip("0")) # The timestamp should be within 30 seconds of right now. self.assertApproximates(timestamp, int(time.time()), 30) self.assertEqual("ThisAnswerShouldDecryptToThis", decrypted[12:])
def test_createChallenge_decryptedAnswerMatches(self): """The HMAC in createChallenge() return value should be valid.""" c = captcha.GimpCaptcha(self.publik, self.sekrit, self.hmacKey, self.cacheDir) challenge = c.createChallenge('ThisAnswerShouldDecryptToThis') decoded = urlsafe_b64decode(challenge) hmac = decoded[:20] orig = decoded[20:] correctHMAC = crypto.getHMAC(self.hmacKey, orig) self.assertEqual(hmac, correctHMAC) decrypted = self.sekrit.decrypt(orig) timestamp = int(decrypted[:12].lstrip('0')) # The timestamp should be within 30 seconds of right now. self.assertApproximates(timestamp, int(time.time()), 30) self.assertEqual('ThisAnswerShouldDecryptToThis', decrypted[12:])
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()
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() logging.info("Bridges allotted for %s distribution: %d" % (self.name, len(self.hashring)))
def createChallenge(self, answer): """Encrypt the CAPTCHA **answer** and HMAC the encrypted data. Take a string containing the answer to a CAPTCHA and encrypts it to :attr:`publicKey`. The resulting encrypted blob is then HMACed with a client-specific :attr:`hmacKey`. These two strings are then joined together in the form: HMAC ";" ENCRYPTED_ANSWER and lastly base64-encoded (in a URL safe manner). :param str answer: The answer to a CAPTCHA. :rtype: str :returns: An HMAC of, as well as a string containing the URL-safe, base64-encoded encrypted **answer**. """ encrypted = self.publicKey.encrypt(answer) hmac = crypto.getHMAC(self.hmacKey, encrypted) challenge = hmac + ';' + encrypted encoded = urlsafe_b64encode(challenge) return encoded
def checkSolution(self, challenge, solution, clientIP): """Process a solved CAPTCHA via :meth:`bridgedb.captcha.GimpCaptcha.check`. :param str challenge: A base64-encoded, encrypted challenge. :param str solution: The client's solution to the captcha :param str clientIP: The client's IP address. :rtupe: bool :returns: True, if the CAPTCHA solution was valid; False otherwise. """ valid = False clientHMACKey = crypto.getHMAC(self.hmacKey, clientIP) try: valid = captcha.GimpCaptcha.check(challenge, solution, self.secretKey, clientHMACKey) except Exception as impossible: logging.error(impossible) raise impossible finally: logging.debug("%sorrect captcha from %r: %r." % ("C" if valid else "Inc", clientIP, solution)) return valid
def getBridges(self, bridgeRequest, interval, clock=None): """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.distributors.email.autoresponder.getMailTo` and :meth:`bridgedb.distributors.email.autoresponder.SMTPAutoresponder.runChecks`. :type bridgeRequest: :class:`~bridgedb.distributors.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. :type interval: str :param interval: The time period when we got this request. This can be any string, so long as it changes with every period. :type clock: :api:`twisted.internet.task.Clock` :param clock: If given, use the clock to ask what time it is, rather than :api:`time.time`. This should likely only be used for testing. :rtype: :any:`list` or ``None`` :returns: A list of :class:`~bridgedb.bridges.Bridges` for the ``bridgeRequest.client``, if allowed. Otherwise, returns ``None``. """ 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() if clock: now = clock.seconds() 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, filterBySubnet=False) db.setEmailTime(bridgeRequest.client, now) db.commit() return result
def createBridgeRings(cfg, proxyList, key): """Create the bridge distributors defined by the config file :type cfg: :class:`Conf` :param cfg: The current configuration, including any in-memory settings (i.e. settings whose values were not obtained from the config file, but were set via a function somewhere) :type proxyList: :class:`~bridgedb.proxy.ProxySet` :param proxyList: The container for the IP addresses of any currently known open proxies. :param bytes key: Hashring master key :rtype: tuple :returns: A :class:`~bridgedb.Bridges.BridgeSplitter` hashring, an :class:`~bridgedb.distributors.https.distributor.HTTPSDistributor` or None, and an :class:`~bridgedb.distributors.email.distributor.EmailDistributor` or None, and an :class:`~bridgedb.distributors.moat.distributor.MoatDistributor` or None. """ # Create a BridgeSplitter to assign the bridges to the different # distributors. hashring = Bridges.BridgeSplitter(crypto.getHMAC(key, "Hashring-Key")) logging.debug("Created hashring: %r" % hashring) # Create ring parameters. ringParams = Bridges.BridgeRingParameters(needPorts=cfg.FORCE_PORTS, needFlags=cfg.FORCE_FLAGS) emailDistributor = ipDistributor = moatDistributor = None # As appropriate, create a Moat distributor. if cfg.MOAT_DIST and cfg.MOAT_SHARE: logging.debug("Setting up Moat Distributor...") moatDistributor = MoatDistributor( cfg.MOAT_N_IP_CLUSTERS, crypto.getHMAC(key, "Moat-Dist-Key"), proxyList, answerParameters=ringParams) hashring.addRing(moatDistributor.hashring, "moat", cfg.MOAT_SHARE) # As appropriate, create an IP-based distributor. if cfg.HTTPS_DIST and cfg.HTTPS_SHARE: logging.debug("Setting up HTTPS Distributor...") ipDistributor = HTTPSDistributor( cfg.N_IP_CLUSTERS, crypto.getHMAC(key, "HTTPS-IP-Dist-Key"), proxyList, answerParameters=ringParams) hashring.addRing(ipDistributor.hashring, "https", cfg.HTTPS_SHARE) # As appropriate, create an email-based distributor. if cfg.EMAIL_DIST and cfg.EMAIL_SHARE: logging.debug("Setting up Email Distributor...") emailDistributor = EmailDistributor( crypto.getHMAC(key, "Email-Dist-Key"), cfg.EMAIL_DOMAIN_MAP.copy(), cfg.EMAIL_DOMAIN_RULES.copy(), answerParameters=ringParams, whitelist=cfg.EMAIL_WHITELIST.copy()) hashring.addRing(emailDistributor.hashring, "email", cfg.EMAIL_SHARE) # As appropriate, tell the hashring to leave some bridges unallocated. if cfg.RESERVED_SHARE: hashring.addRing(Bridges.UnallocatedHolder(), "unallocated", cfg.RESERVED_SHARE) return hashring, emailDistributor, ipDistributor, moatDistributor
def addWebServer(cfg, dist, sched): """Set up a web server. :param cfg: A configuration object from :mod:`bridgedb.Main`. Currently, 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 RECAPTCHA_REMOTEIP GIMP_CAPTCHA_ENABLED GIMP_CAPTCHA_DIR :type dist: :class:`bridgedb.Dist.IPBasedDistributor` :param dist: A bridge distributor. :type sched: :class:`bridgedb.Time.IntervalSchedule` :param sched: DOCDOC """ httpdist = resource.Resource() httpdist.putChild('', WebRoot()) httpdist.putChild('robots.txt', static.File(os.path.join(template_root, 'robots.txt'))) httpdist.putChild('assets', static.File(os.path.join(template_root, 'assets/'))) httpdist.putChild('options', WebResourceOptions()) bridgesResource = WebResourceBridges( dist, sched, cfg.HTTPS_N_BRIDGES_PER_ANSWER, cfg.HTTP_USE_IP_FROM_FORWARDED_HEADER, includeFingerprints=cfg.HTTPS_INCLUDE_FINGERPRINTS) if cfg.RECAPTCHA_ENABLED: protected = ReCaptchaProtectedResource( recaptchaPrivKey=cfg.RECAPTCHA_PRIV_KEY, recaptchaPubKey=cfg.RECAPTCHA_PUB_KEY, remoteip=cfg.RECAPTCHA_REMOTEIP, useForwardedHeader=cfg.HTTP_USE_IP_FROM_FORWARDED_HEADER, protectedResource=bridgesResource) httpdist.putChild('bridges', protected) elif cfg.GIMP_CAPTCHA_ENABLED: # Get the HMAC secret key for CAPTCHA challenges and create a new key # from it for use on the server: captchaKey = crypto.getKey(cfg.GIMP_CAPTCHA_HMAC_KEYFILE) hmacKey = crypto.getHMAC(captchaKey, "Captcha-Key") # Load or create our encryption keys: secretKey, publicKey = crypto.getRSAKey(cfg.GIMP_CAPTCHA_RSA_KEYFILE) protected = GimpCaptchaProtectedResource( secretKey=secretKey, publicKey=publicKey, hmacKey=hmacKey, captchaDir=cfg.GIMP_CAPTCHA_DIR, useForwardedHeader=cfg.HTTP_USE_IP_FROM_FORWARDED_HEADER, protectedResource=bridgesResource) httpdist.putChild('bridges', protected) else: httpdist.putChild('bridges', bridgesResource) site = server.Site(httpdist) if cfg.HTTP_UNENCRYPTED_PORT: ip = cfg.HTTP_UNENCRYPTED_BIND_IP or "" try: reactor.listenTCP(cfg.HTTP_UNENCRYPTED_PORT, site, interface=ip) except CannotListenError as error: raise SystemExit(error) 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) try: reactor.listenSSL(cfg.HTTPS_PORT, site, factory, interface=ip) except CannotListenError as error: raise SystemExit(error) return site
def getBridgesForIP(self, ip, epoch, N=1, countryCode=None, bridgeFilterRules=None): """Return a list of bridges to give to a user. :param str ip: The user's IP address, as a dotted quad. :param str epoch: The time period when we got this request. This can be any string, so long as it changes with every period. :param int N: The number of bridges to try to give back. (default: 1) :param str countryCode: DOCDOC (default: None) :param list bridgeFilterRules: A list of callables used filter the bridges returned in the response to the client. See :mod:`~bridgedb.Filters`. :rtype: list :return: A list of :class:`~bridgedb.Bridges.Bridge`s to include in the response. See :meth:`bridgedb.HTTPServer.WebResource.getBridgeRequestAnswer` for an example of how this is used. """ logging.info("Attempting to return %d bridges to client %s..." % (N, ip)) if not bridgeFilterRules: bridgeFilterRules=[] if not len(self.splitter): logging.warn("Bailing! Splitter has zero bridges!") return [] logging.debug("Bridges in splitter:\t%d" % len(self.splitter)) logging.debug("Client request epoch:\t%s" % epoch) logging.debug("Active bridge filters:\t%s" % ' '.join([x.func_name for x in bridgeFilterRules])) area = self.areaMapper(ip) logging.debug("IP mapped to area:\t%s" % logSafely("{0}.0/24".format(area))) key1 = '' pos = 0 n = self.nClusters # only one of ip categories or area clustering is active # try to match the request to an ip category for category in self.categories: # IP Categories if category.contains(ip): g = filterAssignBridgesToRing(self.splitter.hmac, self.nClusters + len(self.categories), n) bridgeFilterRules.append(g) logging.info("category<%s>%s", epoch, logSafely(area)) pos = self.areaOrderHmac("category<%s>%s" % (epoch, area)) key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring-%d" % n) break n += 1 # if no category matches, use area clustering else: # IP clustering h = int( self.areaClusterHmac(area)[:8], 16) # length of numClusters clusterNum = h % self.nClusters g = filterAssignBridgesToRing(self.splitter.hmac, self.nClusters + len(self.categories), clusterNum) bridgeFilterRules.append(g) pos = self.areaOrderHmac("<%s>%s" % (epoch, area)) key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring-%d" % clusterNum) # try to find a cached copy ruleset = frozenset(bridgeFilterRules) # See if we have a cached copy of the ring, # otherwise, add a new ring and populate it if ruleset in self.splitter.filterRings.keys(): logging.debug("Cache hit %s" % ruleset) _,ring = self.splitter.filterRings[ruleset] # else create the ring and populate it else: logging.debug("Cache miss %s" % ruleset) ring = bridgedb.Bridges.BridgeRing(key1, self.answerParameters) self.splitter.addRing(ring, ruleset, filterBridgesByRules(bridgeFilterRules), populate_from=self.splitter.bridges) # get an appropriate number of bridges numBridgesToReturn = getNumBridgesPerAnswer(ring, max_bridges_per_answer=N) answer = ring.getBridges(pos, numBridgesToReturn) return answer
def getBridgesForIP(self, ip, epoch, N=1, countryCode=None, bridgeFilterRules=None): """Return a list of bridges to give to a user. :param str ip: The user's IP address, as a dotted quad. :param str epoch: The time period when we got this request. This can be any string, so long as it changes with every period. :param int N: The number of bridges to try to give back. (default: 1) :param str countryCode: The two-letter geoip country code of the client's IP address. If given, it is assumed that any bridges distributed to that client should not be blocked in that country. (default: None) :param list bridgeFilterRules: A list of callables used filter the bridges returned in the response to the client. See :mod:`~bridgedb.Filters`. :rtype: list :return: A list of :class:`~bridgedb.Bridges.Bridge`s to include in the response. See :meth:`bridgedb.HTTPServer.WebResource.getBridgeRequestAnswer` for an example of how this is used. """ logging.info("Attempting to return %d bridges to client %s..." % (N, ip)) if not bridgeFilterRules: bridgeFilterRules=[] if not len(self.splitter): logging.warn("Bailing! Splitter has zero bridges!") return [] logging.debug("Bridges in splitter:\t%d" % len(self.splitter)) logging.debug("Client request epoch:\t%s" % epoch) logging.debug("Active bridge filters:\t%s" % ' '.join([x.func_name for x in bridgeFilterRules])) area = self.areaMapper(ip) logging.debug("IP mapped to area:\t%s" % area) key1 = '' pos = 0 n = self.nClusters # only one of ip categories or area clustering is active # try to match the request to an ip category for category in self.categories: # IP Categories if ip in category: # The tag is a tag applied to a proxy IP address when it is # added to the bridgedb.proxy.ProxySet. For Tor Exit relays, # the default is 'exit_relay'. For other proxies loaded from # the PROXY_LIST_FILES config option, the default tag is the # full filename that the IP address originally came from. tag = category.getTag(ip) logging.info("Client was from known proxy (tag: %s): %s" % (tag, ip)) g = filterAssignBridgesToRing(self.splitter.hmac, self.nClusters + len(self.categories), n) bridgeFilterRules.append(g) # Cluster Tor/proxy users into four groups. This means that # no matter how many different Tor Exits or proxies a client # uses, the most they can ever get is four different sets of # bridge lines (per period). group = (int(ipaddr.IPAddress(ip)) % 4) + 1 logging.debug(("Assigning client hashring position based on: " "known-proxy<%s>%s") % (epoch, group)) pos = self.areaOrderHmac("known-proxy<%s>%s" % (epoch, group)) key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring-%d" % n) break n += 1 # if no category matches, use area clustering else: # IP clustering h = int( self.areaClusterHmac(area)[:8], 16) # length of numClusters clusterNum = h % self.nClusters g = filterAssignBridgesToRing(self.splitter.hmac, self.nClusters + len(self.categories), clusterNum) bridgeFilterRules.append(g) pos = self.areaOrderHmac("<%s>%s" % (epoch, area)) key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring-%d" % clusterNum) # try to find a cached copy ruleset = frozenset(bridgeFilterRules) # See if we have a cached copy of the ring, # otherwise, add a new ring and populate it if ruleset in self.splitter.filterRings.keys(): logging.debug("Cache hit %s" % ruleset) _, ring = self.splitter.filterRings[ruleset] # else create the ring and populate it else: logging.debug("Cache miss %s" % ruleset) ring = bridgedb.Bridges.BridgeRing(key1, self.answerParameters) self.splitter.addRing(ring, ruleset, filterBridgesByRules(bridgeFilterRules), populate_from=self.splitter.bridges) # get an appropriate number of bridges numBridgesToReturn = getNumBridgesPerAnswer(ring, max_bridges_per_answer=N) answer = ring.getBridges(pos, numBridgesToReturn) return answer
def addWebServer(cfg, dist, sched): """Set up a web server for HTTP(S)-based bridge distribution. :type cfg: :class:`bridgedb.persistent.Conf` :param cfg: A configuration object from :mod:`bridgedb.Main`. Currently, we use these options:: HTTP_UNENCRYPTED_PORT HTTP_UNENCRYPTED_BIND_IP HTTP_USE_IP_FROM_FORWARDED_HEADER HTTPS_N_BRIDGES_PER_ANSWER HTTPS_INCLUDE_FINGERPRINTS HTTPS_KEY_FILE HTTPS_CERT_FILE HTTPS_PORT HTTPS_BIND_IP HTTPS_USE_IP_FROM_FORWARDED_HEADER RECAPTCHA_ENABLED RECAPTCHA_PUB_KEY RECAPTCHA_SEC_KEY RECAPTCHA_REMOTEIP GIMP_CAPTCHA_ENABLED GIMP_CAPTCHA_DIR GIMP_CAPTCHA_HMAC_KEYFILE GIMP_CAPTCHA_RSA_KEYFILE :type dist: :class:`bridgedb.Dist.IPBasedDistributor` :param dist: A bridge distributor. :type sched: :class:`bridgedb.schedule.ScheduledInterval` :param sched: The scheduled interval at which bridge selection, which are ultimately displayed on the :class:`WebResourceBridges` page, will be shifted. :raises SystemExit: if the servers cannot be started. :rtype: :api:`twisted.web.server.Site` :returns: A webserver. """ captcha = None fwdHeaders = cfg.HTTP_USE_IP_FROM_FORWARDED_HEADER numBridges = cfg.HTTPS_N_BRIDGES_PER_ANSWER fprInclude = cfg.HTTPS_INCLUDE_FINGERPRINTS logging.info("Starting web servers...") httpdist = resource.Resource() httpdist.putChild('', WebRoot()) httpdist.putChild('robots.txt', static.File(os.path.join(TEMPLATE_DIR, 'robots.txt'))) httpdist.putChild('keys', static.File(os.path.join(TEMPLATE_DIR, 'bridgedb.asc'))) httpdist.putChild('assets', static.File(os.path.join(TEMPLATE_DIR, 'assets/'))) httpdist.putChild('options', WebResourceOptions()) httpdist.putChild('howto', WebResourceHowto()) if cfg.RECAPTCHA_ENABLED: publicKey = cfg.RECAPTCHA_PUB_KEY secretKey = cfg.RECAPTCHA_SEC_KEY captcha = partial(ReCaptchaProtectedResource, remoteIP=cfg.RECAPTCHA_REMOTEIP) elif cfg.GIMP_CAPTCHA_ENABLED: # Get the master HMAC secret key for CAPTCHA challenges, and then # create a new HMAC key from it for use on the server. captchaKey = crypto.getKey(cfg.GIMP_CAPTCHA_HMAC_KEYFILE) hmacKey = crypto.getHMAC(captchaKey, "Captcha-Key") # Load or create our encryption keys: secretKey, publicKey = crypto.getRSAKey(cfg.GIMP_CAPTCHA_RSA_KEYFILE) captcha = partial(GimpCaptchaProtectedResource, hmacKey=hmacKey, captchaDir=cfg.GIMP_CAPTCHA_DIR) bridges = WebResourceBridges(dist, sched, numBridges, fwdHeaders, includeFingerprints=fprInclude) if captcha: # Protect the 'bridges' page with a CAPTCHA, if configured to do so: protected = captcha(publicKey=publicKey, secretKey=secretKey, useForwardedHeader=fwdHeaders, protectedResource=bridges) httpdist.putChild('bridges', protected) logging.info("Protecting resources with %s." % captcha.func.__name__) else: httpdist.putChild('bridges', bridges) site = server.Site(httpdist) site.displayTracebacks = False if cfg.HTTP_UNENCRYPTED_PORT: ip = cfg.HTTP_UNENCRYPTED_BIND_IP or "" port = cfg.HTTP_UNENCRYPTED_PORT or 80 try: reactor.listenTCP(port, site, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTP server on %s:%d" % (str(ip), int(port))) if cfg.HTTPS_PORT: ip = cfg.HTTPS_BIND_IP or "" port = cfg.HTTPS_PORT or 443 try: from twisted.internet.ssl import DefaultOpenSSLContextFactory factory = DefaultOpenSSLContextFactory(cfg.HTTPS_KEY_FILE, cfg.HTTPS_CERT_FILE) reactor.listenSSL(port, site, factory, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTPS server on %s:%d" % (str(ip), int(port))) return site
def createBridgeRings(cfg, proxyList, key): """Create the bridge distributors defined by the config file :type cfg: :class:`Conf` :param cfg: The current configuration, including any in-memory settings (i.e. settings whose values were not obtained from the config file, but were set via a function somewhere) :type proxyList: :class:`ProxyCategory` :param proxyList: The container for the IP addresses of any currently known open proxies. :param bytes key: Splitter master key :rtype: tuple :returns: A BridgeSplitter splitter, an IPBasedDistributor or None, and an EmailBasedDistributor or None. """ # Create a BridgeSplitter to assign the bridges to the different # distributors. splitter = Bridges.BridgeSplitter(crypto.getHMAC(key, "Splitter-Key")) logging.debug("Created splitter: %r" % splitter) # Create ring parameters. ringParams = Bridges.BridgeRingParameters(needPorts=cfg.FORCE_PORTS, needFlags=cfg.FORCE_FLAGS) emailDistributor = ipDistributor = None # As appropriate, create an IP-based distributor. if cfg.HTTPS_DIST and cfg.HTTPS_SHARE: logging.debug("Setting up HTTPS Distributor...") categories = [] if proxyList.ipset: logging.debug("Adding proxyList to HTTPS Distributor categories.") categories.append(proxyList) logging.debug("HTTPS Distributor categories: '%s'" % categories) ipDistributor = Dist.IPBasedDistributor( Dist.uniformMap, cfg.N_IP_CLUSTERS, crypto.getHMAC(key, "HTTPS-IP-Dist-Key"), categories, answerParameters=ringParams) splitter.addRing(ipDistributor, "https", cfg.HTTPS_SHARE) # As appropriate, create an email-based distributor. if cfg.EMAIL_DIST and cfg.EMAIL_SHARE: logging.debug("Setting up Email Distributor...") emailDistributor = Dist.EmailBasedDistributor( crypto.getHMAC(key, "Email-Dist-Key"), cfg.EMAIL_DOMAIN_MAP.copy(), cfg.EMAIL_DOMAIN_RULES.copy(), answerParameters=ringParams, whitelist=cfg.EMAIL_WHITELIST.copy()) splitter.addRing(emailDistributor, "email", cfg.EMAIL_SHARE) # As appropriate, tell the splitter to leave some bridges unallocated. if cfg.RESERVED_SHARE: splitter.addRing(Bridges.UnallocatedHolder(), "unallocated", cfg.RESERVED_SHARE) # Add pseudo distributors to splitter for pseudoRing in cfg.FILE_BUCKETS.keys(): splitter.addPseudoRing(pseudoRing) return splitter, emailDistributor, ipDistributor
def addWebServer(config, distributor): """Set up a web server for HTTP(S)-based bridge distribution. :type config: :class:`bridgedb.persistent.Conf` :param config: A configuration object from :mod:`bridgedb.main`. Currently, we use these options:: HTTP_UNENCRYPTED_PORT HTTP_UNENCRYPTED_BIND_IP HTTP_USE_IP_FROM_FORWARDED_HEADER HTTPS_N_BRIDGES_PER_ANSWER HTTPS_INCLUDE_FINGERPRINTS HTTPS_KEY_FILE HTTPS_CERT_FILE HTTPS_PORT HTTPS_BIND_IP HTTPS_USE_IP_FROM_FORWARDED_HEADER HTTPS_ROTATION_PERIOD RECAPTCHA_ENABLED RECAPTCHA_PUB_KEY RECAPTCHA_SEC_KEY RECAPTCHA_REMOTEIP GIMP_CAPTCHA_ENABLED GIMP_CAPTCHA_DIR GIMP_CAPTCHA_HMAC_KEYFILE GIMP_CAPTCHA_RSA_KEYFILE SERVER_PUBLIC_FQDN CSP_ENABLED CSP_REPORT_ONLY CSP_INCLUDE_SELF :type distributor: :class:`bridgedb.distributors.https.distributor.HTTPSDistributor` :param distributor: A bridge distributor. :raises SystemExit: if the servers cannot be started. :rtype: :api:`twisted.web.server.Site` :returns: A webserver. """ captcha = None fwdHeaders = config.HTTP_USE_IP_FROM_FORWARDED_HEADER numBridges = config.HTTPS_N_BRIDGES_PER_ANSWER fprInclude = config.HTTPS_INCLUDE_FINGERPRINTS logging.info("Starting web servers...") setFQDN(config.SERVER_PUBLIC_FQDN) index = IndexResource() options = OptionsResource() howto = HowtoResource() robots = static.File(os.path.join(TEMPLATE_DIR, 'robots.txt')) assets = static.File(os.path.join(TEMPLATE_DIR, 'assets/')) keys = static.Data(bytes(strings.BRIDGEDB_OPENPGP_KEY), 'text/plain') csp = CSPResource(enabled=config.CSP_ENABLED, includeSelf=config.CSP_INCLUDE_SELF, reportViolations=config.CSP_REPORT_ONLY, useForwardedHeader=fwdHeaders) root = CustomErrorHandlingResource() root.putChild('', index) root.putChild('robots.txt', robots) root.putChild('keys', keys) root.putChild('assets', assets) root.putChild('options', options) root.putChild('howto', howto) root.putChild('maintenance', maintenance) root.putChild('error', resource500) root.putChild(CSPResource.reportURI, csp) if config.RECAPTCHA_ENABLED: publicKey = config.RECAPTCHA_PUB_KEY secretKey = config.RECAPTCHA_SEC_KEY captcha = partial(ReCaptchaProtectedResource, remoteIP=config.RECAPTCHA_REMOTEIP) elif config.GIMP_CAPTCHA_ENABLED: # Get the master HMAC secret key for CAPTCHA challenges, and then # create a new HMAC key from it for use on the server. captchaKey = crypto.getKey(config.GIMP_CAPTCHA_HMAC_KEYFILE) hmacKey = crypto.getHMAC(captchaKey, "Captcha-Key") # Load or create our encryption keys: secretKey, publicKey = crypto.getRSAKey(config.GIMP_CAPTCHA_RSA_KEYFILE) captcha = partial(GimpCaptchaProtectedResource, hmacKey=hmacKey, captchaDir=config.GIMP_CAPTCHA_DIR) if config.HTTPS_ROTATION_PERIOD: count, period = config.HTTPS_ROTATION_PERIOD.split() sched = ScheduledInterval(count, period) else: sched = Unscheduled() bridges = BridgesResource(distributor, sched, numBridges, fwdHeaders, includeFingerprints=fprInclude) if captcha: # Protect the 'bridges' page with a CAPTCHA, if configured to do so: protected = captcha(publicKey=publicKey, secretKey=secretKey, useForwardedHeader=fwdHeaders, protectedResource=bridges) root.putChild('bridges', protected) logging.info("Protecting resources with %s." % captcha.func.__name__) else: root.putChild('bridges', bridges) site = Site(root) site.displayTracebacks = False if config.HTTP_UNENCRYPTED_PORT: # pragma: no cover ip = config.HTTP_UNENCRYPTED_BIND_IP or "" port = config.HTTP_UNENCRYPTED_PORT or 80 try: reactor.listenTCP(port, site, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTP server on %s:%d" % (str(ip), int(port))) if config.HTTPS_PORT: # pragma: no cover ip = config.HTTPS_BIND_IP or "" port = config.HTTPS_PORT or 443 try: from twisted.internet.ssl import DefaultOpenSSLContextFactory factory = DefaultOpenSSLContextFactory(config.HTTPS_KEY_FILE, config.HTTPS_CERT_FILE) reactor.listenSSL(port, site, factory, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTPS server on %s:%d" % (str(ip), int(port))) return site
def prepopulateRings(self): """Prepopulate this distributor's hashrings and subhashrings with bridges. The hashring structure for this distributor is influenced by the ``N_IP_CLUSTERS`` configuration option, as well as the number of ``PROXY_LIST_FILES``. Essentially, :data:`totalSubrings` is set to the specified ``N_IP_CLUSTERS``. All of the ``PROXY_LIST_FILES``, plus the list of Tor Exit relays (downloaded into memory with :script:`get-tor-exits`), are stored in :data:`proxies`, and the latter is added as an additional cluster (such that :data:`totalSubrings` becomes ``N_IP_CLUSTERS + 1``). The number of subhashrings which this :class:`Distributor` has active in its hashring is then :data:`totalSubrings`, where the last cluster is reserved for all :data:`proxies`. As an example, if BridgeDB was configured with ``N_IP_CLUSTERS=4`` and ``PROXY_LIST_FILES=["open-socks-proxies.txt"]``, then the total number of subhashrings is five — four for the "clusters", and one for the :data:`proxies`. Thus, the resulting hashring-subhashring structure would look like: +------------------+---------------------------------------------------+-------------+ | | Directly connecting users | Tor / known | | | | proxy users | +------------------+------------+------------+------------+------------+-------------+ | Clusters | Cluster-1 | Cluster-2 | Cluster-3 | Cluster-4 | Cluster-5 | +==================+============+============+============+============+=============+ | Subhashrings | | | | | | | (total, assigned)| (5,1) | (5,2) | (5,3) | (5,4) | (5,5) | +------------------+------------+------------+------------+------------+-------------+ | Filtered | (5,1)-IPv4 | (5,2)-IPv4 | (5,3)-IPv4 | (5,4)-IPv4 | (5,5)-IPv4 | | Subhashrings | | | | | | | bBy requested +------------+------------+------------+------------+-------------+ | bridge type) | (5,1)-IPv6 | (5,2)-IPv6 | (5,3)-IPv6 | (5,4)-IPv6 | (5,5)-IPv6 | | | | | | | | +------------------+------------+------------+------------+------------+-------------+ The "filtered subhashrings" are essentially filtered copies of their respective subhashring, such that they only contain bridges which support IPv4 or IPv6, respectively. Additionally, the contents of ``(5,1)-IPv4`` and ``(5,1)-IPv6`` sets are *not* disjoint. Thus, in this example, we end up with **10 total subhashrings**. """ logging.info("Prepopulating %s distributor hashrings..." % self.name) for filterFn in [byIPv4, byIPv6]: for subring in range(1, self.totalSubrings + 1): filters = self._buildHashringFilters([ filterFn, ], subring) key1 = getHMAC(self.key, "Order-Bridges-In-Ring-%d" % subring) ring = BridgeRing(key1, self.answerParameters) # For consistency with previous implementation of this method, # only set the "name" for "clusters" which are for this # distributor's proxies: if subring == self.proxySubring: ring.setName('{0} Proxy Ring'.format(self.name)) self.hashring.addRing(ring, filters, byFilters(filters), populate_from=self.hashring.bridges)
def getBridgesForEmail(self, emailaddress, epoch, N=1, parameters=None, countryCode=None, bridgeFilterRules=None): """Return a list of bridges to give to a user. :param str emailaddress: The user's email address, as given in a :header:`From:` line. :param epoch: The time period when we got this request. This can be any string, so long as it changes with every period. :param int N: The number of bridges to try to give back. :param parameters: DOCDOC :param countryCode: DOCDOC :param bridgeFilterRules: DOCDOC """ if not bridgeFilterRules: bridgeFilterRules=[] now = time.time() # 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 emailaddress: logging.error(("%s distributor can't get bridges for blank email " "address!") % (self.name, emailaddress)) return [] with bridgedb.Storage.getDB() as db: wasWarned = db.getWarnedEmail(emailaddress) lastSaw = db.getEmailTime(emailaddress) logging.info("Attempting to return for %d bridges for %s..." % (N, emailaddress)) if lastSaw is not None: if emailaddress in self.whitelist.keys(): logging.info(("Whitelisted email address %s was last seen " "%d seconds ago.") % (emailaddress, now - lastSaw)) elif (lastSaw + MAX_EMAIL_RATE) >= now: wait = (lastSaw + MAX_EMAIL_RATE) - now logging.info("Client %s must wait another %d seconds." % (emailaddress, wait)) if wasWarned: raise IgnoreEmail("Client was warned.", emailaddress) else: logging.info("Sending duplicate request warning.") db.setWarnedEmail(emailaddress, True, now) db.commit() raise TooSoonEmail("Must wait %d seconds" % wait, emailaddress) # warning period is over elif wasWarned: db.setWarnedEmail(emailaddress, False) pos = self.emailHmac("<%s>%s" % (epoch, emailaddress)) ring = None ruleset = frozenset(bridgeFilterRules) if ruleset in self.splitter.filterRings.keys(): logging.debug("Cache hit %s" % ruleset) _, ring = self.splitter.filterRings[ruleset] else: # cache miss, add new ring logging.debug("Cache miss %s" % ruleset) # add new ring key1 = getHMAC(self.splitter.key, "Order-Bridges-In-Ring") ring = bridgedb.Bridges.BridgeRing(key1, self.answerParameters) # debug log: cache miss self.splitter.addRing(ring, ruleset, filterBridgesByRules(bridgeFilterRules), populate_from=self.splitter.bridges) numBridgesToReturn = getNumBridgesPerAnswer(ring, max_bridges_per_answer=N) result = ring.getBridges(pos, numBridgesToReturn) db.setEmailTime(emailaddress, now) db.commit() return result
def addMoatServer(config, distributor): """Set up a web server for moat bridge distribution. :type config: :class:`bridgedb.persistent.Conf` :param config: A configuration object from :mod:`bridgedb.main`. Currently, we use these options:: GIMP_CAPTCHA_DIR SERVER_PUBLIC_FQDN SUPPORTED_TRANSPORTS MOAT_DIST MOAT_DIST_VIA_MEEK_ONLY MOAT_TLS_CERT_FILE MOAT_TLS_KEY_FILE MOAT_SERVER_PUBLIC_ROOT MOAT_HTTPS_IP MOAT_HTTPS_PORT MOAT_HTTP_IP MOAT_HTTP_PORT MOAT_BRIDGES_PER_ANSWER MOAT_TRANSPORT_PREFERENCE_LIST MOAT_USE_IP_FROM_FORWARDED_HEADER MOAT_SKIP_LOOPBACK_ADDRESSES MOAT_ROTATION_PERIOD MOAT_GIMP_CAPTCHA_HMAC_KEYFILE MOAT_GIMP_CAPTCHA_RSA_KEYFILE :type distributor: :class:`bridgedb.distributors.moat.distributor.MoatDistributor` :param distributor: A bridge distributor. :raises SystemExit: if the servers cannot be started. :rtype: :api:`twisted.web.server.Site` :returns: A webserver. """ captcha = None fwdHeaders = config.MOAT_USE_IP_FROM_FORWARDED_HEADER numBridges = config.MOAT_BRIDGES_PER_ANSWER skipLoopback = config.MOAT_SKIP_LOOPBACK_ADDRESSES logging.info("Starting moat servers...") setFQDN(config.SERVER_PUBLIC_FQDN) setRoot(config.MOAT_SERVER_PUBLIC_ROOT) setSupportedTransports(config.SUPPORTED_TRANSPORTS) setPreferredTransports(config.MOAT_TRANSPORT_PREFERENCE_LIST) # Get the master HMAC secret key for CAPTCHA challenges, and then # create a new HMAC key from it for use on the server. captchaKey = crypto.getKey(config.MOAT_GIMP_CAPTCHA_HMAC_KEYFILE) hmacKey = crypto.getHMAC(captchaKey, "Moat-Captcha-Key") # Load or create our encryption keys: secretKey, publicKey = crypto.getRSAKey( config.MOAT_GIMP_CAPTCHA_RSA_KEYFILE) sched = Unscheduled() if config.MOAT_ROTATION_PERIOD: count, period = config.MOAT_ROTATION_PERIOD.split() sched = ScheduledInterval(count, period) sitePublicDir = getRoot() meek = CustomErrorHandlingResource() moat = CustomErrorHandlingResource() fetch = CaptchaFetchResource(hmacKey, publicKey, secretKey, config.GIMP_CAPTCHA_DIR, fwdHeaders, skipLoopback) check = CaptchaCheckResource(distributor, sched, numBridges, hmacKey, publicKey, secretKey, fwdHeaders, skipLoopback) moat.putChild(b"fetch", fetch) moat.putChild(b"check", check) meek.putChild(b"moat", moat) root = CustomErrorHandlingResource() root.putChild(b"meek", meek) root.putChild(b"moat", moat) site = Site(root) site.displayTracebacks = False if config.MOAT_HTTP_PORT: # pragma: no cover ip = config.MOAT_HTTP_IP or "" port = config.MOAT_HTTP_PORT or 80 try: reactor.listenTCP(port, site, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started Moat HTTP server on %s:%d" % (str(ip), int(port))) if config.MOAT_HTTPS_PORT: # pragma: no cover ip = config.MOAT_HTTPS_IP or "" port = config.MOAT_HTTPS_PORT or 443 try: from twisted.internet.ssl import DefaultOpenSSLContextFactory factory = DefaultOpenSSLContextFactory(config.MOAT_TLS_KEY_FILE, config.MOAT_TLS_CERT_FILE) reactor.listenSSL(port, site, factory, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started Moat TLS server on %s:%d" % (str(ip), int(port))) return site
def addWebServer(cfg, dist): """Set up a web server for HTTP(S)-based bridge distribution. :type cfg: :class:`bridgedb.persistent.Conf` :param cfg: A configuration object from :mod:`bridgedb.Main`. Currently, we use these options:: HTTP_UNENCRYPTED_PORT HTTP_UNENCRYPTED_BIND_IP HTTP_USE_IP_FROM_FORWARDED_HEADER HTTPS_N_BRIDGES_PER_ANSWER HTTPS_INCLUDE_FINGERPRINTS HTTPS_KEY_FILE HTTPS_CERT_FILE HTTPS_PORT HTTPS_BIND_IP HTTPS_USE_IP_FROM_FORWARDED_HEADER HTTPS_ROTATION_PERIOD RECAPTCHA_ENABLED RECAPTCHA_PUB_KEY RECAPTCHA_SEC_KEY RECAPTCHA_REMOTEIP GIMP_CAPTCHA_ENABLED GIMP_CAPTCHA_DIR GIMP_CAPTCHA_HMAC_KEYFILE GIMP_CAPTCHA_RSA_KEYFILE :type dist: :class:`bridgedb.Dist.IPBasedDistributor` :param dist: A bridge distributor. :raises SystemExit: if the servers cannot be started. :rtype: :api:`twisted.web.server.Site` :returns: A webserver. """ captcha = None fwdHeaders = cfg.HTTP_USE_IP_FROM_FORWARDED_HEADER numBridges = cfg.HTTPS_N_BRIDGES_PER_ANSWER fprInclude = cfg.HTTPS_INCLUDE_FINGERPRINTS logging.info("Starting web servers...") httpdist = resource.Resource() httpdist.putChild('', WebRoot()) httpdist.putChild('robots.txt', static.File(os.path.join(TEMPLATE_DIR, 'robots.txt'))) httpdist.putChild('keys', static.File(os.path.join(TEMPLATE_DIR, 'bridgedb.asc'))) httpdist.putChild('assets', static.File(os.path.join(TEMPLATE_DIR, 'assets/'))) httpdist.putChild('options', WebResourceOptions()) httpdist.putChild('howto', WebResourceHowto()) if cfg.RECAPTCHA_ENABLED: publicKey = cfg.RECAPTCHA_PUB_KEY secretKey = cfg.RECAPTCHA_SEC_KEY captcha = partial(ReCaptchaProtectedResource, remoteIP=cfg.RECAPTCHA_REMOTEIP) elif cfg.GIMP_CAPTCHA_ENABLED: # Get the master HMAC secret key for CAPTCHA challenges, and then # create a new HMAC key from it for use on the server. captchaKey = crypto.getKey(cfg.GIMP_CAPTCHA_HMAC_KEYFILE) hmacKey = crypto.getHMAC(captchaKey, "Captcha-Key") # Load or create our encryption keys: secretKey, publicKey = crypto.getRSAKey(cfg.GIMP_CAPTCHA_RSA_KEYFILE) captcha = partial(GimpCaptchaProtectedResource, hmacKey=hmacKey, captchaDir=cfg.GIMP_CAPTCHA_DIR) if cfg.HTTPS_ROTATION_PERIOD: count, period = cfg.HTTPS_ROTATION_PERIOD.split() sched = ScheduledInterval(count, period) else: sched = Unscheduled() bridges = WebResourceBridges(dist, sched, numBridges, fwdHeaders, includeFingerprints=fprInclude) if captcha: # Protect the 'bridges' page with a CAPTCHA, if configured to do so: protected = captcha(publicKey=publicKey, secretKey=secretKey, useForwardedHeader=fwdHeaders, protectedResource=bridges) httpdist.putChild('bridges', protected) logging.info("Protecting resources with %s." % captcha.func.__name__) else: httpdist.putChild('bridges', bridges) site = server.Site(httpdist) site.displayTracebacks = False if cfg.HTTP_UNENCRYPTED_PORT: ip = cfg.HTTP_UNENCRYPTED_BIND_IP or "" port = cfg.HTTP_UNENCRYPTED_PORT or 80 try: reactor.listenTCP(port, site, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTP server on %s:%d" % (str(ip), int(port))) if cfg.HTTPS_PORT: ip = cfg.HTTPS_BIND_IP or "" port = cfg.HTTPS_PORT or 443 try: from twisted.internet.ssl import DefaultOpenSSLContextFactory factory = DefaultOpenSSLContextFactory(cfg.HTTPS_KEY_FILE, cfg.HTTPS_CERT_FILE) reactor.listenSSL(port, site, factory, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTPS server on %s:%d" % (str(ip), int(port))) return site
def getBridges(self, bridgeRequest, interval): """Return a list of bridges to give to a user. :type bridgeRequest: :class:`bridgedb.distributors.https.request.HTTPSBridgeRequest` :param bridgeRequest: A :class:`~bridgedb.bridgerequest.BridgeRequestBase` with the :data:`~bridgedb.bridgerequest.BridgeRequestBase.client` attribute set to a string containing the client's IP address. :param str interval: The time period when we got this request. This can be any string, so long as it changes with every period. :rtype: list :return: A list of :class:`~bridgedb.Bridges.Bridge`s to include in the response. See :meth:`bridgedb.distributors.https.server.WebResourceBridges.getBridgeRequestAnswer` for an example of how this is used. """ logging.info("Attempting to get bridges for %s..." % bridgeRequest.client) if not len(self.hashring): logging.warn("Bailing! Hashring has zero bridges!") return [] usingProxy = False # First, check if the client's IP is one of the known :data:`proxies`: if bridgeRequest.client in self.proxies: # The tag is a tag applied to a proxy IP address when it is added # to the bridgedb.proxy.ProxySet. For Tor Exit relays, the default # is 'exit_relay'. For other proxies loaded from the # PROXY_LIST_FILES config option, the default tag is the full # filename that the IP address originally came from. usingProxy = True tag = self.proxies.getTag(bridgeRequest.client) logging.info("Client was from known proxy (tag: %s): %s" % (tag, bridgeRequest.client)) subnet = self.getSubnet(bridgeRequest.client, usingProxy) subring = self.mapSubnetToSubring(subnet, usingProxy) position = self.mapClientToHashringPosition(interval, subnet) filters = self._buildHashringFilters(bridgeRequest.filters, subring) logging.debug("Client request within time interval: %s" % interval) logging.debug("Assigned client to subhashring %d/%d" % (subring, self.totalSubrings)) logging.debug("Assigned client to subhashring position: %s" % position.encode('hex')) logging.debug("Total bridges: %d" % len(self.hashring)) logging.debug("Bridge filters: %s" % ' '.join([x.func_name for x in filters])) # Check wheth we have a cached copy of the hashring: if filters in self.hashring.filterRings.keys(): logging.debug("Cache hit %s" % filters) _, ring = self.hashring.filterRings[filters] # Otherwise, construct a new hashring and populate it: else: logging.debug("Cache miss %s" % filters) key1 = getHMAC(self.key, "Order-Bridges-In-Ring-%d" % subring) ring = BridgeRing(key1, self.answerParameters) self.hashring.addRing(ring, filters, byFilters(filters), populate_from=self.hashring.bridges) # Determine the appropriate number of bridges to give to the client: returnNum = self.bridgesPerResponse(ring) answer = ring.getBridges(position, returnNum, filterBySubnet=True) return answer
def createChallenge(self, answer): """Encrypt-then-HMAC a timestamp plus the CAPTCHA **answer**. A challenge string consists of a URL-safe, base64-encoded string which contains an ``HMAC`` concatenated with an ``ENC_BLOB``, in the following form:: CHALLENGE := B64( HMAC | ENC_BLOB ) ENC_BLOB := RSA_ENC( ANSWER_BLOB ) ANSWER_BLOB := ( TIMESTAMP | ANSWER ) where * ``B64`` is a URL-safe base64-encode function, * ``RSA_ENC`` is the PKCS#1 RSA-OAEP encryption function, * and the remaining feilds are specified as follows: +-------------+--------------------------------------------+----------+ | Field | Description | Length | +=============+============================================+==========+ | HMAC | An HMAC of the ``ENC_BLOB``, created with | 20 bytes | | | the client-specific :ivar:`hmacKey`, by | | | | applying :func:`~crypto.getHMAC` to the | | | | ``ENC_BLOB``. | | +-------------+--------------------------------------------+----------+ | ENC_BLOB | An encrypted ``ANSWER_BLOB``, created with | varies | | | a PKCS#1 OAEP-padded RSA :ivar:`publicKey`.| | +-------------+--------------------------------------------+----------+ | ANSWER_BLOB | Contains the concatenated ``TIMESTAMP`` | varies | | | and ``ANSWER``. | | +-------------+--------------------------------------------+----------+ | TIMESTAMP | A Unix Epoch timestamp, in seconds, | 12 bytes | | | left-padded with "0"s. | | +-------------+--------------------------------------------+----------+ | ANSWER | A string containing answer to this | 8 bytes | | | CAPTCHA :ivar:`image`. | | +-------------+--------------------------------------------+----------+ The steps taken to produce a ``CHALLENGE`` are then: 1. Create a ``TIMESTAMP``, and pad it on the left with ``0``s to 12 bytes in length. 2. Next, take the **answer** to this CAPTCHA :ivar:`image: and concatenate the padded ``TIMESTAMP`` and the ``ANSWER``, forming an ``ANSWER_BLOB``. 3. Encrypt the resulting ``ANSWER_BLOB`` to :ivar:`publicKey` to create the ``ENC_BLOB``. 4. Use the client-specific :ivar:`hmacKey` to apply the :func:`~crypto.getHMAC` function to the ``ENC_BLOB``, obtaining an ``HMAC``. 5. Create the final ``CHALLENGE`` string by concatenating the ``HMAC`` and ``ENC_BLOB``, then base64-encoding the result. :param str answer: The answer to a CAPTCHA. :rtype: str :returns: A challenge string. """ timestamp = str(int(time.time())).zfill(12) blob = timestamp + answer encBlob = self.publicKey.encrypt(blob) hmac = crypto.getHMAC(self.hmacKey, encBlob) challenge = urlsafe_b64encode(hmac + encBlob) return challenge
def getBridges(self, bridgeRequest, interval, clock=None): """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.distributors.email.autoresponder.getMailTo` and :meth:`bridgedb.distributors.email.autoresponder.SMTPAutoresponder.runChecks`. :type bridgeRequest: :class:`~bridgedb.distributors.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. :type interval: str :param interval: The time period when we got this request. This can be any string, so long as it changes with every period. :type clock: :api:`twisted.internet.task.Clock` :param clock: If given, use the clock to ask what time it is, rather than :api:`time.time`. This should likely only be used for testing. :rtype: :any:`list` or ``None`` :returns: A list of :class:`~bridgedb.bridges.Bridge` for the ``bridgeRequest.client``, if allowed. Otherwise, returns ``None``. """ 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() if clock: now = clock.seconds() 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, filterBySubnet=False) db.setEmailTime(bridgeRequest.client, now) db.commit() return result
def addWebServer(config, distributor): """Set up a web server for HTTP(S)-based bridge distribution. :type config: :class:`bridgedb.persistent.Conf` :param config: A configuration object from :mod:`bridgedb.Main`. Currently, we use these options:: HTTP_UNENCRYPTED_PORT HTTP_UNENCRYPTED_BIND_IP HTTP_USE_IP_FROM_FORWARDED_HEADER HTTPS_N_BRIDGES_PER_ANSWER HTTPS_INCLUDE_FINGERPRINTS HTTPS_KEY_FILE HTTPS_CERT_FILE HTTPS_PORT HTTPS_BIND_IP HTTPS_USE_IP_FROM_FORWARDED_HEADER HTTPS_ROTATION_PERIOD RECAPTCHA_ENABLED RECAPTCHA_PUB_KEY RECAPTCHA_SEC_KEY RECAPTCHA_REMOTEIP GIMP_CAPTCHA_ENABLED GIMP_CAPTCHA_DIR GIMP_CAPTCHA_HMAC_KEYFILE GIMP_CAPTCHA_RSA_KEYFILE SERVER_PUBLIC_FQDN CSP_ENABLED CSP_REPORT_ONLY CSP_INCLUDE_SELF :type distributor: :class:`bridgedb.https.distributor.HTTPSDistributor` :param distributor: A bridge distributor. :raises SystemExit: if the servers cannot be started. :rtype: :api:`twisted.web.server.Site` :returns: A webserver. """ captcha = None fwdHeaders = config.HTTP_USE_IP_FROM_FORWARDED_HEADER numBridges = config.HTTPS_N_BRIDGES_PER_ANSWER fprInclude = config.HTTPS_INCLUDE_FINGERPRINTS logging.info("Starting web servers...") setFQDN(config.SERVER_PUBLIC_FQDN) index = IndexResource() options = OptionsResource() howto = HowtoResource() robots = static.File(os.path.join(TEMPLATE_DIR, 'robots.txt')) assets = static.File(os.path.join(TEMPLATE_DIR, 'assets/')) keys = static.Data(bytes(strings.BRIDGEDB_OPENPGP_KEY), 'text/plain') csp = CSPResource(enabled=config.CSP_ENABLED, includeSelf=config.CSP_INCLUDE_SELF, reportViolations=config.CSP_REPORT_ONLY, useForwardedHeader=fwdHeaders) root = CustomErrorHandlingResource() root.putChild('', index) root.putChild('robots.txt', robots) root.putChild('keys', keys) root.putChild('assets', assets) root.putChild('options', options) root.putChild('howto', howto) root.putChild('maintenance', maintenance) root.putChild('error', resource500) root.putChild(CSPResource.reportURI, csp) if config.RECAPTCHA_ENABLED: publicKey = config.RECAPTCHA_PUB_KEY secretKey = config.RECAPTCHA_SEC_KEY captcha = partial(ReCaptchaProtectedResource, remoteIP=config.RECAPTCHA_REMOTEIP) elif config.GIMP_CAPTCHA_ENABLED: # Get the master HMAC secret key for CAPTCHA challenges, and then # create a new HMAC key from it for use on the server. captchaKey = crypto.getKey(config.GIMP_CAPTCHA_HMAC_KEYFILE) hmacKey = crypto.getHMAC(captchaKey, "Captcha-Key") # Load or create our encryption keys: secretKey, publicKey = crypto.getRSAKey(config.GIMP_CAPTCHA_RSA_KEYFILE) captcha = partial(GimpCaptchaProtectedResource, hmacKey=hmacKey, captchaDir=config.GIMP_CAPTCHA_DIR) if config.HTTPS_ROTATION_PERIOD: count, period = config.HTTPS_ROTATION_PERIOD.split() sched = ScheduledInterval(count, period) else: sched = Unscheduled() bridges = BridgesResource(distributor, sched, numBridges, fwdHeaders, includeFingerprints=fprInclude) if captcha: # Protect the 'bridges' page with a CAPTCHA, if configured to do so: protected = captcha(publicKey=publicKey, secretKey=secretKey, useForwardedHeader=fwdHeaders, protectedResource=bridges) root.putChild('bridges', protected) logging.info("Protecting resources with %s." % captcha.func.__name__) else: root.putChild('bridges', bridges) site = Site(root) site.displayTracebacks = False if config.HTTP_UNENCRYPTED_PORT: # pragma: no cover ip = config.HTTP_UNENCRYPTED_BIND_IP or "" port = config.HTTP_UNENCRYPTED_PORT or 80 try: reactor.listenTCP(port, site, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTP server on %s:%d" % (str(ip), int(port))) if config.HTTPS_PORT: # pragma: no cover ip = config.HTTPS_BIND_IP or "" port = config.HTTPS_PORT or 443 try: from twisted.internet.ssl import DefaultOpenSSLContextFactory factory = DefaultOpenSSLContextFactory(config.HTTPS_KEY_FILE, config.HTTPS_CERT_FILE) reactor.listenSSL(port, site, factory, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started HTTPS server on %s:%d" % (str(ip), int(port))) return site
def prepopulateRings(self): """Prepopulate this distributor's hashrings and subhashrings with bridges. The hashring structure for this distributor is influenced by the ``N_IP_CLUSTERS`` configuration option, as well as the number of ``PROXY_LIST_FILES``. Essentially, :data:`totalSubrings` is set to the specified ``N_IP_CLUSTERS``. All of the ``PROXY_LIST_FILES``, plus the list of Tor Exit relays (downloaded into memory with :script:`get-tor-exits`), are stored in :data:`proxies`, and the latter is added as an additional cluster (such that :data:`totalSubrings` becomes ``N_IP_CLUSTERS + 1``). The number of subhashrings which this :class:`Distributor` has active in its hashring is then :data:`totalSubrings`, where the last cluster is reserved for all :data:`proxies`. As an example, if BridgeDB was configured with ``N_IP_CLUSTERS=4`` and ``PROXY_LIST_FILES=["open-socks-proxies.txt"]``, then the total number of subhashrings is five — four for the "clusters", and one for the :data:`proxies`. Thus, the resulting hashring-subhashring structure would look like: +------------------+---------------------------------------------------+-------------+ | | Directly connecting users | Tor / known | | | | proxy users | +------------------+------------+------------+------------+------------+-------------+ | Clusters | Cluster-1 | Cluster-2 | Cluster-3 | Cluster-4 | Cluster-5 | +==================+============+============+============+============+=============+ | Subhashrings | | | | | | | (total, assigned)| (5,1) | (5,2) | (5,3) | (5,4) | (5,5) | +------------------+------------+------------+------------+------------+-------------+ | Filtered | (5,1)-IPv4 | (5,2)-IPv4 | (5,3)-IPv4 | (5,4)-IPv4 | (5,5)-IPv4 | | Subhashrings | | | | | | | bBy requested +------------+------------+------------+------------+-------------+ | bridge type) | (5,1)-IPv6 | (5,2)-IPv6 | (5,3)-IPv6 | (5,4)-IPv6 | (5,5)-IPv6 | | | | | | | | +------------------+------------+------------+------------+------------+-------------+ The "filtered subhashrings" are essentially filtered copies of their respective subhashring, such that they only contain bridges which support IPv4 or IPv6, respectively. Additionally, the contents of ``(5,1)-IPv4`` and ``(5,1)-IPv6`` sets are *not* disjoint. Thus, in this example, we end up with **10 total subhashrings**. """ logging.info("Prepopulating %s distributor hashrings..." % self.name) for filterFn in [byIPv4, byIPv6]: for subring in range(1, self.totalSubrings + 1): filters = self._buildHashringFilters([filterFn,], subring) key1 = getHMAC(self.key, "Order-Bridges-In-Ring-%d" % subring) ring = BridgeRing(key1, self.answerParameters) # For consistency with previous implementation of this method, # only set the "name" for "clusters" which are for this # distributor's proxies: if subring == self.proxySubring: ring.setName('{0} Proxy Ring'.format(self.name)) self.hashring.addRing(ring, filters, byFilters(filters), populate_from=self.hashring.bridges) logging.info("Bridges allotted for %s distribution: %d" % (self.name, len(self.hashring))) logging.info("\tNum bridges:\tFilter set:") for (ringname, (filterFn, subring)) in self.hashring.filterRings.items(): filterSet = ' '.join(self.hashring.extractFilterNames(ringname)) logging.info("\t%2d bridges\t%s" % (len(subring), filterSet)) logging.info("Total subrings for %s: %d" % (self.name, len(self.hashring.filterRings)))
def addMoatServer(config, distributor): """Set up a web server for moat bridge distribution. :type config: :class:`bridgedb.persistent.Conf` :param config: A configuration object from :mod:`bridgedb.main`. Currently, we use these options:: GIMP_CAPTCHA_DIR SERVER_PUBLIC_FQDN SUPPORTED_TRANSPORTS MOAT_DIST MOAT_DIST_VIA_MEEK_ONLY MOAT_TLS_CERT_FILE MOAT_TLS_KEY_FILE MOAT_SERVER_PUBLIC_ROOT MOAT_HTTPS_IP MOAT_HTTPS_PORT MOAT_HTTP_IP MOAT_HTTP_PORT MOAT_BRIDGES_PER_ANSWER MOAT_TRANSPORT_PREFERENCE_LIST MOAT_USE_IP_FROM_FORWARDED_HEADER MOAT_SKIP_LOOPBACK_ADDRESSES MOAT_ROTATION_PERIOD MOAT_GIMP_CAPTCHA_HMAC_KEYFILE MOAT_GIMP_CAPTCHA_RSA_KEYFILE :type distributor: :class:`bridgedb.distributors.moat.distributor.MoatDistributor` :param distributor: A bridge distributor. :raises SystemExit: if the servers cannot be started. :rtype: :api:`twisted.web.server.Site` :returns: A webserver. """ captcha = None fwdHeaders = config.MOAT_USE_IP_FROM_FORWARDED_HEADER numBridges = config.MOAT_BRIDGES_PER_ANSWER skipLoopback = config.MOAT_SKIP_LOOPBACK_ADDRESSES logging.info("Starting moat servers...") setFQDN(config.SERVER_PUBLIC_FQDN) setRoot(config.MOAT_SERVER_PUBLIC_ROOT) setSupportedTransports(config.SUPPORTED_TRANSPORTS) setPreferredTransports(config.MOAT_TRANSPORT_PREFERENCE_LIST) # Get the master HMAC secret key for CAPTCHA challenges, and then # create a new HMAC key from it for use on the server. captchaKey = crypto.getKey(config.MOAT_GIMP_CAPTCHA_HMAC_KEYFILE) hmacKey = crypto.getHMAC(captchaKey, "Moat-Captcha-Key") # Load or create our encryption keys: secretKey, publicKey = crypto.getRSAKey(config.MOAT_GIMP_CAPTCHA_RSA_KEYFILE) sched = Unscheduled() if config.MOAT_ROTATION_PERIOD: count, period = config.MOAT_ROTATION_PERIOD.split() sched = ScheduledInterval(count, period) sitePublicDir = getRoot() meek = CustomErrorHandlingResource() moat = CustomErrorHandlingResource() fetch = CaptchaFetchResource(hmacKey, publicKey, secretKey, config.GIMP_CAPTCHA_DIR, fwdHeaders, skipLoopback) check = CaptchaCheckResource(distributor, sched, numBridges, hmacKey, publicKey, secretKey, fwdHeaders, skipLoopback) moat.putChild("fetch", fetch) moat.putChild("check", check) meek.putChild("moat", moat) root = CustomErrorHandlingResource() root.putChild("meek", meek) root.putChild("moat", moat) site = Site(root) site.displayTracebacks = False if config.MOAT_HTTP_PORT: # pragma: no cover ip = config.MOAT_HTTP_IP or "" port = config.MOAT_HTTP_PORT or 80 try: reactor.listenTCP(port, site, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started Moat HTTP server on %s:%d" % (str(ip), int(port))) if config.MOAT_HTTPS_PORT: # pragma: no cover ip = config.MOAT_HTTPS_IP or "" port = config.MOAT_HTTPS_PORT or 443 try: from twisted.internet.ssl import DefaultOpenSSLContextFactory factory = DefaultOpenSSLContextFactory(config.MOAT_TLS_KEY_FILE, config.MOAT_TLS_CERT_FILE) reactor.listenSSL(port, site, factory, interface=ip) except CannotListenError as error: raise SystemExit(error) logging.info("Started Moat TLS server on %s:%d" % (str(ip), int(port))) return site
def getBridges(self, bridgeRequest, interval): """Return a list of bridges to give to a user. :type bridgeRequest: :class:`bridgedb.https.request.HTTPSBridgeRequest` :param bridgeRequest: A :class:`~bridgedb.bridgerequest.BridgeRequestBase` with the :data:`~bridgedb.bridgerequest.BridgeRequestBase.client` attribute set to a string containing the client's IP address. :param str interval: The time period when we got this request. This can be any string, so long as it changes with every period. :rtype: list :return: A list of :class:`~bridgedb.Bridges.Bridge`s to include in the response. See :meth:`bridgedb.https.server.WebResourceBridges.getBridgeRequestAnswer` for an example of how this is used. """ logging.info("Attempting to get bridges for %s..." % bridgeRequest.client) if not len(self.hashring): logging.warn("Bailing! Hashring has zero bridges!") return [] usingProxy = False # First, check if the client's IP is one of the known :data:`proxies`: if bridgeRequest.client in self.proxies: # The tag is a tag applied to a proxy IP address when it is added # to the bridgedb.proxy.ProxySet. For Tor Exit relays, the default # is 'exit_relay'. For other proxies loaded from the # PROXY_LIST_FILES config option, the default tag is the full # filename that the IP address originally came from. usingProxy = True tag = self.proxies.getTag(bridgeRequest.client) logging.info("Client was from known proxy (tag: %s): %s" % (tag, bridgeRequest.client)) subnet = self.getSubnet(bridgeRequest.client, usingProxy) subring = self.mapSubnetToSubring(subnet, usingProxy) position = self.mapClientToHashringPosition(interval, subnet) filters = self._buildHashringFilters(bridgeRequest.filters, subring) logging.debug("Client request within time interval: %s" % interval) logging.debug("Assigned client to subhashring %d/%d" % (subring, self.totalSubrings)) logging.debug("Assigned client to subhashring position: %s" % position.encode('hex')) logging.debug("Total bridges: %d" % len(self.hashring)) logging.debug("Bridge filters: %s" % ' '.join([x.func_name for x in filters])) # Check wheth we have a cached copy of the hashring: if filters in self.hashring.filterRings.keys(): logging.debug("Cache hit %s" % filters) _, ring = self.hashring.filterRings[filters] # Otherwise, construct a new hashring and populate it: else: logging.debug("Cache miss %s" % filters) key1 = getHMAC(self.key, "Order-Bridges-In-Ring-%d" % subring) ring = BridgeRing(key1, self.answerParameters) self.hashring.addRing(ring, filters, byFilters(filters), populate_from=self.hashring.bridges) # Determine the appropriate number of bridges to give to the client: returnNum = self.bridgesPerResponse(ring) answer = ring.getBridges(position, returnNum) return answer