def test_process_challenge_req_good_api_version(alice, monkeypatch, client_version, backend_version, valid): # Cast parameters client_version = ApiVersion(*client_version) backend_version = ApiVersion(*backend_version) ch = AuthenticatedClientHandshake(alice.organization_id, alice.device_id, alice.signing_key, alice.root_verify_key) req = { "handshake": "challenge", "challenge": b"1234567890", "supported_api_versions": [backend_version], } monkeypatch.setattr(ch, "SUPPORTED_API_VERSIONS", [client_version]) if not valid: # Invalid versioning with pytest.raises(HandshakeAPIVersionError) as context: ch.process_challenge_req(packb(req)) assert context.value.client_versions == [client_version] assert context.value.backend_versions == [backend_version] else: # Valid versioning ch.process_challenge_req(packb(req)) assert ch.challenge_data["supported_api_versions"] == [backend_version] assert ch.backend_api_version == backend_version assert ch.client_api_version == client_version
async def check_allowed_cmds(backend_sock, cmds): for cmd in cmds: if cmd == "events_listen": # Must pass wait option otherwise backend will hang forever await backend_sock.send(packb({"cmd": cmd, "wait": False})) else: await backend_sock.send(packb({"cmd": cmd})) rep = await backend_sock.recv() assert unpackb(rep)["status"] != "unknown_command"
async def test_bad_cmd(alice_backend_sock): await alice_backend_sock.send(packb({"cmd": "dummy"})) rep = await alice_backend_sock.recv() assert unpackb(rep) == { "status": "unknown_command", "reason": "Unknown command" }
async def test_update_bad_msg(alice_backend_sock, bad_msg): await alice_backend_sock.send(packb({"cmd": "vlob_update", **bad_msg})) raw_rep = await alice_backend_sock.recv() rep = vlob_update_serializer.rep_loads(raw_rep) # Id and version are invalid anyway, but here we test another layer # so it's not important as long as we get our `bad_message` status assert rep["status"] == "bad_message"
async def test_block_read_bad_msg(alice_backend_sock, bad_msg): await alice_backend_sock.send(packb({"cmd": "block_read", **bad_msg})) raw_rep = await alice_backend_sock.recv() rep = block_read_serializer.rep_loads(raw_rep) # Valid ID doesn't exists in database but this is ok given here we test # another layer so it's not important as long as we get our # `bad_message` status assert rep["status"] == "bad_message"
def test_process_challenge_req_good_multiple_api_version( alice, monkeypatch, client_versions, backend_versions, expected_client_version, expected_backend_version, ): # Cast parameters client_versions = [ApiVersion(*args) for args in client_versions] backend_versions = [ApiVersion(*args) for args in backend_versions] if expected_client_version: expected_client_version = ApiVersion(*expected_client_version) if expected_backend_version: expected_backend_version = ApiVersion(*expected_backend_version) ch = AuthenticatedClientHandshake(alice.organization_id, alice.device_id, alice.signing_key, alice.root_verify_key) req = { "handshake": "challenge", "challenge": b"1234567890", "supported_api_versions": list(backend_versions), } monkeypatch.setattr(ch, "SUPPORTED_API_VERSIONS", client_versions) if expected_client_version is None: # Invalid versioning with pytest.raises(HandshakeAPIVersionError) as context: ch.process_challenge_req(packb(req)) assert context.value.client_versions == client_versions assert context.value.backend_versions == backend_versions else: # Valid versioning ch.process_challenge_req(packb(req)) assert ch.challenge_data["supported_api_versions"] == list( backend_versions) assert ch.backend_api_version == expected_backend_version assert ch.client_api_version == expected_client_version
async def test_handshake_invalid_format(backend, server_factory): async with server_factory(backend.handle_client) as server: stream = server.connection_factory() transport = await Transport.init_for_client(stream, server.addr.hostname) await transport.recv() # Get challenge req = {"handshake": "dummy", "client_api_version": API_VERSION} await transport.send(packb(req)) result_req = await transport.recv() assert unpackb(result_req) == { "handshake": "result", "result": "bad_protocol", "help": "{'handshake': ['Invalid value, should be `answer`']}", }
def test_process_answer_req_bad_format(req, alice): for key, good_value in [ ("organization_id", alice.organization_id), ("device_id", alice.device_id), ("rvk", alice.root_verify_key.encode()), ("token", uuid4()), ]: if req.get(key) == "<good>": req[key] = good_value req["client_api_version"] = API_V2_VERSION sh = ServerHandshake() sh.build_challenge_req() with pytest.raises(InvalidMessageError): sh.process_answer_req(packb(req))
async def _handle_client_loop(self, transport, client_ctx): # Retrieve the allowed commands according to api version and auth type api_cmds = self.apis[client_ctx.handshake_type] raw_req = None while True: # raw_req can be already defined if we received a new request # while processing a command raw_req = raw_req or await transport.recv() req = unpackb(raw_req) if get_log_level() <= LOG_LEVEL_DEBUG: client_ctx.logger.debug("Request", req=_filter_binary_fields(req)) try: cmd = req.get("cmd", "<missing>") if not isinstance(cmd, str): raise KeyError() cmd_func = api_cmds[cmd] except KeyError: rep = {"status": "unknown_command", "reason": "Unknown command"} else: try: rep = await cmd_func(client_ctx, req) except InvalidMessageError as exc: rep = { "status": "bad_message", "errors": exc.errors, "reason": "Invalid message.", } except ProtocolError as exc: rep = {"status": "bad_message", "reason": str(exc)} except CancelledByNewRequest as exc: # Long command handling such as message_get can be cancelled # when the peer send a new request raw_req = exc.new_raw_req continue if get_log_level() <= LOG_LEVEL_DEBUG: client_ctx.logger.debug("Response", rep=_filter_binary_fields(rep)) else: client_ctx.logger.info("Request", cmd=cmd, status=rep["status"]) raw_rep = packb(rep) await transport.send(raw_rep) raw_req = None
def test_build_result_req_bad_challenge(alice): sh = ServerHandshake() sh.build_challenge_req() answer = { "handshake": "answer", "type": HandshakeType.AUTHENTICATED.value, "client_api_version": API_V2_VERSION, "organization_id": alice.organization_id, "device_id": alice.device_id, "rvk": alice.root_verify_key.encode(), "answer": alice.signing_key.sign(sh.challenge + b"-dummy"), } sh.process_answer_req(packb(answer)) with pytest.raises(HandshakeFailedChallenge): sh.build_result_req(alice.verify_key)
def test_build_bad_outcomes(alice, method, expected_result): sh = ServerHandshake() sh.build_challenge_req() answer = { "handshake": "answer", "type": HandshakeType.AUTHENTICATED.value, "client_api_version": API_V2_VERSION, "organization_id": alice.organization_id, "device_id": alice.device_id, "rvk": alice.root_verify_key.encode(), "answer": alice.signing_key.sign(sh.challenge), } sh.process_answer_req(packb(answer)) req = getattr(sh, method)() assert unpackb(req) == { "handshake": "result", "result": expected_result, "help": ANY }
async def test_anonymous_handshake_invalid_format(backend, server_factory): async with server_factory(backend.handle_client) as server: stream = server.connection_factory() transport = await Transport.init_for_client(stream, server.addr.hostname) await transport.recv() # Get challenge req = { "handshake": "foo", "type": "anonymous", "client_api_version": ApiVersion(1, 1), "organization_id": "zob", } await transport.send(packb(req)) result_req = await transport.recv() assert unpackb(result_req) == { "handshake": "result", "result": "bad_protocol", "help": "{'handshake': ['Invalid value, should be `answer`']}", }
async def test_handshake_incompatible_version(backend, server_factory): async with server_factory(backend.handle_client) as server: stream = server.connection_factory() transport = await Transport.init_for_client(stream, server.addr.hostname) incompatible_version = ApiVersion(API_VERSION.version + 1, 0) await transport.recv() # Get challenge req = { "handshake": "answer", "type": "anonymous", "client_api_version": incompatible_version, "organization_id": OrganizationID("Org"), "token": "whatever", } await transport.send(packb(req)) result_req = await transport.recv() assert unpackb(result_req) == { "handshake": "result", "result": "bad_protocol", "help": "No overlap between client API versions {3.0} and backend API versions {2.0, 1.2}", }
async def test_connection(alice_backend_sock): await alice_backend_sock.send(packb({"cmd": "ping", "ping": "42"})) rep = await alice_backend_sock.recv() assert unpackb(rep) == {"status": "ok", "pong": "42"}
def test_process_result_req_bad_format(req): ch = BaseClientHandshake() with pytest.raises(InvalidMessageError): ch.process_result_req(packb(req))
def test_process_challenge_req_bad_format(alice, req): ch = AuthenticatedClientHandshake(alice.organization_id, alice.device_id, alice.signing_key, alice.root_verify_key) with pytest.raises(InvalidMessageError): ch.process_challenge_req(packb(req))
async def test_api_user_get_bad_msg(alice_backend_sock, bad_msg): await alice_backend_sock.send(packb({"cmd": "user_get", **bad_msg})) raw_rep = await alice_backend_sock.recv() rep = user_get_serializer.rep_loads(raw_rep) assert rep["status"] == "bad_message"
def test_process_result_req_bad_outcome(result, exc_cls): ch = BaseClientHandshake() with pytest.raises(exc_cls): ch.process_result_req(packb({"handshake": "result", "result": result}))
async def test_create_bad_msg(alice_backend_sock, bad_msg): await alice_backend_sock.send(packb({"cmd": "vlob_create", **bad_msg})) raw_rep = await alice_backend_sock.recv() rep = vlob_create_serializer.rep_loads(raw_rep) assert rep["status"] == "bad_message"
async def handle_client_websocket(self, stream, event, first_request_data=None): selected_logger = logger try: transport = await Transport.init_for_server( stream, first_request_data=first_request_data ) except TransportClosedByPeer as exc: selected_logger.info("Connection dropped: client has left", reason=str(exc)) return except TransportError as exc: selected_logger.info("Connection dropped: websocket error", reason=str(exc)) return selected_logger = transport.logger try: client_ctx, error_infos = await do_handshake(self, transport) if not client_ctx: # Invalid handshake selected_logger.info("Connection dropped: bad handshake", **error_infos) return selected_logger = client_ctx.logger selected_logger.info("Connection established") if isinstance(client_ctx, AuthenticatedClientContext): with trio.CancelScope() as cancel_scope: with self.event_bus.connection_context() as client_ctx.event_bus_ctx: def _on_revoked(event, organization_id, user_id): if ( organization_id == client_ctx.organization_id and user_id == client_ctx.user_id ): cancel_scope.cancel() client_ctx.event_bus_ctx.connect(BackendEvent.USER_REVOKED, _on_revoked) await self._handle_client_loop(transport, client_ctx) elif isinstance(client_ctx, InvitedClientContext): await self.invite.claimer_joined( organization_id=client_ctx.organization_id, greeter=client_ctx.invitation.greeter_user_id, token=client_ctx.invitation.token, ) try: with trio.CancelScope() as cancel_scope: with self.event_bus.connection_context() as event_bus_ctx: def _on_invite_status_changed( event, organization_id, greeter, token, status ): if ( status == InvitationStatus.DELETED and organization_id == client_ctx.organization_id and token == client_ctx.invitation.token ): cancel_scope.cancel() event_bus_ctx.connect( BackendEvent.INVITE_STATUS_CHANGED, _on_invite_status_changed ) await self._handle_client_loop(transport, client_ctx) finally: with trio.CancelScope(shield=True): await self.invite.claimer_left( organization_id=client_ctx.organization_id, greeter=client_ctx.invitation.greeter_user_id, token=client_ctx.invitation.token, ) else: await self._handle_client_loop(transport, client_ctx) await transport.aclose() except TransportClosedByPeer as exc: selected_logger.info("Connection dropped: client has left", reason=str(exc)) except (TransportError, MessageSerializationError) as exc: rep = {"status": "invalid_msg_format", "reason": "Invalid message format"} try: await transport.send(packb(rep)) except TransportError: pass await transport.aclose() selected_logger.info("Connection dropped: invalid data", reason=str(exc))