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
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): 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
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
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()
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()
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"]})
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)
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)
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)
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")
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"}})
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 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": {} })
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)
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)
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_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()))
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"]})
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()))
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)
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_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)
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": {}})
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
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
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
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)
def test_unicode_not_allowed(self): """ A unicode argument should never be allowed. """ with self.assertRaises(ValueError): compat.native_string(u"bar")
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)
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, 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
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())