Exemple #1
0
    def _process(self, request, event):

        # The topic we're going to send to
        topic = self._options["topic"]

        message = {}
        message[u"headers"] = {
            native_string(x): [native_string(z) for z in y]
            for x, y in request.requestHeaders.getAllRawHeaders()}
        message[u"body"] = event

        publish_options = PublishOptions(acknowledge=True)

        def _succ(result):
            return self._complete_request(
                request, 202, b"OK", reason="Successfully sent webhook from {ip} to {topic}",
                topic=topic, ip=request.getClientIP(), log_category="AR201")

        def _err(result):
            return self._fail_request(
                request, 500, "Unable to send webhook from {ip} to {topic}",
                topic=topic, ip=request.getClientIP(), body=b"NOT OK",
                log_failure=result, log_category="AR457")

        d = self._session.publish(topic,
                                  json.loads(json.dumps(message)),
                                  options=publish_options)
        d.addCallback(_succ)
        d.addErrback(_err)
        return d
Exemple #2
0
    def _process(self, request, event):

        # The topic we're going to send to
        topic = self._options["topic"]

        message = {}
        message["headers"] = {
            native_string(x): [native_string(z) for z in y]
            for x, y in request.requestHeaders.getAllRawHeaders()}
        message["body"] = event

        publish_options = PublishOptions(acknowledge=True)

        def _succ(result):
            self.log.info("Successfully sent webhook from {ip} to {topic}",
                          topic=topic, ip=request.getClientIP())
            request.setResponseCode(202)
            request.write(b"OK")
            request.finish()

        def _err(result):
            self.log.error("Unable to send webhook from {ip} to {topic}",
                           topic=topic, ip=request.getClientIP(),
                           log_failure=result)
            request.setResponseCode(500)
            request.write(b"NOT OK")

        d = self._session.publish(topic,
                                  json.loads(json.dumps(message)),
                                  options=publish_options)
        d.addCallback(_succ)
        d.addErrback(_err)

        return NOT_DONE_YET
    def _process(self, request, event):

        # The topic we're going to send to
        topic = self._options["topic"]

        message = {}
        message[u"headers"] = {
            native_string(x): [native_string(z) for z in y]
            for x, y in request.requestHeaders.getAllRawHeaders()}
        message[u"body"] = event

        publish_options = PublishOptions(acknowledge=True)

        def _succ(result):
            return self._complete_request(
                request, 202, b"OK", reason="Successfully sent webhook from {ip} to {topic}",
                topic=topic, ip=request.getClientIP(), log_category="AR201")

        def _err(result):
            return self._fail_request(
                request, 500, "Unable to send webhook from {ip} to {topic}",
                topic=topic, ip=request.getClientIP(), body=b"NOT OK",
                log_failure=result, log_category="AR457")

        d = self._session.publish(topic,
                                  json.loads(json.dumps(message)),
                                  options=publish_options)
        d.addCallback(_succ)
        d.addErrback(_err)
        return d
Exemple #4
0
    def _process(self, request, event):

        # The topic we're going to send to
        topic = self._options["topic"]

        message = {}
        message[u"headers"] = {
            native_string(x): [native_string(z) for z in y]
            for x, y in request.requestHeaders.getAllRawHeaders()}
        message[u"body"] = event

        publish_options = PublishOptions(acknowledge=True)

        def _succ(result):
            response_text = self._options.get("success_response", u"OK").encode('utf8')
            return self._complete_request(
                request, 202, response_text,
                reason="Successfully sent webhook from {ip} to {topic}",
                topic=topic,
                ip=request.getClientIP(),
                log_category="AR201",
            )

        def _err(result):
            response_text = self._options.get("error_response", u"NOT OK").encode('utf8')
            error_message = str(result.value)
            authorization_problem = False
            if isinstance(result.value, ApplicationError):
                error_message = '{}: {}'.format(
                    result.value.error,
                    result.value.args[0],
                )
                if result.value.error == u"wamp.error.not_authorized":
                    authorization_problem = True
            self.log.error(
                u"Unable to send webhook from {ip} to '{topic}' topic: {err}",
                ip=request.getClientIP(),
                body=response_text,
                log_failure=result,
                log_category="AR457",
                topic=topic,
                err=error_message,
            )
            if authorization_problem:
                self.log.error(
                    u"Session realm={realm} role={role}",
                    realm=self._session._realm,
                    role=self._session._authrole,
                )
            request.setResponseCode(500)
            request.write(response_text)
            request.finish()

        d = self._session.publish(topic,
                                  json.loads(json.dumps(message)),
                                  options=publish_options)
        d.addCallback(_succ)
        d.addErrback(_err)
        return d
Exemple #5
0
    def _process(self, request, event):

        # The topic we're going to send to
        topic = self._options["topic"]

        message = {}
        message["headers"] = {
            native_string(x): [native_string(z) for z in y]
            for x, y in request.requestHeaders.getAllRawHeaders()}
        message["body"] = event

        publish_options = PublishOptions(acknowledge=True)

        def _succ(result):
            response_text = self._options.get("success_response", "OK").encode('utf8')
            return self._complete_request(
                request, 202, response_text,
                reason="Successfully sent webhook from {ip} to {topic}",
                topic=topic,
                ip=request.getClientIP(),
                log_category="AR201",
            )

        def _err(result):
            response_text = self._options.get("error_response", "NOT OK").encode('utf8')
            error_message = str(result.value)
            authorization_problem = False
            if isinstance(result.value, ApplicationError):
                error_message = '{}: {}'.format(
                    result.value.error,
                    result.value.args[0],
                )
                if result.value.error == "wamp.error.not_authorized":
                    authorization_problem = True
            self.log.error(
                "Unable to send webhook from {ip} to '{topic}' topic: {err}",
                ip=request.getClientIP(),
                body=response_text,
                log_failure=result,
                log_category="AR457",
                topic=topic,
                err=error_message,
            )
            if authorization_problem:
                self.log.error(
                    "Session realm={realm} role={role}",
                    realm=self._session._realm,
                    role=self._session._authrole,
                )
            request.setResponseCode(500)
            request.write(response_text)
            request.finish()

        d = self._session.publish(topic,
                                  json.loads(json.dumps(message)),
                                  options=publish_options)
        d.addCallback(_succ)
        d.addErrback(_err)
        return d
Exemple #6
0
    def test_cb_failure(self):
        """
        Test that calls with no procedure in the request body are rejected.
        """
        resource = CallerResource({}, None)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"procedure": "foo"}',
            )

        self.assertEqual(request.code, 500)
        self.assertEqual(
            json.loads(native_string(request.get_written_data())),
            {
                "error": "wamp.error.runtime_error",
                "args": ["Sorry, Crossbar.io has encountered a problem."],
                "kwargs": {},
            },
        )

        errors = l.get_category("AR500")
        self.assertEqual(len(errors), 1)

        # We manually logged the errors; we can flush them from the log
        self.flushLoggedErrors()
Exemple #7
0
    def test_cb_failure(self):
        """
        Test that calls with no procedure in the request body are rejected.
        """
        resource = CallerResource({}, None)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"procedure": "foo"}')

        self.assertEqual(request.code, 500)
        self.assertEqual(
            json.loads(native_string(request.get_written_data())), {
                "error": "wamp.error.runtime_error",
                "args": ["Sorry, Crossbar.io has encountered a problem."],
                "kwargs": {}
            })

        errors = l.get_category("AR500")
        self.assertEqual(len(errors), 1)

        # We manually logged the errors; we can flush them from the log
        self.flushLoggedErrors()
