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

Merge branch 'fix/24637' into develop

parents 91305fa5 4c799735
Loading
Loading
Loading
Loading
+5 −4
Original line number Original line Diff line number Diff line
@@ -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:
+9 −0
Original line number Original line Diff line number Diff line
@@ -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.
+60 −13
Original line number Original line Diff line number Diff line
@@ -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")
@@ -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()
@@ -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]
@@ -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)


@@ -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)
+52 −3
Original line number Original line Diff line number Diff line
@@ -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`."""


@@ -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')
@@ -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)


@@ -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()