diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index af652a7659..98e0003ad8 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyrignt 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -438,7 +439,7 @@ class FederationClient(FederationBase): Fails with a ``RuntimeError`` if no servers were reachable. """ - valid_memberships = {Membership.JOIN, Membership.LEAVE} + valid_memberships = {Membership.JOIN, Membership.LEAVE, Membership.KNOCK} if membership not in valid_memberships: raise RuntimeError( "make_membership_event called with membership='%s', must be one of %s" @@ -785,6 +786,48 @@ class FederationClient(FederationBase): # content. return resp[1] + def send_knock(self, destinations, pdu): + """Sends a knock event to one of a list of homeservers. + + Doing so will cause the remote server to add teh event to the graph, + and send the event out to the rest of the federation. + + Args: + destinations (str): Candidate homeservers which are probably participating in the room. + pdu (BaseEvent): event to be sent + + Return: + Deferred: resolves to None. + + Fails with a ``SynapseError`` if the chosen remote server + returns a 300/400 code. + + Fails with a ``RuntimeError`` if no servers were reachable. + """ + + @defer.inlineCallbacks + def send_request(destination): + content = yield self._do_send_knock(destination, pdu) + + logger.debug("Got content: %s", content) + return None + + return self._try_destination_list("send_knock", destinations, send_request) + + @defer.inlineCallbacks + def _do_send_knock(self, destination, pdu): + time_now = self._clock.time_msec() + + # knock only has the v2 api, no need to fall back to v1 + content = yield self.transport_layer.send_knock_v2( + destination=destination, + room_id=pdu.room_id, + event_id=pdu.event_id, + content=pdu.get_pdu_json(time_now) + ) + + return content + def get_public_rooms( self, destination, diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 198257414b..fe6372fa41 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -299,6 +300,17 @@ class TransportLayerClient(object): return response + @defer.inlineCallbacks + @log_function + def send_knock_v2(self, destination, room_id, event_id, content): + path = _create_v2_path("/send_knock/%s/%s", room_id, event_id) + + response = yield self.client.put_json( + destination=destination, path=path, data=content + ) + + return response + @defer.inlineCallbacks @log_function def send_invite_v1(self, destination, room_id, event_id, content): diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index d8cf9ed299..69e10abbe6 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -2,6 +2,7 @@ # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -541,6 +542,22 @@ class FederationV2SendLeaveServlet(BaseFederationServlet): return 200, content +class FederationMakeKnockServlet(BaseFederationServlet): + PATH = "/make_knock/(?P[^/]*)/(?P[^/]*)" + + async def on_GET(self, origin, content, query, context, user_id): + content = await self.handler.on_make_knock_request(origin, context, user_id) + return 200, content + +class FederationV2MakeKnockServlet(BaseFederationServlet): + PATH = "/send_knock/(?P[^/]*)/(?P[^/]*)" + + PREFIX = FEDERATION_V2_PREFIX + + async def on_PUT(self, origin, content, query, room_id, event_id): + content = await self.handler.on_send_knock_request(origin, content, room_id) + return 200, content + class FederationEventAuthServlet(BaseFederationServlet): PATH = "/event_auth/(?P[^/]*)/(?P[^/]*)" diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d4f9a792fc..6dfcc5a07c 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2,6 +2,7 @@ # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -1273,6 +1274,34 @@ class FederationHandler(BaseHandler): return True + @log_function + @defer.inlineCallbacks + def do_knock(self, target_hosts, room_id, knockee, content): + """ Sends the knock to the remote server. + + This first triggers a /make_knock/ request that returns a partial + event that we can fill out and sign. This is then sent to the + remote server via /send_knock/. + + Knockees must be signed by the knockee's server before distributing. + """ + logger.debug("Knocking %s to %s", knockee, room_id) + + origin, event, event_format_version = yield self._make_and_verify_event( + target_hosts, room_id, knockee, "knock", content, + ) + + # Try the host that we successfully called /make_knock/ on first for + # the /send_knock/ request. + try: + target_hosts.remove(origin) + target_hosts.insert(0, origin) + except ValueError: + pass + + yield self.federation_client.send_knock(target_hosts, event) + return event + async def _handle_queued_pdus(self, room_queue): """Process PDUs which got queued up while we were busy send_joining. @@ -1628,6 +1657,107 @@ class FederationHandler(BaseHandler): return None + @defer.inlineCallbacks + @log_function + def on_make_kock_request(self, origin, room_id, user_id): + """ We've received a /make_knock/ request, so we create a partial + knock event for the room and return that. We do *not* persist or + process it until the other server has signed it and sent it back. + + Args: + origin (str): The (verified) server name of the requesting server. + room_id (str): Room to create knock event in + user_id (str): The user to create the knock for + + Returns: + Deferred[FrozenEvent] + """ + if get_domain_from_id(user_id) != origin: + logger.info( + "Get /make_knock request for user %r from different origin %s, ignoring", + user_id, + origin, + ) + raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) + + room_version = yield self.store.get_room_version(room_id) + builder = self.event_builder_factory.new( + room_version, + { + "type": EventTypes.Member, + "content": {"membership": Membership.KNOCK}, + "room_id": room_id, + "sender": user_id, + "state_key": user_id, + }, + ) + + event, context = yield self.event_creation_handler.create_new_client_event( + builder=builder + ) + + event_allowed = yield self.third_party_event_rules.check_event_allowed( + event, context + ) + if not event_allowed: + logger.warning("Creation of leave %s forbidden by third-party rules", event) + raise SynapseError( + 403, "This event is not allowed in this context", Codes.FORBIDDEN + ) + + try: + # The remote hasn't signed it yet, obviously. We'll do the full checks + # when we get the event back in `on_send_knock_request` + yield self.auth.check_from_context( + room_version, event, context, do_sig_check=False + ) + except AuthError as e: + logger.warning("Failed to create new knock %r because %s", event, e) + raise e + + return event + + @defer.inlineCallbacks + @log_function + def on_send_knock_request(self, origin, pdu): + """ We have received a knock event for a room. Fully process it.""" + event = pdu + + logger.debug( + "on_send_knock_request: Got event: %s, signatures: %s", + event.event_id, + event.signatures, + ) + + if get_domain_from_id(event.sender) != origin: + logger.info( + "Got /send_knock request for user %r from different origin %s", + event.sender, + origin, + ) + raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) + + event.internal_metadata.outlier = False + + context = yield self._handle_new_event(origin, event) + + event_allowed = yield self.third_party_event_rules.check_event_allowed( + event, context + ) + if not event_allowed: + logger.info("Sending of leave %s forbidden by third-party rules", event) + raise SynapseError( + 403, "This event is not allowed in this context", Codes.FORBIDDEN + ) + + logger.debug( + "on_send_knock_request: After _handle_new_event: %s, sigs: %s", + event.event_id, + event.signatures, + ) + + return None + @defer.inlineCallbacks def get_state_for_pdu(self, room_id, event_id): """Returns the state at the event. i.e. not including said event. diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 6096ab9abe..0279877eb8 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -489,7 +489,11 @@ class RoomMemberHandler(object): content["displayname"] = yield profile.get_displayname(target) content["avatar_url"] = yield profile.get_avatar_url(target) - raise SynapseError(500, "Not yet implemented") + remote_knock_response = yield self._remote_knock( + requester, remote_room_hosts, room_id, target, content + ) + + return remote_knock_response res = yield self._local_membership_update( requester=requester, @@ -1012,6 +1016,25 @@ class RoomMemberMasterHandler(RoomMemberHandler): yield self.store.locally_reject_invite(target.to_string(), room_id) return {} + @defer.inlineCallbacks + def _remote_knock(self, requester, remote_room_hosts, room_id, user, content): + # filter ourselves out of remote_room_hosts + remote_room_hosts = [ + host for host in remote_room_hosts if host != self.hs.hostname + ] + + if len(remote_room_hosts) == 0: + raise SynapseError(404, "No known servers") + + ret = yield fed_handler.do_knock( + remote_room_hosts, room_id, user.to_string(), content=content, + ) + return ret + + yield self.federation_handler.send_knock( + remote_room_hosts, room_id, user.to_string(), content + ) + def _user_joined_room(self, target, room_id): """Implements RoomMemberHandler._user_joined_room """ diff --git a/synapse/rest/client/v2_alpha/knock.py b/synapse/rest/client/v2_alpha/knock.py index 946aa5a620..b4ffcfd808 100644 --- a/synapse/rest/client/v2_alpha/knock.py +++ b/synapse/rest/client/v2_alpha/knock.py @@ -17,13 +17,19 @@ import logging from synapse.api.errors import AuthError, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.rest.client.transactions import HttpTransactionCache +from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID from ._base import client_patterns logger = logging.getLogger(__name__) +class TransactionRestServlet(RestServlet): + def __init__(self, hs): + super(TransactionRestServlet, self).__init__() + self.txns = HttpTransactionCache(hs) -class KnockServlet(RestServlet): +class KnockServlet(TransactionRestServlet): """ POST /rooms/{roomId}/knock """ @@ -57,5 +63,73 @@ class KnockServlet(RestServlet): return 200, {} + def on_PUT(self, request, room_id, txn_id): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id, txn_id + ) + +class KnockRoomALiasServlet(TransactionRestServlet): + """ + POST /knock/{roomIdOrAlias} + """ + + PATTERNS = client_patterns( + "/knock/(?P[^/]*)" + ) + + def __init__(self, hs): + super(KnockRoomALiasServlet, self).__init__() + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + + async def on_POST(self, request, room_identifier, txn_id=None): + requester = await self.auth.get_user_by_req(request) + + content = parse_json_object_from_request(request) + event_content = None + if "reason" in content: + event_content = {"reason": content["reason"]} + + if RoomID.is_valid(room_identifier): + room_id = room_identifier + try: + remote_room_hosts = [ + x.decode("ascii") for x in request.args[b"server_name"] + ] + except Exception: + remote_room_hosts = None + elif RoomAlias.is_valid(room_identifier): + handler = self.room_member_handler + room_alias = RoomAlias.from_string(room_identifier) + room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias) + room_id = room_id.to_string() + else: + raise SynapseError( + 400, "%s was not legal room ID or room alias" % (room_identifier,) + ) + + await self.room_member_handler.update_membership( + requester=requester, + target=requester.user, + room_id=room_id, + action="knock", + txn_id=txn_id, + third_party_signed=None, + remote_room_hosts=remote_room_hosts, + content=event_content, + ) + + return 200, {} + + def on_PUT(self, request, room_identifier, txn_id): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_identifier, txn_id + ) + def register_servlets(hs, http_server): KnockServlet(hs).register(http_server) + KnockServlet(hs).register(http_server)