Loading README.rst +5 −4 Original line number Original line Diff line number Diff line Loading @@ -559,13 +559,14 @@ server is unable to distribute the requested Bridges, the server responds ``200 OK`` with the following JSON:: OK`` with the following JSON:: { { "error": { "errors": [{ "id": "1", "id": "6", "type": "moat-bridges", "version": "0.1.0", "code": "404", "code": "404", "status": "Not Found", "status": "Not Found", "title": "Could not fetch the type of bridges you requested", "detail": "DETAILS", "detail": "DETAILS", } }] } } where: where: Loading bridgedb/bridgerequest.py +9 −0 Original line number Original line Diff line number Diff line Loading @@ -117,6 +117,15 @@ class BridgeRequestBase(object): self.client = 'default' self.client = 'default' self.valid = False self.valid = False def __str__(self): """Return a human-readable string describing this bridge request.""" return "%s(ipVersion=%d, transports=%s, notBlockedIn=%s, valid=%s)" % \ (self.__class__.__name__, self.ipVersion, self.transports, self.notBlockedIn, self.valid) @property @property def ipVersion(self): def ipVersion(self): """The IP version of bridge addresses to distribute to the client. """The IP version of bridge addresses to distribute to the client. Loading bridgedb/distributors/moat/server.py +60 −13 Original line number Original line Diff line number Diff line Loading @@ -209,6 +209,7 @@ class JsonAPIErrorResource(JsonAPIResource): resource403 = JsonAPIErrorResource(code=403, status="Forbidden") resource403 = JsonAPIErrorResource(code=403, status="Forbidden") resource404 = JsonAPIErrorResource(code=404, status="Not Found") resource406 = JsonAPIErrorResource(code=406, status="Not Acceptable") resource406 = JsonAPIErrorResource(code=406, status="Not Acceptable") resource415 = JsonAPIErrorResource(code=415, status="Unsupported Media Type") resource415 = JsonAPIErrorResource(code=415, status="Unsupported Media Type") resource419 = JsonAPIErrorResource(code=419, status="No You're A Teapot") resource419 = JsonAPIErrorResource(code=419, status="No You're A Teapot") Loading Loading @@ -497,21 +498,21 @@ class CaptchaCheckResource(CaptchaResource): self.nBridgesToGive = N self.nBridgesToGive = N self.useForwardedHeader = useForwardedHeader self.useForwardedHeader = useForwardedHeader def getBridgeLines(self, ip, data): def createBridgeRequest(self, ip, data): """Get bridge lines for a client's HTTP request. """Create an appropriate :class:`MoatBridgeRequest` from the ``data`` of a client's request. :param str ip: The client's IP address. :param str ip: The client's IP address. :param dict data: The decoded JSON API data from the client's request. :param dict data: The decoded JSON API data from the client's request. :rtype: list or None :rtype: :class:`MoatBridgeRequest` :returns: A list of bridge lines. :returns: An object which specifies the filters for retreiving the type of bridges that the client requested. """ """ bridgeLines = None logging.debug("Creating moat bridge request object for %s." % ip) interval = self.schedule.intervalStart(time.time()) logging.debug("Replying to JSON API request from %s." % ip) bridgeRequest = MoatBridgeRequest() if ip and data: if ip and data: bridgeRequest = MoatBridgeRequest() bridgeRequest.client = IPAddress(ip) bridgeRequest.client = IPAddress(ip) bridgeRequest.isValid(True) bridgeRequest.isValid(True) bridgeRequest.withIPversion() bridgeRequest.withIPversion() Loading @@ -519,6 +520,23 @@ class CaptchaCheckResource(CaptchaResource): bridgeRequest.withoutBlockInCountry(data) bridgeRequest.withoutBlockInCountry(data) bridgeRequest.generateFilters() bridgeRequest.generateFilters() return bridgeRequest def getBridgeLines(self, bridgeRequest): """Get bridge lines 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`). :rtype: list :returns: A list of bridge lines. """ bridgeLines = 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) bridges = self.distributor.getBridges(bridgeRequest, interval) bridgeLines = [replaceControlChars(bridge.getBridgeLine(bridgeRequest)) bridgeLines = [replaceControlChars(bridge.getBridgeLine(bridgeRequest)) for bridge in bridges] for bridge in bridges] Loading Loading @@ -599,17 +617,34 @@ class CaptchaCheckResource(CaptchaResource): return valid return valid def failureResponse(self, id, request): def failureResponse(self, id, request, bridgeRequest=None): """Respond with status code "419 No You're A Teapot".""" """Respond with status code "419 No You're A Teapot" if the captcha error_response = resource419 verification failed, or status code "404 Not Found" if there error_response.type = 'moat-bridges' were none of the type of bridges requested. :param int id: The JSON API "id" field of the ``JsonAPIErrorResource`` which should be returned. :type request: :api:`twisted.web.http.Request` :param request: The current request we're handling. :type bridgeRequest: :class:`MoatBridgeRequest` :param bridgeRequest: A valid bridge request object with pre-generated filters (as returned by :meth:`createBridgeRequest`). """ if id == 4: if id == 4: error_response = resource419 error_response.id = 4 error_response.id = 4 error_response.detail = "The CAPTCHA solution was incorrect." error_response.detail = "The CAPTCHA solution was incorrect." elif id == 5: elif id == 5: error_response = resource419 error_response.id = 5 error_response.id = 5 error_response.detail = "The CAPTCHA challenge timed out." error_response.detail = "The CAPTCHA challenge timed out." elif id == 6: error_response = resource404 error_response.id = 6 error_response.detail = ("No bridges available to fulfill " "request: %s.") % bridgeRequest error_response.type = 'moat-bridges' return error_response.render(request) return error_response.render(request) Loading Loading @@ -665,7 +700,19 @@ class CaptchaCheckResource(CaptchaResource): if valid: if valid: qrcode = None qrcode = None bridgeLines = self.getBridgeLines(clientIP, client_data) bridgeRequest = self.createBridgeRequest(clientIP, client_data) bridgeLines = self.getBridgeLines(bridgeRequest) # If we can only return less than the configured # MOAT_BRIDGES_PER_ANSWER then log a warning. if len(bridgeLines) < self.nBridgesToGive: logging.warn(("Not enough bridges of the type specified to " "fulfill the following request: %s") % bridgeRequest) # If we have no bridges at all to give to the client, then # return a JSON API 404 error. if not bridgeLines: return self.failureResponse(6, request) if include_qrcode: if include_qrcode: qrjpeg = generateQR(bridgeLines) qrjpeg = generateQR(bridgeLines) Loading bridgedb/test/test_distributors_moat_server.py +52 −3 Original line number Original line Diff line number Diff line Loading @@ -581,6 +581,14 @@ class CaptchaFetchResourceTests(unittest.TestCase): self.assert_data_is_ok(decoded) self.assert_data_is_ok(decoded) class MockCaptchaCheckResource(server.CaptchaCheckResource): """A mocked :class:`server.CaptchaCheckResource` whose ``getBridgeLines`` method returns no bridges. """ def getBridgeLines(self, bridgeRequest): return list() class CaptchaCheckResourceTests(unittest.TestCase): class CaptchaCheckResourceTests(unittest.TestCase): """Tests for :class:`bridgedb.distributors.moat.server.CaptchaCheckResource`.""" """Tests for :class:`bridgedb.distributors.moat.server.CaptchaCheckResource`.""" Loading Loading @@ -611,6 +619,14 @@ class CaptchaCheckResourceTests(unittest.TestCase): "iz_PdOD2GIGPeclwiHAWM1pOS4cQVsTQR_z4ojZbpLiSp35n4Qbb11YOoreovZzlbS" "iz_PdOD2GIGPeclwiHAWM1pOS4cQVsTQR_z4ojZbpLiSp35n4Qbb11YOoreovZzlbS" "7W38rAsTirkdeugcNq82AxKP3phEkyRcw--CzV") "7W38rAsTirkdeugcNq82AxKP3phEkyRcw--CzV") def mock_getBridgeLines(self): self.resource = MockCaptchaCheckResource(self.distributor, self.schedule, 3, self.hmacKey, self.publicKey, self.secretKey, useForwardedHeader=False) def create_POST_with_data(self, data): def create_POST_with_data(self, data): request = DummyRequest([self.pagename]) request = DummyRequest([self.pagename]) request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json') request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json') Loading Loading @@ -826,13 +842,24 @@ class CaptchaCheckResourceTests(unittest.TestCase): self.assertEqual(error['type'], "moat-bridges") self.assertEqual(error['type'], "moat-bridges") self.assertEqual(error['id'], 4) self.assertEqual(error['id'], 4) def test_createBridgeRequest(self): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) encoded_content = request.content.read() content = json.loads(encoded_content)['data'][0] bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', content) self.assertTrue(bridgeRequest.isValid()) def test_getBridgeLines(self): def test_getBridgeLines(self): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) encoded_content = request.content.read() encoded_content = request.content.read() content = json.loads(encoded_content)['data'][0] content = json.loads(encoded_content)['data'][0] bridgelines = self.resource.getBridgeLines('3.3.3.3', content) bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', content) bridgelines = self.resource.getBridgeLines(bridgeRequest) self.assertTrue(bridgelines) self.assertTrue(bridgelines) Loading @@ -840,9 +867,31 @@ class CaptchaCheckResourceTests(unittest.TestCase): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) bridgelines = self.resource.getBridgeLines('3.3.3.3', None) bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', None) bridgelines = self.resource.getBridgeLines(bridgeRequest) self.assertFalse(bridgeRequest.isValid()) self.assertEqual(len(bridgelines), 0) def test_failureResponse_no_bridges(self): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) encoded_content = request.content.read() content = json.loads(encoded_content)['data'][0] bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', content) response = self.resource.failureResponse(6, request, bridgeRequest) self.assertIn("No bridges available", response) def test_render_POST_no_bridges(self): self.mock_getBridgeLines() request = self.create_valid_POST_make_new_challenge() response = self.resource.render(request) self.assertIsNone(bridgelines) self.assertIn("No bridges available", response) def test_render_POST_unexpired(self): def test_render_POST_unexpired(self): request = self.create_valid_POST_make_new_challenge() request = self.create_valid_POST_make_new_challenge() Loading Loading
README.rst +5 −4 Original line number Original line Diff line number Diff line Loading @@ -559,13 +559,14 @@ server is unable to distribute the requested Bridges, the server responds ``200 OK`` with the following JSON:: OK`` with the following JSON:: { { "error": { "errors": [{ "id": "1", "id": "6", "type": "moat-bridges", "version": "0.1.0", "code": "404", "code": "404", "status": "Not Found", "status": "Not Found", "title": "Could not fetch the type of bridges you requested", "detail": "DETAILS", "detail": "DETAILS", } }] } } where: where: Loading
bridgedb/bridgerequest.py +9 −0 Original line number Original line Diff line number Diff line Loading @@ -117,6 +117,15 @@ class BridgeRequestBase(object): self.client = 'default' self.client = 'default' self.valid = False self.valid = False def __str__(self): """Return a human-readable string describing this bridge request.""" return "%s(ipVersion=%d, transports=%s, notBlockedIn=%s, valid=%s)" % \ (self.__class__.__name__, self.ipVersion, self.transports, self.notBlockedIn, self.valid) @property @property def ipVersion(self): def ipVersion(self): """The IP version of bridge addresses to distribute to the client. """The IP version of bridge addresses to distribute to the client. Loading
bridgedb/distributors/moat/server.py +60 −13 Original line number Original line Diff line number Diff line Loading @@ -209,6 +209,7 @@ class JsonAPIErrorResource(JsonAPIResource): resource403 = JsonAPIErrorResource(code=403, status="Forbidden") resource403 = JsonAPIErrorResource(code=403, status="Forbidden") resource404 = JsonAPIErrorResource(code=404, status="Not Found") resource406 = JsonAPIErrorResource(code=406, status="Not Acceptable") resource406 = JsonAPIErrorResource(code=406, status="Not Acceptable") resource415 = JsonAPIErrorResource(code=415, status="Unsupported Media Type") resource415 = JsonAPIErrorResource(code=415, status="Unsupported Media Type") resource419 = JsonAPIErrorResource(code=419, status="No You're A Teapot") resource419 = JsonAPIErrorResource(code=419, status="No You're A Teapot") Loading Loading @@ -497,21 +498,21 @@ class CaptchaCheckResource(CaptchaResource): self.nBridgesToGive = N self.nBridgesToGive = N self.useForwardedHeader = useForwardedHeader self.useForwardedHeader = useForwardedHeader def getBridgeLines(self, ip, data): def createBridgeRequest(self, ip, data): """Get bridge lines for a client's HTTP request. """Create an appropriate :class:`MoatBridgeRequest` from the ``data`` of a client's request. :param str ip: The client's IP address. :param str ip: The client's IP address. :param dict data: The decoded JSON API data from the client's request. :param dict data: The decoded JSON API data from the client's request. :rtype: list or None :rtype: :class:`MoatBridgeRequest` :returns: A list of bridge lines. :returns: An object which specifies the filters for retreiving the type of bridges that the client requested. """ """ bridgeLines = None logging.debug("Creating moat bridge request object for %s." % ip) interval = self.schedule.intervalStart(time.time()) logging.debug("Replying to JSON API request from %s." % ip) bridgeRequest = MoatBridgeRequest() if ip and data: if ip and data: bridgeRequest = MoatBridgeRequest() bridgeRequest.client = IPAddress(ip) bridgeRequest.client = IPAddress(ip) bridgeRequest.isValid(True) bridgeRequest.isValid(True) bridgeRequest.withIPversion() bridgeRequest.withIPversion() Loading @@ -519,6 +520,23 @@ class CaptchaCheckResource(CaptchaResource): bridgeRequest.withoutBlockInCountry(data) bridgeRequest.withoutBlockInCountry(data) bridgeRequest.generateFilters() bridgeRequest.generateFilters() return bridgeRequest def getBridgeLines(self, bridgeRequest): """Get bridge lines 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`). :rtype: list :returns: A list of bridge lines. """ bridgeLines = 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) bridges = self.distributor.getBridges(bridgeRequest, interval) bridgeLines = [replaceControlChars(bridge.getBridgeLine(bridgeRequest)) bridgeLines = [replaceControlChars(bridge.getBridgeLine(bridgeRequest)) for bridge in bridges] for bridge in bridges] Loading Loading @@ -599,17 +617,34 @@ class CaptchaCheckResource(CaptchaResource): return valid return valid def failureResponse(self, id, request): def failureResponse(self, id, request, bridgeRequest=None): """Respond with status code "419 No You're A Teapot".""" """Respond with status code "419 No You're A Teapot" if the captcha error_response = resource419 verification failed, or status code "404 Not Found" if there error_response.type = 'moat-bridges' were none of the type of bridges requested. :param int id: The JSON API "id" field of the ``JsonAPIErrorResource`` which should be returned. :type request: :api:`twisted.web.http.Request` :param request: The current request we're handling. :type bridgeRequest: :class:`MoatBridgeRequest` :param bridgeRequest: A valid bridge request object with pre-generated filters (as returned by :meth:`createBridgeRequest`). """ if id == 4: if id == 4: error_response = resource419 error_response.id = 4 error_response.id = 4 error_response.detail = "The CAPTCHA solution was incorrect." error_response.detail = "The CAPTCHA solution was incorrect." elif id == 5: elif id == 5: error_response = resource419 error_response.id = 5 error_response.id = 5 error_response.detail = "The CAPTCHA challenge timed out." error_response.detail = "The CAPTCHA challenge timed out." elif id == 6: error_response = resource404 error_response.id = 6 error_response.detail = ("No bridges available to fulfill " "request: %s.") % bridgeRequest error_response.type = 'moat-bridges' return error_response.render(request) return error_response.render(request) Loading Loading @@ -665,7 +700,19 @@ class CaptchaCheckResource(CaptchaResource): if valid: if valid: qrcode = None qrcode = None bridgeLines = self.getBridgeLines(clientIP, client_data) bridgeRequest = self.createBridgeRequest(clientIP, client_data) bridgeLines = self.getBridgeLines(bridgeRequest) # If we can only return less than the configured # MOAT_BRIDGES_PER_ANSWER then log a warning. if len(bridgeLines) < self.nBridgesToGive: logging.warn(("Not enough bridges of the type specified to " "fulfill the following request: %s") % bridgeRequest) # If we have no bridges at all to give to the client, then # return a JSON API 404 error. if not bridgeLines: return self.failureResponse(6, request) if include_qrcode: if include_qrcode: qrjpeg = generateQR(bridgeLines) qrjpeg = generateQR(bridgeLines) Loading
bridgedb/test/test_distributors_moat_server.py +52 −3 Original line number Original line Diff line number Diff line Loading @@ -581,6 +581,14 @@ class CaptchaFetchResourceTests(unittest.TestCase): self.assert_data_is_ok(decoded) self.assert_data_is_ok(decoded) class MockCaptchaCheckResource(server.CaptchaCheckResource): """A mocked :class:`server.CaptchaCheckResource` whose ``getBridgeLines`` method returns no bridges. """ def getBridgeLines(self, bridgeRequest): return list() class CaptchaCheckResourceTests(unittest.TestCase): class CaptchaCheckResourceTests(unittest.TestCase): """Tests for :class:`bridgedb.distributors.moat.server.CaptchaCheckResource`.""" """Tests for :class:`bridgedb.distributors.moat.server.CaptchaCheckResource`.""" Loading Loading @@ -611,6 +619,14 @@ class CaptchaCheckResourceTests(unittest.TestCase): "iz_PdOD2GIGPeclwiHAWM1pOS4cQVsTQR_z4ojZbpLiSp35n4Qbb11YOoreovZzlbS" "iz_PdOD2GIGPeclwiHAWM1pOS4cQVsTQR_z4ojZbpLiSp35n4Qbb11YOoreovZzlbS" "7W38rAsTirkdeugcNq82AxKP3phEkyRcw--CzV") "7W38rAsTirkdeugcNq82AxKP3phEkyRcw--CzV") def mock_getBridgeLines(self): self.resource = MockCaptchaCheckResource(self.distributor, self.schedule, 3, self.hmacKey, self.publicKey, self.secretKey, useForwardedHeader=False) def create_POST_with_data(self, data): def create_POST_with_data(self, data): request = DummyRequest([self.pagename]) request = DummyRequest([self.pagename]) request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json') request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json') Loading Loading @@ -826,13 +842,24 @@ class CaptchaCheckResourceTests(unittest.TestCase): self.assertEqual(error['type'], "moat-bridges") self.assertEqual(error['type'], "moat-bridges") self.assertEqual(error['id'], 4) self.assertEqual(error['id'], 4) def test_createBridgeRequest(self): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) encoded_content = request.content.read() content = json.loads(encoded_content)['data'][0] bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', content) self.assertTrue(bridgeRequest.isValid()) def test_getBridgeLines(self): def test_getBridgeLines(self): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) encoded_content = request.content.read() encoded_content = request.content.read() content = json.loads(encoded_content)['data'][0] content = json.loads(encoded_content)['data'][0] bridgelines = self.resource.getBridgeLines('3.3.3.3', content) bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', content) bridgelines = self.resource.getBridgeLines(bridgeRequest) self.assertTrue(bridgelines) self.assertTrue(bridgelines) Loading @@ -840,9 +867,31 @@ class CaptchaCheckResourceTests(unittest.TestCase): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) bridgelines = self.resource.getBridgeLines('3.3.3.3', None) bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', None) bridgelines = self.resource.getBridgeLines(bridgeRequest) self.assertFalse(bridgeRequest.isValid()) self.assertEqual(len(bridgelines), 0) def test_failureResponse_no_bridges(self): request = self.create_valid_POST_with_challenge(self.expiredChallenge) request.client = requesthelper.IPv4Address('TCP', '3.3.3.3', 443) encoded_content = request.content.read() content = json.loads(encoded_content)['data'][0] bridgeRequest = self.resource.createBridgeRequest('3.3.3.3', content) response = self.resource.failureResponse(6, request, bridgeRequest) self.assertIn("No bridges available", response) def test_render_POST_no_bridges(self): self.mock_getBridgeLines() request = self.create_valid_POST_make_new_challenge() response = self.resource.render(request) self.assertIsNone(bridgelines) self.assertIn("No bridges available", response) def test_render_POST_unexpired(self): def test_render_POST_unexpired(self): request = self.create_valid_POST_make_new_challenge() request = self.create_valid_POST_make_new_challenge() Loading