Unverified Commit 6ace9a35 authored by Isis Lovecruft's avatar Isis Lovecruft
Browse files

Merge branch 'fix/24637' into develop

parents 91305fa5 4c799735
......@@ -559,13 +559,14 @@ server is unable to distribute the requested Bridges, the server responds ``200
OK`` with the following JSON::
{
"error": {
"id": "1",
"errors": [{
"id": "6",
"type": "moat-bridges",
"version": "0.1.0",
"code": "404",
"status": "Not Found",
"title": "Could not fetch the type of bridges you requested",
"detail": "DETAILS",
}
}]
}
where:
......
......@@ -117,6 +117,15 @@ class BridgeRequestBase(object):
self.client = 'default'
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
def ipVersion(self):
"""The IP version of bridge addresses to distribute to the client.
......
......@@ -209,6 +209,7 @@ class JsonAPIErrorResource(JsonAPIResource):
resource403 = JsonAPIErrorResource(code=403, status="Forbidden")
resource404 = JsonAPIErrorResource(code=404, status="Not Found")
resource406 = JsonAPIErrorResource(code=406, status="Not Acceptable")
resource415 = JsonAPIErrorResource(code=415, status="Unsupported Media Type")
resource419 = JsonAPIErrorResource(code=419, status="No You're A Teapot")
......@@ -497,21 +498,21 @@ class CaptchaCheckResource(CaptchaResource):
self.nBridgesToGive = N
self.useForwardedHeader = useForwardedHeader
def getBridgeLines(self, ip, data):
"""Get bridge lines for a client's HTTP request.
def createBridgeRequest(self, ip, data):
"""Create an appropriate :class:`MoatBridgeRequest` from the ``data``
of a client's request.
:param str ip: The client's IP address.
:param dict data: The decoded JSON API data from the client's request.
:rtype: list or None
:returns: A list of bridge lines.
:rtype: :class:`MoatBridgeRequest`
:returns: An object which specifies the filters for retreiving
the type of bridges that the client requested.
"""
bridgeLines = None
interval = self.schedule.intervalStart(time.time())
logging.debug("Creating moat bridge request object for %s." % ip)
logging.debug("Replying to JSON API request from %s." % ip)
bridgeRequest = MoatBridgeRequest()
if ip and data:
bridgeRequest = MoatBridgeRequest()
bridgeRequest.client = IPAddress(ip)
bridgeRequest.isValid(True)
bridgeRequest.withIPversion()
......@@ -519,6 +520,23 @@ class CaptchaCheckResource(CaptchaResource):
bridgeRequest.withoutBlockInCountry(data)
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)
bridgeLines = [replaceControlChars(bridge.getBridgeLine(bridgeRequest))
for bridge in bridges]
......@@ -599,17 +617,34 @@ class CaptchaCheckResource(CaptchaResource):
return valid
def failureResponse(self, id, request):
"""Respond with status code "419 No You're A Teapot"."""
error_response = resource419
error_response.type = 'moat-bridges'
def failureResponse(self, id, request, bridgeRequest=None):
"""Respond with status code "419 No You're A Teapot" if the captcha
verification failed, or status code "404 Not Found" if there
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:
error_response = resource419
error_response.id = 4
error_response.detail = "The CAPTCHA solution was incorrect."
elif id == 5:
error_response = resource419
error_response.id = 5
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)
......@@ -665,7 +700,19 @@ class CaptchaCheckResource(CaptchaResource):
if valid:
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:
qrjpeg = generateQR(bridgeLines)
......
......@@ -581,6 +581,14 @@ class CaptchaFetchResourceTests(unittest.TestCase):
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):
"""Tests for :class:`bridgedb.distributors.moat.server.CaptchaCheckResource`."""
......@@ -611,6 +619,14 @@ class CaptchaCheckResourceTests(unittest.TestCase):
"iz_PdOD2GIGPeclwiHAWM1pOS4cQVsTQR_z4ojZbpLiSp35n4Qbb11YOoreovZzlbS"
"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):
request = DummyRequest([self.pagename])
request.requestHeaders.addRawHeader('Content-Type', 'application/vnd.api+json')
......@@ -826,13 +842,24 @@ class CaptchaCheckResourceTests(unittest.TestCase):
self.assertEqual(error['type'], "moat-bridges")
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):
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]
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)
......@@ -840,9 +867,31 @@ class CaptchaCheckResourceTests(unittest.TestCase):
request = self.create_valid_POST_with_challenge(self.expiredChallenge)
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):
request = self.create_valid_POST_make_new_challenge()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment