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(a)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.