Exemple #8
0
    def test_basic_publish(self):
        """
        Test a very basic publish to a topic.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"topic": "com.test.messages", "args": [1]}')

        self.assertEqual(len(session._published_messages), 1)
        self.assertEqual(session._published_messages[0]["args"], (1,))

        self.assertEqual(request.code, 200)

        logs = l.get_category("AR200")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)

        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"id": session._published_messages[0]["id"]})
Exemple #9
0
    def test_add2(self):
        """
        Test a very basic call where you square root a number. This has one
        arg, no kwargs, and no authorisation.
        """
        session = TestSession(types.ComponentConfig(u'realm1'))
        self.session_factory.add(session, authrole=u"test_role")

        session2 = ApplicationSession(types.ComponentConfig(u'realm1'))
        self.session_factory.add(session2, authrole=u"test_role")
        resource = CallerResource({}, session2)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"procedure": "com.myapp.sqrt", "args": [2]}')

        self.assertEqual(request.code, 200)
        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"args": [1.4142135623730951]})

        logs = l.get_category("AR202")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)
Exemple #10
0
    def test_add2(self):
        """
        Test a very basic call where you add two numbers together. This has two
        args, no kwargs, and no authorisation.
        """
        session = MockSession(self)
        session._addProcedureCall("com.test.add2",
                                  args=(1, 2),
                                  kwargs={},
                                  response=3)

        resource = CallerResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"procedure": "com.test.add2", "args": [1,2]}')

        self.assertEqual(request.code, 200)
        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"args": [3]})

        logs = l.get_category("AR202")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)
Exemple #11
0
    def test_failure(self):
        """
        A failed call returns the error to the client.
        """
        session = MockSession(self)
        session._addFailingProcedureCall("com.test.add2",
                                         args=(1, 2),
                                         kwargs={},
                                         response=ApplicationError(
                                             "wamp.test.broke", "broken!"))

        resource = CallerResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"procedure": "com.test.add2", "args": [1,2]}')

        self.flushLoggedErrors()
        self.assertEqual(request.code, 400)
        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {
                             u"error": u"wamp.test.broke",
                             u"args": [u"broken!"]
                         })

        logs = l.get_category("AR458")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 400)
Exemple #12
0
    def test_basic_publish(self):
        """
        Test a very basic publish to a topic.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"topic": "com.test.messages", "args": [1]}')

        self.assertEqual(len(session._published_messages), 1)
        self.assertEqual(session._published_messages[0]["args"], (1,))

        self.assertEqual(request.code, 200)

        logs = l.get_category("AR200")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)

        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"id": session._published_messages[0]["id"]})
        # ensure we have all the format-keys AR200 asks for (can we
        # extract these from the _log_categories string instead?)
        self.assertIn('code', logs[0])
        self.assertIn('reason', logs[0])
    def render(self, request):
        self.log.debug(
            "[render] method={request.method} path={request.path} args={request.args}",
            request=request)

        try:
            if request.method not in (b"POST", b"PUT", b"OPTIONS"):
                return self._deny_request(
                    request, 405,
                    u"HTTP/{0} not allowed (only HTTP/POST or HTTP/PUT)".
                    format(native_string(request.method)))
            else:
                self._set_common_headers(request)

                if request.method == b"OPTIONS":
                    # http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.7
                    request.setHeader(b'allow', b'POST,PUT,OPTIONS')

                    # https://www.w3.org/TR/cors/#access-control-allow-methods-response-header
                    request.setHeader(b'access-control-allow-methods',
                                      b'POST,PUT,OPTIONS')

                    request.setResponseCode(200)
                    return b''
                else:
                    return self._render_request(request)
        except Exception as e:
            self.log.failure("Unhandled server error. {exc}", exc=e)
            return self._deny_request(request,
                                      500,
                                      "Unhandled server error.",
                                      exc=e)
Exemple #14
0
    def test_basic(self):
        """
        A message, when a request has gone through to it, publishes a WAMP
        message on the configured topic.
        """
        session = MockPublisherSession(self)
        resource = WebhookResource({u"topic": u"com.test.webhook"}, session)

        request = yield renderResource(
            resource, b"/",
            method=b"POST",
            headers={b"Content-Type": []},
            body=b'{"foo": "has happened"}')

        self.assertEqual(len(session._published_messages), 1)
        self.assertEqual(
            {
                u"body": u'{"foo": "has happened"}',
                u"headers": {
                    u"Content-Type": [],
                    u'Date': [u'Sun, 1 Jan 2013 15:21:01 GMT'],
                    u'Host': [u'localhost:8000']
                }
            },
            session._published_messages[0]["args"][0])

        self.assertEqual(request.code, 202)
        self.assertEqual(native_string(request.get_written_data()),
                         "OK")
Exemple #15
0
    def test_publish_error(self):
        """
        A publish that errors will return the error to the client.
        """
        class RejectingPublisherSession(object):
            """
            A mock WAMP session.
            """
            def publish(self, topic, *args, **kwargs):
                return maybeDeferred(self._publish, topic, *args, **kwargs)

            def _publish(self, topic, *args, **kwargs):
                raise ApplicationError(u'wamp.error.not_authorized', foo="bar")

        session = RejectingPublisherSession()
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"topic": "com.test.messages", "args": [1]}')

        self.assertEqual(request.code, 200)

        logs = l.get_category("AR456")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)

        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"error": "wamp.error.not_authorized",
                          "args": [], "kwargs": {"foo": "bar"}})
Exemple #16
0
    def test_add2(self):
        """
        Test a very basic call where you square root a number. This has one
        arg, no kwargs, and no authorisation.
        """
        session = TestSession(types.ComponentConfig(u'realm1'))
        self.session_factory.add(session, authrole=u"test_role")

        session2 = ApplicationSession(types.ComponentConfig(u'realm1'))
        self.session_factory.add(session2, authrole=u"test_role")
        resource = CallerResource({}, session2)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"procedure": "com.myapp.sqrt", "args": [2]}')

        self.assertEqual(request.code, 200)
        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"args": [1.4142135623730951]})

        logs = l.get_category("AR202")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)
    def test_add2(self):
        """
        Test a very basic call where you add two numbers together. This has two
        args, no kwargs, and no authorisation.
        """
        session = MockSession(self)
        session._addProcedureCall("com.test.add2",
                                  args=(1, 2),
                                  kwargs={},
                                  response=3)

        resource = CallerResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"procedure": "com.test.add2", "args": [1,2]}')

        self.assertEqual(request.code, 200)
        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"args": [3]})

        logs = l.get_category("AR202")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)
Exemple #18
0
    def test_basic_publish(self):
        """
        Test a very basic publish to a topic.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"topic": "com.test.messages", "args": [1]}')

        self.assertEqual(len(session._published_messages), 1)
        self.assertEqual(session._published_messages[0]["args"], (1, ))

        self.assertEqual(request.code, 200)

        logs = l.get_category("AR200")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)

        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"id": session._published_messages[0]["id"]})
        # ensure we have all the format-keys AR200 asks for (can we
        # extract these from the _log_categories string instead?)
        self.assertIn('code', logs[0])
        self.assertIn('reason', logs[0])
Exemple #19
0
    def test_publish_needs_topic(self):
        """
        Test that attempted publishes without a topic will be rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{}')

        self.assertEqual(len(session._published_messages), 0)

        self.assertEqual(request.code, 400)
        errors = l.get_category("AR455")
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0]["code"], 400)

        self.assertEqual(
            json.loads(native_string(request.get_written_data())), {
                "error": log_categories["AR455"].format(key="topic"),
                "args": [],
                "kwargs": {}
            })
