diff --git a/src/_secureaccesstokenauthorizer/resource.py b/src/_secureaccesstokenauthorizer/resource.py index bab22f4eb0f39a28321b18ce13017296e10346c8..d8b3626e862d35dbb034345344af368ebbb947cf 100644 --- a/src/_secureaccesstokenauthorizer/resource.py +++ b/src/_secureaccesstokenauthorizer/resource.py @@ -92,6 +92,9 @@ class _PaymentReferenceNumberCollection(Resource): def render_PUT(self, request): + """ + Record a PRN and begin attempting to redeem it. + """ try: payload = loads(request.content.read()) except Exception: @@ -99,15 +102,7 @@ class _PaymentReferenceNumberCollection(Resource): if payload.keys() != [u"payment-reference-number"]: return bad_request().render(request) prn = payload[u"payment-reference-number"] - if not isinstance(prn, unicode): - return bad_request().render(request) - if len(prn) != 44: - # TODO. 44 is the length of 32 bytes base64 encoded. This model - # information presumably belongs somewhere else. - return bad_request().render(request) - try: - urlsafe_b64decode(prn.encode("ascii")) - except Exception: + if not is_syntactic_prn(prn): return bad_request().render(request) self._controller.redeem(prn) @@ -126,10 +121,8 @@ class _PaymentReferenceNumberCollection(Resource): def getChild(self, segment, request): - prn = segment - try: - urlsafe_b64decode(prn) - except Exception: + prn = segment.decode("utf-8") + if not is_syntactic_prn(prn): return bad_request() try: payment_reference = self._store.get(prn) @@ -138,9 +131,38 @@ class _PaymentReferenceNumberCollection(Resource): return PaymentReferenceView(payment_reference) +def is_syntactic_prn(prn): + """ + :param prn: A candidate object to inspect. + + :return bool: ``True`` if and only if ``prn`` is a unicode string + containing a syntactically valid payment reference number. This says + **nothing** about the validity of the represented PRN itself. A + ``True`` result only means the unicode string can be **interpreted** + as a PRN. + """ + if not isinstance(prn, unicode): + return False + if len(prn) != 44: + # TODO. 44 is the length of 32 bytes base64 encoded. This model + # information presumably belongs somewhere else. + return False + try: + urlsafe_b64decode(prn.encode("ascii")) + except Exception: + return False + return True + class PaymentReferenceView(Resource): + """ + This class implements a view for a ``PaymentReference`` instance. + """ def __init__(self, reference): + """ + :param PaymentReference reference: The model object for which to provide a + view. + """ self._reference = reference Resource.__init__(self) @@ -151,6 +173,10 @@ class PaymentReferenceView(Resource): def bad_request(): + """ + :return IResource: A resource which can be rendered to produce a **BAD + REQUEST** response. + """ return ErrorPage( BAD_REQUEST, b"Bad Request", b"Bad Request", ) diff --git a/src/_secureaccesstokenauthorizer/tests/test_client_resource.py b/src/_secureaccesstokenauthorizer/tests/test_client_resource.py index 3c1216b6f48fc13a55bf50c0d9c345b3999c36e9..be45313d683e21db316ab7013a42e552db9670d4 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_client_resource.py +++ b/src/_secureaccesstokenauthorizer/tests/test_client_resource.py @@ -30,6 +30,9 @@ from json import ( from io import ( BytesIO, ) +from urllib import ( + quote, +) from testtools import ( TestCase, @@ -122,6 +125,11 @@ tahoe_configs_with_client_config = tahoe_configs(storage_client_plugins={ }) def is_not_json(bytestring): + """ + :param bytes bytestring: A candidate byte string to inspect. + + :return bool: ``False`` if and only if ``bytestring`` is JSON encoded. + """ try: loads(bytestring) except: @@ -129,16 +137,29 @@ def is_not_json(bytestring): return False def not_payment_reference_numbers(): - return text().filter( - lambda t: ( - # exclude / because it changes url dispatch and makes tests fail - # differently. - u"/" not in t and not is_urlsafe_base64(t) + """ + Builds unicode strings which are not legal payment reference numbers. + """ + return one_of( + text().filter( + lambda t: ( + not is_urlsafe_base64(t) + ), + ), + payment_reference_numbers().map( + # Turn a valid PRN into a PRN that is invalid only by containing a + # character from the base64 alphabet in place of one from the + # urlsafe-base64 alphabet. + lambda prn: u"/" + prn[1:], ), ) - def is_urlsafe_base64(text): + """ + :param unicode text: A candidate unicode string to inspect. + + :return bool: ``True`` if and only if ``text`` is urlsafe-base64 encoded + """ try: urlsafe_b64decode(text) except: @@ -172,6 +193,13 @@ def invalid_bodies(): def root_from_config(config): + """ + Create a client root resource from a Tahoe-LAFS configuration. + + :param _Config config: The Tahoe-LAFS configuration. + + :return IResource: The root client resource. + """ return from_configuration( config, PaymentReferenceStore.from_node_config( @@ -269,20 +297,25 @@ class PaymentReferenceNumberTests(TestCase): ), ) - @given(tahoe_configs_with_client_config, payment_reference_numbers()) - def test_get_invalid_prn(self, get_config, prn): + @given(tahoe_configs_with_client_config, not_payment_reference_numbers()) + def test_get_invalid_prn(self, get_config, not_prn): """ When a syntactically invalid PRN is requested with a ``GET`` to a child of ``PaymentReferenceNumberCollection`` the response is **BAD REQUEST**. """ tempdir = self.useFixture(TempDir()) - not_prn = prn[1:] config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") root = root_from_config(config) agent = RequestTraversalAgent(root) + url = u"http://127.0.0.1/payment-reference-number/{}".format( + quote( + not_prn.encode("utf-8"), + safe=b"", + ).decode("utf-8"), + ).encode("ascii") requesting = agent.request( b"GET", - u"http://127.0.0.1/payment-reference-number/{}".format(not_prn).encode("utf-8"), + url, ) self.assertThat( requesting, @@ -345,7 +378,12 @@ class PaymentReferenceNumberTests(TestCase): getting = agent.request( b"GET", - u"http://127.0.0.1/payment-reference-number/{}".format(prn).encode("ascii"), + u"http://127.0.0.1/payment-reference-number/{}".format( + quote( + prn.encode("utf-8"), + safe=b"", + ).decode("utf-8"), + ).encode("ascii"), ) self.assertThat(