[tor-commits] [bridgedb] branch main updated: Respond with dummy bridges if the request is not comming from the shim

gitolite role git at cupani.torproject.org
Thu Jul 21 10:37:52 UTC 2022


This is an automated email from the git hooks/post-receive script.

meskio pushed a commit to branch main
in repository bridgedb.

The following commit(s) were added to refs/heads/main by this push:
     new 3ab5afe  Respond with dummy bridges if the request is not comming from the shim
3ab5afe is described below

commit 3ab5afee2bc37e87499b5f704d715a0b7b38783f
Author: meskio <meskio at torproject.org>
AuthorDate: Wed Jul 13 15:57:57 2022 +0200

    Respond with dummy bridges if the request is not comming from the shim
    
    Check if the shim-token header is present and valid on moat requests, to
    make sure the request is comming from the shim. If not respond with
    dummy bridges comming from a configured file.
---
 bridgedb.conf                             |  8 +++++
 bridgedb/bridges.py                       | 37 +++++++++++++++++++++
 bridgedb/configure.py                     |  9 ++++++
 bridgedb/distributors/moat/distributor.py | 53 +++++++++++++++++++++++++++++++
 bridgedb/distributors/moat/server.py      | 37 ++++++++++++++++++---
 bridgedb/main.py                          |  2 ++
 bridgedb/test/moat_helpers.py             |  3 +-
 bridgedb/test/test_bridges.py             | 22 +++++++++++++
 bridgedb/test/test_main.py                |  1 +
 9 files changed, 167 insertions(+), 5 deletions(-)

diff --git a/bridgedb.conf b/bridgedb.conf
index fc56db8..829ff76 100644
--- a/bridgedb.conf
+++ b/bridgedb.conf
@@ -424,6 +424,14 @@ MOAT_ROTATION_PERIOD = "3 hours"
 MOAT_GIMP_CAPTCHA_HMAC_KEYFILE = 'moat_captcha_hmac_key'
 MOAT_GIMP_CAPTCHA_RSA_KEYFILE = 'moat_captcha_rsa_key'
 
+# The file containing the token that should be included on the header
+# 'shim-token' on each request or dummy bridges will be provided.
+MOAT_SHIM_TOKEN_FILE = ""
+
+# The file containing bridges to be distributed for requests without a valid
+# shim-token. The file should contain one bridge per line.
+MOAT_DUMMY_BRIDGES_FILE = ""
+
 #-------------------------------
 # HTTP(S) Distribution Options  \
 #------------------------------------------------------------------------------
diff --git a/bridgedb/bridges.py b/bridgedb/bridges.py
index 6cd3057..5f95040 100644
--- a/bridgedb/bridges.py
+++ b/bridgedb/bridges.py
@@ -1583,6 +1583,43 @@ class Bridge(BridgeBackwardsCompatibility):
         for country in resource["blocked_in"]:
             self.setBlockedIn(country)
 