Exemple #20
0
    def test_publish_error(self):
        """
        A publish that errors will return the error to the client.
        """
        class RejectingPublisherSession(object):
            """
            A mock WAMP session.
            """
            def publish(self, topic, *args, **kwargs):
                return maybeDeferred(self._publish, topic, *args, **kwargs)

            def _publish(self, topic, *args, **kwargs):
                raise ApplicationError(u'wamp.error.not_authorized', foo="bar")

        session = RejectingPublisherSession()
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"topic": "com.test.messages", "args": [1]}')

        self.assertEqual(request.code, 200)

        logs = l.get_category("AR456")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 200)

        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"error": "wamp.error.not_authorized",
                          "args": [], "kwargs": {"foo": "bar"}})
Exemple #21
0
    def render(self, request):
        self.log.debug("[render] method={request.method} path={request.path} args={request.args}",
                       request=request)

        if request.method != b"POST":
            return self._deny_request(request, 405, u"HTTP/{0} not allowed".format(native_string(request.method)))
        else:
            return self.render_POST(request)
Exemple #22
0
    def render(self, request):
        self.log.debug(
            "[render] method={request.method} path={request.path} args={request.args}",
            request=request)

        if request.method != b"POST":
            return self._deny_request(
                request, 405,
                u"HTTP/{0} not allowed".format(native_string(request.method)))
        else:
            return self.render_POST(request)
Exemple #23
0
    def test_failure(self):
        """
        A failed call returns the error to the client.
        """
        session = TestSession(types.ComponentConfig(u"realm1"))
        self.session_factory.add(session, authrole=u"test_role")

        session2 = ApplicationSession(types.ComponentConfig(u"realm1"))
        self.session_factory.add(session2, authrole=u"test_role")
        resource = CallerResource({}, session2)

        tests = [
            (
                u"com.myapp.sqrt",
                (0,),
                {u"error": u"wamp.error.runtime_error", u"args": [u"don't ask foolish questions ;)"], u"kwargs": {}},
            ),
            (u"com.myapp.checkname", ("foo",), {u"error": u"com.myapp.error.reserved", u"args": [], u"kwargs": {}}),
            (
                u"com.myapp.checkname",
                ("*",),
                {u"error": u"com.myapp.error.invalid_length", u"args": [], u"kwargs": {"min": 3, "max": 10}},
            ),
            (
                u"com.myapp.checkname",
                ("hello",),
                {u"error": u"com.myapp.error.mixed_case", u"args": ["hello", "HELLO"], u"kwargs": {}},
            ),
            (u"com.myapp.compare", (1, 10), {u"error": u"com.myapp.error1", u"args": [9], u"kwargs": {}}),
        ]

        for procedure, args, err in tests:
            with LogCapturer() as l:
                request = yield renderResource(
                    resource,
                    b"/",
                    method=b"POST",
                    headers={b"Content-Type": [b"application/json"]},
                    body=dump_json({"procedure": procedure, "args": args}).encode("utf8"),
                )

            self.assertEqual(request.code, 200)
            self.assertEqual(json.loads(native_string(request.get_written_data())), err)

            logs = l.get_category("AR458")
            self.assertEqual(len(logs), 1)
            self.assertEqual(logs[0]["code"], 200)

        # We manually logged the errors; we can flush them from the log
        self.flushLoggedErrors()
Exemple #24
0
    def test_too_large_body(self):
        """
        A too large body will mean the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"post_body_limit": 1}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn("HTTP/POST body length ({}) exceeds maximum ({})".format(len(publishBody), 1),
                      native_string(request.getWrittenData()))
Exemple #25
0
    def test_multiple_content_length(self):
        """
        Requests with multiple Content-Length headers will be rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"],
                     b"Content-Length": ["1", "10"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn("Multiple Content-Length headers are not allowed",
                      native_string(request.getWrittenData()))
    def test_good_signature(self):
        """
        A valid, correct signature will mean the request is processed.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource(resourceOptions, session)

        request = yield renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody,
            sign=True, signKey="bazapp", signSecret="foobar")

        self.assertEqual(request.code, 202)
        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"id": session._published_messages[0]["id"]})
Exemple #27
0
    def test_not_matching_bodylength(self):
        """
        A body length that is different than the Content-Length header will mean
        the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"post_body_limit": 1}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"],
                     b"Content-Length": [1]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn("HTTP/POST body length ({}) is different to Content-Length ({})".format(len(publishBody), 1),
                      native_string(request.getWrittenData()))
Exemple #28
0
    def test_publish_cberror(self):
        """
        A publish that errors with a Crossbar failure will return a generic
        error to the client and log the exception.
        """
        class RejectingPublisherSession(object):
            """
            A mock WAMP session.
            """
            def publish(self, topic, *args, **kwargs):
                return maybeDeferred(self._publish, topic, *args, **kwargs)

            def _publish(self, topic, *args, **kwargs):
                raise ValueError("ono")

        session = RejectingPublisherSession()
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource,
                b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"topic": "com.test.messages", "args": [1]}')

        self.assertEqual(request.code, 500)

        logs = l.get_category("AR456")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 500)

        logs = l.get_category("AR500")
        self.assertEqual(len(logs), 1)

        self.assertEqual(
            json.loads(native_string(request.get_written_data())), {
                "error": "wamp.error.runtime_error",
                "args": ["Sorry, Crossbar.io has encountered a problem."],
                "kwargs": {}
            })

        # We manually logged it, so this one is OK
        self.flushLoggedErrors(ValueError)
Exemple #29
0
    def test_failure(self):
        """
        A failed call returns the error to the client.
        """
        session = TestSession(types.ComponentConfig(u'realm1'))
        self.session_factory.add(session, authrole=u"test_role")

        session2 = ApplicationSession(types.ComponentConfig(u'realm1'))
        self.session_factory.add(session2, authrole=u"test_role")
        resource = CallerResource({}, session2)

        tests = [
            (u"com.myapp.sqrt", (0,),
             {u"error": u"wamp.error.runtime_error", u"args": [u"don't ask foolish questions ;)"], u"kwargs": {}}),
            (u"com.myapp.checkname", ("foo",),
             {u"error": u"com.myapp.error.reserved", u"args": [], u"kwargs": {}}),
            (u"com.myapp.checkname", ("*",),
             {u"error": u"com.myapp.error.invalid_length", u"args": [], u"kwargs": {"min": 3, "max": 10}}),
            (u"com.myapp.checkname", ("hello",),
             {u"error": u"com.myapp.error.mixed_case", u"args": ["hello", "HELLO"], u"kwargs": {}}),
            (u"com.myapp.compare", (1, 10),
             {u"error": u"com.myapp.error1", u"args": [9], u"kwargs": {}}),
        ]

        for procedure, args, err in tests:
            with LogCapturer() as l:
                request = yield renderResource(
                    resource, b"/",
                    method=b"POST",
                    headers={b"Content-Type": [b"application/json"]},
                    body=dump_json({"procedure": procedure, "args": args}).encode('utf8'))

            self.assertEqual(request.code, 200)
            self.assertEqual(json.loads(native_string(request.get_written_data())),
                             err)

            logs = l.get_category("AR458")
            self.assertEqual(len(logs), 1)
            self.assertEqual(logs[0]["code"], 200)

        # We manually logged the errors; we can flush them from the log
        self.flushLoggedErrors()
    def test_good_signature(self):
        """
        A valid, correct signature will mean the request is processed.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource(resourceOptions, session)

        request = yield renderResource(
            resource,
            b"/",
            method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody,
            sign=True,
            signKey="bazapp",
            signSecret="foobar")

        self.assertEqual(request.code, 202)
        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"id": session._published_messages[0]["id"]})
Exemple #31
0
    def test_publish_cberror(self):
        """
        A publish that errors with a Crossbar failure will return a generic
        error to the client and log the exception.
        """
        class RejectingPublisherSession(object):
            """
            A mock WAMP session.
            """
            def publish(self, topic, *args, **kwargs):
                return maybeDeferred(self._publish, topic, *args, **kwargs)

            def _publish(self, topic, *args, **kwargs):
                raise ValueError("ono")

        session = RejectingPublisherSession()
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{"topic": "com.test.messages", "args": [1]}')

        self.assertEqual(request.code, 500)

        logs = l.get_category("AR456")
        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0]["code"], 500)

        logs = l.get_category("AR500")
        self.assertEqual(len(logs), 1)

        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"error": "wamp.error.runtime_error",
                          "args": ["Sorry, Crossbar.io has encountered a problem."],
                          "kwargs": {}})

        # We manually logged it, so this one is OK
        self.flushLoggedErrors(ValueError)
Exemple #32
0
    def test_publish_needs_topic(self):
        """
        Test that attempted publishes without a topic will be rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        with LogCapturer() as l:
            request = yield renderResource(
                resource, b"/",
                method=b"POST",
                headers={b"Content-Type": [b"application/json"]},
                body=b'{}')

        self.assertEqual(len(session._published_messages), 0)

        self.assertEqual(request.code, 400)
        errors = l.get_category("AR455")
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0]["code"], 400)

        self.assertEqual(json.loads(native_string(request.get_written_data())),
                         {"error": log_categories["AR455"].format(key="topic"),
                          "args": [], "kwargs": {}})
