class RoomCreationHandler(BaseHandler): PRESETS_DICT = { RoomCreationPreset.PRIVATE_CHAT: { "join_rules": JoinRules.INVITE, "history_visibility": "shared", "original_invitees_have_ops": False, "guest_can_join": True, }, RoomCreationPreset.TRUSTED_PRIVATE_CHAT: { "join_rules": JoinRules.INVITE, "history_visibility": "shared", "original_invitees_have_ops": True, "guest_can_join": True, }, RoomCreationPreset.PUBLIC_CHAT: { "join_rules": JoinRules.PUBLIC, "history_visibility": "shared", "original_invitees_have_ops": False, "guest_can_join": False, }, } def __init__(self, hs): super(RoomCreationHandler, self).__init__(hs) self.spam_checker = hs.get_spam_checker() self.event_creation_handler = hs.get_event_creation_handler() self.room_member_handler = hs.get_room_member_handler() self.config = hs.config # linearizer to stop two upgrades happening at once self._upgrade_linearizer = Linearizer("room_upgrade_linearizer") # If a user tries to update the same room multiple times in quick # succession, only process the first attempt and return its result to # subsequent requests self._upgrade_response_cache = ResponseCache( hs, "room_upgrade", timeout_ms=FIVE_MINUTES_IN_MS) self._server_notices_mxid = hs.config.server_notices_mxid self.third_party_event_rules = hs.get_third_party_event_rules() @defer.inlineCallbacks def upgrade_room(self, requester, old_room_id, new_version): """Replace a room with a new room with a different version Args: requester (synapse.types.Requester): the user requesting the upgrade old_room_id (unicode): the id of the room to be replaced new_version (unicode): the new room version to use Returns: Deferred[unicode]: the new room id """ yield self.ratelimit(requester) user_id = requester.user.to_string() # Check if this room is already being upgraded by another person for key in self._upgrade_response_cache.pending_result_cache: if key[0] == old_room_id and key[1] != user_id: # Two different people are trying to upgrade the same room. # Send the second an error. # # Note that this of course only gets caught if both users are # on the same homeserver. raise SynapseError( 400, "An upgrade for this room is currently in progress") # Upgrade the room # # If this user has sent multiple upgrade requests for the same room # and one of them is not complete yet, cache the response and # return it to all subsequent requests ret = yield self._upgrade_response_cache.wrap( (old_room_id, user_id), self._upgrade_room, requester, old_room_id, new_version, # args for _upgrade_room ) return ret @defer.inlineCallbacks def _upgrade_room(self, requester, old_room_id, new_version): user_id = requester.user.to_string() # start by allocating a new room id r = yield self.store.get_room(old_room_id) if r is None: raise NotFoundError("Unknown room id %s" % (old_room_id, )) new_room_id = yield self._generate_room_id(creator_id=user_id, is_public=r["is_public"]) logger.info("Creating new room %s to replace %s", new_room_id, old_room_id) # we create and auth the tombstone event before properly creating the new # room, to check our user has perms in the old room. ( tombstone_event, tombstone_context, ) = yield self.event_creation_handler.create_event( requester, { "type": EventTypes.Tombstone, "state_key": "", "room_id": old_room_id, "sender": user_id, "content": { "body": "This room has been replaced", "replacement_room": new_room_id, }, }, token_id=requester.access_token_id, ) old_room_version = yield self.store.get_room_version(old_room_id) yield self.auth.check_from_context(old_room_version, tombstone_event, tombstone_context) yield self.clone_existing_room( requester, old_room_id=old_room_id, new_room_id=new_room_id, new_room_version=new_version, tombstone_event_id=tombstone_event.event_id, ) # now send the tombstone yield self.event_creation_handler.send_nonmember_event( requester, tombstone_event, tombstone_context) old_room_state = yield tombstone_context.get_current_state_ids( self.store) # update any aliases yield self._move_aliases_to_new_room(requester, old_room_id, new_room_id, old_room_state) # Copy over user push rules, tags and migrate room directory state yield self.room_member_handler.transfer_room_state_on_room_upgrade( old_room_id, new_room_id) # finally, shut down the PLs in the old room, and update them in the new # room. yield self._update_upgraded_room_pls( requester, old_room_id, new_room_id, old_room_state, ) return new_room_id @defer.inlineCallbacks def _update_upgraded_room_pls( self, requester, old_room_id, new_room_id, old_room_state, ): """Send updated power levels in both rooms after an upgrade Args: requester (synapse.types.Requester): the user requesting the upgrade old_room_id (str): the id of the room to be replaced new_room_id (str): the id of the replacement room old_room_state (dict[tuple[str, str], str]): the state map for the old room Returns: Deferred """ old_room_pl_event_id = old_room_state.get((EventTypes.PowerLevels, "")) if old_room_pl_event_id is None: logger.warning( "Not supported: upgrading a room with no PL event. Not setting PLs " "in old room.") return old_room_pl_state = yield self.store.get_event(old_room_pl_event_id) # we try to stop regular users from speaking by setting the PL required # to send regular events and invites to 'Moderator' level. That's normally # 50, but if the default PL in a room is 50 or more, then we set the # required PL above that. pl_content = dict(old_room_pl_state.content) users_default = int(pl_content.get("users_default", 0)) restricted_level = max(users_default + 1, 50) updated = False for v in ("invite", "events_default"): current = int(pl_content.get(v, 0)) if current < restricted_level: logger.info( "Setting level for %s in %s to %i (was %i)", v, old_room_id, restricted_level, current, ) pl_content[v] = restricted_level updated = True else: logger.info("Not setting level for %s (already %i)", v, current) if updated: try: yield self.event_creation_handler.create_and_send_nonmember_event( requester, { "type": EventTypes.PowerLevels, "state_key": "", "room_id": old_room_id, "sender": requester.user.to_string(), "content": pl_content, }, ratelimit=False, ) except AuthError as e: logger.warning("Unable to update PLs in old room: %s", e) logger.info("Setting correct PLs in new room") yield self.event_creation_handler.create_and_send_nonmember_event( requester, { "type": EventTypes.PowerLevels, "state_key": "", "room_id": new_room_id, "sender": requester.user.to_string(), "content": old_room_pl_state.content, }, ratelimit=False, ) @defer.inlineCallbacks def clone_existing_room(self, requester, old_room_id, new_room_id, new_room_version, tombstone_event_id): """Populate a new room based on an old room Args: requester (synapse.types.Requester): the user requesting the upgrade old_room_id (unicode): the id of the room to be replaced new_room_id (unicode): the id to give the new room (should already have been created with _gemerate_room_id()) new_room_version (unicode): the new room version to use tombstone_event_id (unicode|str): the ID of the tombstone event in the old room. Returns: Deferred """ user_id = requester.user.to_string() if not self.spam_checker.user_may_create_room(user_id): raise SynapseError(403, "You are not permitted to create rooms") creation_content = { "room_version": new_room_version, "predecessor": { "room_id": old_room_id, "event_id": tombstone_event_id }, } # Check if old room was non-federatable # Get old room's create event old_room_create_event = yield self.store.get_create_event_for_room( old_room_id) # Check if the create event specified a non-federatable room if not old_room_create_event.content.get("m.federate", True): # If so, mark the new room as non-federatable as well creation_content["m.federate"] = False initial_state = dict() # Replicate relevant room events types_to_copy = ( (EventTypes.JoinRules, ""), (EventTypes.Name, ""), (EventTypes.Topic, ""), (EventTypes.RoomHistoryVisibility, ""), (EventTypes.GuestAccess, ""), (EventTypes.RoomAvatar, ""), (EventTypes.Encryption, ""), (EventTypes.ServerACL, ""), (EventTypes.RelatedGroups, ""), (EventTypes.PowerLevels, ""), ) old_room_state_ids = yield self.store.get_filtered_current_state_ids( old_room_id, StateFilter.from_types(types_to_copy)) # map from event_id to BaseEvent old_room_state_events = yield self.store.get_events( old_room_state_ids.values()) for k, old_event_id in iteritems(old_room_state_ids): old_event = old_room_state_events.get(old_event_id) if old_event: initial_state[k] = old_event.content # Resolve the minimum power level required to send any state event # We will give the upgrading user this power level temporarily (if necessary) such that # they are able to copy all of the state events over, then revert them back to their # original power level afterwards in _update_upgraded_room_pls # Copy over user power levels now as this will not be possible with >100PL users once # the room has been created power_levels = initial_state[(EventTypes.PowerLevels, "")] # Calculate the minimum power level needed to clone the room event_power_levels = power_levels.get("events", {}) state_default = power_levels.get("state_default", 0) ban = power_levels.get("ban") needed_power_level = max(state_default, ban, max(event_power_levels.values())) # Raise the requester's power level in the new room if necessary current_power_level = power_levels["users"][requester.user.to_string()] if current_power_level < needed_power_level: # Assign this power level to the requester power_levels["users"][ requester.user.to_string()] = needed_power_level # Set the power levels to the modified state initial_state[(EventTypes.PowerLevels, "")] = power_levels yield self._send_events_for_new_room( requester, new_room_id, # we expect to override all the presets with initial_state, so this is # somewhat arbitrary. preset_config=RoomCreationPreset.PRIVATE_CHAT, invite_list=[], initial_state=initial_state, creation_content=creation_content, ) # Transfer membership events old_room_member_state_ids = yield self.store.get_filtered_current_state_ids( old_room_id, StateFilter.from_types([(EventTypes.Member, None)])) # map from event_id to BaseEvent old_room_member_state_events = yield self.store.get_events( old_room_member_state_ids.values()) for k, old_event in iteritems(old_room_member_state_events): # Only transfer ban events if ("membership" in old_event.content and old_event.content["membership"] == "ban"): yield self.room_member_handler.update_membership( requester, UserID.from_string(old_event["state_key"]), new_room_id, "ban", ratelimit=False, content=old_event.content, ) # XXX invites/joins # XXX 3pid invites @defer.inlineCallbacks def _move_aliases_to_new_room(self, requester, old_room_id, new_room_id, old_room_state): directory_handler = self.hs.get_handlers().directory_handler aliases = yield self.store.get_aliases_for_room(old_room_id) # check to see if we have a canonical alias. canonical_alias = None canonical_alias_event_id = old_room_state.get( (EventTypes.CanonicalAlias, "")) if canonical_alias_event_id: canonical_alias_event = yield self.store.get_event( canonical_alias_event_id) if canonical_alias_event: canonical_alias = canonical_alias_event.content.get( "alias", "") # first we try to remove the aliases from the old room (we suppress sending # the room_aliases event until the end). # # Note that we'll only be able to remove aliases that (a) aren't owned by an AS, # and (b) unless the user is a server admin, which the user created. # # This is probably correct - given we don't allow such aliases to be deleted # normally, it would be odd to allow it in the case of doing a room upgrade - # but it makes the upgrade less effective, and you have to wonder why a room # admin can't remove aliases that point to that room anyway. # (cf https://github.com/matrix-org/synapse/issues/2360) # removed_aliases = [] for alias_str in aliases: alias = RoomAlias.from_string(alias_str) try: yield directory_handler.delete_association(requester, alias, send_event=False) removed_aliases.append(alias_str) except SynapseError as e: logger.warning("Unable to remove alias %s from old room: %s", alias, e) # if we didn't find any aliases, or couldn't remove anyway, we can skip the rest # of this. if not removed_aliases: return try: # this can fail if, for some reason, our user doesn't have perms to send # m.room.aliases events in the old room (note that we've already checked that # they have perms to send a tombstone event, so that's not terribly likely). # # If that happens, it's regrettable, but we should carry on: it's the same # as when you remove an alias from the directory normally - it just means that # the aliases event gets out of sync with the directory # (cf https://github.com/vector-im/riot-web/issues/2369) yield directory_handler.send_room_alias_update_event( requester, old_room_id) except AuthError as e: logger.warning( "Failed to send updated alias event on old room: %s", e) # we can now add any aliases we successfully removed to the new room. for alias in removed_aliases: try: yield directory_handler.create_association( requester, RoomAlias.from_string(alias), new_room_id, servers=(self.hs.hostname, ), send_event=False, check_membership=False, ) logger.info("Moved alias %s to new room", alias) except SynapseError as e: # I'm not really expecting this to happen, but it could if the spam # checking module decides it shouldn't, or similar. logger.error("Error adding alias %s to new room: %s", alias, e) try: if canonical_alias and (canonical_alias in removed_aliases): yield self.event_creation_handler.create_and_send_nonmember_event( requester, { "type": EventTypes.CanonicalAlias, "state_key": "", "room_id": new_room_id, "sender": requester.user.to_string(), "content": { "alias": canonical_alias }, }, ratelimit=False, ) yield directory_handler.send_room_alias_update_event( requester, new_room_id) except SynapseError as e: # again I'm not really expecting this to fail, but if it does, I'd rather # we returned the new room to the client at this point. logger.error("Unable to send updated alias events in new room: %s", e) @defer.inlineCallbacks def create_room(self, requester, config, ratelimit=True, creator_join_profile=None): """ Creates a new room. Args: requester (synapse.types.Requester): The user who requested the room creation. config (dict) : A dict of configuration options. ratelimit (bool): set to False to disable the rate limiter creator_join_profile (dict|None): Set to override the displayname and avatar for the creating user in this room. If unset, displayname and avatar will be derived from the user's profile. If set, should contain the values to go in the body of the 'join' event (typically `avatar_url` and/or `displayname`. Returns: Deferred[dict]: a dict containing the keys `room_id` and, if an alias was requested, `room_alias`. Raises: SynapseError if the room ID couldn't be stored, or something went horribly wrong. ResourceLimitError if server is blocked to some resource being exceeded """ user_id = requester.user.to_string() yield self.auth.check_auth_blocking(user_id) if (self._server_notices_mxid is not None and requester.user.to_string() == self._server_notices_mxid): # allow the server notices mxid to create rooms is_requester_admin = True else: is_requester_admin = yield self.auth.is_server_admin( requester.user) # Check whether the third party rules allows/changes the room create # request. yield self.third_party_event_rules.on_create_room( requester, config, is_requester_admin=is_requester_admin) if not is_requester_admin and not self.spam_checker.user_may_create_room( user_id): raise SynapseError(403, "You are not permitted to create rooms") if ratelimit: yield self.ratelimit(requester) room_version = config.get("room_version", self.config.default_room_version.identifier) if not isinstance(room_version, string_types): raise SynapseError(400, "room_version must be a string", Codes.BAD_JSON) if room_version not in KNOWN_ROOM_VERSIONS: raise SynapseError( 400, "Your homeserver does not support this room version", Codes.UNSUPPORTED_ROOM_VERSION, ) if "room_alias_name" in config: for wchar in string.whitespace: if wchar in config["room_alias_name"]: raise SynapseError(400, "Invalid characters in room alias") room_alias = RoomAlias(config["room_alias_name"], self.hs.hostname) mapping = yield self.store.get_association_from_room_alias( room_alias) if mapping: raise SynapseError(400, "Room alias already taken", Codes.ROOM_IN_USE) else: room_alias = None invite_list = config.get("invite", []) for i in invite_list: try: uid = UserID.from_string(i) parse_and_validate_server_name(uid.domain) except Exception: raise SynapseError(400, "Invalid user_id: %s" % (i, )) yield self.event_creation_handler.assert_accepted_privacy_policy( requester) power_level_content_override = config.get( "power_level_content_override") if (power_level_content_override and "users" in power_level_content_override and user_id not in power_level_content_override["users"]): raise SynapseError( 400, "Not a valid power_level_content_override: 'users' did not contain %s" % (user_id, ), ) invite_3pid_list = config.get("invite_3pid", []) visibility = config.get("visibility", None) is_public = visibility == "public" room_id = yield self._generate_room_id(creator_id=user_id, is_public=is_public) directory_handler = self.hs.get_handlers().directory_handler if room_alias: yield directory_handler.create_association( requester=requester, room_id=room_id, room_alias=room_alias, servers=[self.hs.hostname], send_event=False, check_membership=False, ) preset_config = config.get( "preset", RoomCreationPreset.PRIVATE_CHAT if visibility == "private" else RoomCreationPreset.PUBLIC_CHAT, ) raw_initial_state = config.get("initial_state", []) initial_state = OrderedDict() for val in raw_initial_state: initial_state[(val["type"], val.get("state_key", ""))] = val["content"] creation_content = config.get("creation_content", {}) # override any attempt to set room versions via the creation_content creation_content["room_version"] = room_version yield self._send_events_for_new_room( requester, room_id, preset_config=preset_config, invite_list=invite_list, initial_state=initial_state, creation_content=creation_content, room_alias=room_alias, power_level_content_override=power_level_content_override, creator_join_profile=creator_join_profile, ) if "name" in config: name = config["name"] yield self.event_creation_handler.create_and_send_nonmember_event( requester, { "type": EventTypes.Name, "room_id": room_id, "sender": user_id, "state_key": "", "content": { "name": name }, }, ratelimit=False, ) if "topic" in config: topic = config["topic"] yield self.event_creation_handler.create_and_send_nonmember_event( requester, { "type": EventTypes.Topic, "room_id": room_id, "sender": user_id, "state_key": "", "content": { "topic": topic }, }, ratelimit=False, ) for invitee in invite_list: content = {} is_direct = config.get("is_direct", None) if is_direct: content["is_direct"] = is_direct yield self.room_member_handler.update_membership( requester, UserID.from_string(invitee), room_id, "invite", ratelimit=False, content=content, ) for invite_3pid in invite_3pid_list: id_server = invite_3pid["id_server"] id_access_token = invite_3pid.get("id_access_token") # optional address = invite_3pid["address"] medium = invite_3pid["medium"] yield self.hs.get_room_member_handler().do_3pid_invite( room_id, requester.user, medium, address, id_server, requester, txn_id=None, id_access_token=id_access_token, ) result = {"room_id": room_id} if room_alias: result["room_alias"] = room_alias.to_string() yield directory_handler.send_room_alias_update_event( requester, room_id) return result @defer.inlineCallbacks def _send_events_for_new_room( self, creator, # A Requester object. room_id, preset_config, invite_list, initial_state, creation_content, room_alias=None, power_level_content_override=None, creator_join_profile=None, ): def create(etype, content, **kwargs): e = {"type": etype, "content": content} e.update(event_keys) e.update(kwargs) return e @defer.inlineCallbacks def send(etype, content, **kwargs): event = create(etype, content, **kwargs) logger.info("Sending %s in new room", etype) yield self.event_creation_handler.create_and_send_nonmember_event( creator, event, ratelimit=False) config = RoomCreationHandler.PRESETS_DICT[preset_config] creator_id = creator.user.to_string() event_keys = { "room_id": room_id, "sender": creator_id, "state_key": "" } creation_content.update({"creator": creator_id}) yield send(etype=EventTypes.Create, content=creation_content) logger.info("Sending %s in new room", EventTypes.Member) yield self.room_member_handler.update_membership( creator, creator.user, room_id, "join", ratelimit=False, content=creator_join_profile, ) # We treat the power levels override specially as this needs to be one # of the first events that get sent into a room. pl_content = initial_state.pop((EventTypes.PowerLevels, ""), None) if pl_content is not None: yield send(etype=EventTypes.PowerLevels, content=pl_content) else: power_level_content = { "users": { creator_id: 100 }, "users_default": 0, "events": { EventTypes.Name: 50, EventTypes.PowerLevels: 100, EventTypes.RoomHistoryVisibility: 100, EventTypes.CanonicalAlias: 50, EventTypes.RoomAvatar: 50, }, "events_default": 0, "state_default": 50, "ban": 50, "kick": 50, "redact": 50, "invite": 0, } if config["original_invitees_have_ops"]: for invitee in invite_list: power_level_content["users"][invitee] = 100 if power_level_content_override: power_level_content.update(power_level_content_override) yield send(etype=EventTypes.PowerLevels, content=power_level_content) if room_alias and (EventTypes.CanonicalAlias, "") not in initial_state: yield send( etype=EventTypes.CanonicalAlias, content={"alias": room_alias.to_string()}, ) if (EventTypes.JoinRules, "") not in initial_state: yield send(etype=EventTypes.JoinRules, content={"join_rule": config["join_rules"]}) if (EventTypes.RoomHistoryVisibility, "") not in initial_state: yield send( etype=EventTypes.RoomHistoryVisibility, content={"history_visibility": config["history_visibility"]}, ) if config["guest_can_join"]: if (EventTypes.GuestAccess, "") not in initial_state: yield send(etype=EventTypes.GuestAccess, content={"guest_access": "can_join"}) for (etype, state_key), content in initial_state.items(): yield send(etype=etype, state_key=state_key, content=content) @defer.inlineCallbacks def _generate_room_id(self, creator_id, is_public): # autogen room IDs and try to create it. We may clash, so just # try a few times till one goes through, giving up eventually. attempts = 0 while attempts < 5: try: random_string = stringutils.random_string(18) gen_room_id = RoomID(random_string, self.hs.hostname).to_string() if isinstance(gen_room_id, bytes): gen_room_id = gen_room_id.decode("utf-8") yield self.store.store_room( room_id=gen_room_id, room_creator_user_id=creator_id, is_public=is_public, ) return gen_room_id except StoreError: attempts += 1 raise StoreError(500, "Couldn't generate a room ID.")
class ReplicationEndpoint(metaclass=abc.ABCMeta): """Helper base class for defining new replication HTTP endpoints. This creates an endpoint under `/_synapse/replication/:NAME/:PATH_ARGS..` (with a `/:txn_id` suffix for cached requests), where NAME is a name, PATH_ARGS are a tuple of parameters to be encoded in the URL. For example, if `NAME` is "send_event" and `PATH_ARGS` is `("event_id",)`, with `CACHE` set to true then this generates an endpoint: /_synapse/replication/send_event/:event_id/:txn_id For POST/PUT requests the payload is serialized to json and sent as the body, while for GET requests the payload is added as query parameters. See `_serialize_payload` for details. Incoming requests are handled by overriding `_handle_request`. Servers must call `register` to register the path with the HTTP server. Requests can be sent by calling the client returned by `make_client`. Requests are sent to master process by default, but can be sent to other named processes by specifying an `instance_name` keyword argument. Attributes: NAME (str): A name for the endpoint, added to the path as well as used in logging and metrics. PATH_ARGS (tuple[str]): A list of parameters to be added to the path. Adding parameters to the path (rather than payload) can make it easier to follow along in the log files. METHOD (str): The method of the HTTP request, defaults to POST. Can be one of POST, PUT or GET. If GET then the payload is sent as query parameters rather than a JSON body. CACHE (bool): Whether server should cache the result of the request/ If true then transparently adds a txn_id to all requests, and `_handle_request` must return a Deferred. RETRY_ON_TIMEOUT(bool): Whether or not to retry the request when a 504 is received. """ NAME = abc.abstractproperty() # type: str # type: ignore PATH_ARGS = abc.abstractproperty() # type: Tuple[str, ...] # type: ignore METHOD = "POST" CACHE = True RETRY_ON_TIMEOUT = True def __init__(self, hs): if self.CACHE: self.response_cache = ResponseCache( hs, "repl." + self.NAME, timeout_ms=30 * 60 * 1000 ) # type: ResponseCache[str] # We reserve `instance_name` as a parameter to sending requests, so we # assert here that sub classes don't try and use the name. assert ( "instance_name" not in self.PATH_ARGS ), "`instance_name` is a reserved parameter name" assert ( "instance_name" not in signature(self.__class__._serialize_payload).parameters ), "`instance_name` is a reserved parameter name" assert self.METHOD in ("PUT", "POST", "GET") @abc.abstractmethod async def _serialize_payload(**kwargs): """Static method that is called when creating a request. Concrete implementations should have explicit parameters (rather than kwargs) so that an appropriate exception is raised if the client is called with unexpected parameters. All PATH_ARGS must appear in argument list. Returns: dict: If POST/PUT request then dictionary must be JSON serialisable, otherwise must be appropriate for adding as query args. """ return {} @abc.abstractmethod async def _handle_request(self, request, **kwargs): """Handle incoming request. This is called with the request object and PATH_ARGS. Returns: tuple[int, dict]: HTTP status code and a JSON serialisable dict to be used as response body of request. """ pass @classmethod def make_client(cls, hs): """Create a client that makes requests. Returns a callable that accepts the same parameters as `_serialize_payload`. """ clock = hs.get_clock() client = hs.get_simple_http_client() local_instance_name = hs.get_instance_name() master_host = hs.config.worker_replication_host master_port = hs.config.worker_replication_http_port instance_map = hs.config.worker.instance_map outgoing_gauge = _pending_outgoing_requests.labels(cls.NAME) @trace(opname="outgoing_replication_request") @outgoing_gauge.track_inprogress() async def send_request(instance_name="master", **kwargs): if instance_name == local_instance_name: raise Exception("Trying to send HTTP request to self") if instance_name == "master": host = master_host port = master_port elif instance_name in instance_map: host = instance_map[instance_name].host port = instance_map[instance_name].port else: raise Exception( "Instance %r not in 'instance_map' config" % (instance_name,) ) data = await cls._serialize_payload(**kwargs) url_args = [ urllib.parse.quote(kwargs[name], safe="") for name in cls.PATH_ARGS ] if cls.CACHE: txn_id = random_string(10) url_args.append(txn_id) if cls.METHOD == "POST": request_func = client.post_json_get_json elif cls.METHOD == "PUT": request_func = client.put_json elif cls.METHOD == "GET": request_func = client.get_json else: # We have already asserted in the constructor that a # compatible was picked, but lets be paranoid. raise Exception( "Unknown METHOD on %s replication endpoint" % (cls.NAME,) ) uri = "http://%s:%s/_synapse/replication/%s/%s" % ( host, port, cls.NAME, "/".join(url_args), ) try: # We keep retrying the same request for timeouts. This is so that we # have a good idea that the request has either succeeded or failed on # the master, and so whether we should clean up or not. while True: headers = {} # type: Dict[bytes, List[bytes]] inject_active_span_byte_dict(headers, None, check_destination=False) try: result = await request_func(uri, data, headers=headers) break except RequestTimedOutError: if not cls.RETRY_ON_TIMEOUT: raise logger.warning("%s request timed out; retrying", cls.NAME) # If we timed out we probably don't need to worry about backing # off too much, but lets just wait a little anyway. await clock.sleep(1) except HttpResponseException as e: # We convert to SynapseError as we know that it was a SynapseError # on the main process that we should send to the client. (And # importantly, not stack traces everywhere) _outgoing_request_counter.labels(cls.NAME, e.code).inc() raise e.to_synapse_error() except Exception as e: _outgoing_request_counter.labels(cls.NAME, "ERR").inc() raise SynapseError(502, "Failed to talk to main process") from e _outgoing_request_counter.labels(cls.NAME, 200).inc() return result return send_request def register(self, http_server): """Called by the server to register this as a handler to the appropriate path. """ url_args = list(self.PATH_ARGS) handler = self._handle_request method = self.METHOD if self.CACHE: handler = self._cached_handler # type: ignore url_args.append("txn_id") args = "/".join("(?P<%s>[^/]+)" % (arg,) for arg in url_args) pattern = re.compile("^/_synapse/replication/%s/%s$" % (self.NAME, args)) http_server.register_paths( method, [pattern], handler, self.__class__.__name__, ) def _cached_handler(self, request, txn_id, **kwargs): """Called on new incoming requests when caching is enabled. Checks if there is a cached response for the request and returns that, otherwise calls `_handle_request` and caches its response. """ # We just use the txn_id here, but we probably also want to use the # other PATH_ARGS as well. assert self.CACHE return self.response_cache.wrap(txn_id, self._handle_request, request, **kwargs)
class ReplicationEndpoint(object): """Helper base class for defining new replication HTTP endpoints. This creates an endpoint under `/_synapse/replication/:NAME/:PATH_ARGS..` (with an `/:txn_id` prefix for cached requests.), where NAME is a name, PATH_ARGS are a tuple of parameters to be encoded in the URL. For example, if `NAME` is "send_event" and `PATH_ARGS` is `("event_id",)`, with `CACHE` set to true then this generates an endpoint: /_synapse/replication/send_event/:event_id/:txn_id For POST/PUT requests the payload is serialized to json and sent as the body, while for GET requests the payload is added as query parameters. See `_serialize_payload` for details. Incoming requests are handled by overriding `_handle_request`. Servers must call `register` to register the path with the HTTP server. Requests can be sent by calling the client returned by `make_client`. Attributes: NAME (str): A name for the endpoint, added to the path as well as used in logging and metrics. PATH_ARGS (tuple[str]): A list of parameters to be added to the path. Adding parameters to the path (rather than payload) can make it easier to follow along in the log files. METHOD (str): The method of the HTTP request, defaults to POST. Can be one of POST, PUT or GET. If GET then the payload is sent as query parameters rather than a JSON body. CACHE (bool): Whether server should cache the result of the request/ If true then transparently adds a txn_id to all requests, and `_handle_request` must return a Deferred. RETRY_ON_TIMEOUT(bool): Whether or not to retry the request when a 504 is received. """ __metaclass__ = abc.ABCMeta NAME = abc.abstractproperty() PATH_ARGS = abc.abstractproperty() METHOD = "POST" CACHE = True RETRY_ON_TIMEOUT = True def __init__(self, hs): if self.CACHE: self.response_cache = ResponseCache(hs, "repl." + self.NAME, timeout_ms=30 * 60 * 1000) assert self.METHOD in ("PUT", "POST", "GET") @abc.abstractmethod def _serialize_payload(**kwargs): """Static method that is called when creating a request. Concrete implementations should have explicit parameters (rather than kwargs) so that an appropriate exception is raised if the client is called with unexpected parameters. All PATH_ARGS must appear in argument list. Returns: Deferred[dict]|dict: If POST/PUT request then dictionary must be JSON serialisable, otherwise must be appropriate for adding as query args. """ return {} @abc.abstractmethod def _handle_request(self, request, **kwargs): """Handle incoming request. This is called with the request object and PATH_ARGS. Returns: Deferred[dict]: A JSON serialisable dict to be used as response body of request. """ pass @classmethod def make_client(cls, hs): """Create a client that makes requests. Returns a callable that accepts the same parameters as `_serialize_payload`. """ clock = hs.get_clock() host = hs.config.worker_replication_host port = hs.config.worker_replication_http_port client = hs.get_simple_http_client() @defer.inlineCallbacks def send_request(**kwargs): data = yield cls._serialize_payload(**kwargs) url_args = [ urllib.parse.quote(kwargs[name], safe="") for name in cls.PATH_ARGS ] if cls.CACHE: txn_id = random_string(10) url_args.append(txn_id) if cls.METHOD == "POST": request_func = client.post_json_get_json elif cls.METHOD == "PUT": request_func = client.put_json elif cls.METHOD == "GET": request_func = client.get_json else: # We have already asserted in the constructor that a # compatible was picked, but lets be paranoid. raise Exception("Unknown METHOD on %s replication endpoint" % (cls.NAME, )) uri = "http://%s:%s/_synapse/replication/%s/%s" % ( host, port, cls.NAME, "/".join(url_args), ) try: # We keep retrying the same request for timeouts. This is so that we # have a good idea that the request has either succeeded or failed on # the master, and so whether we should clean up or not. while True: try: result = yield request_func(uri, data) break except CodeMessageException as e: if e.code != 504 or not cls.RETRY_ON_TIMEOUT: raise logger.warn("%s request timed out", cls.NAME) # If we timed out we probably don't need to worry about backing # off too much, but lets just wait a little anyway. yield clock.sleep(1) except HttpResponseException as e: # We convert to SynapseError as we know that it was a SynapseError # on the master process that we should send to the client. (And # importantly, not stack traces everywhere) raise e.to_synapse_error() except RequestSendFailed as e: raise_from(SynapseError(502, "Failed to talk to master"), e) return result return send_request def register(self, http_server): """Called by the server to register this as a handler to the appropriate path. """ url_args = list(self.PATH_ARGS) handler = self._handle_request method = self.METHOD if self.CACHE: handler = self._cached_handler url_args.append("txn_id") args = "/".join("(?P<%s>[^/]+)" % (arg, ) for arg in url_args) pattern = re.compile("^/_synapse/replication/%s/%s$" % (self.NAME, args)) http_server.register_paths(method, [pattern], handler, self.__class__.__name__) def _cached_handler(self, request, txn_id, **kwargs): """Called on new incoming requests when caching is enabled. Checks if there is a cached response for the request and returns that, otherwise calls `_handle_request` and caches its response. """ # We just use the txn_id here, but we probably also want to use the # other PATH_ARGS as well. assert self.CACHE return self.response_cache.wrap(txn_id, self._handle_request, request, **kwargs)
class FederationServer(FederationBase): def __init__(self, hs): super(FederationServer, self).__init__(hs) self.auth = hs.get_auth() self.handler = hs.get_handlers().federation_handler self._server_linearizer = Linearizer("fed_server") self._transaction_linearizer = Linearizer("fed_txn_handler") self.transaction_actions = TransactionActions(self.store) self.registry = hs.get_federation_registry() # We cache responses to state queries, as they take a while and often # come in waves. self._state_resp_cache = ResponseCache(hs, "state_resp", timeout_ms=30000) @defer.inlineCallbacks @log_function def on_backfill_request(self, origin, room_id, versions, limit): with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) pdus = yield self.handler.on_backfill_request( origin, room_id, versions, limit) res = self._transaction_from_pdus(pdus).get_dict() defer.returnValue((200, res)) @defer.inlineCallbacks @log_function def on_incoming_transaction(self, origin, transaction_data): # keep this as early as possible to make the calculated origin ts as # accurate as possible. request_time = self._clock.time_msec() transaction = Transaction(**transaction_data) if not transaction.transaction_id: raise Exception("Transaction missing transaction_id") logger.debug("[%s] Got transaction", transaction.transaction_id) # use a linearizer to ensure that we don't process the same transaction # multiple times in parallel. with (yield self._transaction_linearizer.queue( (origin, transaction.transaction_id), )): result = yield self._handle_incoming_transaction( origin, transaction, request_time, ) defer.returnValue(result) @defer.inlineCallbacks def _handle_incoming_transaction(self, origin, transaction, request_time): """ Process an incoming transaction and return the HTTP response Args: origin (unicode): the server making the request transaction (Transaction): incoming transaction request_time (int): timestamp that the HTTP request arrived at Returns: Deferred[(int, object)]: http response code and body """ response = yield self.transaction_actions.have_responded( origin, transaction) if response: logger.debug("[%s] We've already responded to this request", transaction.transaction_id) defer.returnValue(response) return logger.debug("[%s] Transaction is new", transaction.transaction_id) received_pdus_counter.inc(len(transaction.pdus)) origin_host, _ = parse_server_name(origin) pdus_by_room = {} for p in transaction.pdus: if "unsigned" in p: unsigned = p["unsigned"] if "age" in unsigned: p["age"] = unsigned["age"] if "age" in p: p["age_ts"] = request_time - int(p["age"]) del p["age"] event = event_from_pdu_json(p) room_id = event.room_id pdus_by_room.setdefault(room_id, []).append(event) pdu_results = {} # we can process different rooms in parallel (which is useful if they # require callouts to other servers to fetch missing events), but # impose a limit to avoid going too crazy with ram/cpu. @defer.inlineCallbacks def process_pdus_for_room(room_id): logger.debug("Processing PDUs for %s", room_id) try: yield self.check_server_matches_acl(origin_host, room_id) except AuthError as e: logger.warn( "Ignoring PDUs for room %s from banned server", room_id, ) for pdu in pdus_by_room[room_id]: event_id = pdu.event_id pdu_results[event_id] = e.error_dict() return for pdu in pdus_by_room[room_id]: event_id = pdu.event_id with nested_logging_context(event_id): try: yield self._handle_received_pdu(origin, pdu) pdu_results[event_id] = {} except FederationError as e: logger.warn("Error handling PDU %s: %s", event_id, e) pdu_results[event_id] = {"error": str(e)} except Exception as e: f = failure.Failure() pdu_results[event_id] = {"error": str(e)} logger.error( "Failed to handle PDU %s: %s", event_id, f.getTraceback().rstrip(), ) yield concurrently_execute( process_pdus_for_room, pdus_by_room.keys(), TRANSACTION_CONCURRENCY_LIMIT, ) if hasattr(transaction, "edus"): for edu in (Edu(**x) for x in transaction.edus): yield self.received_edu(origin, edu.edu_type, edu.content) response = { "pdus": pdu_results, } logger.debug("Returning: %s", str(response)) yield self.transaction_actions.set_response(origin, transaction, 200, response) defer.returnValue((200, response)) @defer.inlineCallbacks def received_edu(self, origin, edu_type, content): received_edus_counter.inc() yield self.registry.on_edu(edu_type, origin, content) @defer.inlineCallbacks @log_function def on_context_state_request(self, origin, room_id, event_id): if not event_id: raise NotImplementedError("Specify an event") origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) in_room = yield self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") # we grab the linearizer to protect ourselves from servers which hammer # us. In theory we might already have the response to this query # in the cache so we could return it without waiting for the linearizer # - but that's non-trivial to get right, and anyway somewhat defeats # the point of the linearizer. with (yield self._server_linearizer.queue((origin, room_id))): resp = yield self._state_resp_cache.wrap( (room_id, event_id), self._on_context_state_request_compute, room_id, event_id, ) defer.returnValue((200, resp)) @defer.inlineCallbacks def on_state_ids_request(self, origin, room_id, event_id): if not event_id: raise NotImplementedError("Specify an event") origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) in_room = yield self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") state_ids = yield self.handler.get_state_ids_for_pdu( room_id, event_id, ) auth_chain_ids = yield self.store.get_auth_chain_ids(state_ids) defer.returnValue((200, { "pdu_ids": state_ids, "auth_chain_ids": auth_chain_ids, })) @defer.inlineCallbacks def _on_context_state_request_compute(self, room_id, event_id): pdus = yield self.handler.get_state_for_pdu( room_id, event_id, ) auth_chain = yield self.store.get_auth_chain( [pdu.event_id for pdu in pdus]) for event in auth_chain: # We sign these again because there was a bug where we # incorrectly signed things the first time round if self.hs.is_mine_id(event.event_id): event.signatures.update( compute_event_signature(event, self.hs.hostname, self.hs.config.signing_key[0])) defer.returnValue({ "pdus": [pdu.get_pdu_json() for pdu in pdus], "auth_chain": [pdu.get_pdu_json() for pdu in auth_chain], }) @defer.inlineCallbacks @log_function def on_pdu_request(self, origin, event_id): pdu = yield self.handler.get_persisted_pdu(origin, event_id) if pdu: defer.returnValue( (200, self._transaction_from_pdus([pdu]).get_dict())) else: defer.returnValue((404, "")) @defer.inlineCallbacks @log_function def on_pull_request(self, origin, versions): raise NotImplementedError("Pull transactions not implemented") @defer.inlineCallbacks def on_query_request(self, query_type, args): received_queries_counter.labels(query_type).inc() resp = yield self.registry.on_query(query_type, args) defer.returnValue((200, resp)) @defer.inlineCallbacks def on_make_join_request(self, origin, room_id, user_id, supported_versions): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) room_version = yield self.store.get_room_version(room_id) if room_version not in supported_versions: logger.warn("Room version %s not in %s", room_version, supported_versions) raise IncompatibleRoomVersionError(room_version=room_version) pdu = yield self.handler.on_make_join_request(room_id, user_id) time_now = self._clock.time_msec() defer.returnValue({ "event": pdu.get_pdu_json(time_now), "room_version": room_version, }) @defer.inlineCallbacks def on_invite_request(self, origin, content): pdu = event_from_pdu_json(content) origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, pdu.room_id) ret_pdu = yield self.handler.on_invite_request(origin, pdu) time_now = self._clock.time_msec() defer.returnValue((200, {"event": ret_pdu.get_pdu_json(time_now)})) @defer.inlineCallbacks def on_send_join_request(self, origin, content): logger.debug("on_send_join_request: content: %s", content) pdu = event_from_pdu_json(content) origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_join_request: pdu sigs: %s", pdu.signatures) res_pdus = yield self.handler.on_send_join_request(origin, pdu) time_now = self._clock.time_msec() defer.returnValue((200, { "state": [p.get_pdu_json(time_now) for p in res_pdus["state"]], "auth_chain": [p.get_pdu_json(time_now) for p in res_pdus["auth_chain"]], })) @defer.inlineCallbacks def on_make_leave_request(self, origin, room_id, user_id): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) pdu = yield self.handler.on_make_leave_request(room_id, user_id) time_now = self._clock.time_msec() defer.returnValue({"event": pdu.get_pdu_json(time_now)}) @defer.inlineCallbacks def on_send_leave_request(self, origin, content): logger.debug("on_send_leave_request: content: %s", content) pdu = event_from_pdu_json(content) origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures) yield self.handler.on_send_leave_request(origin, pdu) defer.returnValue((200, {})) @defer.inlineCallbacks def on_event_auth(self, origin, room_id, event_id): with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) time_now = self._clock.time_msec() auth_pdus = yield self.handler.on_event_auth(event_id) res = { "auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus], } defer.returnValue((200, res)) @defer.inlineCallbacks def on_query_auth_request(self, origin, content, room_id, event_id): """ Content is a dict with keys:: auth_chain (list): A list of events that give the auth chain. missing (list): A list of event_ids indicating what the other side (`origin`) think we're missing. rejects (dict): A mapping from event_id to a 2-tuple of reason string and a proof (or None) of why the event was rejected. The keys of this dict give the list of events the `origin` has rejected. Args: origin (str) content (dict) event_id (str) Returns: Deferred: Results in `dict` with the same format as `content` """ with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) auth_chain = [ event_from_pdu_json(e) for e in content["auth_chain"] ] signed_auth = yield self._check_sigs_and_hash_and_fetch( origin, auth_chain, outlier=True) ret = yield self.handler.on_query_auth( origin, event_id, room_id, signed_auth, content.get("rejects", []), content.get("missing", []), ) time_now = self._clock.time_msec() send_content = { "auth_chain": [e.get_pdu_json(time_now) for e in ret["auth_chain"]], "rejects": ret.get("rejects", []), "missing": ret.get("missing", []), } defer.returnValue((200, send_content)) @log_function def on_query_client_keys(self, origin, content): return self.on_query_request("client_keys", content) def on_query_user_devices(self, origin, user_id): return self.on_query_request("user_devices", user_id) @defer.inlineCallbacks @log_function def on_claim_client_keys(self, origin, content): query = [] for user_id, device_keys in content.get("one_time_keys", {}).items(): for device_id, algorithm in device_keys.items(): query.append((user_id, device_id, algorithm)) results = yield self.store.claim_e2e_one_time_keys(query) json_result = {} for user_id, device_keys in results.items(): for device_id, keys in device_keys.items(): for key_id, json_bytes in keys.items(): json_result.setdefault(user_id, {})[device_id] = { key_id: json.loads(json_bytes) } logger.info( "Claimed one-time-keys: %s", ",".join(("%s for %s:%s" % (key_id, user_id, device_id) for user_id, user_keys in iteritems(json_result) for device_id, device_keys in iteritems(user_keys) for key_id, _ in iteritems(device_keys))), ) defer.returnValue({"one_time_keys": json_result}) @defer.inlineCallbacks @log_function def on_get_missing_events(self, origin, room_id, earliest_events, latest_events, limit): with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) logger.info( "on_get_missing_events: earliest_events: %r, latest_events: %r," " limit: %d", earliest_events, latest_events, limit, ) missing_events = yield self.handler.on_get_missing_events( origin, room_id, earliest_events, latest_events, limit, ) if len(missing_events) < 5: logger.info("Returning %d events: %r", len(missing_events), missing_events) else: logger.info("Returning %d events", len(missing_events)) time_now = self._clock.time_msec() defer.returnValue({ "events": [ev.get_pdu_json(time_now) for ev in missing_events], }) @log_function def on_openid_userinfo(self, token): ts_now_ms = self._clock.time_msec() return self.store.get_user_id_for_open_id_token(token, ts_now_ms) def _transaction_from_pdus(self, pdu_list): """Returns a new Transaction containing the given PDUs suitable for transmission. """ time_now = self._clock.time_msec() pdus = [p.get_pdu_json(time_now) for p in pdu_list] return Transaction( origin=self.server_name, pdus=pdus, origin_server_ts=int(time_now), destination=None, ) @defer.inlineCallbacks def _handle_received_pdu(self, origin, pdu): """ Process a PDU received in a federation /send/ transaction. If the event is invalid, then this method throws a FederationError. (The error will then be logged and sent back to the sender (which probably won't do anything with it), and other events in the transaction will be processed as normal). It is likely that we'll then receive other events which refer to this rejected_event in their prev_events, etc. When that happens, we'll attempt to fetch the rejected event again, which will presumably fail, so those second-generation events will also get rejected. Eventually, we get to the point where there are more than 10 events between any new events and the original rejected event. Since we only try to backfill 10 events deep on received pdu, we then accept the new event, possibly introducing a discontinuity in the DAG, with new forward extremities, so normal service is approximately returned, until we try to backfill across the discontinuity. Args: origin (str): server which sent the pdu pdu (FrozenEvent): received pdu Returns (Deferred): completes with None Raises: FederationError if the signatures / hash do not match, or if the event was unacceptable for any other reason (eg, too large, too many prev_events, couldn't find the prev_events) """ # check that it's actually being sent from a valid destination to # workaround bug #1753 in 0.18.5 and 0.18.6 if origin != get_domain_from_id(pdu.event_id): # We continue to accept join events from any server; this is # necessary for the federation join dance to work correctly. # (When we join over federation, the "helper" server is # responsible for sending out the join event, rather than the # origin. See bug #1893). if not (pdu.type == 'm.room.member' and pdu.content and pdu.content.get("membership", None) == 'join'): logger.info("Discarding PDU %s from invalid origin %s", pdu.event_id, origin) return else: logger.info("Accepting join PDU %s from %s", pdu.event_id, origin) # Check signature. try: pdu = yield self._check_sigs_and_hash(pdu) except SynapseError as e: raise FederationError( "ERROR", e.code, e.msg, affected=pdu.event_id, ) yield self.handler.on_receive_pdu( origin, pdu, sent_to_us_directly=True, ) def __str__(self): return "<ReplicationLayer(%s)>" % self.server_name @defer.inlineCallbacks def exchange_third_party_invite( self, sender_user_id, target_user_id, room_id, signed, ): ret = yield self.handler.exchange_third_party_invite( sender_user_id, target_user_id, room_id, signed, ) defer.returnValue(ret) @defer.inlineCallbacks def on_exchange_third_party_invite_request(self, origin, room_id, event_dict): ret = yield self.handler.on_exchange_third_party_invite_request( origin, room_id, event_dict) defer.returnValue(ret) @defer.inlineCallbacks def check_server_matches_acl(self, server_name, room_id): """Check if the given server is allowed by the server ACLs in the room Args: server_name (str): name of server, *without any port part* room_id (str): ID of the room to check Raises: AuthError if the server does not match the ACL """ state_ids = yield self.store.get_current_state_ids(room_id) acl_event_id = state_ids.get((EventTypes.ServerACL, "")) if not acl_event_id: return acl_event = yield self.store.get_event(acl_event_id) if server_matches_acl_event(server_name, acl_event): return raise AuthError(code=403, msg="Server is banned from room")
class RoomListHandler(BaseHandler): def __init__(self, hs): super(RoomListHandler, self).__init__(hs) self.response_cache = ResponseCache(hs, "room_list") self.remote_response_cache = ResponseCache(hs, "remote_room_list", timeout_ms=30 * 1000) def get_local_public_room_list( self, limit=None, since_token=None, search_filter=None, network_tuple=EMTPY_THIRD_PARTY_ID, ): """Generate a local public room list. There are multiple different lists: the main one plus one per third party network. A client can ask for a specific list or to return all. Args: limit (int) since_token (str) search_filter (dict) network_tuple (ThirdPartyInstanceID): Which public list to use. This can be (None, None) to indicate the main list, or a particular appservice and network id to use an appservice specific one. Setting to None returns all public rooms across all lists. """ logger.info( "Getting public room list: limit=%r, since=%r, search=%r, network=%r", limit, since_token, bool(search_filter), network_tuple, ) if search_filter: # We explicitly don't bother caching searches or requests for # appservice specific lists. logger.info("Bypassing cache as search request.") return self._get_public_room_list( limit, since_token, search_filter, network_tuple=network_tuple, ) key = (limit, since_token, network_tuple) return self.response_cache.wrap( key, self._get_public_room_list, limit, since_token, network_tuple=network_tuple, ) @defer.inlineCallbacks def _get_public_room_list( self, limit=None, since_token=None, search_filter=None, network_tuple=EMTPY_THIRD_PARTY_ID, ): if since_token and since_token != "END": since_token = RoomListNextBatch.from_token(since_token) else: since_token = None rooms_to_order_value = {} rooms_to_num_joined = {} newly_visible = [] newly_unpublished = [] if since_token: stream_token = since_token.stream_ordering current_public_id = yield self.store.get_current_public_room_stream_id( ) public_room_stream_id = since_token.public_room_stream_id newly_visible, newly_unpublished = yield self.store.get_public_room_changes( public_room_stream_id, current_public_id, network_tuple=network_tuple, ) else: stream_token = yield self.store.get_room_max_stream_ordering() public_room_stream_id = yield self.store.get_current_public_room_stream_id( ) room_ids = yield self.store.get_public_room_ids_at_stream_id( public_room_stream_id, network_tuple=network_tuple, ) # We want to return rooms in a particular order: the number of joined # users. We then arbitrarily use the room_id as a tie breaker. @defer.inlineCallbacks def get_order_for_room(room_id): # Most of the rooms won't have changed between the since token and # now (especially if the since token is "now"). So, we can ask what # the current users are in a room (that will hit a cache) and then # check if the room has changed since the since token. (We have to # do it in that order to avoid races). # If things have changed then fall back to getting the current state # at the since token. joined_users = yield self.store.get_users_in_room(room_id) if self.store.has_room_changed_since(room_id, stream_token): latest_event_ids = yield self.store.get_forward_extremeties_for_room( room_id, stream_token) if not latest_event_ids: return joined_users = yield self.state_handler.get_current_user_in_room( room_id, latest_event_ids, ) num_joined_users = len(joined_users) rooms_to_num_joined[room_id] = num_joined_users if num_joined_users == 0: return # We want larger rooms to be first, hence negating num_joined_users rooms_to_order_value[room_id] = (-num_joined_users, room_id) logger.info("Getting ordering for %i rooms since %s", len(room_ids), stream_token) yield concurrently_execute(get_order_for_room, room_ids, 10) sorted_entries = sorted(rooms_to_order_value.items(), key=lambda e: e[1]) sorted_rooms = [room_id for room_id, _ in sorted_entries] # `sorted_rooms` should now be a list of all public room ids that is # stable across pagination. Therefore, we can use indices into this # list as our pagination tokens. # Filter out rooms that we don't want to return rooms_to_scan = [ r for r in sorted_rooms if r not in newly_unpublished and rooms_to_num_joined[room_id] > 0 ] total_room_count = len(rooms_to_scan) if since_token: # Filter out rooms we've already returned previously # `since_token.current_limit` is the index of the last room we # sent down, so we exclude it and everything before/after it. if since_token.direction_is_forward: rooms_to_scan = rooms_to_scan[since_token.current_limit + 1:] else: rooms_to_scan = rooms_to_scan[:since_token.current_limit] rooms_to_scan.reverse() logger.info("After sorting and filtering, %i rooms remain", len(rooms_to_scan)) # _append_room_entry_to_chunk will append to chunk but will stop if # len(chunk) > limit # # Normally we will generate enough results on the first iteration here, # but if there is a search filter, _append_room_entry_to_chunk may # filter some results out, in which case we loop again. # # We don't want to scan over the entire range either as that # would potentially waste a lot of work. # # XXX if there is no limit, we may end up DoSing the server with # calls to get_current_state_ids for every single room on the # server. Surely we should cap this somehow? # if limit: step = limit + 1 else: # step cannot be zero step = len(rooms_to_scan) if len(rooms_to_scan) != 0 else 1 chunk = [] for i in range(0, len(rooms_to_scan), step): batch = rooms_to_scan[i:i + step] logger.info("Processing %i rooms for result", len(batch)) yield concurrently_execute( lambda r: self._append_room_entry_to_chunk( r, rooms_to_num_joined[r], chunk, limit, search_filter), batch, 5, ) logger.info("Now %i rooms in result", len(chunk)) if len(chunk) >= limit + 1: break chunk.sort(key=lambda e: (-e["num_joined_members"], e["room_id"])) # Work out the new limit of the batch for pagination, or None if we # know there are no more results that would be returned. # i.e., [since_token.current_limit..new_limit] is the batch of rooms # we've returned (or the reverse if we paginated backwards) # We tried to pull out limit + 1 rooms above, so if we have <= limit # then we know there are no more results to return new_limit = None if chunk and (not limit or len(chunk) > limit): if not since_token or since_token.direction_is_forward: if limit: chunk = chunk[:limit] last_room_id = chunk[-1]["room_id"] else: if limit: chunk = chunk[-limit:] last_room_id = chunk[0]["room_id"] new_limit = sorted_rooms.index(last_room_id) results = { "chunk": chunk, "total_room_count_estimate": total_room_count, } if since_token: results["new_rooms"] = bool(newly_visible) if not since_token or since_token.direction_is_forward: if new_limit is not None: results["next_batch"] = RoomListNextBatch( stream_ordering=stream_token, public_room_stream_id=public_room_stream_id, current_limit=new_limit, direction_is_forward=True, ).to_token() if since_token: results["prev_batch"] = since_token.copy_and_replace( direction_is_forward=False, current_limit=since_token.current_limit + 1, ).to_token() else: if new_limit is not None: results["prev_batch"] = RoomListNextBatch( stream_ordering=stream_token, public_room_stream_id=public_room_stream_id, current_limit=new_limit, direction_is_forward=False, ).to_token() if since_token: results["next_batch"] = since_token.copy_and_replace( direction_is_forward=True, current_limit=since_token.current_limit - 1, ).to_token() defer.returnValue(results) @defer.inlineCallbacks def _append_room_entry_to_chunk(self, room_id, num_joined_users, chunk, limit, search_filter): """Generate the entry for a room in the public room list and append it to the `chunk` if it matches the search filter """ if limit and len(chunk) > limit + 1: # We've already got enough, so lets just drop it. return result = yield self.generate_room_entry(room_id, num_joined_users) if result and _matches_room_entry(result, search_filter): chunk.append(result) @cachedInlineCallbacks(num_args=1, cache_context=True) def generate_room_entry(self, room_id, num_joined_users, cache_context, with_alias=True, allow_private=False): """Returns the entry for a room """ result = { "room_id": room_id, "num_joined_members": num_joined_users, } current_state_ids = yield self.store.get_current_state_ids( room_id, on_invalidate=cache_context.invalidate, ) event_map = yield self.store.get_events([ event_id for key, event_id in iteritems(current_state_ids) if key[0] in ( EventTypes.JoinRules, EventTypes.Name, EventTypes.Topic, EventTypes.CanonicalAlias, EventTypes.RoomHistoryVisibility, EventTypes.GuestAccess, "m.room.avatar", ) ]) current_state = {(ev.type, ev.state_key): ev for ev in event_map.values()} # Double check that this is actually a public room. join_rules_event = current_state.get((EventTypes.JoinRules, "")) if join_rules_event: join_rule = join_rules_event.content.get("join_rule", None) if not allow_private and join_rule and join_rule != JoinRules.PUBLIC: defer.returnValue(None) if with_alias: aliases = yield self.store.get_aliases_for_room( room_id, on_invalidate=cache_context.invalidate) if aliases: result["aliases"] = aliases name_event = yield current_state.get((EventTypes.Name, "")) if name_event: name = name_event.content.get("name", None) if name: result["name"] = name topic_event = current_state.get((EventTypes.Topic, "")) if topic_event: topic = topic_event.content.get("topic", None) if topic: result["topic"] = topic canonical_event = current_state.get((EventTypes.CanonicalAlias, "")) if canonical_event: canonical_alias = canonical_event.content.get("alias", None) if canonical_alias: result["canonical_alias"] = canonical_alias visibility_event = current_state.get( (EventTypes.RoomHistoryVisibility, "")) visibility = None if visibility_event: visibility = visibility_event.content.get("history_visibility", None) result["world_readable"] = visibility == "world_readable" guest_event = current_state.get((EventTypes.GuestAccess, "")) guest = None if guest_event: guest = guest_event.content.get("guest_access", None) result["guest_can_join"] = guest == "can_join" avatar_event = current_state.get(("m.room.avatar", "")) if avatar_event: avatar_url = avatar_event.content.get("url", None) if avatar_url: result["avatar_url"] = avatar_url defer.returnValue(result) @defer.inlineCallbacks def get_remote_public_room_list( self, server_name, limit=None, since_token=None, search_filter=None, include_all_networks=False, third_party_instance_id=None, ): if search_filter: # We currently don't support searching across federation, so we have # to do it manually without pagination limit = None since_token = None res = yield self._get_remote_list_cached( server_name, limit=limit, since_token=since_token, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, ) if search_filter: res = { "chunk": [ entry for entry in list(res.get("chunk", [])) if _matches_room_entry(entry, search_filter) ] } defer.returnValue(res) def _get_remote_list_cached( self, server_name, limit=None, since_token=None, search_filter=None, include_all_networks=False, third_party_instance_id=None, ): repl_layer = self.hs.get_federation_client() if search_filter: # We can't cache when asking for search return repl_layer.get_public_rooms( server_name, limit=limit, since_token=since_token, search_filter=search_filter, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, ) key = ( server_name, limit, since_token, include_all_networks, third_party_instance_id, ) return self.remote_response_cache.wrap( key, repl_layer.get_public_rooms, server_name, limit=limit, since_token=since_token, search_filter=search_filter, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, )
class InitialSyncHandler(BaseHandler): def __init__(self, hs: "HomeServer"): super(InitialSyncHandler, self).__init__(hs) self.hs = hs self.state = hs.get_state_handler() self.clock = hs.get_clock() self.validator = EventValidator() self.snapshot_cache = ResponseCache(hs, "initial_sync_cache") self._event_serializer = hs.get_event_client_serializer() self.storage = hs.get_storage() self.state_store = self.storage.state def snapshot_all_rooms( self, user_id: str, pagin_config: PaginationConfig, as_client_event: bool = True, include_archived: bool = False, ) -> JsonDict: """Retrieve a snapshot of all rooms the user is invited or has joined. This snapshot may include messages for all rooms where the user is joined, depending on the pagination config. Args: user_id: The ID of the user making the request. pagin_config: The pagination config used to determine how many messages *PER ROOM* to return. as_client_event: True to get events in client-server format. include_archived: True to get rooms that the user has left Returns: A JsonDict with the same format as the response to `/intialSync` API """ key = ( user_id, pagin_config.from_token, pagin_config.to_token, pagin_config.direction, pagin_config.limit, as_client_event, include_archived, ) return self.snapshot_cache.wrap( key, self._snapshot_all_rooms, user_id, pagin_config, as_client_event, include_archived, ) async def _snapshot_all_rooms( self, user_id: str, pagin_config: PaginationConfig, as_client_event: bool = True, include_archived: bool = False, ) -> JsonDict: memberships = [Membership.INVITE, Membership.JOIN] if include_archived: memberships.append(Membership.LEAVE) room_list = await self.store.get_rooms_for_local_user_where_membership_is( user_id=user_id, membership_list=memberships) user = UserID.from_string(user_id) rooms_ret = [] now_token = self.hs.get_event_sources().get_current_token() presence_stream = self.hs.get_event_sources().sources["presence"] pagination_config = PaginationConfig(from_token=now_token) presence, _ = await presence_stream.get_pagination_rows( user, pagination_config.get_source_config("presence"), None) receipt_stream = self.hs.get_event_sources().sources["receipt"] receipt, _ = await receipt_stream.get_pagination_rows( user, pagination_config.get_source_config("receipt"), None) tags_by_room = await self.store.get_tags_for_user(user_id) account_data, account_data_by_room = await self.store.get_account_data_for_user( user_id) public_room_ids = await self.store.get_public_room_ids() limit = pagin_config.limit if limit is None: limit = 10 async def handle_room(event: RoomsForUser): d = { "room_id": event.room_id, "membership": event.membership, "visibility": ("public" if event.room_id in public_room_ids else "private"), } if event.membership == Membership.INVITE: time_now = self.clock.time_msec() d["inviter"] = event.sender invite_event = await self.store.get_event(event.event_id) d["invite"] = await self._event_serializer.serialize_event( invite_event, time_now, as_client_event) rooms_ret.append(d) if event.membership not in (Membership.JOIN, Membership.LEAVE): return try: if event.membership == Membership.JOIN: room_end_token = now_token.room_key deferred_room_state = run_in_background( self.state_handler.get_current_state, event.room_id) elif event.membership == Membership.LEAVE: room_end_token = "s%d" % (event.stream_ordering, ) deferred_room_state = run_in_background( self.state_store.get_state_for_events, [event.event_id]) deferred_room_state.addCallback( lambda states: states[event.event_id]) (messages, token), current_state = await make_deferred_yieldable( defer.gatherResults([ run_in_background( self.store.get_recent_events_for_room, event.room_id, limit=limit, end_token=room_end_token, ), deferred_room_state, ])).addErrback(unwrapFirstError) messages = await filter_events_for_client( self.storage, user_id, messages) start_token = now_token.copy_and_replace("room_key", token) end_token = now_token.copy_and_replace("room_key", room_end_token) time_now = self.clock.time_msec() d["messages"] = { "chunk": (await self._event_serializer.serialize_events( messages, time_now=time_now, as_client_event=as_client_event)), "start": start_token.to_string(), "end": end_token.to_string(), } d["state"] = await self._event_serializer.serialize_events( current_state.values(), time_now=time_now, as_client_event=as_client_event, ) account_data_events = [] tags = tags_by_room.get(event.room_id) if tags: account_data_events.append({ "type": "m.tag", "content": { "tags": tags } }) account_data = account_data_by_room.get(event.room_id, {}) for account_data_type, content in account_data.items(): account_data_events.append({ "type": account_data_type, "content": content }) d["account_data"] = account_data_events except Exception: logger.exception("Failed to get snapshot") await concurrently_execute(handle_room, room_list, 10) account_data_events = [] for account_data_type, content in account_data.items(): account_data_events.append({ "type": account_data_type, "content": content }) now = self.clock.time_msec() ret = { "rooms": rooms_ret, "presence": [{ "type": "m.presence", "content": format_user_presence_state(event, now), } for event in presence], "account_data": account_data_events, "receipts": receipt, "end": now_token.to_string(), } return ret async def room_initial_sync(self, requester: Requester, room_id: str, pagin_config: PaginationConfig) -> JsonDict: """Capture the a snapshot of a room. If user is currently a member of the room this will be what is currently in the room. If the user left the room this will be what was in the room when they left. Args: requester: The user to get a snapshot for. room_id: The room to get a snapshot of. pagin_config: The pagination config used to determine how many messages to return. Raises: AuthError if the user wasn't in the room. Returns: A JSON serialisable dict with the snapshot of the room. """ blocked = await self.store.is_room_blocked(room_id) if blocked: raise SynapseError(403, "This room has been blocked on this server") user_id = requester.user.to_string() ( membership, member_event_id, ) = await self.auth.check_user_in_room_or_world_readable( room_id, user_id, allow_departed_users=True, ) is_peeking = member_event_id is None if membership == Membership.JOIN: result = await self._room_initial_sync_joined( user_id, room_id, pagin_config, membership, is_peeking) elif membership == Membership.LEAVE: result = await self._room_initial_sync_parted( user_id, room_id, pagin_config, membership, member_event_id, is_peeking) account_data_events = [] tags = await self.store.get_tags_for_room(user_id, room_id) if tags: account_data_events.append({ "type": "m.tag", "content": { "tags": tags } }) account_data = await self.store.get_account_data_for_room( user_id, room_id) for account_data_type, content in account_data.items(): account_data_events.append({ "type": account_data_type, "content": content }) result["account_data"] = account_data_events return result async def _room_initial_sync_parted( self, user_id: str, room_id: str, pagin_config: PaginationConfig, membership: Membership, member_event_id: str, is_peeking: bool, ) -> JsonDict: room_state = await self.state_store.get_state_for_events( [member_event_id]) room_state = room_state[member_event_id] limit = pagin_config.limit if pagin_config else None if limit is None: limit = 10 stream_token = await self.store.get_stream_token_for_event( member_event_id) messages, token = await self.store.get_recent_events_for_room( room_id, limit=limit, end_token=stream_token) messages = await filter_events_for_client(self.storage, user_id, messages, is_peeking=is_peeking) start_token = StreamToken.START.copy_and_replace("room_key", token) end_token = StreamToken.START.copy_and_replace("room_key", stream_token) time_now = self.clock.time_msec() return { "membership": membership, "room_id": room_id, "messages": { "chunk": (await self._event_serializer.serialize_events(messages, time_now)), "start": start_token.to_string(), "end": end_token.to_string(), }, "state": (await self._event_serializer.serialize_events(room_state.values(), time_now)), "presence": [], "receipts": [], } async def _room_initial_sync_joined( self, user_id: str, room_id: str, pagin_config: PaginationConfig, membership: Membership, is_peeking: bool, ) -> JsonDict: current_state = await self.state.get_current_state(room_id=room_id) # TODO: These concurrently time_now = self.clock.time_msec() state = await self._event_serializer.serialize_events( current_state.values(), time_now) now_token = self.hs.get_event_sources().get_current_token() limit = pagin_config.limit if pagin_config else None if limit is None: limit = 10 room_members = [ m for m in current_state.values() if m.type == EventTypes.Member and m.content["membership"] == Membership.JOIN ] presence_handler = self.hs.get_presence_handler() async def get_presence(): # If presence is disabled, return an empty list if not self.hs.config.use_presence: return [] states = await presence_handler.get_states( [m.user_id for m in room_members]) return [{ "type": EventTypes.Presence, "content": format_user_presence_state(s, time_now), } for s in states] async def get_receipts(): receipts = await self.store.get_linearized_receipts_for_room( room_id, to_key=now_token.receipt_key) if not receipts: receipts = [] return receipts presence, receipts, (messages, token) = await make_deferred_yieldable( defer.gatherResults( [ run_in_background(get_presence), run_in_background(get_receipts), run_in_background( self.store.get_recent_events_for_room, room_id, limit=limit, end_token=now_token.room_key, ), ], consumeErrors=True, ).addErrback(unwrapFirstError)) messages = await filter_events_for_client(self.storage, user_id, messages, is_peeking=is_peeking) start_token = now_token.copy_and_replace("room_key", token) end_token = now_token time_now = self.clock.time_msec() ret = { "room_id": room_id, "messages": { "chunk": (await self._event_serializer.serialize_events(messages, time_now)), "start": start_token.to_string(), "end": end_token.to_string(), }, "state": state, "presence": presence, "receipts": receipts, } if not is_peeking: ret["membership"] = membership return ret
class ApplicationServiceApi(SimpleHttpClient): """This class manages HS -> AS communications, including querying and pushing. """ def __init__(self, hs): super(ApplicationServiceApi, self).__init__(hs) self.clock = hs.get_clock() self.protocol_meta_cache = ResponseCache(hs, "as_protocol_meta", timeout_ms=HOUR_IN_MS) @defer.inlineCallbacks def query_user(self, service, user_id): if service.url is None: defer.returnValue(False) uri = service.url + ("/users/%s" % urllib.quote(user_id)) response = None try: response = yield self.get_json(uri, {"access_token": service.hs_token}) if response is not None: # just an empty json object defer.returnValue(True) except CodeMessageException as e: if e.code == 404: defer.returnValue(False) return logger.warning("query_user to %s received %s", uri, e.code) except Exception as ex: logger.warning("query_user to %s threw exception %s", uri, ex) defer.returnValue(False) @defer.inlineCallbacks def query_alias(self, service, alias): if service.url is None: defer.returnValue(False) uri = service.url + ("/rooms/%s" % urllib.quote(alias)) response = None try: response = yield self.get_json(uri, {"access_token": service.hs_token}) if response is not None: # just an empty json object defer.returnValue(True) except CodeMessageException as e: logger.warning("query_alias to %s received %s", uri, e.code) if e.code == 404: defer.returnValue(False) return except Exception as ex: logger.warning("query_alias to %s threw exception %s", uri, ex) defer.returnValue(False) @defer.inlineCallbacks def query_3pe(self, service, kind, protocol, fields): if kind == ThirdPartyEntityKind.USER: required_field = "userid" elif kind == ThirdPartyEntityKind.LOCATION: required_field = "alias" else: raise ValueError("Unrecognised 'kind' argument %r to query_3pe()", kind) if service.url is None: defer.returnValue([]) uri = "%s%s/thirdparty/%s/%s" % (service.url, APP_SERVICE_PREFIX, kind, urllib.quote(protocol)) try: response = yield self.get_json(uri, fields) if not isinstance(response, list): logger.warning( "query_3pe to %s returned an invalid response %r", uri, response) defer.returnValue([]) ret = [] for r in response: if _is_valid_3pe_result(r, field=required_field): ret.append(r) else: logger.warning( "query_3pe to %s returned an invalid result %r", uri, r) defer.returnValue(ret) except Exception as ex: logger.warning("query_3pe to %s threw exception %s", uri, ex) defer.returnValue([]) def get_3pe_protocol(self, service, protocol): if service.url is None: defer.returnValue({}) @defer.inlineCallbacks def _get(): uri = "%s%s/thirdparty/protocol/%s" % ( service.url, APP_SERVICE_PREFIX, urllib.quote(protocol)) try: info = yield self.get_json(uri, {}) if not _is_valid_3pe_metadata(info): logger.warning( "query_3pe_protocol to %s did not return a" " valid result", uri) defer.returnValue(None) for instance in info.get("instances", []): network_id = instance.get("network_id", None) if network_id is not None: instance["instance_id"] = ThirdPartyInstanceID( service.id, network_id, ).to_string() defer.returnValue(info) except Exception as ex: logger.warning("query_3pe_protocol to %s threw exception %s", uri, ex) defer.returnValue(None) key = (service.id, protocol) return self.protocol_meta_cache.wrap(key, _get) @defer.inlineCallbacks def push_bulk(self, service, events, txn_id=None): if service.url is None: defer.returnValue(True) events = self._serialize(events) if txn_id is None: logger.warning("push_bulk: Missing txn ID sending events to %s", service.url) txn_id = str(0) txn_id = str(txn_id) uri = service.url + ("/transactions/%s" % urllib.quote(txn_id)) try: yield self.put_json(uri=uri, json_body={"events": events}, args={"access_token": service.hs_token}) sent_transactions_counter.labels(service.id).inc() sent_events_counter.labels(service.id).inc(len(events)) defer.returnValue(True) return except CodeMessageException as e: logger.warning("push_bulk to %s received %s", uri, e.code) except Exception as ex: logger.warning("push_bulk to %s threw exception %s", uri, ex) failed_transactions_counter.labels(service.id).inc() defer.returnValue(False) def _serialize(self, events): time_now = self.clock.time_msec() return [ serialize_event(e, time_now, as_client_event=True) for e in events ]
class RoomListHandler(BaseHandler): def __init__(self, hs): super(RoomListHandler, self).__init__(hs) self.enable_room_list_search = hs.config.enable_room_list_search self.response_cache = ResponseCache(hs, "room_list") self.remote_response_cache = ResponseCache(hs, "remote_room_list", timeout_ms=30 * 1000) def get_local_public_room_list(self, limit=None, since_token=None, search_filter=None, network_tuple=EMPTY_THIRD_PARTY_ID, from_federation=False): """Generate a local public room list. There are multiple different lists: the main one plus one per third party network. A client can ask for a specific list or to return all. Args: limit (int|None) since_token (str|None) search_filter (dict|None) network_tuple (ThirdPartyInstanceID): Which public list to use. This can be (None, None) to indicate the main list, or a particular appservice and network id to use an appservice specific one. Setting to None returns all public rooms across all lists. """ if not self.enable_room_list_search: return defer.succeed({ "chunk": [], "total_room_count_estimate": 0, }) logger.info( "Getting public room list: limit=%r, since=%r, search=%r, network=%r", limit, since_token, bool(search_filter), network_tuple, ) if search_filter: # We explicitly don't bother caching searches or requests for # appservice specific lists. logger.info("Bypassing cache as search request.") # XXX: Quick hack to stop room directory queries taking too long. # Timeout request after 60s. Probably want a more fundamental # solution at some point timeout = self.clock.time() + 60 return self._get_public_room_list( limit, since_token, search_filter, network_tuple=network_tuple, timeout=timeout, ) key = (limit, since_token, network_tuple) return self.response_cache.wrap( key, self._get_public_room_list, limit, since_token, network_tuple=network_tuple, from_federation=from_federation, ) @defer.inlineCallbacks def _get_public_room_list(self, limit=None, since_token=None, search_filter=None, network_tuple=EMPTY_THIRD_PARTY_ID, from_federation=False, timeout=None,): """Generate a public room list. Args: limit (int|None): Maximum amount of rooms to return. since_token (str|None) search_filter (dict|None): Dictionary to filter rooms by. network_tuple (ThirdPartyInstanceID): Which public list to use. This can be (None, None) to indicate the main list, or a particular appservice and network id to use an appservice specific one. Setting to None returns all public rooms across all lists. from_federation (bool): Whether this request originated from a federating server or a client. Used for room filtering. timeout (int|None): Amount of seconds to wait for a response before timing out. """ if since_token and since_token != "END": since_token = RoomListNextBatch.from_token(since_token) else: since_token = None rooms_to_order_value = {} rooms_to_num_joined = {} newly_visible = [] newly_unpublished = [] if since_token: stream_token = since_token.stream_ordering current_public_id = yield self.store.get_current_public_room_stream_id() public_room_stream_id = since_token.public_room_stream_id newly_visible, newly_unpublished = yield self.store.get_public_room_changes( public_room_stream_id, current_public_id, network_tuple=network_tuple, ) else: stream_token = yield self.store.get_room_max_stream_ordering() public_room_stream_id = yield self.store.get_current_public_room_stream_id() room_ids = yield self.store.get_public_room_ids_at_stream_id( public_room_stream_id, network_tuple=network_tuple, ) # We want to return rooms in a particular order: the number of joined # users. We then arbitrarily use the room_id as a tie breaker. @defer.inlineCallbacks def get_order_for_room(room_id): # Most of the rooms won't have changed between the since token and # now (especially if the since token is "now"). So, we can ask what # the current users are in a room (that will hit a cache) and then # check if the room has changed since the since token. (We have to # do it in that order to avoid races). # If things have changed then fall back to getting the current state # at the since token. joined_users = yield self.store.get_users_in_room(room_id) if self.store.has_room_changed_since(room_id, stream_token): latest_event_ids = yield self.store.get_forward_extremeties_for_room( room_id, stream_token ) if not latest_event_ids: return joined_users = yield self.state_handler.get_current_users_in_room( room_id, latest_event_ids, ) num_joined_users = len(joined_users) rooms_to_num_joined[room_id] = num_joined_users if num_joined_users == 0: return # We want larger rooms to be first, hence negating num_joined_users rooms_to_order_value[room_id] = (-num_joined_users, room_id) logger.info("Getting ordering for %i rooms since %s", len(room_ids), stream_token) yield concurrently_execute(get_order_for_room, room_ids, 10) sorted_entries = sorted(rooms_to_order_value.items(), key=lambda e: e[1]) sorted_rooms = [room_id for room_id, _ in sorted_entries] # `sorted_rooms` should now be a list of all public room ids that is # stable across pagination. Therefore, we can use indices into this # list as our pagination tokens. # Filter out rooms that we don't want to return rooms_to_scan = [ r for r in sorted_rooms if r not in newly_unpublished and rooms_to_num_joined[r] > 0 ] total_room_count = len(rooms_to_scan) if since_token: # Filter out rooms we've already returned previously # `since_token.current_limit` is the index of the last room we # sent down, so we exclude it and everything before/after it. if since_token.direction_is_forward: rooms_to_scan = rooms_to_scan[since_token.current_limit + 1:] else: rooms_to_scan = rooms_to_scan[:since_token.current_limit] rooms_to_scan.reverse() logger.info("After sorting and filtering, %i rooms remain", len(rooms_to_scan)) # _append_room_entry_to_chunk will append to chunk but will stop if # len(chunk) > limit # # Normally we will generate enough results on the first iteration here, # but if there is a search filter, _append_room_entry_to_chunk may # filter some results out, in which case we loop again. # # We don't want to scan over the entire range either as that # would potentially waste a lot of work. # # XXX if there is no limit, we may end up DoSing the server with # calls to get_current_state_ids for every single room on the # server. Surely we should cap this somehow? # if limit: step = limit + 1 else: # step cannot be zero step = len(rooms_to_scan) if len(rooms_to_scan) != 0 else 1 chunk = [] for i in range(0, len(rooms_to_scan), step): if timeout and self.clock.time() > timeout: raise Exception("Timed out searching room directory") batch = rooms_to_scan[i:i + step] logger.info("Processing %i rooms for result", len(batch)) yield concurrently_execute( lambda r: self._append_room_entry_to_chunk( r, rooms_to_num_joined[r], chunk, limit, search_filter, from_federation=from_federation, ), batch, 5, ) logger.info("Now %i rooms in result", len(chunk)) if len(chunk) >= limit + 1: break chunk.sort(key=lambda e: (-e["num_joined_members"], e["room_id"])) # Work out the new limit of the batch for pagination, or None if we # know there are no more results that would be returned. # i.e., [since_token.current_limit..new_limit] is the batch of rooms # we've returned (or the reverse if we paginated backwards) # We tried to pull out limit + 1 rooms above, so if we have <= limit # then we know there are no more results to return new_limit = None if chunk and (not limit or len(chunk) > limit): if not since_token or since_token.direction_is_forward: if limit: chunk = chunk[:limit] last_room_id = chunk[-1]["room_id"] else: if limit: chunk = chunk[-limit:] last_room_id = chunk[0]["room_id"] new_limit = sorted_rooms.index(last_room_id) results = { "chunk": chunk, "total_room_count_estimate": total_room_count, } if since_token: results["new_rooms"] = bool(newly_visible) if not since_token or since_token.direction_is_forward: if new_limit is not None: results["next_batch"] = RoomListNextBatch( stream_ordering=stream_token, public_room_stream_id=public_room_stream_id, current_limit=new_limit, direction_is_forward=True, ).to_token() if since_token: results["prev_batch"] = since_token.copy_and_replace( direction_is_forward=False, current_limit=since_token.current_limit + 1, ).to_token() else: if new_limit is not None: results["prev_batch"] = RoomListNextBatch( stream_ordering=stream_token, public_room_stream_id=public_room_stream_id, current_limit=new_limit, direction_is_forward=False, ).to_token() if since_token: results["next_batch"] = since_token.copy_and_replace( direction_is_forward=True, current_limit=since_token.current_limit - 1, ).to_token() defer.returnValue(results) @defer.inlineCallbacks def _append_room_entry_to_chunk(self, room_id, num_joined_users, chunk, limit, search_filter, from_federation=False): """Generate the entry for a room in the public room list and append it to the `chunk` if it matches the search filter Args: room_id (str): The ID of the room. num_joined_users (int): The number of joined users in the room. chunk (list) limit (int|None): Maximum amount of rooms to display. Function will return if length of chunk is greater than limit + 1. search_filter (dict|None) from_federation (bool): Whether this request originated from a federating server or a client. Used for room filtering. """ if limit and len(chunk) > limit + 1: # We've already got enough, so lets just drop it. return result = yield self.generate_room_entry(room_id, num_joined_users) if not result: return if from_federation and not result.get("m.federate", True): # This is a room that other servers cannot join. Do not show them # this room. return if _matches_room_entry(result, search_filter): chunk.append(result) @cachedInlineCallbacks(num_args=1, cache_context=True) def generate_room_entry(self, room_id, num_joined_users, cache_context, with_alias=True, allow_private=False): """Returns the entry for a room Args: room_id (str): The room's ID. num_joined_users (int): Number of users in the room. cache_context: Information for cached responses. with_alias (bool): Whether to return the room's aliases in the result. allow_private (bool): Whether invite-only rooms should be shown. Returns: Deferred[dict|None]: Returns a room entry as a dictionary, or None if this room was determined not to be shown publicly. """ result = { "room_id": room_id, "num_joined_members": num_joined_users, } current_state_ids = yield self.store.get_current_state_ids( room_id, on_invalidate=cache_context.invalidate, ) event_map = yield self.store.get_events([ event_id for key, event_id in iteritems(current_state_ids) if key[0] in ( EventTypes.Create, EventTypes.JoinRules, EventTypes.Name, EventTypes.Topic, EventTypes.CanonicalAlias, EventTypes.RoomHistoryVisibility, EventTypes.GuestAccess, "m.room.avatar", ) ]) current_state = { (ev.type, ev.state_key): ev for ev in event_map.values() } # Double check that this is actually a public room. join_rules_event = current_state.get((EventTypes.JoinRules, "")) if join_rules_event: join_rule = join_rules_event.content.get("join_rule", None) if not allow_private and join_rule and join_rule != JoinRules.PUBLIC: defer.returnValue(None) # Return whether this room is open to federation users or not create_event = current_state.get((EventTypes.Create, "")) result["m.federate"] = create_event.content.get("m.federate", True) if with_alias: aliases = yield self.store.get_aliases_for_room( room_id, on_invalidate=cache_context.invalidate ) if aliases: result["aliases"] = aliases name_event = yield current_state.get((EventTypes.Name, "")) if name_event: name = name_event.content.get("name", None) if name: result["name"] = name topic_event = current_state.get((EventTypes.Topic, "")) if topic_event: topic = topic_event.content.get("topic", None) if topic: result["topic"] = topic canonical_event = current_state.get((EventTypes.CanonicalAlias, "")) if canonical_event: canonical_alias = canonical_event.content.get("alias", None) if canonical_alias: result["canonical_alias"] = canonical_alias visibility_event = current_state.get((EventTypes.RoomHistoryVisibility, "")) visibility = None if visibility_event: visibility = visibility_event.content.get("history_visibility", None) result["world_readable"] = visibility == "world_readable" guest_event = current_state.get((EventTypes.GuestAccess, "")) guest = None if guest_event: guest = guest_event.content.get("guest_access", None) result["guest_can_join"] = guest == "can_join" avatar_event = current_state.get(("m.room.avatar", "")) if avatar_event: avatar_url = avatar_event.content.get("url", None) if avatar_url: result["avatar_url"] = avatar_url defer.returnValue(result) @defer.inlineCallbacks def get_remote_public_room_list(self, server_name, limit=None, since_token=None, search_filter=None, include_all_networks=False, third_party_instance_id=None,): if not self.enable_room_list_search: defer.returnValue({ "chunk": [], "total_room_count_estimate": 0, }) if search_filter: # We currently don't support searching across federation, so we have # to do it manually without pagination limit = None since_token = None res = yield self._get_remote_list_cached( server_name, limit=limit, since_token=since_token, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, ) if search_filter: res = {"chunk": [ entry for entry in list(res.get("chunk", [])) if _matches_room_entry(entry, search_filter) ]} defer.returnValue(res) def _get_remote_list_cached(self, server_name, limit=None, since_token=None, search_filter=None, include_all_networks=False, third_party_instance_id=None,): repl_layer = self.hs.get_federation_client() if search_filter: # We can't cache when asking for search return repl_layer.get_public_rooms( server_name, limit=limit, since_token=since_token, search_filter=search_filter, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, ) key = ( server_name, limit, since_token, include_all_networks, third_party_instance_id, ) return self.remote_response_cache.wrap( key, repl_layer.get_public_rooms, server_name, limit=limit, since_token=since_token, search_filter=search_filter, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, )
class RoomListHandler(BaseHandler): def __init__(self, hs): super(RoomListHandler, self).__init__(hs) self.enable_room_list_search = hs.config.enable_room_list_search self.response_cache = ResponseCache(hs, "room_list") self.remote_response_cache = ResponseCache( hs, "remote_room_list", timeout_ms=30 * 1000 ) def get_local_public_room_list( self, limit=None, since_token=None, search_filter=None, network_tuple=EMPTY_THIRD_PARTY_ID, from_federation=False, ): """Generate a local public room list. There are multiple different lists: the main one plus one per third party network. A client can ask for a specific list or to return all. Args: limit (int|None) since_token (str|None) search_filter (dict|None) network_tuple (ThirdPartyInstanceID): Which public list to use. This can be (None, None) to indicate the main list, or a particular appservice and network id to use an appservice specific one. Setting to None returns all public rooms across all lists. from_federation (bool): true iff the request comes from the federation API """ if not self.enable_room_list_search: return defer.succeed({"chunk": [], "total_room_count_estimate": 0}) logger.info( "Getting public room list: limit=%r, since=%r, search=%r, network=%r", limit, since_token, bool(search_filter), network_tuple, ) if search_filter: # We explicitly don't bother caching searches or requests for # appservice specific lists. logger.info("Bypassing cache as search request.") return self._get_public_room_list( limit, since_token, search_filter, network_tuple=network_tuple ) key = (limit, since_token, network_tuple) return self.response_cache.wrap( key, self._get_public_room_list, limit, since_token, network_tuple=network_tuple, from_federation=from_federation, ) @defer.inlineCallbacks def _get_public_room_list( self, limit=None, since_token=None, search_filter=None, network_tuple=EMPTY_THIRD_PARTY_ID, from_federation=False, ): """Generate a public room list. Args: limit (int|None): Maximum amount of rooms to return. since_token (str|None) search_filter (dict|None): Dictionary to filter rooms by. network_tuple (ThirdPartyInstanceID): Which public list to use. This can be (None, None) to indicate the main list, or a particular appservice and network id to use an appservice specific one. Setting to None returns all public rooms across all lists. from_federation (bool): Whether this request originated from a federating server or a client. Used for room filtering. """ # Pagination tokens work by storing the room ID sent in the last batch, # plus the direction (forwards or backwards). Next batch tokens always # go forwards, prev batch tokens always go backwards. if since_token: batch_token = RoomListNextBatch.from_token(since_token) bounds = (batch_token.last_joined_members, batch_token.last_room_id) forwards = batch_token.direction_is_forward else: batch_token = None bounds = None forwards = True # we request one more than wanted to see if there are more pages to come probing_limit = limit + 1 if limit is not None else None results = yield self.store.get_largest_public_rooms( network_tuple, search_filter, probing_limit, bounds=bounds, forwards=forwards, ignore_non_federatable=from_federation, ) def build_room_entry(room): entry = { "room_id": room["room_id"], "name": room["name"], "topic": room["topic"], "canonical_alias": room["canonical_alias"], "num_joined_members": room["joined_members"], "avatar_url": room["avatar"], "world_readable": room["history_visibility"] == "world_readable", "guest_can_join": room["guest_access"] == "can_join", } # Filter out Nones – rather omit the field altogether return {k: v for k, v in entry.items() if v is not None} results = [build_room_entry(r) for r in results] response = {} num_results = len(results) if limit is not None: more_to_come = num_results == probing_limit # Depending on direction we trim either the front or back. if forwards: results = results[:limit] else: results = results[-limit:] else: more_to_come = False if num_results > 0: final_entry = results[-1] initial_entry = results[0] if forwards: if batch_token: # If there was a token given then we assume that there # must be previous results. response["prev_batch"] = RoomListNextBatch( last_joined_members=initial_entry["num_joined_members"], last_room_id=initial_entry["room_id"], direction_is_forward=False, ).to_token() if more_to_come: response["next_batch"] = RoomListNextBatch( last_joined_members=final_entry["num_joined_members"], last_room_id=final_entry["room_id"], direction_is_forward=True, ).to_token() else: if batch_token: response["next_batch"] = RoomListNextBatch( last_joined_members=final_entry["num_joined_members"], last_room_id=final_entry["room_id"], direction_is_forward=True, ).to_token() if more_to_come: response["prev_batch"] = RoomListNextBatch( last_joined_members=initial_entry["num_joined_members"], last_room_id=initial_entry["room_id"], direction_is_forward=False, ).to_token() for room in results: # populate search result entries with additional fields, namely # 'aliases' room_id = room["room_id"] aliases = yield self.store.get_aliases_for_room(room_id) if aliases: room["aliases"] = aliases response["chunk"] = results response["total_room_count_estimate"] = yield self.store.count_public_rooms( network_tuple, ignore_non_federatable=from_federation ) return response @cachedInlineCallbacks(num_args=1, cache_context=True) def generate_room_entry( self, room_id, num_joined_users, cache_context, with_alias=True, allow_private=False, ): """Returns the entry for a room Args: room_id (str): The room's ID. num_joined_users (int): Number of users in the room. cache_context: Information for cached responses. with_alias (bool): Whether to return the room's aliases in the result. allow_private (bool): Whether invite-only rooms should be shown. Returns: Deferred[dict|None]: Returns a room entry as a dictionary, or None if this room was determined not to be shown publicly. """ result = {"room_id": room_id, "num_joined_members": num_joined_users} current_state_ids = yield self.store.get_current_state_ids( room_id, on_invalidate=cache_context.invalidate ) event_map = yield self.store.get_events( [ event_id for key, event_id in iteritems(current_state_ids) if key[0] in ( EventTypes.Create, EventTypes.JoinRules, EventTypes.Name, EventTypes.Topic, EventTypes.CanonicalAlias, EventTypes.RoomHistoryVisibility, EventTypes.GuestAccess, "m.room.avatar", ) ] ) current_state = {(ev.type, ev.state_key): ev for ev in event_map.values()} # Double check that this is actually a public room. join_rules_event = current_state.get((EventTypes.JoinRules, "")) if join_rules_event: join_rule = join_rules_event.content.get("join_rule", None) if not allow_private and join_rule and join_rule != JoinRules.PUBLIC: return None # Return whether this room is open to federation users or not create_event = current_state.get((EventTypes.Create, "")) result["m.federate"] = create_event.content.get("m.federate", True) if with_alias: aliases = yield self.store.get_aliases_for_room( room_id, on_invalidate=cache_context.invalidate ) if aliases: result["aliases"] = aliases name_event = yield current_state.get((EventTypes.Name, "")) if name_event: name = name_event.content.get("name", None) if name: result["name"] = name topic_event = current_state.get((EventTypes.Topic, "")) if topic_event: topic = topic_event.content.get("topic", None) if topic: result["topic"] = topic canonical_event = current_state.get((EventTypes.CanonicalAlias, "")) if canonical_event: canonical_alias = canonical_event.content.get("alias", None) if canonical_alias: result["canonical_alias"] = canonical_alias visibility_event = current_state.get((EventTypes.RoomHistoryVisibility, "")) visibility = None if visibility_event: visibility = visibility_event.content.get("history_visibility", None) result["world_readable"] = visibility == "world_readable" guest_event = current_state.get((EventTypes.GuestAccess, "")) guest = None if guest_event: guest = guest_event.content.get("guest_access", None) result["guest_can_join"] = guest == "can_join" avatar_event = current_state.get(("m.room.avatar", "")) if avatar_event: avatar_url = avatar_event.content.get("url", None) if avatar_url: result["avatar_url"] = avatar_url return result @defer.inlineCallbacks def get_remote_public_room_list( self, server_name, limit=None, since_token=None, search_filter=None, include_all_networks=False, third_party_instance_id=None, ): if not self.enable_room_list_search: return {"chunk": [], "total_room_count_estimate": 0} if search_filter: # Searching across federation is defined in MSC2197. # However, the remote homeserver may or may not actually support it. # So we first try an MSC2197 remote-filtered search, then fall back # to a locally-filtered search if we must. try: res = yield self._get_remote_list_cached( server_name, limit=limit, since_token=since_token, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, search_filter=search_filter, ) return res except HttpResponseException as hre: syn_err = hre.to_synapse_error() if hre.code in (404, 405) or syn_err.errcode in ( Codes.UNRECOGNIZED, Codes.NOT_FOUND, ): logger.debug("Falling back to locally-filtered /publicRooms") else: raise # Not an error that should trigger a fallback. # if we reach this point, then we fall back to the situation where # we currently don't support searching across federation, so we have # to do it manually without pagination limit = None since_token = None res = yield self._get_remote_list_cached( server_name, limit=limit, since_token=since_token, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, ) if search_filter: res = { "chunk": [ entry for entry in list(res.get("chunk", [])) if _matches_room_entry(entry, search_filter) ] } return res def _get_remote_list_cached( self, server_name, limit=None, since_token=None, search_filter=None, include_all_networks=False, third_party_instance_id=None, ): repl_layer = self.hs.get_federation_client() if search_filter: # We can't cache when asking for search return repl_layer.get_public_rooms( server_name, limit=limit, since_token=since_token, search_filter=search_filter, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, ) key = ( server_name, limit, since_token, include_all_networks, third_party_instance_id, ) return self.remote_response_cache.wrap( key, repl_layer.get_public_rooms, server_name, limit=limit, since_token=since_token, search_filter=search_filter, include_all_networks=include_all_networks, third_party_instance_id=third_party_instance_id, )
class ReplicationEndpoint(object): """Helper base class for defining new replication HTTP endpoints. This creates an endpoint under `/_synapse/replication/:NAME/:PATH_ARGS..` (with an `/:txn_id` prefix for cached requests.), where NAME is a name, PATH_ARGS are a tuple of parameters to be encoded in the URL. For example, if `NAME` is "send_event" and `PATH_ARGS` is `("event_id",)`, with `CACHE` set to true then this generates an endpoint: /_synapse/replication/send_event/:event_id/:txn_id For POST/PUT requests the payload is serialized to json and sent as the body, while for GET requests the payload is added as query parameters. See `_serialize_payload` for details. Incoming requests are handled by overriding `_handle_request`. Servers must call `register` to register the path with the HTTP server. Requests can be sent by calling the client returned by `make_client`. Attributes: NAME (str): A name for the endpoint, added to the path as well as used in logging and metrics. PATH_ARGS (tuple[str]): A list of parameters to be added to the path. Adding parameters to the path (rather than payload) can make it easier to follow along in the log files. METHOD (str): The method of the HTTP request, defaults to POST. Can be one of POST, PUT or GET. If GET then the payload is sent as query parameters rather than a JSON body. CACHE (bool): Whether server should cache the result of the request/ If true then transparently adds a txn_id to all requests, and `_handle_request` must return a Deferred. RETRY_ON_TIMEOUT(bool): Whether or not to retry the request when a 504 is received. """ __metaclass__ = abc.ABCMeta NAME = abc.abstractproperty() PATH_ARGS = abc.abstractproperty() METHOD = "POST" CACHE = True RETRY_ON_TIMEOUT = True def __init__(self, hs): if self.CACHE: self.response_cache = ResponseCache( hs, "repl." + self.NAME, timeout_ms=30 * 60 * 1000, ) assert self.METHOD in ("PUT", "POST", "GET") @abc.abstractmethod def _serialize_payload(**kwargs): """Static method that is called when creating a request. Concrete implementations should have explicit parameters (rather than kwargs) so that an appropriate exception is raised if the client is called with unexpected parameters. All PATH_ARGS must appear in argument list. Returns: Deferred[dict]|dict: If POST/PUT request then dictionary must be JSON serialisable, otherwise must be appropriate for adding as query args. """ return {} @abc.abstractmethod def _handle_request(self, request, **kwargs): """Handle incoming request. This is called with the request object and PATH_ARGS. Returns: Deferred[dict]: A JSON serialisable dict to be used as response body of request. """ pass @classmethod def make_client(cls, hs): """Create a client that makes requests. Returns a callable that accepts the same parameters as `_serialize_payload`. """ clock = hs.get_clock() host = hs.config.worker_replication_host port = hs.config.worker_replication_http_port client = hs.get_simple_http_client() @defer.inlineCallbacks def send_request(**kwargs): data = yield cls._serialize_payload(**kwargs) url_args = [ urllib.parse.quote(kwargs[name], safe='') for name in cls.PATH_ARGS ] if cls.CACHE: txn_id = random_string(10) url_args.append(txn_id) if cls.METHOD == "POST": request_func = client.post_json_get_json elif cls.METHOD == "PUT": request_func = client.put_json elif cls.METHOD == "GET": request_func = client.get_json else: # We have already asserted in the constructor that a # compatible was picked, but lets be paranoid. raise Exception( "Unknown METHOD on %s replication endpoint" % (cls.NAME,) ) uri = "http://%s:%s/_synapse/replication/%s/%s" % ( host, port, cls.NAME, "/".join(url_args) ) try: # We keep retrying the same request for timeouts. This is so that we # have a good idea that the request has either succeeded or failed on # the master, and so whether we should clean up or not. while True: try: result = yield request_func(uri, data) break except CodeMessageException as e: if e.code != 504 or not cls.RETRY_ON_TIMEOUT: raise logger.warn("%s request timed out", cls.NAME) # If we timed out we probably don't need to worry about backing # off too much, but lets just wait a little anyway. yield clock.sleep(1) except HttpResponseException as e: # We convert to SynapseError as we know that it was a SynapseError # on the master process that we should send to the client. (And # importantly, not stack traces everywhere) raise e.to_synapse_error() defer.returnValue(result) return send_request def register(self, http_server): """Called by the server to register this as a handler to the appropriate path. """ url_args = list(self.PATH_ARGS) handler = self._handle_request method = self.METHOD if self.CACHE: handler = self._cached_handler url_args.append("txn_id") args = "/".join("(?P<%s>[^/]+)" % (arg,) for arg in url_args) pattern = re.compile("^/_synapse/replication/%s/%s$" % ( self.NAME, args )) http_server.register_paths(method, [pattern], handler) def _cached_handler(self, request, txn_id, **kwargs): """Called on new incoming requests when caching is enabled. Checks if there is a cached response for the request and returns that, otherwise calls `_handle_request` and caches its response. """ # We just use the txn_id here, but we probably also want to use the # other PATH_ARGS as well. assert self.CACHE return self.response_cache.wrap( txn_id, self._handle_request, request, **kwargs )
class FederationServer(FederationBase): def __init__(self, hs): super(FederationServer, self).__init__(hs) self.auth = hs.get_auth() self.handler = hs.get_handlers().federation_handler self._server_linearizer = Linearizer("fed_server") self._transaction_linearizer = Linearizer("fed_txn_handler") self.transaction_actions = TransactionActions(self.store) self.registry = hs.get_federation_registry() # We cache responses to state queries, as they take a while and often # come in waves. self._state_resp_cache = ResponseCache(hs, "state_resp", timeout_ms=30000) @defer.inlineCallbacks @log_function def on_backfill_request(self, origin, room_id, versions, limit): with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) pdus = yield self.handler.on_backfill_request( origin, room_id, versions, limit ) res = self._transaction_from_pdus(pdus).get_dict() defer.returnValue((200, res)) @defer.inlineCallbacks @log_function def on_incoming_transaction(self, origin, transaction_data): # keep this as early as possible to make the calculated origin ts as # accurate as possible. request_time = self._clock.time_msec() transaction = Transaction(**transaction_data) if not transaction.transaction_id: raise Exception("Transaction missing transaction_id") logger.debug("[%s] Got transaction", transaction.transaction_id) # use a linearizer to ensure that we don't process the same transaction # multiple times in parallel. with (yield self._transaction_linearizer.queue( (origin, transaction.transaction_id), )): result = yield self._handle_incoming_transaction( origin, transaction, request_time, ) defer.returnValue(result) @defer.inlineCallbacks def _handle_incoming_transaction(self, origin, transaction, request_time): """ Process an incoming transaction and return the HTTP response Args: origin (unicode): the server making the request transaction (Transaction): incoming transaction request_time (int): timestamp that the HTTP request arrived at Returns: Deferred[(int, object)]: http response code and body """ response = yield self.transaction_actions.have_responded(origin, transaction) if response: logger.debug( "[%s] We've already responded to this request", transaction.transaction_id ) defer.returnValue(response) return logger.debug("[%s] Transaction is new", transaction.transaction_id) # Reject if PDU count > 50 and EDU count > 100 if (len(transaction.pdus) > 50 or (hasattr(transaction, "edus") and len(transaction.edus) > 100)): logger.info( "Transaction PDU or EDU count too large. Returning 400", ) response = {} yield self.transaction_actions.set_response( origin, transaction, 400, response ) defer.returnValue((400, response)) received_pdus_counter.inc(len(transaction.pdus)) origin_host, _ = parse_server_name(origin) pdus_by_room = {} for p in transaction.pdus: if "unsigned" in p: unsigned = p["unsigned"] if "age" in unsigned: p["age"] = unsigned["age"] if "age" in p: p["age_ts"] = request_time - int(p["age"]) del p["age"] # We try and pull out an event ID so that if later checks fail we # can log something sensible. We don't mandate an event ID here in # case future event formats get rid of the key. possible_event_id = p.get("event_id", "<Unknown>") # Now we get the room ID so that we can check that we know the # version of the room. room_id = p.get("room_id") if not room_id: logger.info( "Ignoring PDU as does not have a room_id. Event ID: %s", possible_event_id, ) continue try: room_version = yield self.store.get_room_version(room_id) except NotFoundError: logger.info("Ignoring PDU for unknown room_id: %s", room_id) continue try: format_ver = room_version_to_event_format(room_version) except UnsupportedRoomVersionError: # this can happen if support for a given room version is withdrawn, # so that we still get events for said room. logger.info( "Ignoring PDU for room %s with unknown version %s", room_id, room_version, ) continue event = event_from_pdu_json(p, format_ver) pdus_by_room.setdefault(room_id, []).append(event) pdu_results = {} # we can process different rooms in parallel (which is useful if they # require callouts to other servers to fetch missing events), but # impose a limit to avoid going too crazy with ram/cpu. @defer.inlineCallbacks def process_pdus_for_room(room_id): logger.debug("Processing PDUs for %s", room_id) try: yield self.check_server_matches_acl(origin_host, room_id) except AuthError as e: logger.warn( "Ignoring PDUs for room %s from banned server", room_id, ) for pdu in pdus_by_room[room_id]: event_id = pdu.event_id pdu_results[event_id] = e.error_dict() return for pdu in pdus_by_room[room_id]: event_id = pdu.event_id with nested_logging_context(event_id): try: yield self._handle_received_pdu( origin, pdu ) pdu_results[event_id] = {} except FederationError as e: logger.warn("Error handling PDU %s: %s", event_id, e) pdu_results[event_id] = {"error": str(e)} except Exception as e: f = failure.Failure() pdu_results[event_id] = {"error": str(e)} logger.error( "Failed to handle PDU %s", event_id, exc_info=(f.type, f.value, f.getTracebackObject()), ) yield concurrently_execute( process_pdus_for_room, pdus_by_room.keys(), TRANSACTION_CONCURRENCY_LIMIT, ) if hasattr(transaction, "edus"): for edu in (Edu(**x) for x in transaction.edus): yield self.received_edu( origin, edu.edu_type, edu.content ) response = { "pdus": pdu_results, } logger.debug("Returning: %s", str(response)) yield self.transaction_actions.set_response( origin, transaction, 200, response ) defer.returnValue((200, response)) @defer.inlineCallbacks def received_edu(self, origin, edu_type, content): received_edus_counter.inc() yield self.registry.on_edu(edu_type, origin, content) @defer.inlineCallbacks @log_function def on_context_state_request(self, origin, room_id, event_id): if not event_id: raise NotImplementedError("Specify an event") origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) in_room = yield self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") # we grab the linearizer to protect ourselves from servers which hammer # us. In theory we might already have the response to this query # in the cache so we could return it without waiting for the linearizer # - but that's non-trivial to get right, and anyway somewhat defeats # the point of the linearizer. with (yield self._server_linearizer.queue((origin, room_id))): resp = yield self._state_resp_cache.wrap( (room_id, event_id), self._on_context_state_request_compute, room_id, event_id, ) defer.returnValue((200, resp)) @defer.inlineCallbacks def on_state_ids_request(self, origin, room_id, event_id): if not event_id: raise NotImplementedError("Specify an event") origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) in_room = yield self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") state_ids = yield self.handler.get_state_ids_for_pdu( room_id, event_id, ) auth_chain_ids = yield self.store.get_auth_chain_ids(state_ids) defer.returnValue((200, { "pdu_ids": state_ids, "auth_chain_ids": auth_chain_ids, })) @defer.inlineCallbacks def _on_context_state_request_compute(self, room_id, event_id): pdus = yield self.handler.get_state_for_pdu( room_id, event_id, ) auth_chain = yield self.store.get_auth_chain( [pdu.event_id for pdu in pdus] ) for event in auth_chain: # We sign these again because there was a bug where we # incorrectly signed things the first time round if self.hs.is_mine_id(event.event_id): event.signatures.update( compute_event_signature( event.get_pdu_json(), self.hs.hostname, self.hs.config.signing_key[0] ) ) defer.returnValue({ "pdus": [pdu.get_pdu_json() for pdu in pdus], "auth_chain": [pdu.get_pdu_json() for pdu in auth_chain], }) @defer.inlineCallbacks @log_function def on_pdu_request(self, origin, event_id): pdu = yield self.handler.get_persisted_pdu(origin, event_id) if pdu: defer.returnValue( (200, self._transaction_from_pdus([pdu]).get_dict()) ) else: defer.returnValue((404, "")) @defer.inlineCallbacks def on_query_request(self, query_type, args): received_queries_counter.labels(query_type).inc() resp = yield self.registry.on_query(query_type, args) defer.returnValue((200, resp)) @defer.inlineCallbacks def on_make_join_request(self, origin, room_id, user_id, supported_versions): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) room_version = yield self.store.get_room_version(room_id) if room_version not in supported_versions: logger.warn("Room version %s not in %s", room_version, supported_versions) raise IncompatibleRoomVersionError(room_version=room_version) pdu = yield self.handler.on_make_join_request(room_id, user_id) time_now = self._clock.time_msec() defer.returnValue({ "event": pdu.get_pdu_json(time_now), "room_version": room_version, }) @defer.inlineCallbacks def on_invite_request(self, origin, content, room_version): if room_version not in KNOWN_ROOM_VERSIONS: raise SynapseError( 400, "Homeserver does not support this room version", Codes.UNSUPPORTED_ROOM_VERSION, ) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(content, format_ver) origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, pdu.room_id) ret_pdu = yield self.handler.on_invite_request(origin, pdu) time_now = self._clock.time_msec() defer.returnValue({"event": ret_pdu.get_pdu_json(time_now)}) @defer.inlineCallbacks def on_send_join_request(self, origin, content, room_id): logger.debug("on_send_join_request: content: %s", content) room_version = yield self.store.get_room_version(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(content, format_ver) origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_join_request: pdu sigs: %s", pdu.signatures) res_pdus = yield self.handler.on_send_join_request(origin, pdu) time_now = self._clock.time_msec() defer.returnValue((200, { "state": [p.get_pdu_json(time_now) for p in res_pdus["state"]], "auth_chain": [ p.get_pdu_json(time_now) for p in res_pdus["auth_chain"] ], })) @defer.inlineCallbacks def on_make_leave_request(self, origin, room_id, user_id): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) pdu = yield self.handler.on_make_leave_request(room_id, user_id) room_version = yield self.store.get_room_version(room_id) time_now = self._clock.time_msec() defer.returnValue({ "event": pdu.get_pdu_json(time_now), "room_version": room_version, }) @defer.inlineCallbacks def on_send_leave_request(self, origin, content, room_id): logger.debug("on_send_leave_request: content: %s", content) room_version = yield self.store.get_room_version(room_id) format_ver = room_version_to_event_format(room_version) pdu = event_from_pdu_json(content, format_ver) origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, pdu.room_id) logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures) yield self.handler.on_send_leave_request(origin, pdu) defer.returnValue((200, {})) @defer.inlineCallbacks def on_event_auth(self, origin, room_id, event_id): with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) time_now = self._clock.time_msec() auth_pdus = yield self.handler.on_event_auth(event_id) res = { "auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus], } defer.returnValue((200, res)) @defer.inlineCallbacks def on_query_auth_request(self, origin, content, room_id, event_id): """ Content is a dict with keys:: auth_chain (list): A list of events that give the auth chain. missing (list): A list of event_ids indicating what the other side (`origin`) think we're missing. rejects (dict): A mapping from event_id to a 2-tuple of reason string and a proof (or None) of why the event was rejected. The keys of this dict give the list of events the `origin` has rejected. Args: origin (str) content (dict) event_id (str) Returns: Deferred: Results in `dict` with the same format as `content` """ with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) room_version = yield self.store.get_room_version(room_id) format_ver = room_version_to_event_format(room_version) auth_chain = [ event_from_pdu_json(e, format_ver) for e in content["auth_chain"] ] signed_auth = yield self._check_sigs_and_hash_and_fetch( origin, auth_chain, outlier=True, room_version=room_version, ) ret = yield self.handler.on_query_auth( origin, event_id, room_id, signed_auth, content.get("rejects", []), content.get("missing", []), ) time_now = self._clock.time_msec() send_content = { "auth_chain": [ e.get_pdu_json(time_now) for e in ret["auth_chain"] ], "rejects": ret.get("rejects", []), "missing": ret.get("missing", []), } defer.returnValue( (200, send_content) ) @log_function def on_query_client_keys(self, origin, content): return self.on_query_request("client_keys", content) def on_query_user_devices(self, origin, user_id): return self.on_query_request("user_devices", user_id) @defer.inlineCallbacks @log_function def on_claim_client_keys(self, origin, content): query = [] for user_id, device_keys in content.get("one_time_keys", {}).items(): for device_id, algorithm in device_keys.items(): query.append((user_id, device_id, algorithm)) results = yield self.store.claim_e2e_one_time_keys(query) json_result = {} for user_id, device_keys in results.items(): for device_id, keys in device_keys.items(): for key_id, json_bytes in keys.items(): json_result.setdefault(user_id, {})[device_id] = { key_id: json.loads(json_bytes) } logger.info( "Claimed one-time-keys: %s", ",".join(( "%s for %s:%s" % (key_id, user_id, device_id) for user_id, user_keys in iteritems(json_result) for device_id, device_keys in iteritems(user_keys) for key_id, _ in iteritems(device_keys) )), ) defer.returnValue({"one_time_keys": json_result}) @defer.inlineCallbacks @log_function def on_get_missing_events(self, origin, room_id, earliest_events, latest_events, limit): with (yield self._server_linearizer.queue((origin, room_id))): origin_host, _ = parse_server_name(origin) yield self.check_server_matches_acl(origin_host, room_id) logger.info( "on_get_missing_events: earliest_events: %r, latest_events: %r," " limit: %d", earliest_events, latest_events, limit, ) missing_events = yield self.handler.on_get_missing_events( origin, room_id, earliest_events, latest_events, limit, ) if len(missing_events) < 5: logger.info( "Returning %d events: %r", len(missing_events), missing_events ) else: logger.info("Returning %d events", len(missing_events)) time_now = self._clock.time_msec() defer.returnValue({ "events": [ev.get_pdu_json(time_now) for ev in missing_events], }) @log_function def on_openid_userinfo(self, token): ts_now_ms = self._clock.time_msec() return self.store.get_user_id_for_open_id_token(token, ts_now_ms) def _transaction_from_pdus(self, pdu_list): """Returns a new Transaction containing the given PDUs suitable for transmission. """ time_now = self._clock.time_msec() pdus = [p.get_pdu_json(time_now) for p in pdu_list] return Transaction( origin=self.server_name, pdus=pdus, origin_server_ts=int(time_now), destination=None, ) @defer.inlineCallbacks def _handle_received_pdu(self, origin, pdu): """ Process a PDU received in a federation /send/ transaction. If the event is invalid, then this method throws a FederationError. (The error will then be logged and sent back to the sender (which probably won't do anything with it), and other events in the transaction will be processed as normal). It is likely that we'll then receive other events which refer to this rejected_event in their prev_events, etc. When that happens, we'll attempt to fetch the rejected event again, which will presumably fail, so those second-generation events will also get rejected. Eventually, we get to the point where there are more than 10 events between any new events and the original rejected event. Since we only try to backfill 10 events deep on received pdu, we then accept the new event, possibly introducing a discontinuity in the DAG, with new forward extremities, so normal service is approximately returned, until we try to backfill across the discontinuity. Args: origin (str): server which sent the pdu pdu (FrozenEvent): received pdu Returns (Deferred): completes with None Raises: FederationError if the signatures / hash do not match, or if the event was unacceptable for any other reason (eg, too large, too many prev_events, couldn't find the prev_events) """ # check that it's actually being sent from a valid destination to # workaround bug #1753 in 0.18.5 and 0.18.6 if origin != get_domain_from_id(pdu.sender): # We continue to accept join events from any server; this is # necessary for the federation join dance to work correctly. # (When we join over federation, the "helper" server is # responsible for sending out the join event, rather than the # origin. See bug #1893. This is also true for some third party # invites). if not ( pdu.type == 'm.room.member' and pdu.content and pdu.content.get("membership", None) in ( Membership.JOIN, Membership.INVITE, ) ): logger.info( "Discarding PDU %s from invalid origin %s", pdu.event_id, origin ) return else: logger.info( "Accepting join PDU %s from %s", pdu.event_id, origin ) # We've already checked that we know the room version by this point room_version = yield self.store.get_room_version(pdu.room_id) # Check signature. try: pdu = yield self._check_sigs_and_hash(room_version, pdu) except SynapseError as e: raise FederationError( "ERROR", e.code, e.msg, affected=pdu.event_id, ) yield self.handler.on_receive_pdu( origin, pdu, sent_to_us_directly=True, ) def __str__(self): return "<ReplicationLayer(%s)>" % self.server_name @defer.inlineCallbacks def exchange_third_party_invite( self, sender_user_id, target_user_id, room_id, signed, ): ret = yield self.handler.exchange_third_party_invite( sender_user_id, target_user_id, room_id, signed, ) defer.returnValue(ret) @defer.inlineCallbacks def on_exchange_third_party_invite_request(self, origin, room_id, event_dict): ret = yield self.handler.on_exchange_third_party_invite_request( origin, room_id, event_dict ) defer.returnValue(ret) @defer.inlineCallbacks def check_server_matches_acl(self, server_name, room_id): """Check if the given server is allowed by the server ACLs in the room Args: server_name (str): name of server, *without any port part* room_id (str): ID of the room to check Raises: AuthError if the server does not match the ACL """ state_ids = yield self.store.get_current_state_ids(room_id) acl_event_id = state_ids.get((EventTypes.ServerACL, "")) if not acl_event_id: return acl_event = yield self.store.get_event(acl_event_id) if server_matches_acl_event(server_name, acl_event): return raise AuthError(code=403, msg="Server is banned from room")
class ReplicationSendEventRestServlet(RestServlet): """Handles events newly created on workers, including persisting and notifying. The API looks like: POST /_synapse/replication/send_event/:event_id { "event": { .. serialized event .. }, "internal_metadata": { .. serialized internal_metadata .. }, "rejected_reason": .., // The event.rejected_reason field "context": { .. serialized event context .. }, "requester": { .. serialized requester .. }, "ratelimit": true, "extra_users": [], } """ PATTERNS = [ re.compile("^/_synapse/replication/send_event/(?P<event_id>[^/]+)$") ] def __init__(self, hs): super(ReplicationSendEventRestServlet, self).__init__() self.event_creation_handler = hs.get_event_creation_handler() self.store = hs.get_datastore() self.clock = hs.get_clock() # The responses are tiny, so we may as well cache them for a while self.response_cache = ResponseCache(hs, "send_event", timeout_ms=30 * 60 * 1000) def on_PUT(self, request, event_id): return self.response_cache.wrap(event_id, self._handle_request, request) @defer.inlineCallbacks def _handle_request(self, request): with Measure(self.clock, "repl_send_event_parse"): content = parse_json_object_from_request(request) event_dict = content["event"] internal_metadata = content["internal_metadata"] rejected_reason = content["rejected_reason"] event = FrozenEvent(event_dict, internal_metadata, rejected_reason) requester = Requester.deserialize(self.store, content["requester"]) context = yield EventContext.deserialize(self.store, content["context"]) ratelimit = content["ratelimit"] extra_users = [ UserID.from_string(u) for u in content["extra_users"] ] if requester.user: request.authenticated_entity = requester.user.to_string() logger.info( "Got event to send with ID: %s into room: %s", event.event_id, event.room_id, ) yield self.event_creation_handler.persist_and_notify_client_event( requester, event, context, ratelimit=ratelimit, extra_users=extra_users, ) defer.returnValue((200, {}))
class ApplicationServiceApi(SimpleHttpClient): """This class manages HS -> AS communications, including querying and pushing. """ def __init__(self, hs): super(ApplicationServiceApi, self).__init__(hs) self.clock = hs.get_clock() self.protocol_meta_cache = ResponseCache(hs, "as_protocol_meta", timeout_ms=HOUR_IN_MS) @defer.inlineCallbacks def query_user(self, service, user_id): if service.url is None: defer.returnValue(False) uri = service.url + ("/users/%s" % urllib.parse.quote(user_id)) response = None try: response = yield self.get_json(uri, { "access_token": service.hs_token }) if response is not None: # just an empty json object defer.returnValue(True) except CodeMessageException as e: if e.code == 404: defer.returnValue(False) return logger.warning("query_user to %s received %s", uri, e.code) except Exception as ex: logger.warning("query_user to %s threw exception %s", uri, ex) defer.returnValue(False) @defer.inlineCallbacks def query_alias(self, service, alias): if service.url is None: defer.returnValue(False) uri = service.url + ("/rooms/%s" % urllib.parse.quote(alias)) response = None try: response = yield self.get_json(uri, { "access_token": service.hs_token }) if response is not None: # just an empty json object defer.returnValue(True) except CodeMessageException as e: logger.warning("query_alias to %s received %s", uri, e.code) if e.code == 404: defer.returnValue(False) return except Exception as ex: logger.warning("query_alias to %s threw exception %s", uri, ex) defer.returnValue(False) @defer.inlineCallbacks def query_3pe(self, service, kind, protocol, fields): if kind == ThirdPartyEntityKind.USER: required_field = "userid" elif kind == ThirdPartyEntityKind.LOCATION: required_field = "alias" else: raise ValueError( "Unrecognised 'kind' argument %r to query_3pe()", kind ) if service.url is None: defer.returnValue([]) uri = "%s%s/thirdparty/%s/%s" % ( service.url, APP_SERVICE_PREFIX, kind, urllib.parse.quote(protocol) ) try: response = yield self.get_json(uri, fields) if not isinstance(response, list): logger.warning( "query_3pe to %s returned an invalid response %r", uri, response ) defer.returnValue([]) ret = [] for r in response: if _is_valid_3pe_result(r, field=required_field): ret.append(r) else: logger.warning( "query_3pe to %s returned an invalid result %r", uri, r ) defer.returnValue(ret) except Exception as ex: logger.warning("query_3pe to %s threw exception %s", uri, ex) defer.returnValue([]) def get_3pe_protocol(self, service, protocol): if service.url is None: defer.returnValue({}) @defer.inlineCallbacks def _get(): uri = "%s%s/thirdparty/protocol/%s" % ( service.url, APP_SERVICE_PREFIX, urllib.parse.quote(protocol) ) try: info = yield self.get_json(uri, {}) if not _is_valid_3pe_metadata(info): logger.warning("query_3pe_protocol to %s did not return a" " valid result", uri) defer.returnValue(None) for instance in info.get("instances", []): network_id = instance.get("network_id", None) if network_id is not None: instance["instance_id"] = ThirdPartyInstanceID( service.id, network_id, ).to_string() defer.returnValue(info) except Exception as ex: logger.warning("query_3pe_protocol to %s threw exception %s", uri, ex) defer.returnValue(None) key = (service.id, protocol) return self.protocol_meta_cache.wrap(key, _get) @defer.inlineCallbacks def push_bulk(self, service, events, txn_id=None): if service.url is None: defer.returnValue(True) events = self._serialize(events) if txn_id is None: logger.warning("push_bulk: Missing txn ID sending events to %s", service.url) txn_id = str(0) txn_id = str(txn_id) uri = service.url + ("/transactions/%s" % urllib.parse.quote(txn_id)) try: yield self.put_json( uri=uri, json_body={ "events": events }, args={ "access_token": service.hs_token }) sent_transactions_counter.labels(service.id).inc() sent_events_counter.labels(service.id).inc(len(events)) defer.returnValue(True) return except CodeMessageException as e: logger.warning("push_bulk to %s received %s", uri, e.code) except Exception as ex: logger.warning("push_bulk to %s threw exception %s", uri, ex) failed_transactions_counter.labels(service.id).inc() defer.returnValue(False) def _serialize(self, events): time_now = self.clock.time_msec() return [ serialize_event(e, time_now, as_client_event=True) for e in events ]
class ReplicationSendEventRestServlet(RestServlet): """Handles events newly created on workers, including persisting and notifying. The API looks like: POST /_synapse/replication/send_event/:event_id { "event": { .. serialized event .. }, "internal_metadata": { .. serialized internal_metadata .. }, "rejected_reason": .., // The event.rejected_reason field "context": { .. serialized event context .. }, "requester": { .. serialized requester .. }, "ratelimit": true, "extra_users": [], } """ PATTERNS = [re.compile("^/_synapse/replication/send_event/(?P<event_id>[^/]+)$")] def __init__(self, hs): super(ReplicationSendEventRestServlet, self).__init__() self.event_creation_handler = hs.get_event_creation_handler() self.store = hs.get_datastore() self.clock = hs.get_clock() # The responses are tiny, so we may as well cache them for a while self.response_cache = ResponseCache(hs, "send_event", timeout_ms=30 * 60 * 1000) def on_PUT(self, request, event_id): return self.response_cache.wrap( event_id, self._handle_request, request ) @defer.inlineCallbacks def _handle_request(self, request): with Measure(self.clock, "repl_send_event_parse"): content = parse_json_object_from_request(request) event_dict = content["event"] internal_metadata = content["internal_metadata"] rejected_reason = content["rejected_reason"] event = FrozenEvent(event_dict, internal_metadata, rejected_reason) requester = Requester.deserialize(self.store, content["requester"]) context = yield EventContext.deserialize(self.store, content["context"]) ratelimit = content["ratelimit"] extra_users = [UserID.from_string(u) for u in content["extra_users"]] if requester.user: request.authenticated_entity = requester.user.to_string() logger.info( "Got event to send with ID: %s into room: %s", event.event_id, event.room_id, ) yield self.event_creation_handler.persist_and_notify_client_event( requester, event, context, ratelimit=ratelimit, extra_users=extra_users, ) defer.returnValue((200, {}))