def test_no_procedure(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"{}") self.assertEqual(request.code, 400) errors = l.get_category("AR455") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
def test_qos_0_sends_no_ack(self): """ When a QoS 0 Publish packet is recieved, we don't send back a PubACK. """ got_packets = [] class PubHandler(BasicHandler): def process_publish_qos_0(self, event): got_packets.append(event) return succeed(None) h = PubHandler() r, t, p, cp = make_test_items(h) pub = Publish(duplicate=False, qos_level=0, retain=False, topic_name=u"foo", packet_identifier=None, payload=b"bar").serialise() data = (Connect(client_id=u"test123", flags=ConnectFlags(clean_session=True)).serialise() + pub) with LogCapturer("trace") as logs: for x in iterbytes(data): p.dataReceived(x) events = cp.data_received(t.value()) self.assertFalse(t.disconnecting) # Just the connack, no puback. self.assertEqual(len(events), 1) # The publish handler should have been called self.assertEqual(len(got_packets), 1) self.assertEqual(got_packets[0].serialise(), pub) # We should get a debug message saying we got the publish messages = logs.get_category("MQ201") self.assertEqual(len(messages), 1) self.assertEqual(messages[0]["publish"].serialise(), pub)
def test_unknown_key(self): """ An unknown key in a request should mean the request is rejected. """ session = MockPublisherSession(self) resource = PublisherResource(resourceOptions, session) with LogCapturer() as l: request = yield renderResource( resource, b"/", method=b"POST", headers={b"Content-Type": [b"application/json"]}, body=publishBody, sign=True, signKey="spamapp", signSecret="foobar") self.assertEqual(request.code, 401) errors = l.get_category("AR460") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 401)
def test_unknown_encoding(self): """ A body, when the Content-Type has been set to something other than charset=utf-8, will error out. """ session = MockPublisherSession(self) resource = PublisherResource({}, session) with LogCapturer("debug") as l: request = self.successResultOf(renderResource( resource, b"/", method=b"POST", headers={b"Content-Type": [b"application/json;charset=blarg"]}, body=b'{"args": ["\x61\x62\x63\xe9"]}')) self.assertEqual(request.code, 400) errors = l.get_category("AR450") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
def test_multiple_content_length(self): """ Requests with multiple Content-Length headers will be rejected. """ session = MockPublisherSession(self) resource = PublisherResource({}, session) with LogCapturer("debug") as l: 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) errors = l.get_category("AR463") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
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) with LogCapturer() as l: 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"]}) logs = l.get_category("AR203") self.assertEqual(len(logs), 1)
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) with LogCapturer("debug") as l: 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) errors = l.get_category("AR465") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
def test_bad_method(self): """ An incorrect method will mean the request is rejected. """ session = MockPublisherSession(self) resource = PublisherResource({}, session) with LogCapturer("debug") as l: request = self.successResultOf( renderResource( resource, b"/", method=b"BLBLBLB", headers={b"Content-Type": [b"application/json"]}, body=publishBody)) self.assertEqual(request.code, 405) errors = l.get_category("AR405") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 405)
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)
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_too_large_body(self): """ A too large body will mean the request is rejected. """ session = MockPublisherSession(self) resource = PublisherResource({"post_body_limit": 1}, session) with LogCapturer("debug") as l: request = self.successResultOf( renderResource( resource, b"/", method=b"POST", headers={b"Content-Type": [b"application/json"]}, body=publishBody)) self.assertEqual(request.code, 413) errors = l.get_category("AR413") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 413)
def test_JSON_list_body(self): """ A body that is not a JSON dict will be rejected by the server. """ session = MockPublisherSession(self) resource = PublisherResource({}, session) with LogCapturer("debug") as l: request = self.successResultOf( renderResource( resource, b"/", method=b"POST", headers={b"Content-Type": [b"application/json"]}, body=b"[{},{}]")) self.assertEqual(request.code, 400) errors = l.get_category("AR454") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
def test_incorrect_secret(self): """ An incorrect secret (but an otherwise well-formed signature) will mean the request is rejected. """ session = MockPublisherSession(self) resource = PublisherResource(resourceOptions, session) with LogCapturer() as l: request = yield renderResource( resource, b"/", method=b"POST", headers={b"Content-Type": [b"application/json"]}, body=publishBody, sign=True, signKey="bazapp", signSecret="foobar2") self.assertEqual(request.code, 401) errors = l.get_category("AR459") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 401)
def test_recv_packet(self): """ On receiving a packet, a trace log message is emitted with details of the received packet. """ h = BasicHandler() r, t, p, cp = make_test_items(h) data = ( # CONNECT b"101300044d51545404020002000774657374313233") with LogCapturer("trace") as logs: for x in iterbytes(unhexlify(data)): p.dataReceived(x) sent_logs = logs.get_category("MQ100") self.assertEqual(len(sent_logs), 1) self.assertEqual(sent_logs[0]["log_level"], LogLevel.debug) self.assertEqual(sent_logs[0]["txaio_trace"], True) self.assertIn("Connect", logs.log_text.getvalue())
def test_wrong_seq(self): """ A missing sequence in a request should mean the request is rejected. """ session = MockPublisherSession(self) resource = PublisherResource(resourceOptions, session) signedParams = makeSignedArguments({}, "bazapp", "foobar", publishBody) signedParams[b'seq'] = [b"notaseq"] with LogCapturer() as l: request = yield renderResource( resource, b"/", method=b"POST", headers={b"Content-Type": [b"application/json"]}, body=publishBody, params=signedParams) self.assertEqual(request.code, 400) errors = l.get_category("AR462") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
def test_qos_2_failure_drops_connection(self): """ Transient failures (like an exception from handler.process_publish_qos_2) will cause the connection it happened on to be dropped. Compliance statement MQTT-4.8.0-2 """ class PubHandler(BasicHandler): def process_publish_qos_2(self, event): raise Exception("boom!") h = PubHandler() r, t, p, cp = make_test_items(h) data = (Connect(client_id=u"test123", flags=ConnectFlags(clean_session=True)).serialise() + Publish(duplicate=False, qos_level=2, retain=False, topic_name=u"foo", packet_identifier=1, payload=b"bar").serialise()) with LogCapturer("trace") as logs: for x in iterbytes(data): p.dataReceived(x) sent_logs = logs.get_category("MQ505") self.assertEqual(len(sent_logs), 1) self.assertEqual(sent_logs[0]["log_level"], LogLevel.critical) self.assertEqual(sent_logs[0]["log_failure"].value.args[0], "boom!") events = cp.data_received(t.value()) self.assertEqual(len(events), 1) self.assertTrue(t.disconnecting) # We got the error, we need to flush it so it doesn't make the test # error 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_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_lose_conn_on_reserved_qos3(self): """ If we get, somehow, a QoS "3" Publish (one with both QoS bits set to 3), we will drop the connection. Compliance statement: MQTT-3.3.1-4 """ h = BasicHandler() r, t, p, cp = make_test_items(h) conn = Connect(client_id="test123", flags=ConnectFlags(clean_session=False)) pub = Publish(duplicate=False, qos_level=3, retain=False, topic_name="foo", packet_identifier=1, payload=b"bar") with LogCapturer("trace") as logs: p._handle_events([conn, pub]) sent_logs = logs.get_category("MQ403") self.assertEqual(len(sent_logs), 1) self.assertEqual(sent_logs[0]["log_level"], LogLevel.error) self.assertTrue(t.disconnecting)
def test_ASCII_denied(self): """ A body with an ASCII charset is denied, it must be UTF-8. """ session = MockPublisherSession(self) resource = PublisherResource({}, session) with LogCapturer("debug") as l: request = self.successResultOf( renderResource(resource, b"/", method=b"POST", headers={ b"Content-Type": [b"application/json; charset=ascii"] }, body=b'')) self.assertEqual(request.code, 400) errors = l.get_category("AR450") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
def test_exception_in_subscribe_drops_connection(self): """ Transient failures (like an exception from handler.process_subscribe) will cause the connection it happened on to be dropped. Compliance statement MQTT-4.8.0-2 """ class SubHandler(BasicHandler): @inlineCallbacks def process_subscribe(self, event): raise Exception("boom!") h = SubHandler() r, t, p, cp = make_test_items(h) data = (Connect(client_id=u"test123", flags=ConnectFlags(clean_session=True)).serialise() + Subscribe(packet_identifier=1234, topic_requests=[SubscriptionTopicRequest(u"a", 0) ]).serialise()) with LogCapturer("trace") as logs: for x in iterbytes(data): p.dataReceived(x) sent_logs = logs.get_category("MQ501") self.assertEqual(len(sent_logs), 1) self.assertEqual(sent_logs[0]["log_level"], LogLevel.critical) self.assertEqual(sent_logs[0]["log_failure"].value.args[0], "boom!") events = cp.data_received(t.value()) self.assertEqual(len(events), 1) self.assertTrue(t.disconnecting) # We got the error, we need to flush it so it doesn't make the test # error self.flushLoggedErrors()
def test_lose_conn_on_unimplemented_packet(self): """ If we get a valid, but unimplemented for that role packet (e.g. SubACK, which we will only ever send, and getting it is a protocol violation), we will drop the connection. Compliance statement: MQTT-4.8.0-1 """ sessions = {} # This shouldn't normally happen, but just in case. from crossbar.adapter.mqtt import protocol protocol.server_packet_handlers[protocol.P_SUBACK] = SubACK self.addCleanup( lambda: protocol.server_packet_handlers.pop(protocol.P_SUBACK)) h = BasicHandler() r = Clock() t = StringTransport() p = MQTTServerTwistedProtocol(h, r, sessions) p.makeConnection(t) data = (Connect(client_id=u"test123", flags=ConnectFlags(clean_session=False)).serialise() + SubACK(1, [1]).serialise()) with LogCapturer("trace") as logs: for x in iterbytes(data): p.dataReceived(x) sent_logs = logs.get_category("MQ402") self.assertEqual(len(sent_logs), 1) self.assertEqual(sent_logs[0]["log_level"], LogLevel.error) self.assertEqual(sent_logs[0]["packet_id"], "SubACK") self.assertTrue(t.disconnecting)
def test_outdated_delta(self): """ If the delta between now and the timestamp in the request is larger than C{timestamp_delta_limit}, the request is rejected. """ custOpts = {"timestamp_delta_limit": 1} custOpts.update(resourceOptions) session = MockPublisherSession(self) resource = PublisherResource(custOpts, session) signedParams = makeSignedArguments({}, "bazapp", "foobar", publishBody) signedParams[b'timestamp'] = [b"2011-10-14T16:59:51.123Z"] with LogCapturer() as l: request = yield renderResource( resource, b"/", method=b"POST", headers={b"Content-Type": [b"application/json"]}, body=publishBody, params=signedParams) self.assertEqual(request.code, 400) errors = l.get_category("AR462") self.assertEqual(len(errors), 1) self.assertEqual(errors[0]["code"], 400)
class TestamentTests(unittest.TestCase): def setUp(self): self.logs = LogCapturer() self.logs.__enter__() self.addCleanup(lambda: self.logs.__exit__(None, None, None)) def test_destroy_testament_sent_on_destroy(self): """ If one session calls wamp.session.add_testament and then the session is destroyed, the message it filed as a testament will be sent to subscribers of the chosen topic. """ router, server_factory = make_router() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({'args': a, 'kwargs': kw}), u'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session(server_factory, ObservingSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}) pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Then leave... session.leave() pump.flush() ob_pump.flush() # Testament is sent self.assertEqual(ob_session.events, [{'args': (u"hello",), 'kwargs': {}}]) def test_destroy_testament_not_sent_when_cleared(self): """ If one session calls wamp.session.add_testament, then the same session calls wamp.session.flush_testaments, and then the session is destroyed, the message it filed as a testament will not be sent, as it was deleted. """ router, server_factory = make_router() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({'args': a, 'kwargs': kw}), u'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session(server_factory, ObservingSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}) pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the testament d = session.call(u"wamp.session.flush_testaments") pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # Then leave... session.leave() pump.flush() ob_pump.flush() # No testaments were sent self.assertEqual(ob_session.events, []) def test_add_testament_needs_valid_scope(self): """ Only 'detatched' and 'destroyed' are valid scopes for add_testament. """ router, server_factory = make_router() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}, scope=u"bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, (u"scope must be destroyed or detatched",)) def test_flush_testament_needs_valid_scope(self): """ Only 'detatched' and 'destroyed' are valid scopes for flush_testament. """ router, server_factory = make_router() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call(u"wamp.session.flush_testaments", scope=u"bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, (u"scope must be destroyed or detatched",)) def test_one_scope_does_not_affect_other(self): """ Adding a testament to one scope and flushing the other maintains the added testament. """ router, server_factory = make_router() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({'args': a, 'kwargs': kw}), u'com.test.dc') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session(server_factory, ObservingSession) # Add a destroyed testament d = session.call(u"wamp.session.add_testament", u"com.test.dc", [u'destroyed'], {}, scope=u"destroyed") pump.flush() self.assertEqual(self.successResultOf(d), None) # Add a detatched testament d = session.call(u"wamp.session.add_testament", u"com.test.dc", [u'detatched'], {}, scope=u"detatched") pump.flush() self.assertEqual(self.successResultOf(d), None) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the destroyed testament d = session.call(u"wamp.session.flush_testaments", scope=u"destroyed") pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # Then leave... session.leave() pump.flush() ob_pump.flush() # Just the detatched testament is sent self.assertEqual(ob_session.events, [{"args": (u'detatched',), "kwargs": {}}])
class MQTTAdapterTests(TestCase): def setUp(self): self.logs = LogCapturer() self.logs.__enter__() self.addCleanup(lambda: self.logs.__exit__(None, None, None)) def _test_basic_publish(self): reactor, router, server_factory, session_factory = build_mqtt_server() session, pump = connect_application_session( server_factory, ObservingSession, component_config=ComponentConfig(realm=u"mqtt")) client_transport, client_protocol, mqtt_pump = connect_mqtt_server(server_factory) client_transport.write( Connect(client_id=u"testclient", username=u"test123", password=u"password", flags=ConnectFlags(clean_session=False, username=True, password=True)).serialise()) mqtt_pump.flush() # We get a CONNECT self.assertEqual(client_protocol.data, ConnACK(session_present=False, return_code=0).serialise()) client_protocol.data = b"" client_transport.write( Publish(duplicate=False, qos_level=0, retain=False, topic_name=u"test", payload=b'{"kwargs": {"bar": "baz"}}').serialise()) mqtt_pump.flush() pump.flush() # This needs to be replaced with the real deal, see https://github.com/crossbario/crossbar/issues/885 self.assertEqual(len(session.events), 1) self.assertEqual( session.events, [{"args": tuple(), "kwargs": {u'bar': u'baz'}}]) def _test_tls_auth(self): """ A MQTT client can connect using mutually authenticated TLS authentication. """ reactor, router, server_factory, session_factory = build_mqtt_server() real_reactor = selectreactor.SelectReactor() logger = make_logger() session, pump = connect_application_session( server_factory, ObservingSession, component_config=ComponentConfig(realm=u"mqtt")) endpoint = create_listening_endpoint_from_config({ "type": "tcp", "port": 1099, "interface": "0.0.0.0", "tls": { "certificate": "server.crt", "key": "server.key", "dhparam": "dhparam", "ca_certificates": [ "ca.cert.pem", "intermediate.cert.pem" ]}, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) client_endpoint = create_connecting_endpoint_from_config({ "type": "tcp", "host": "127.0.0.1", "port": 1099, "tls": { "certificate": "client.crt", "hostname": u"localhost", "key": "client.key", "ca_certificates": [ "ca.cert.pem", "intermediate.cert.pem" ]}, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) p = [] l = endpoint.listen(server_factory) class TestProtocol(Protocol): data = b"" expected = (ConnACK(session_present=False, return_code=0).serialise() + PubACK(packet_identifier=1).serialise()) def dataReceived(self_, data): self_.data = self_.data + data if len(self_.data) == len(self_.expected): self.assertEqual(self_.data, self_.expected) real_reactor.stop() @l.addCallback def _listening(factory): d = client_endpoint.connect(Factory.forProtocol(TestProtocol)) @d.addCallback def _(proto): p.append(proto) proto.transport.write( Connect(client_id=u"test123", flags=ConnectFlags(clean_session=False)).serialise()) proto.transport.write( Publish(duplicate=False, qos_level=1, retain=False, topic_name=u"test", payload=b"{}", packet_identifier=1).serialise()) lc = LoopingCall(pump.flush) lc.clock = real_reactor lc.start(0.01) def timeout(): print("Timing out :(") real_reactor.stop() print(self.logs.log_text.getvalue()) # Timeout, just in case real_reactor.callLater(10, timeout) real_reactor.run() client_protocol = p[0] # We get a CONNECT self.assertEqual(client_protocol.data, ConnACK(session_present=False, return_code=0).serialise() + PubACK(packet_identifier=1).serialise()) client_protocol.data = b"" pump.flush() # This needs to be replaced with the real deal, see https://github.com/crossbario/crossbar/issues/885 self.assertEqual(len(session.events), 1) self.assertEqual( session.events, [{"args": tuple(), "kwargs": {}}]) def test_tls_auth_denied(self): """ A MQTT client offering the wrong certificate won't be authenticated. """ reactor, router, server_factory, session_factory = build_mqtt_server() real_reactor = selectreactor.SelectReactor() logger = make_logger() session, pump = connect_application_session( server_factory, ObservingSession, component_config=ComponentConfig(realm=u"mqtt")) endpoint = create_listening_endpoint_from_config({ "type": "tcp", "port": 1099, "interface": "0.0.0.0", "tls": { "certificate": "server.crt", "key": "server.key", "dhparam": "dhparam", "ca_certificates": [ "ca.cert.pem", "intermediate.cert.pem" ]}, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) client_endpoint = create_connecting_endpoint_from_config({ "type": "tcp", "host": "127.0.0.1", "port": 1099, "tls": { # BAD key: trusted by the CA, but wrong ID "certificate": "client_1.crt", "hostname": u"localhost", "key": "client_1.key", "ca_certificates": [ "ca.cert.pem", "intermediate.cert.pem" ]}, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) p = [] l = endpoint.listen(server_factory) class TestProtocol(Protocol): data = b"" expected = ( ConnACK(session_present=False, return_code=1).serialise()) def dataReceived(self_, data): self_.data = self_.data + data if len(self_.data) == len(self_.expected): self.assertEqual(self_.data, self_.expected) real_reactor.stop() @l.addCallback def _listening(factory): d = client_endpoint.connect(Factory.forProtocol(TestProtocol)) @d.addCallback def _(proto): p.append(proto) proto.transport.write( Connect(client_id=u"test123", flags=ConnectFlags(clean_session=False)).serialise()) proto.transport.write( Publish(duplicate=False, qos_level=1, retain=False, topic_name=u"test", payload=b"{}", packet_identifier=1).serialise()) lc = LoopingCall(pump.flush) lc.clock = real_reactor lc.start(0.01) def timeout(): print("Timing out :(") real_reactor.stop() print(self.logs.log_text.getvalue()) # Timeout, just in case real_reactor.callLater(10, timeout) real_reactor.run() client_protocol = p[0] # We get a CONNECT self.assertEqual(client_protocol.data, ConnACK(session_present=False, return_code=1).serialise()) client_protocol.data = b"" pump.flush() # No events! self.assertEqual(len(session.events), 0) def _test_basic_subscribe(self): """ The MQTT client can subscribe to a WAMP topic and get messages. """ reactor, router, server_factory, session_factory = build_mqtt_server() client_transport, client_protocol, mqtt_pump = connect_mqtt_server(server_factory) session, pump = connect_application_session( server_factory, ApplicationSession, component_config=ComponentConfig(realm=u"mqtt")) client_transport.write( Connect(client_id=u"testclient", username=u"test123", password=u"password", flags=ConnectFlags(clean_session=False, username=True, password=True)).serialise()) client_transport.write( Subscribe(packet_identifier=1, topic_requests=[ SubscriptionTopicRequest(topic_filter=u"com/test/wamp", max_qos=0) ]).serialise()) mqtt_pump.flush() self.assertEqual( client_protocol.data, (ConnACK(session_present=False, return_code=0).serialise() + SubACK(packet_identifier=1, return_codes=[0]).serialise())) client_protocol.data = b"" session.publish(u"com.test.wamp", u"bar") pump.flush() reactor.advance(0.1) mqtt_pump.flush() self.assertEqual( client_protocol.data, Publish(duplicate=False, qos_level=0, retain=False, topic_name=u"com/test/wamp", payload=b'{"args":["bar"]}').serialise() ) def _test_retained(self): """ The MQTT client can set and receive retained messages. """ reactor, router, server_factory, session_factory = build_mqtt_server() client_transport, client_protocol, mqtt_pump = connect_mqtt_server(server_factory) client_transport.write( Connect(client_id=u"testclient", username=u"test123", password=u"password", flags=ConnectFlags(clean_session=False, username=True, password=True)).serialise()) client_transport.write( Publish(duplicate=False, qos_level=1, retain=True, topic_name=u"com/test/wamp", packet_identifier=123, payload=b'{}').serialise()) mqtt_pump.flush() self.assertEqual( client_protocol.data, ( ConnACK(session_present=False, return_code=0).serialise() + PubACK(packet_identifier=123).serialise() )) client_protocol.data = b"" client_transport.write( Subscribe(packet_identifier=1, topic_requests=[ SubscriptionTopicRequest(topic_filter=u"com/test/wamp", max_qos=0) ]).serialise()) mqtt_pump.flush() self.assertEqual( client_protocol.data, SubACK(packet_identifier=1, return_codes=[0]).serialise()) client_protocol.data = b"" reactor.advance(0.1) mqtt_pump.flush() # This needs to be replaced with the real deal, see https://github.com/crossbario/crossbar/issues/885 self.assertEqual( client_protocol.data, Publish(duplicate=False, qos_level=0, retain=True, topic_name=u"com/test/wamp", payload=json.dumps( {}, sort_keys=True).encode('utf8') ).serialise() ) def _test_lastwill(self): """ FIXME: reactivate this test. The MQTT client can set a last will message which will be published when it disconnects. """ reactor, router, server_factory, session_factory = build_mqtt_server() session, pump = connect_application_session( server_factory, ObservingSession, component_config=ComponentConfig(realm=u"mqtt")) client_transport, client_protocol, mqtt_pump = connect_mqtt_server(server_factory) client_transport.write( Connect(client_id=u"testclient", username=u"test123", password=u"password", will_topic=u"test", will_message=b'{"args":["foobar"]}', flags=ConnectFlags(clean_session=False, username=True, password=True, will=True)).serialise()) mqtt_pump.flush() # We get a CONNECT self.assertEqual(client_protocol.data, ConnACK(session_present=False, return_code=0).serialise()) client_protocol.data = b"" client_transport.write(Disconnect().serialise()) mqtt_pump.flush() pump.flush() self.assertEqual(client_transport.disconnected, True) # This needs to be replaced with the real deal, see https://github.com/crossbario/crossbar/issues/885 self.assertEqual(len(session.events), 1) self.assertEqual( session.events, [{"args": [u"foobar"]}])
def test_qos_2_sends_ack(self): """ When a QoS 2 Publish packet is recieved, we send a PubREC with the same packet identifier as the original Publish, wait for a PubREL, and then send a PubCOMP. Compliance statement MQTT-4.3.3-2 Spec part 3.4, 4.3.3 """ got_packets = [] class PubHandler(BasicHandler): def process_publish_qos_2(self, event): got_packets.append(event) return succeed(None) h = PubHandler() r, t, p, cp = make_test_items(h) pub = Publish(duplicate=False, qos_level=2, retain=False, topic_name=u"foo", packet_identifier=1, payload=b"bar").serialise() data = (Connect(client_id=u"test123", flags=ConnectFlags(clean_session=True)).serialise() + pub) with LogCapturer("trace") as logs: for x in iterbytes(data): p.dataReceived(x) events = cp.data_received(t.value()) self.assertFalse(t.disconnecting) # ConnACK + PubREC with the same packet ID self.assertEqual(len(events), 2) self.assertEqual(events[1], PubREC(packet_identifier=1)) # The publish handler should have been called self.assertEqual(len(got_packets), 1) self.assertEqual(got_packets[0].serialise(), pub) # We should get a debug message saying we got the publish messages = logs.get_category("MQ203") self.assertEqual(len(messages), 1) self.assertEqual(messages[0]["publish"].serialise(), pub) # Clear the client transport t.clear() # Now we send the PubREL pubrel = PubREL(packet_identifier=1) for x in iterbytes(pubrel.serialise()): p.dataReceived(x) events = cp.data_received(t.value()) self.assertFalse(t.disconnecting) # We should get a PubCOMP in response self.assertEqual(len(events), 1) self.assertEqual(events[0], PubCOMP(packet_identifier=1))
class TestamentTests(unittest.TestCase): # FIXME: # [ERROR] Traceback (most recent call last): # File "/home/oberstet/scm/crossbario/crossbar/crossbar/router/test/test_testament.py", line 203, in test_one_scope_does_not_affect_other # d = session.call(u"wamp.session.add_testament", u"com.test.dc", # builtins.AttributeError: 'NoneType' object has no attribute 'call' skip = True def setUp(self): self.logs = LogCapturer() self.logs.__enter__() self.addCleanup(lambda: self.logs.__exit__(None, None, None)) def test_destroy_testament_sent_on_destroy(self): """ If one session calls wamp.session.add_testament and then the session is destroyed, the message it filed as a testament will be sent to subscribers of the chosen topic. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({'args': a, 'kwargs': kw}), u'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session(server_factory, ObservingSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}) pump.flush() # Make sure it returns a publication ID self.assertIsInstance(self.successResultOf(d), (int, )) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Then leave... session.leave() pump.flush() ob_pump.flush() # Testament is sent self.assertEqual(ob_session.events, [{'args': (u"hello",), 'kwargs': {}}]) def test_destroy_testament_not_sent_when_cleared(self): """ If one session calls wamp.session.add_testament, then the same session calls wamp.session.flush_testaments, and then the session is destroyed, the message it filed as a testament will not be sent, as it was deleted. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({'args': a, 'kwargs': kw}), u'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session(server_factory, ObservingSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}) pump.flush() # Make sure it returns an integer (the testament event publication ID) self.assertIsInstance(self.successResultOf(d), (int, )) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the testament d = session.call(u"wamp.session.flush_testaments") pump.flush() # Make sure it returns flushed count 1 self.assertEqual(self.successResultOf(d), 1) # Then leave... session.leave() pump.flush() ob_pump.flush() # No testaments were sent self.assertEqual(ob_session.events, []) def test_add_testament_needs_valid_scope(self): """ Only 'detached' and 'destroyed' are valid scopes for add_testament. """ router, server_factory, router_factory = make_router_and_realm() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}, scope=u"bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, (u"scope must be destroyed or detached",)) def test_flush_testament_needs_valid_scope(self): """ Only 'detached' and 'destroyed' are valid scopes for flush_testament. """ router, server_factory, router_factory = make_router_and_realm() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call(u"wamp.session.flush_testaments", scope=u"bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, (u"scope must be destroyed or detached",)) def test_one_scope_does_not_affect_other(self): """ Adding a testament to one scope and flushing the other maintains the added testament. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({'args': a, 'kwargs': kw}), u'com.test.dc') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session(server_factory, ObservingSession) # Add a destroyed testament d = session.call(u"wamp.session.add_testament", u"com.test.dc", [u'destroyed'], {}, scope=u"destroyed") pump.flush() self.assertIsInstance(self.successResultOf(d), (int, )) # Add a detached testament d = session.call(u"wamp.session.add_testament", u"com.test.dc", [u'detached'], {}, scope=u"detached") pump.flush() self.assertIsInstance(self.successResultOf(d), (int, )) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the destroyed testament d = session.call(u"wamp.session.flush_testaments", scope=u"destroyed") pump.flush() # Make sure it returns number of flushed testaments self.assertEqual(self.successResultOf(d), 1) # Then leave... session.leave() pump.flush() ob_pump.flush() # Just the detached testament is sent self.assertEqual(ob_session.events, [{"args": (u'detached',), "kwargs": {}}])
class TestamentTests(unittest.TestCase): # FIXME: # [ERROR] Traceback (most recent call last): # File "/home/oberstet/scm/crossbario/crossbar/crossbar/router/test/test_testament.py", line 203, in test_one_scope_does_not_affect_other # d = session.call("wamp.session.add_testament", "com.test.dc", # builtins.AttributeError: 'NoneType' object has no attribute 'call' skip = True def setUp(self): self.logs = LogCapturer() self.logs.__enter__() self.addCleanup(lambda: self.logs.__exit__(None, None, None)) def test_destroy_testament_sent_on_destroy(self): """ If one session calls wamp.session.add_testament and then the session is destroyed, the message it filed as a testament will be sent to subscribers of the chosen topic. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({ 'args': a, 'kwargs': kw }), 'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session( server_factory, ObservingSession) d = session.call("wamp.session.add_testament", "com.test.destroyed", ['hello'], {}) pump.flush() # Make sure it returns a publication ID self.assertIsInstance(self.successResultOf(d), (int, )) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Then leave... session.leave() pump.flush() ob_pump.flush() # Testament is sent self.assertEqual(ob_session.events, [{ 'args': ("hello", ), 'kwargs': {} }]) def test_destroy_testament_not_sent_when_cleared(self): """ If one session calls wamp.session.add_testament, then the same session calls wamp.session.flush_testaments, and then the session is destroyed, the message it filed as a testament will not be sent, as it was deleted. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({ 'args': a, 'kwargs': kw }), 'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session( server_factory, ObservingSession) d = session.call("wamp.session.add_testament", "com.test.destroyed", ['hello'], {}) pump.flush() # Make sure it returns an integer (the testament event publication ID) self.assertIsInstance(self.successResultOf(d), (int, )) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the testament d = session.call("wamp.session.flush_testaments") pump.flush() # Make sure it returns flushed count 1 self.assertEqual(self.successResultOf(d), 1) # Then leave... session.leave() pump.flush() ob_pump.flush() # No testaments were sent self.assertEqual(ob_session.events, []) def test_add_testament_needs_valid_scope(self): """ Only 'detached' and 'destroyed' are valid scopes for add_testament. """ router, server_factory, router_factory = make_router_and_realm() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call("wamp.session.add_testament", "com.test.destroyed", ['hello'], {}, scope="bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, ("scope must be destroyed or detached", )) def test_flush_testament_needs_valid_scope(self): """ Only 'detached' and 'destroyed' are valid scopes for flush_testament. """ router, server_factory, router_factory = make_router_and_realm() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call("wamp.session.flush_testaments", scope="bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, ("scope must be destroyed or detached", )) def test_one_scope_does_not_affect_other(self): """ Adding a testament to one scope and flushing the other maintains the added testament. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({ 'args': a, 'kwargs': kw }), 'com.test.dc') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session( server_factory, ObservingSession) # Add a destroyed testament d = session.call("wamp.session.add_testament", "com.test.dc", ['destroyed'], {}, scope="destroyed") pump.flush() self.assertIsInstance(self.successResultOf(d), (int, )) # Add a detached testament d = session.call("wamp.session.add_testament", "com.test.dc", ['detached'], {}, scope="detached") pump.flush() self.assertIsInstance(self.successResultOf(d), (int, )) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the destroyed testament d = session.call("wamp.session.flush_testaments", scope="destroyed") pump.flush() # Make sure it returns number of flushed testaments self.assertEqual(self.successResultOf(d), 1) # Then leave... session.leave() pump.flush() ob_pump.flush() # Just the detached testament is sent self.assertEqual(ob_session.events, [{ "args": ('detached', ), "kwargs": {} }])
def setUp(self): self.logs = LogCapturer() self.logs.__enter__() self.addCleanup(lambda: self.logs.__exit__(None, None, None))
class TestamentTests(unittest.TestCase): def setUp(self): self.logs = LogCapturer() self.logs.__enter__() self.addCleanup(lambda: self.logs.__exit__(None, None, None)) def test_destroy_testament_sent_on_destroy(self): """ If one session calls wamp.session.add_testament and then the session is destroyed, the message it filed as a testament will be sent to subscribers of the chosen topic. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({ 'args': a, 'kwargs': kw }), u'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session( server_factory, ObservingSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}) pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Then leave... session.leave() pump.flush() ob_pump.flush() # Testament is sent self.assertEqual(ob_session.events, [{ 'args': (u"hello", ), 'kwargs': {} }]) def test_destroy_testament_not_sent_when_cleared(self): """ If one session calls wamp.session.add_testament, then the same session calls wamp.session.flush_testaments, and then the session is destroyed, the message it filed as a testament will not be sent, as it was deleted. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({ 'args': a, 'kwargs': kw }), u'com.test.destroyed') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session( server_factory, ObservingSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}) pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the testament d = session.call(u"wamp.session.flush_testaments") pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # Then leave... session.leave() pump.flush() ob_pump.flush() # No testaments were sent self.assertEqual(ob_session.events, []) def test_add_testament_needs_valid_scope(self): """ Only 'detatched' and 'destroyed' are valid scopes for add_testament. """ router, server_factory, router_factory = make_router_and_realm() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call(u"wamp.session.add_testament", u"com.test.destroyed", [u'hello'], {}, scope=u"bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, (u"scope must be destroyed or detatched", )) def test_flush_testament_needs_valid_scope(self): """ Only 'detatched' and 'destroyed' are valid scopes for flush_testament. """ router, server_factory, router_factory = make_router_and_realm() session, pump = connect_application_session(server_factory, ApplicationSession) d = session.call(u"wamp.session.flush_testaments", scope=u"bar") pump.flush() # Make sure it returns a failure failure = self.failureResultOf(d) self.assertEqual(failure.value.args, (u"scope must be destroyed or detatched", )) def test_one_scope_does_not_affect_other(self): """ Adding a testament to one scope and flushing the other maintains the added testament. """ router, server_factory, router_factory = make_router_and_realm() class ObservingSession(ApplicationSession): @inlineCallbacks def onJoin(self, details): self.events = [] self.s = yield self.subscribe( lambda *a, **kw: self.events.append({ 'args': a, 'kwargs': kw }), u'com.test.dc') session, pump = connect_application_session(server_factory, ApplicationSession) ob_session, ob_pump = connect_application_session( server_factory, ObservingSession) # Add a destroyed testament d = session.call(u"wamp.session.add_testament", u"com.test.dc", [u'destroyed'], {}, scope=u"destroyed") pump.flush() self.assertEqual(self.successResultOf(d), None) # Add a detatched testament d = session.call(u"wamp.session.add_testament", u"com.test.dc", [u'detatched'], {}, scope=u"detatched") pump.flush() self.assertEqual(self.successResultOf(d), None) # No testament sent yet pump.flush() ob_pump.flush() self.assertEqual(ob_session.events, []) # Flush the destroyed testament d = session.call(u"wamp.session.flush_testaments", scope=u"destroyed") pump.flush() # Make sure it returns None self.assertEqual(self.successResultOf(d), None) # Then leave... session.leave() pump.flush() ob_pump.flush() # Just the detatched testament is sent self.assertEqual(ob_session.events, [{ "args": (u'detatched', ), "kwargs": {} }])