Exemple #33
0
    def _render_request(self, request):
        """
        Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller
        processor.
        """
        # read HTTP/POST|PUT body
        body = request.content.read()

        args = {native_string(x): y[0] for x, y in request.args.items()}
        headers = request.requestHeaders

        # check content type + charset encoding
        #
        content_type_header = headers.getRawHeaders(b"content-type", [])

        if len(content_type_header) > 0:
            content_type_elements = [
                x.strip().lower()
                for x in content_type_header[0].split(b";")
            ]
        else:
            content_type_elements = []

        if self.decode_as_json:
            # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES
            # (but we allow missing content type .. will catch later during JSON
            # parsing anyway)
            if len(content_type_elements) > 0 and \
               content_type_elements[0] not in _ALLOWED_CONTENT_TYPES:
                return self._deny_request(
                    request, 400,
                    u"bad content type: if a content type is present, it MUST be one of '{}', not '{}'".format(list(_ALLOWED_CONTENT_TYPES), content_type_elements[0]),
                    log_category="AR452"
                )

        encoding_parts = {}

        if len(content_type_elements) > 1:
            try:
                for item in content_type_elements:
                    if b"=" not in item:
                        # Don't bother looking at things "like application/json"
                        continue

                    # Parsing things like:
                    # charset=utf-8
                    _ = native_string(item).split("=")
                    assert len(_) == 2

                    # We don't want duplicates
                    key = _[0].strip().lower()
                    assert key not in encoding_parts
                    encoding_parts[key] = _[1].strip().lower()
            except:
                return self._deny_request(request, 400,
                                          u"mangled Content-Type header",
                                          log_category="AR450")

        charset_encoding = encoding_parts.get("charset", "utf-8")

        if charset_encoding not in ["utf-8", 'utf8']:
            return self._deny_request(
                request, 400,
                (u"'{charset_encoding}' is not an accepted charset encoding, "
                 u"must be utf-8"),
                log_category="AR450",
                charset_encoding=charset_encoding)

        # enforce "post_body_limit"
        #
        body_length = len(body)
        content_length_header = headers.getRawHeaders(b"content-length", [])

        if len(content_length_header) == 1:
            content_length = int(content_length_header[0])
        elif len(content_length_header) > 1:
            return self._deny_request(
                request, 400,
                u"Multiple Content-Length headers are not allowed",
                log_category="AR463")
        else:
            content_length = body_length

        if body_length != content_length:
            # Prevent the body length from being different to the given
            # Content-Length. This is so that clients can't lie and bypass
            # length restrictions by giving an incorrect header with a large
            # body.
            return self._deny_request(request, 400, u"HTTP/POST|PUT body length ({0}) is different to Content-Length ({1})".format(body_length, content_length))

        if self._post_body_limit and content_length > self._post_body_limit:
            return self._deny_request(
                request, 413,
                u"HTTP/POST|PUT body length ({0}) exceeds maximum ({1})".format(content_length, self._post_body_limit)
            )

        #
        # parse/check HTTP/POST|PUT query parameters
        #

        # key
        #
        if 'key' in args:
            key_str = args["key"]
        else:
            if self._secret:
                return self._deny_request(
                    request, 400, u"signed request required, but mandatory 'key' field missing",
                    log_category="AR461")

        # timestamp
        #
        if 'timestamp' in args:
            timestamp_str = args["timestamp"]
            try:
                ts = datetime.datetime.strptime(native_string(timestamp_str), "%Y-%m-%dT%H:%M:%S.%fZ")
                delta = abs((ts - datetime.datetime.utcnow()).total_seconds())
                if self._timestamp_delta_limit and delta > self._timestamp_delta_limit:
                    return self._deny_request(
                        request, 400, u"request expired (delta {0} seconds)".format(delta),
                        log_category="AR462")
            except ValueError as e:
                return self._deny_request(
                    request, 400,
                    u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')".format(native_string(timestamp_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request, 400, u"signed request required, but mandatory 'timestamp' field missing",
                    log_category="AR461")

        # seq
        #
        if 'seq' in args:
            seq_str = args["seq"]
            try:
                # FIXME: check sequence
                seq = int(seq_str)  # noqa
            except:
                return self._deny_request(
                    request, 400, u"invalid sequence number '{0}' (must be an integer)".format(native_string(seq_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request, 400, u"signed request required, but mandatory 'seq' field missing",
                    log_category="AR461")

        # nonce
        #
        if 'nonce' in args:
            nonce_str = args["nonce"]
            try:
                # FIXME: check nonce
                nonce = int(nonce_str)  # noqa
            except:
                return self._deny_request(
                    request, 400, u"invalid nonce '{0}' (must be an integer)".format(native_string(nonce_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request, 400, u"signed request required, but mandatory 'nonce' field missing",
                    log_category="AR461")

        # signature
        #
        if 'signature' in args:
            signature_str = args["signature"]
        else:
            if self._secret:
                return self._deny_request(
                    request, 400, u"signed request required, but mandatory 'signature' field missing",
                    log_category="AR461")

        # do more checks if signed requests are required
        #
        if self._secret:

            if key_str != self._key:
                return self._deny_request(
                    request, 401, u"unknown key '{0}' in signed request".format(native_string(key_str)),
                    log_category="AR460")

            # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature
            hm = hmac.new(self._secret, None, hashlib.sha256)
            hm.update(key_str)
            hm.update(timestamp_str)
            hm.update(seq_str)
            hm.update(nonce_str)
            hm.update(body)
            signature_recomputed = base64.urlsafe_b64encode(hm.digest())

            if signature_str != signature_recomputed:
                return self._deny_request(request, 401, u"invalid request signature",
                                          log_category="AR459")
            else:
                self.log.debug("REST request signature valid.",
                               log_category="AR203")

        # user_agent = headers.get("user-agent", "unknown")
        client_ip = request.getClientIP()
        is_secure = request.isSecure()

        # enforce client IP address
        #
        if self._require_ip:
            ip = IPAddress(native_string(client_ip))
            allowed = False
            for net in self._require_ip:
                if ip in net:
                    allowed = True
                    break
            if not allowed:
                return self._deny_request(request, 400, u"request denied based on IP address")

        # enforce TLS
        #
        if self._require_tls:
            if not is_secure:
                return self._deny_request(request, 400, u"request denied because not using TLS")

        # FIXME: authorize request
        authorized = True

        if not authorized:
            return self._deny_request(request, 401, u"not authorized")

        _validator.reset()
        validation_result = _validator.validate(body)

        # validate() returns a 4-tuple, of which item 0 is whether it
        # is valid
        if not validation_result[0]:
            return self._deny_request(
                request, 400,
                u"invalid request event - HTTP/POST|PUT body was invalid UTF-8",
                log_category="AR451")

        event = body.decode('utf8')

        if self.decode_as_json:
            try:
                event = json.loads(event)
            except Exception as e:
                return self._deny_request(
                    request, 400,
                    (u"invalid request event - HTTP/POST|PUT body must be "
                     u"valid JSON: {exc}"), exc=e, log_category="AR453")

            if not isinstance(event, dict):
                return self._deny_request(
                    request, 400,
                    (u"invalid request event - HTTP/POST|PUT body must be "
                     u"a JSON dict"), log_category="AR454")

        d = self._process(request, event)

        if isinstance(d, bytes):
            # If it's bytes, return it directly
            return d
        else:
            # If it's a Deferred, let it run.
            d.addCallback(lambda _: request.finish())

        return server.NOT_DONE_YET
Exemple #34
0
    def _render_request(self, request):
        """
        Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller
        processor.
        """
        # read HTTP/POST|PUT body
        body = request.content.read()

        args = {native_string(x): y[0] for x, y in request.args.items()}
        headers = request.requestHeaders

        # check content type + charset encoding
        #
        content_type_header = headers.getRawHeaders(b"content-type", [])

        if len(content_type_header) > 0:
            content_type_elements = [
                x.strip().lower() for x in content_type_header[0].split(b";")
            ]
        else:
            content_type_elements = []

        if self.decode_as_json:
            # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES
            # (but we allow missing content type .. will catch later during JSON
            # parsing anyway)
            if len(content_type_elements) > 0 and \
               content_type_elements[0] not in _ALLOWED_CONTENT_TYPES:
                return self._deny_request(
                    request,
                    400,
                    accepted=list(_ALLOWED_CONTENT_TYPES),
                    given=content_type_elements[0],
                    log_category="AR452")

        encoding_parts = {}

        if len(content_type_elements) > 1:
            try:
                for item in content_type_elements:
                    if b"=" not in item:
                        # Don't bother looking at things "like application/json"
                        continue

                    # Parsing things like:
                    # charset=utf-8
                    _ = native_string(item).split("=")
                    assert len(_) == 2

                    # We don't want duplicates
                    key = _[0].strip().lower()
                    assert key not in encoding_parts
                    encoding_parts[key] = _[1].strip().lower()
            except:
                return self._deny_request(request, 400, log_category="AR450")

        charset_encoding = encoding_parts.get("charset", "utf-8")

        if charset_encoding not in ["utf-8", 'utf8']:
            return self._deny_request(request, 400, log_category="AR450")

        # enforce "post_body_limit"
        #
        body_length = len(body)
        content_length_header = headers.getRawHeaders(b"content-length", [])

        if len(content_length_header) == 1:
            content_length = int(content_length_header[0])
        elif len(content_length_header) > 1:
            return self._deny_request(request, 400, log_category="AR463")
        else:
            content_length = body_length

        if body_length != content_length:
            # Prevent the body length from being different to the given
            # Content-Length. This is so that clients can't lie and bypass
            # length restrictions by giving an incorrect header with a large
            # body.
            return self._deny_request(request,
                                      400,
                                      bodylen=body_length,
                                      conlen=content_length,
                                      log_category="AR465")

        if self._post_body_limit and content_length > self._post_body_limit:
            return self._deny_request(request,
                                      413,
                                      length=content_length,
                                      accepted=self._post_body_limit)

        #
        # parse/check HTTP/POST|PUT query parameters
        #

        # key
        #
        if 'key' in args:
            key_str = args["key"]
        else:
            if self._secret:
                return self._deny_request(request,
                                          400,
                                          reason=u"'key' field missing",
                                          log_category="AR461")

        # timestamp
        #
        if 'timestamp' in args:
            timestamp_str = args["timestamp"]
            try:
                ts = datetime.datetime.strptime(native_string(timestamp_str),
                                                "%Y-%m-%dT%H:%M:%S.%fZ")
                delta = abs((ts - datetime.datetime.utcnow()).total_seconds())
                if self._timestamp_delta_limit and delta > self._timestamp_delta_limit:
                    return self._deny_request(request,
                                              400,
                                              log_category="AR464")
            except ValueError:
                return self._deny_request(
                    request,
                    400,
                    reason=
                    u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')"
                    .format(native_string(timestamp_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request,
                    400,
                    reason=
                    u"signed request required, but mandatory 'timestamp' field missing",
                    log_category="AR461")

        # seq
        #
        if 'seq' in args:
            seq_str = args["seq"]
            try:
                # FIXME: check sequence
                seq = int(seq_str)  # noqa
            except:
                return self._deny_request(
                    request,
                    400,
                    reason=u"invalid sequence number '{0}' (must be an integer)"
                    .format(native_string(seq_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(request,
                                          400,
                                          reason=u"'seq' field missing",
                                          log_category="AR461")

        # nonce
        #
        if 'nonce' in args:
            nonce_str = args["nonce"]
            try:
                # FIXME: check nonce
                nonce = int(nonce_str)  # noqa
            except:
                return self._deny_request(
                    request,
                    400,
                    reason=u"invalid nonce '{0}' (must be an integer)".format(
                        native_string(nonce_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(request,
                                          400,
                                          reason=u"'nonce' field missing",
                                          log_category="AR461")

        # signature
        #
        if 'signature' in args:
            signature_str = args["signature"]
        else:
            if self._secret:
                return self._deny_request(request,
                                          400,
                                          reason=u"'signature' field missing",
                                          log_category="AR461")

        # do more checks if signed requests are required
        #
        if self._secret:

            if key_str != self._key:
                return self._deny_request(
                    request,
                    401,
                    reason=u"unknown key '{0}' in signed request".format(
                        native_string(key_str)),
                    log_category="AR460")

            # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature
            hm = hmac.new(self._secret, None, hashlib.sha256)
            hm.update(key_str)
            hm.update(timestamp_str)
            hm.update(seq_str)
            hm.update(nonce_str)
            hm.update(body)
            signature_recomputed = base64.urlsafe_b64encode(hm.digest())

            if signature_str != signature_recomputed:
                return self._deny_request(request, 401, log_category="AR459")
            else:
                self.log.debug("REST request signature valid.",
                               log_category="AR203")

        # user_agent = headers.get("user-agent", "unknown")
        client_ip = request.getClientIP()
        is_secure = request.isSecure()

        # enforce client IP address
        #
        if self._require_ip:
            ip = ip_address(client_ip)
            allowed = False
            for net in self._require_ip:
                if ip in net:
                    allowed = True
                    break
            if not allowed:
                return self._deny_request(request, 400, log_category="AR466")

        # enforce TLS
        #
        if self._require_tls:
            if not is_secure:
                return self._deny_request(
                    request,
                    400,
                    reason=u"request denied because not using TLS")

        # authenticate request
        #

        # TODO: also support HTTP Basic AUTH for ticket

        def on_auth_ok(value):
            if value is True:
                # treat like original behavior and just accept the request_id
                pass
            elif isinstance(value, types.Accept):
                self._session._authid = value.authid
                self._session._authrole = value.authrole
                # realm?
            else:
                # FIXME: not returning deny request... probably not ideal
                request.write(
                    self._deny_request(request,
                                       401,
                                       reason=u"not authorized",
                                       log_category="AR401"))
                request.finish()
                return

            _validator.reset()
            validation_result = _validator.validate(body)

            # validate() returns a 4-tuple, of which item 0 is whether it
            # is valid
            if not validation_result[0]:
                request.write(
                    self._deny_request(request, 400, log_category="AR451"))
                request.finish()
                return

            event = body.decode('utf8')

            if self.decode_as_json:
                try:
                    event = json.loads(event)
                except Exception as e:
                    request.write(
                        self._deny_request(request,
                                           400,
                                           exc=e,
                                           log_category="AR453"))
                    request.finish()
                    return

                if not isinstance(event, dict):
                    request.write(
                        self._deny_request(request, 400, log_category="AR454"))
                    request.finish()
                    return

            d = maybeDeferred(self._process, request, event)

            def finish(value):
                if isinstance(value, bytes):
                    request.write(value)
                request.finish()

            d.addCallback(finish)

        def on_auth_error(err):
            # XXX: is it ideal to write to the request?
            request.write(
                self._deny_request(request,
                                   401,
                                   reason=u"not authorized",
                                   log_category="AR401"))

            request.finish()
            return

        authmethod = None
        authid = None
        signature = None

        authorization_header = headers.getRawHeaders(b"authorization", [])
        if len(authorization_header) == 1:
            # HTTP Basic Authorization will be processed as ticket authentication
            authorization = authorization_header[0]
            auth_scheme, auth_details = authorization.split(b" ", 1)

            if auth_scheme.lower() == b"basic":
                try:
                    credentials = binascii.a2b_base64(auth_details + b'===')
                    credentials = credentials.split(b":", 1)
                    if len(credentials) == 2:
                        authmethod = "ticket"
                        authid = credentials[0].decode("utf-8")
                        signature = credentials[1].decode("utf-8")
                    else:
                        return self._deny_request(request,
                                                  401,
                                                  reason=u"not authorized",
                                                  log_category="AR401")
                except binascii.Error:
                    # authentication failed
                    return self._deny_request(request,
                                              401,
                                              reason=u"not authorized",
                                              log_category="AR401")
        elif 'authmethod' in args and args['authmethod'].decode(
                "utf-8") == 'ticket':
            if "ticket" not in args or "authid" not in args:
                # AR401 - fail if the ticket or authid are not in the args
                on_auth_ok(False)
            else:
                authmethod = "ticket"
                authid = args['authid'].decode("utf-8")
                signature = args['ticket'].decode("utf-8")

        if authmethod and authid and signature:

            hdetails = types.HelloDetails(authid=authid,
                                          authmethods=[authmethod])

            # wire up some variables for the authenticators to work, this is hackish

            # a custom header based authentication scheme can be implemented
            # without adding alternate authenticators by forwarding all headers.
            self._session._transport._transport_info = {
                "http_headers_received": {
                    native_string(x).lower(): native_string(y[0])
                    for x, y in request.requestHeaders.getAllRawHeaders()
                }
            }

            self._session._pending_session_id = None
            self._session._router_factory = self._session._transport._routerFactory

            if authmethod == "ticket":
                self._pending_auth = PendingAuthTicket(
                    self._session, self._auth_config['ticket'])
                self._pending_auth.hello(self._session._realm, hdetails)

            auth_d = maybeDeferred(self._pending_auth.authenticate, signature)
            auth_d.addCallbacks(on_auth_ok, on_auth_error)

        else:
            # don't return the value or it will be written to the request
            on_auth_ok(True)

        return server.NOT_DONE_YET
Exemple #35
0
 def __init__(self, templates, directory):
     Resource.__init__(self)
     self._page = templates.get_template('cb_web_404.html')
     self._directory = native_string(directory)
    def _render_request(self, request):
        """
        Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller
        processor.
        """
        # read HTTP/POST|PUT body
        body = request.content.read()

        args = {native_string(x): y[0] for x, y in request.args.items()}
        headers = request.requestHeaders

        # check content type + charset encoding
        #
        content_type_header = headers.getRawHeaders(b"content-type", [])

        if len(content_type_header) > 0:
            content_type_elements = [
                x.strip().lower() for x in content_type_header[0].split(b";")
            ]
        else:
            content_type_elements = []

        if self.decode_as_json:
            # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES
            # (but we allow missing content type .. will catch later during JSON
            # parsing anyway)
            if len(content_type_elements) > 0 and \
               content_type_elements[0] not in _ALLOWED_CONTENT_TYPES:
                return self._deny_request(
                    request,
                    400,
                    u"bad content type: if a content type is present, it MUST be one of '{}', not '{}'"
                    .format(list(_ALLOWED_CONTENT_TYPES),
                            content_type_elements[0]),
                    log_category="AR452")

        encoding_parts = {}

        if len(content_type_elements) > 1:
            try:
                for item in content_type_elements:
                    if b"=" not in item:
                        # Don't bother looking at things "like application/json"
                        continue

                    # Parsing things like:
                    # charset=utf-8
                    _ = native_string(item).split("=")
                    assert len(_) == 2

                    # We don't want duplicates
                    key = _[0].strip().lower()
                    assert key not in encoding_parts
                    encoding_parts[key] = _[1].strip().lower()
            except:
                return self._deny_request(request,
                                          400,
                                          u"mangled Content-Type header",
                                          log_category="AR450")

        charset_encoding = encoding_parts.get("charset", "utf-8")

        if charset_encoding not in ["utf-8", 'utf8']:
            return self._deny_request(
                request,
                400,
                (u"'{charset_encoding}' is not an accepted charset encoding, "
                 u"must be utf-8"),
                log_category="AR450",
                charset_encoding=charset_encoding)

        # enforce "post_body_limit"
        #
        body_length = len(body)
        content_length_header = headers.getRawHeaders(b"content-length", [])

        if len(content_length_header) == 1:
            content_length = int(content_length_header[0])
        elif len(content_length_header) > 1:
            return self._deny_request(
                request,
                400,
                u"Multiple Content-Length headers are not allowed",
                log_category="AR463")
        else:
            content_length = body_length

        if body_length != content_length:
            # Prevent the body length from being different to the given
            # Content-Length. This is so that clients can't lie and bypass
            # length restrictions by giving an incorrect header with a large
            # body.
            return self._deny_request(
                request, 400,
                u"HTTP/POST|PUT body length ({0}) is different to Content-Length ({1})"
                .format(body_length, content_length))

        if self._post_body_limit and content_length > self._post_body_limit:
            return self._deny_request(
                request, 413,
                u"HTTP/POST|PUT body length ({0}) exceeds maximum ({1})".
                format(content_length, self._post_body_limit))

        #
        # parse/check HTTP/POST|PUT query parameters
        #

        # key
        #
        if 'key' in args:
            key_str = args["key"]
        else:
            if self._secret:
                return self._deny_request(
                    request,
                    400,
                    u"signed request required, but mandatory 'key' field missing",
                    log_category="AR461")

        # timestamp
        #
        if 'timestamp' in args:
            timestamp_str = args["timestamp"]
            try:
                ts = datetime.datetime.strptime(native_string(timestamp_str),
                                                "%Y-%m-%dT%H:%M:%S.%fZ")
                delta = abs((ts - datetime.datetime.utcnow()).total_seconds())
                if self._timestamp_delta_limit and delta > self._timestamp_delta_limit:
                    return self._deny_request(
                        request,
                        400,
                        u"request expired (delta {0} seconds)".format(delta),
                        log_category="AR462")
            except ValueError as e:
                return self._deny_request(
                    request,
                    400,
                    u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')"
                    .format(native_string(timestamp_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request,
                    400,
                    u"signed request required, but mandatory 'timestamp' field missing",
                    log_category="AR461")

        # seq
        #
        if 'seq' in args:
            seq_str = args["seq"]
            try:
                # FIXME: check sequence
                seq = int(seq_str)  # noqa
            except:
                return self._deny_request(
                    request,
                    400,
                    u"invalid sequence number '{0}' (must be an integer)".
                    format(native_string(seq_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request,
                    400,
                    u"signed request required, but mandatory 'seq' field missing",
                    log_category="AR461")

        # nonce
        #
        if 'nonce' in args:
            nonce_str = args["nonce"]
            try:
                # FIXME: check nonce
                nonce = int(nonce_str)  # noqa
            except:
                return self._deny_request(
                    request,
                    400,
                    u"invalid nonce '{0}' (must be an integer)".format(
                        native_string(nonce_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request,
                    400,
                    u"signed request required, but mandatory 'nonce' field missing",
                    log_category="AR461")

        # signature
        #
        if 'signature' in args:
            signature_str = args["signature"]
        else:
            if self._secret:
                return self._deny_request(
                    request,
                    400,
                    u"signed request required, but mandatory 'signature' field missing",
                    log_category="AR461")

        # do more checks if signed requests are required
        #
        if self._secret:

            if key_str != self._key:
                return self._deny_request(
                    request,
                    401,
                    u"unknown key '{0}' in signed request".format(
                        native_string(key_str)),
                    log_category="AR460")

            # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature
            hm = hmac.new(self._secret, None, hashlib.sha256)
            hm.update(key_str)
            hm.update(timestamp_str)
            hm.update(seq_str)
            hm.update(nonce_str)
            hm.update(body)
            signature_recomputed = base64.urlsafe_b64encode(hm.digest())

            if signature_str != signature_recomputed:
                return self._deny_request(request,
                                          401,
                                          u"invalid request signature",
                                          log_category="AR459")
            else:
                self.log.debug("REST request signature valid.",
                               log_category="AR203")

        # user_agent = headers.get("user-agent", "unknown")
        client_ip = request.getClientIP()
        is_secure = request.isSecure()

        # enforce client IP address
        #
        if self._require_ip:
            ip = IPAddress(native_string(client_ip))
            allowed = False
            for net in self._require_ip:
                if ip in net:
                    allowed = True
                    break
            if not allowed:
                return self._deny_request(
                    request, 400, u"request denied based on IP address")

        # enforce TLS
        #
        if self._require_tls:
            if not is_secure:
                return self._deny_request(
                    request, 400, u"request denied because not using TLS")

        # FIXME: authorize request
        authorized = True

        if not authorized:
            return self._deny_request(request, 401, u"not authorized")

        _validator.reset()
        validation_result = _validator.validate(body)

        # validate() returns a 4-tuple, of which item 0 is whether it
        # is valid
        if not validation_result[0]:
            return self._deny_request(
                request,
                400,
                u"invalid request event - HTTP/POST|PUT body was invalid UTF-8",
                log_category="AR451")

        event = body.decode('utf8')

        if self.decode_as_json:
            try:
                event = json.loads(event)
            except Exception as e:
                return self._deny_request(
                    request,
                    400,
                    (u"invalid request event - HTTP/POST|PUT body must be "
                     u"valid JSON: {exc}"),
                    exc=e,
                    log_category="AR453")

            if not isinstance(event, dict):
                return self._deny_request(
                    request,
                    400,
                    (u"invalid request event - HTTP/POST|PUT body must be "
                     u"a JSON dict"),
                    log_category="AR454")

        d = self._process(request, event)

        if isinstance(d, bytes):
            # If it's bytes, return it directly
            return d
        else:
            # If it's a Deferred, let it run.
            d.addCallback(lambda _: request.finish())

        return server.NOT_DONE_YET
Exemple #37
0
    def render(self, request):
        self.log.debug("[render] method={request.method} path={request.path} args={request.args}",
                       request=request)

        try:
            if request.method not in (b"POST", b"PUT", b"OPTIONS"):
                return self._deny_request(request, 405, u"HTTP/{0} not allowed (only HTTP/POST or HTTP/PUT)".format(native_string(request.method)))
            else:
                self._set_common_headers(request)

                if request.method == b"OPTIONS":
                    # http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.7
                    request.setHeader(b'allow', b'POST,PUT,OPTIONS')

                    # https://www.w3.org/TR/cors/#access-control-allow-methods-response-header
                    request.setHeader(b'access-control-allow-methods', b'POST,PUT,OPTIONS')

                    request.setResponseCode(200)
                    return b''
                else:
                    return self._render_request(request)
        except Exception as e:
            self.log.failure("Unhandled server error. {exc}", exc=e)
            return self._deny_request(request, 500, "Unhandled server error.", exc=e)
Exemple #38
0
 def test_unicode_not_allowed(self):
     """
     A unicode argument should never be allowed.
     """
     with self.assertRaises(ValueError):
         compat.native_string(u"bar")
Exemple #39
0
 def test_bytes_always_native(self):
     """
     C{native_string}, with a bytes input, will always give a str output.
     """
     self.assertEqual(type(compat.native_string(b"foo")), str)
Exemple #40
0
 def __init__(self, templates, directory):
     Resource.__init__(self)
     self._page = templates.get_template("cb_web_404.html")
     self._directory = native_string(directory)
Exemple #41
0
    def _render_request(self, request):
        """
        Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller
        processor.
        """
        # read HTTP/POST|PUT body
        body = request.content.read()

        args = {native_string(x): y[0] for x, y in request.args.items()}
        headers = request.requestHeaders

        # check content type + charset encoding
        #
        content_type_header = headers.getRawHeaders(b"content-type", [])

        if len(content_type_header) > 0:
            content_type_elements = [
                x.strip().lower()
                for x in content_type_header[0].split(b";")
            ]
        else:
            content_type_elements = []

        if self.decode_as_json:
            # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES
            # (but we allow missing content type .. will catch later during JSON
            # parsing anyway)
            if len(content_type_elements) > 0 and \
               content_type_elements[0] not in _ALLOWED_CONTENT_TYPES:
                return self._deny_request(
                    request, 400,
                    accepted=list(_ALLOWED_CONTENT_TYPES),
                    given=content_type_elements[0],
                    log_category="AR452"
                )

        encoding_parts = {}

        if len(content_type_elements) > 1:
            try:
                for item in content_type_elements:
                    if b"=" not in item:
                        # Don't bother looking at things "like application/json"
                        continue

                    # Parsing things like:
                    # charset=utf-8
                    _ = native_string(item).split("=")
                    assert len(_) == 2

                    # We don't want duplicates
                    key = _[0].strip().lower()
                    assert key not in encoding_parts
                    encoding_parts[key] = _[1].strip().lower()
            except:
                return self._deny_request(request, 400, log_category="AR450")

        charset_encoding = encoding_parts.get("charset", "utf-8")

        if charset_encoding not in ["utf-8", 'utf8']:
            return self._deny_request(
                request, 400,
                log_category="AR450")

        # enforce "post_body_limit"
        #
        body_length = len(body)
        content_length_header = headers.getRawHeaders(b"content-length", [])

        if len(content_length_header) == 1:
            content_length = int(content_length_header[0])
        elif len(content_length_header) > 1:
            return self._deny_request(
                request, 400,
                log_category="AR463")
        else:
            content_length = body_length

        if body_length != content_length:
            # Prevent the body length from being different to the given
            # Content-Length. This is so that clients can't lie and bypass
            # length restrictions by giving an incorrect header with a large
            # body.
            return self._deny_request(request, 400, bodylen=body_length,
                                      conlen=content_length,
                                      log_category="AR465")

        if self._post_body_limit and content_length > self._post_body_limit:
            return self._deny_request(
                request, 413,
                length=content_length,
                accepted=self._post_body_limit
            )

        #
        # parse/check HTTP/POST|PUT query parameters
        #

        # key
        #
        if 'key' in args:
            key_str = args["key"]
        else:
            if self._secret:
                return self._deny_request(
                    request, 400,
                    reason=u"'key' field missing",
                    log_category="AR461")

        # timestamp
        #
        if 'timestamp' in args:
            timestamp_str = args["timestamp"]
            try:
                ts = datetime.datetime.strptime(native_string(timestamp_str), "%Y-%m-%dT%H:%M:%S.%fZ")
                delta = abs((ts - datetime.datetime.utcnow()).total_seconds())
                if self._timestamp_delta_limit and delta > self._timestamp_delta_limit:
                    return self._deny_request(
                        request, 400,
                        log_category="AR464")
            except ValueError:
                return self._deny_request(
                    request, 400,
                    reason=u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')".format(native_string(timestamp_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request, 400, reason=u"signed request required, but mandatory 'timestamp' field missing",
                    log_category="AR461")

        # seq
        #
        if 'seq' in args:
            seq_str = args["seq"]
            try:
                # FIXME: check sequence
                seq = int(seq_str)  # noqa
            except:
                return self._deny_request(
                    request, 400,
                    reason=u"invalid sequence number '{0}' (must be an integer)".format(native_string(seq_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request, 400,
                    reason=u"'seq' field missing",
                    log_category="AR461")

        # nonce
        #
        if 'nonce' in args:
            nonce_str = args["nonce"]
            try:
                # FIXME: check nonce
                nonce = int(nonce_str)  # noqa
            except:
                return self._deny_request(
                    request, 400,
                    reason=u"invalid nonce '{0}' (must be an integer)".format(native_string(nonce_str)),
                    log_category="AR462")
        else:
            if self._secret:
                return self._deny_request(
                    request, 400,
                    reason=u"'nonce' field missing",
                    log_category="AR461")

        # signature
        #
        if 'signature' in args:
            signature_str = args["signature"]
        else:
            if self._secret:
                return self._deny_request(
                    request, 400,
                    reason=u"'signature' field missing",
                    log_category="AR461")

        # do more checks if signed requests are required
        #
        if self._secret:

            if key_str != self._key:
                return self._deny_request(
                    request, 401,
                    reason=u"unknown key '{0}' in signed request".format(native_string(key_str)),
                    log_category="AR460")

            # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature
            hm = hmac.new(self._secret, None, hashlib.sha256)
            hm.update(key_str)
            hm.update(timestamp_str)
            hm.update(seq_str)
            hm.update(nonce_str)
            hm.update(body)
            signature_recomputed = base64.urlsafe_b64encode(hm.digest())

            if signature_str != signature_recomputed:
                return self._deny_request(request, 401,
                                          log_category="AR459")
            else:
                self.log.debug("REST request signature valid.",
                               log_category="AR203")

        # user_agent = headers.get("user-agent", "unknown")
        client_ip = request.getClientIP()
        is_secure = request.isSecure()

        # enforce client IP address
        #
        if self._require_ip:
            ip = ip_address(client_ip)
            allowed = False
            for net in self._require_ip:
                if ip in net:
                    allowed = True
                    break
            if not allowed:
                return self._deny_request(request, 400, log_category="AR466")

        # enforce TLS
        #
        if self._require_tls:
            if not is_secure:
                return self._deny_request(request, 400,
                                          reason=u"request denied because not using TLS")

        # authenticate request
        #

        # TODO: also support HTTP Basic AUTH for ticket

        def on_auth_ok(value):
            if value is True:
                # treat like original behavior and just accept the request_id
                pass
            elif isinstance(value, types.Accept):
                self._session._authid = value.authid
                self._session._authrole = value.authrole
                # realm?
            else:
                # FIXME: not returning deny request... probably not ideal
                request.write(self._deny_request(request, 401, reason=u"not authorized", log_category="AR401"))
                request.finish()
                return

            _validator.reset()
            validation_result = _validator.validate(body)

            # validate() returns a 4-tuple, of which item 0 is whether it
            # is valid
            if not validation_result[0]:
                request.write(self._deny_request(
                    request, 400,
                    log_category="AR451"))
                request.finish()
                return

            event = body.decode('utf8')

            if self.decode_as_json:
                try:
                    event = json.loads(event)
                except Exception as e:
                    request.write(self._deny_request(
                        request, 400,
                        exc=e, log_category="AR453"))
                    request.finish()
                    return

                if not isinstance(event, dict):
                    request.write(self._deny_request(
                        request, 400,
                        log_category="AR454"))
                    request.finish()
                    return

            d = maybeDeferred(self._process, request, event)

            def finish(value):
                if isinstance(value, bytes):
                    request.write(value)
                request.finish()

            d.addCallback(finish)

        def on_auth_error(err):
            # XXX: is it ideal to write to the request?
            request.write(self._deny_request(request, 401, reason=u"not authorized", log_category="AR401"))

            request.finish()
            return

        authmethod = None
        authid = None
        signature = None

        authorization_header = headers.getRawHeaders(b"authorization", [])
        if len(authorization_header) == 1:
            # HTTP Basic Authorization will be processed as ticket authentication
            authorization = authorization_header[0]
            auth_scheme, auth_details = authorization.split(b" ", 1)

            if auth_scheme.lower() == b"basic":
                try:
                    credentials = binascii.a2b_base64(auth_details + b'===')
                    credentials = credentials.split(b":", 1)
                    if len(credentials) == 2:
                        authmethod = "ticket"
                        authid = credentials[0].decode("utf-8")
                        signature = credentials[1].decode("utf-8")
                    else:
                        return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")
                except binascii.Error:
                    # authentication failed
                    return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")
        elif 'authmethod' in args and args['authmethod'].decode("utf-8") == 'ticket':
            if "ticket" not in args or "authid" not in args:
                # AR401 - fail if the ticket or authid are not in the args
                on_auth_ok(False)
            else:
                authmethod = "ticket"
                authid = args['authid'].decode("utf-8")
                signature = args['ticket'].decode("utf-8")

        if authmethod and authid and signature:

            hdetails = types.HelloDetails(
                authid=authid,
                authmethods=[authmethod]
            )

            # wire up some variables for the authenticators to work, this is hackish

            # a custom header based authentication scheme can be implemented
            # without adding alternate authenticators by forwarding all headers.
            self._session._transport._transport_info = {
                "http_headers_received": {
                    native_string(x).lower(): native_string(y[0]) for x, y in request.requestHeaders.getAllRawHeaders()
                }
            }

            self._session._pending_session_id = None
            self._session._router_factory = self._session._transport._routerFactory

            if authmethod == "ticket":
                self._pending_auth = PendingAuthTicket(self._session, self._auth_config['ticket'])
                self._pending_auth.hello(self._session._realm, hdetails)

            auth_d = maybeDeferred(self._pending_auth.authenticate, signature)
            auth_d.addCallbacks(on_auth_ok, on_auth_error)

        else:
            # don't return the value or it will be written to the request
            on_auth_ok(True)

        return server.NOT_DONE_YET
Exemple #42
0
 def __init__(self, templates, directory):
     Resource.__init__(self)
     self._page = templates.get_template('cb_web_404.html')
     self._directory = native_string(directory)
     self._pid = u'{}'.format(os.getpid())