+    def updateFromBridgeLine(self, bridge_line):
+        """Update this bridge's attributes from a bridge line
+
+        :type bridge_line: str
+        """
+        parts = bridge_line.split(" ")
+        if len(parts) < 2:
+            raise MalformedBridgeInfo("Not a valid bridge line: %s" % (bridge_line,))
+        elif len(parts) == 2:
+            tpe = ""
+            addrstr = parts[0]
+            self.fingerprint = parts[1]
+            arguments = {}
+        else:
+            tpe = parts[0]
+            self.fingerprint = parts[2]
+            addrstr = parts[1]
+            arguments = dict([arg.split("=") for arg in parts[3:]])
+
+        addr = addrstr.split(":")
+        self.address = addr[0]
+        if not self.address or len(addr) != 2:
+            raise MalformedBridgeInfo("Invalid address for a bridge (%s): %s" % (self.fingerprint, addrstr))
+        port = int(addr[1])
+
+        if not tpe:
+            self.orPort = port
+        else:
+            transport = PluggableTransport(
+                    fingerprint=self.fingerprint,
+                    methodname=tpe,
+                    address=addr[0],
+                    port=port,
+                    arguments=arguments
+                    )
+            self.transports = [transport]
+
     def updateFromNetworkStatus(self, descriptor, ignoreNetworkstatus=False):
         """Update this bridge's attributes from a parsed networkstatus
         document.
diff --git a/bridgedb/configure.py b/bridgedb/configure.py
index 8befc78..fd997e0 100644
--- a/bridgedb/configure.py
+++ b/bridgedb/configure.py
@@ -163,6 +163,15 @@ def loadConfig(configFile=None, configCls=None):
     else:
         config.RDSYS_TOKEN = ""
 
+    if os.path.isfile(config.MOAT_SHIM_TOKEN_FILE):
+        with open(config.MOAT_SHIM_TOKEN_FILE) as f:
+            setattr(config, "MOAT_SHIM_TOKEN", f.read())
+        if not os.path.isfile(config.MOAT_DUMMY_BRIDGES_FILE):
+            logging.warning("The dummy bridges file '%s' doesn't exist" % (config.MOAT_DUMMY_BRIDGES_FILE,))
+    else:
+        config.MOAT_SHIM_TOKEN = None
+        logging.info("No shim-token provided, moat will answer each request with bridge authority bridges.")
+
     return config
 
 
diff --git a/bridgedb/distributors/moat/distributor.py b/bridgedb/distributors/moat/distributor.py
index c4687eb..82a6d3a 100644
--- a/bridgedb/distributors/moat/distributor.py
+++ b/bridgedb/distributors/moat/distributor.py
@@ -19,7 +19,11 @@ A Distributor that hands out bridges through a web interface.
 .. inheritance-diagram:: MoatDistributor
     :parts: 1
 """
+import logging
 
+from bridgedb.bridgerings import BridgeRing
+from bridgedb.bridges import Bridge, MalformedBridgeInfo
+from bridgedb.crypto import getHMAC
 from bridgedb.distributors.https.distributor import HTTPSDistributor
 
 class MoatDistributor(HTTPSDistributor):
@@ -63,3 +67,52 @@ class MoatDistributor(HTTPSDistributor):
         """
         super(MoatDistributor, self).__init__(totalSubrings, key, proxies,
                                               answerParameters)
+
+    def prepopulateRings(self):
+        """Prepopulate this distributor's hashrings and subhashrings with
+        bridges.
+        """
+        super(MoatDistributor, self).prepopulateRings()
+        dummyKey = getHMAC(self.key, "dummy-bridges")
+        self.dummyHashring = BridgeRing(dummyKey, self.answerParameters)
+
+    def loadDummyBridges(self, dummyBridgesFile):
+        """Load dummy bridges from a file
+        """
+        with open(dummyBridgesFile) as f:
+            bridge_line = f.readline()
+            bridge = Bridge()
+            try:
+                bridge.updateFromBridgeLine(bridge_line)
+            except MalformedBridgeInfo as e:
+                logging.warning("Got a malformed dummy bridge: %s" % e)
+            self.dummyHashring.insert(bridge)
+
+    def getBridges(self, bridgeRequest, interval, dummyBridges=False):
+        """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.
+        :param bool dummyBridges: if it should provide dummyBridges or actual bridges from
+            from the bridge authority.
+        :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.
+        """
+        if not dummyBridges:
+            return super(MoatDistributor, self).getBridges(bridgeRequest, interval)
+
+        if not len(self.dummyHashring):
+            logging.warn("Bailing! Hashring has zero bridges!")
+            return []
+
+        usingProxy = False
+        subnet = self.getSubnet(bridgeRequest.client, usingProxy)
+        position = self.mapClientToHashringPosition(interval, subnet)
+        return self.dummyHashring.getBridges(position, self._bridgesPerResponseMax, filterBySubnet=True)
diff --git a/bridgedb/distributors/moat/server.py b/bridgedb/distributors/moat/server.py
index fe03fc4..fe59b99 100644
--- a/bridgedb/distributors/moat/server.py
+++ b/bridgedb/distributors/moat/server.py
@@ -492,7 +492,8 @@ class CaptchaCheckResource(CaptchaResource):
 
     def __init__(self, distributor, schedule, N=1,
                  hmacKey=None, publicKey=None, secretKey=None,
-                 useForwardedHeader=True, skipInvalid=False):
+                 useForwardedHeader=True, skipInvalid=False,
+                 shim_token=None):
         """Create a new resource for checking CAPTCHA solutions and returning
         bridges to a client.
 
@@ -507,6 +508,8 @@ class CaptchaCheckResource(CaptchaResource):
             X-Forwarded-For header instead of the source IP address.
         :param bool skipInvalid: Skip invalid (e.g., loopback, private) addresses
             when parsing the X-Forwarded-For header.
+        :param bytes shim_token: the token that should be included on the header
+            'shim-token' on each request or dummy bridges will be provided.
         """
         CaptchaResource.__init__(self, hmacKey, publicKey, secretKey,
                                  useForwardedHeader)
@@ -514,6 +517,7 @@ class CaptchaCheckResource(CaptchaResource):
         self.schedule = schedule
         self.nBridgesToGive = N
         self.useForwardedHeader = useForwardedHeader
+        self.shim_token = shim_token
 
     def createBridgeRequest(self, ip, data):
         """Create an appropriate :class:`MoatBridgeRequest` from the ``data``
@@ -539,9 +543,30 @@ class CaptchaCheckResource(CaptchaResource):
 
         return bridgeRequest
 
-    def getBridges(self, bridgeRequest):
+    def getBridges(self, bridgeRequest, dummyBridges=False):
         """Get bridges for a client's HTTP request.
 
+        :type bridgeRequest: :class:`MoatBridgeRequest`
+        :param bridgeRequest: A valid bridge request object with pre-generated
+            filters (as returned by :meth:`createBridgeRequest`).
+        :param bool dummyBridges: if it should provide dummyBridges or actual bridges from
+            from the bridge authority.
+        :rtype: list
+        :return: A list of :class:`~bridgedb.bridges.Bridge`s.
+        """
+        bridges = list()
+        interval = self.schedule.intervalStart(time.time())
+
+        logging.debug("Replying to JSON API request from %s." % bridgeRequest.client)
+
+        if bridgeRequest.isValid():
+            bridges = self.distributor.getBridges(bridgeRequest, interval, dummyBridges)
+
+        return bridges
+
+    def getDummyBridges(self, bridgeRequest):
+        """Get dummy bridges for a client's HTTP request.
+
         :type bridgeRequest: :class:`MoatBridgeRequest`
         :param bridgeRequest: A valid bridge request object with pre-generated
             filters (as returned by :meth:`createBridgeRequest`).
@@ -737,7 +762,9 @@ class CaptchaCheckResource(CaptchaResource):
         if valid:
             qrcode = None
             bridgeRequest = self.createBridgeRequest(clientIP, client_data)
-            bridges = self.getBridges(bridgeRequest)
+            bridges = []
+            dummyBridges = self.shim_token and request.getHeader('shim-token') == self.shim_token
+            bridges = self.getBridges(bridgeRequest, dummyBridges)
             bridgeLines = self.getBridgeLines(bridgeRequest, bridges)
             moatMetrix.recordValidMoatRequest(request)
 
@@ -810,6 +837,7 @@ def addMoatServer(config, distributor):
     fwdHeaders = config.MOAT_USE_IP_FROM_FORWARDED_HEADER
     numBridges = config.MOAT_BRIDGES_PER_ANSWER
     skipInvalid = config.MOAT_SKIP_LOOPBACK_ADDRESSES
+    shim_token = config.MOAT_SHIM_TOKEN
 
     logging.info("Starting moat servers...")
 
@@ -839,7 +867,8 @@ def addMoatServer(config, distributor):
                                  fwdHeaders, skipInvalid)
     check = CaptchaCheckResource(distributor, sched, numBridges,
                                  hmacKey, publicKey, secretKey,
-                                 fwdHeaders, skipInvalid)
+                                 fwdHeaders, skipInvalid,
+                                 shim_token)
 
     moat.putChild(b"fetch", fetch)
     moat.putChild(b"check", check)
diff --git a/bridgedb/main.py b/bridgedb/main.py
index 72c1f0e..1691c96 100644
--- a/bridgedb/main.py
+++ b/bridgedb/main.py
@@ -101,6 +101,8 @@ def load(cfg, proxyList, key):
             proxyList,
             answerParameters=ringParams)
         moatDistributor.prepopulateRings()
+        if cfg.MOAT_DUMMY_BRIDGES_FILE:
+            moatDistributor.loadDummyBridges(cfg.MOAT_DUMMY_BRIDGES_FILE)
         rdsys.start_stream("moat", cfg.RDSYS_TOKEN, cfg.RDSYS_ADDRESS, moatDistributor.hashring)
 
     # As appropriate, create an IP-based distributor.
diff --git a/bridgedb/test/moat_helpers.py b/bridgedb/test/moat_helpers.py
index 236b529..452c207 100644
--- a/bridgedb/test/moat_helpers.py
+++ b/bridgedb/test/moat_helpers.py
@@ -69,6 +69,7 @@ MOAT_N_IP_CLUSTERS = %r
 MOAT_ROTATION_PERIOD = %r
 MOAT_GIMP_CAPTCHA_HMAC_KEYFILE = %r
 MOAT_GIMP_CAPTCHA_RSA_KEYFILE = %r
+MOAT_SHIM_TOKEN = ""
 """ % (GIMP_CAPTCHA_DIR,
        SERVER_PUBLIC_FQDN,
        SUPPORTED_TRANSPORTS,
@@ -107,7 +108,7 @@ class DummyMoatDistributor(object):
     _bridge_class = util.DummyBridge
     _bridgesPerResponseMin = 3
 
-    def getBridges(self, bridgeRequest=None, epoch=None):
+    def getBridges(self, bridgeRequest=None, epoch=None, dummyBridges=False):
         """Needed because it's called in
         :meth:`BridgesResource.getBridgeRequestAnswer`."""
         return [self._bridge_class() for _ in range(self._bridgesPerResponseMin)]
diff --git a/bridgedb/test/test_bridges.py b/bridgedb/test/test_bridges.py
index 82d9584..61a11ec 100644
--- a/bridgedb/test/test_bridges.py
+++ b/bridgedb/test/test_bridges.py
@@ -1840,6 +1840,28 @@ class BridgeTests(unittest.TestCase):
         self.assertNotIn('scramblesuit',
                          [pt.methodname for pt in self.bridge.transports])
 
+    def test_Bridge_updateFromBridgeLine_vanilla(self):
+        self.bridge.updateFromBridgeLine("1.1.1.1:1111 2C3225C4805331025E211F4B6E5BF45C333FDD2C")
+
+        self.assertEqual(self.bridge.fingerprint, "2C3225C4805331025E211F4B6E5BF45C333FDD2C") 
+        self.assertEqual(str(self.bridge.address), "1.1.1.1")
+        self.assertEqual(self.bridge.orPort, 1111)
+        self.assertEqual(len(self.bridge.transports), 0)
+
+    def test_Bridge_updateFromBridgeLine_obfs4(self):
+        self.bridge.updateFromBridgeLine("obfs4 1.1.1.1:1111 2C3225C4805331025E211F4B6E5BF45C333FDD2C cert=UXj/cWm0qolGrROYpkl0UyD/7PEhzkoZkZXrOpjRKwImvkpQZwmF0nSzBXfyfbT9afBZEw iat-mode=1")
+
+        self.assertEqual(self.bridge.fingerprint, "2C3225C4805331025E211F4B6E5BF45C333FDD2C") 
+        self.assertEqual(len(self.bridge.transports), 1)
+
+        pt = self.bridge.transports[0]
+        self.assertEqual(pt.fingerprint, "2C3225C4805331025E211F4B6E5BF45C333FDD2C") 
+        self.assertEqual(pt.methodname, "obfs4")
+        self.assertEqual(str(pt.address), "1.1.1.1")
+        self.assertEqual(pt.port, 1111)
+        self.assertEqual(pt.arguments["iat-mode"], "1")
+        self.assertEqual(pt.arguments["cert"], "UXj/cWm0qolGrROYpkl0UyD/7PEhzkoZkZXrOpjRKwImvkpQZwmF0nSzBXfyfbT9afBZEw")
+
     def test_runsVersions(self):
         """Calling runsVersions() should tell us if a bridge is running any of
         the given versions.
diff --git a/bridgedb/test/test_main.py b/bridgedb/test/test_main.py
index 71738b4..e3d319e 100644
--- a/bridgedb/test/test_main.py
+++ b/bridgedb/test/test_main.py
@@ -337,6 +337,7 @@ HTTPS_SHARE = 10
 EMAIL_SHARE = 5
 RESERVED_SHARE = 2
 RDSYS_TOKEN_FILE = "rdsys-token"
+MOAT_SHIM_TOKEN_FILE = "shim-token"
 """
         configFile = self._writeConfig(config)
         

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the tor-commits mailing list