async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict: time_now = self._clock.time_msec() try: return await self.transport_layer.send_leave_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) except HttpResponseException as e: # If an error is received that is due to an unrecognised endpoint, # fallback to the v1 endpoint. Otherwise consider it a legitmate error # and raise. if not self._is_unknown_endpoint(e): raise logger.debug( "Couldn't send_leave with the v2 API, falling back to the v1 API") resp = await self.transport_layer.send_leave_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) # We expect the v1 API to respond with [200, content], so we only return the # content. return resp[1]
async def _do_send_join(self, room_version: RoomVersion, destination: str, pdu: EventBase) -> SendJoinResponse: time_now = self._clock.time_msec() try: return await self.transport_layer.send_join_v2( room_version=room_version, destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) except HttpResponseException as e: # If an error is received that is due to an unrecognised endpoint, # fallback to the v1 endpoint. Otherwise consider it a legitmate error # and raise. if not self._is_unknown_endpoint(e): raise logger.debug( "Couldn't send_join with the v2 API, falling back to the v1 API") return await self.transport_layer.send_join_v1( room_version=room_version, destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), )
async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict: time_now = self._clock.time_msec() try: return await self.transport_layer.send_leave_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) except HttpResponseException as e: if e.code in [400, 404]: err = e.to_synapse_error() # If we receive an error response that isn't a generic error, or an # unrecognised endpoint error, we assume that the remote understands # the v2 invite API and this is a legitimate error. if err.errcode not in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: raise err else: raise e.to_synapse_error() logger.debug("Couldn't send_leave with the v2 API, falling back to the v1 API") resp = await self.transport_layer.send_leave_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) # We expect the v1 API to respond with [200, content], so we only return the # content. return resp[1]
async def _do_send_invite(self, destination: str, pdu: EventBase, room_version: RoomVersion) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. Returns: The event as a dict as returned by the remote server """ time_now = self._clock.time_msec() try: content = await self.transport_layer.send_invite_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content={ "event": pdu.get_pdu_json(time_now), "room_version": room_version.identifier, "invite_room_state": pdu.unsigned.get("invite_room_state", []), }, ) return content except HttpResponseException as e: if e.code in [400, 404]: err = e.to_synapse_error() # If we receive an error response that isn't a generic error, we # assume that the remote understands the v2 invite API and this # is a legitimate error. if err.errcode != Codes.UNKNOWN: raise err # Otherwise, we assume that the remote server doesn't understand # the v2 invite API. That's ok provided the room uses old-style event # IDs. if room_version.event_format != EventFormatVersions.V1: raise SynapseError( 400, "User's homeserver does not support this room version", Codes.UNSUPPORTED_ROOM_VERSION, ) elif e.code == 403: raise e.to_synapse_error() else: raise # Didn't work, try v1 API. # Note the v1 API returns a tuple of `(200, content)` _, content = await self.transport_layer.send_invite_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) return content
async def _do_send_invite(self, destination: str, pdu: EventBase, room_version: RoomVersion) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. Returns: The event as a dict as returned by the remote server Raises: SynapseError: if the remote server returns an error or if the server only supports the v1 endpoint and a room version other than "1" or "2" is requested. """ time_now = self._clock.time_msec() try: return await self.transport_layer.send_invite_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content={ "event": pdu.get_pdu_json(time_now), "room_version": room_version.identifier, "invite_room_state": pdu.unsigned.get("invite_room_state", []), }, ) except HttpResponseException as e: # If an error is received that is due to an unrecognised endpoint, # fallback to the v1 endpoint if the room uses old-style event IDs. # Otherwise consider it a legitmate error and raise. err = e.to_synapse_error() if self._is_unknown_endpoint(e, err): if room_version.event_format != EventFormatVersions.V1: raise SynapseError( 400, "User's homeserver does not support this room version", Codes.UNSUPPORTED_ROOM_VERSION, ) else: raise err # Didn't work, try v1 API. # Note the v1 API returns a tuple of `(200, content)` _, content = await self.transport_layer.send_invite_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) return content
def check_event_content_hash(event: EventBase, hash_algorithm: Hasher = hashlib.sha256) -> bool: """Check whether the hash for this PDU matches the contents""" name, expected_hash = compute_content_hash(event.get_pdu_json(), hash_algorithm) logger.debug( "Verifying content hash on %s (expecting: %s)", event.event_id, encode_base64(expected_hash), ) # some malformed events lack a 'hashes'. Protect against it being missing # or a weird type by basically treating it the same as an unhashed event. hashes = event.get("hashes") # nb it might be a frozendict or a dict if not isinstance(hashes, collections.abc.Mapping): raise SynapseError(400, "Malformed 'hashes': %s" % (type(hashes), ), Codes.UNAUTHORIZED) if name not in hashes: raise SynapseError( 400, "Algorithm %s not in hashes %s" % (name, list(hashes)), Codes.UNAUTHORIZED, ) message_hash_base64 = hashes[name] try: message_hash_bytes = decode_base64(message_hash_base64) except Exception: raise SynapseError(400, "Invalid base64: %s" % (message_hash_base64, ), Codes.UNAUTHORIZED) return message_hash_bytes == expected_hash
async def _serialize_payload( # type: ignore[override] event_id: str, store: "DataStore", event: EventBase, context: EventContext, requester: Requester, ratelimit: bool, extra_users: List[UserID], ) -> JsonDict: """ Args: event_id store requester event context ratelimit extra_users: Any extra users to notify about event """ serialized_context = await context.serialize(event, store) payload = { "event": event.get_pdu_json(), "room_version": event.room_version.identifier, "event_format_version": event.format_version, "internal_metadata": event.internal_metadata.get_dict(), "outlier": event.internal_metadata.is_outlier(), "rejected_reason": event.rejected_reason, "context": serialized_context, "requester": requester.serialize(), "ratelimit": ratelimit, "extra_users": [u.to_string() for u in extra_users], } return payload
async def _check_sigs_and_hash(self, room_version: RoomVersion, pdu: EventBase) -> EventBase: """Checks that event is correctly signed by the sending server. Args: room_version: The room version of the PDU pdu: the event to be checked Returns: * the original event if the checks pass * a redacted version of the event (if the signature matched but the hash did not) * throws a SynapseError if the signature check failed.""" try: await _check_sigs_on_pdu(self.keyring, room_version, pdu) except SynapseError as e: logger.warning( "Signature check failed for %s: %s", pdu.event_id, e, ) raise if not check_event_content_hash(pdu): # let's try to distinguish between failures because the event was # redacted (which are somewhat expected) vs actual ball-tampering # incidents. # # This is just a heuristic, so we just assume that if the keys are # about the same between the redacted and received events, then the # received event was probably a redacted copy (but we then use our # *actual* redacted copy to be on the safe side.) redacted_event = prune_event(pdu) if set(redacted_event.keys()) == set(pdu.keys()) and set( redacted_event.content.keys()) == set(pdu.content.keys()): logger.info( "Event %s seems to have been redacted; using our redacted copy", pdu.event_id, ) else: logger.warning( "Event %s content has been tampered, redacting", pdu.event_id, ) return redacted_event result = await self.spam_checker.check_event_for_spam(pdu) if result: logger.warning( "Event contains spam, redacting %s: %s", pdu.event_id, pdu.get_pdu_json(), ) return prune_event(pdu) return pdu
def _check_size_limits(event: EventBase) -> None: if len(event.user_id) > 255: raise EventSizeError("'user_id' too large") if len(event.room_id) > 255: raise EventSizeError("'room_id' too large") if event.is_state() and len(event.state_key) > 255: raise EventSizeError("'state_key' too large") if len(event.type) > 255: raise EventSizeError("'type' too large") if len(event.event_id) > 255: raise EventSizeError("'event_id' too large") if len(encode_canonical_json(event.get_pdu_json())) > MAX_PDU_SIZE: raise EventSizeError("event too large")
def _check_size_limits(event: EventBase) -> None: def too_big(field): raise EventSizeError("%s too large" % (field, )) if len(event.user_id) > 255: too_big("user_id") if len(event.room_id) > 255: too_big("room_id") if event.is_state() and len(event.state_key) > 255: too_big("state_key") if len(event.type) > 255: too_big("type") if len(event.event_id) > 255: too_big("event_id") if len(encode_canonical_json(event.get_pdu_json())) > 65536: too_big("event")
def from_event( server_name: str, event: EventBase, minimum_valid_until_ms: int, ) -> "VerifyJsonRequest": """Create a VerifyJsonRequest to verify all signatures on an event object for the given server. """ key_ids = list(event.signatures.get(server_name, [])) return VerifyJsonRequest( server_name, # We defer creating the redacted json object, as it uses a lot more # memory than the Event object itself. lambda: prune_event_dict(event.room_version, event.get_pdu_json()), minimum_valid_until_ms, key_ids=key_ids, )
def callback(_, pdu: EventBase): with PreserveLoggingContext(ctx): if not check_event_content_hash(pdu): # let's try to distinguish between failures because the event was # redacted (which are somewhat expected) vs actual ball-tampering # incidents. # # This is just a heuristic, so we just assume that if the keys are # about the same between the redacted and received events, then the # received event was probably a redacted copy (but we then use our # *actual* redacted copy to be on the safe side.) redacted_event = prune_event(pdu) if set(redacted_event.keys()) == set(pdu.keys()) and set( redacted_event.content.keys()) == set( pdu.content.keys()): logger.info( "Event %s seems to have been redacted; using our redacted " "copy", pdu.event_id, ) else: logger.warning( "Event %s content has been tampered, redacting", pdu.event_id, ) return redacted_event result = yield defer.ensureDeferred( self.spam_checker.check_event_for_spam(pdu)) if result: logger.warning( "Event contains spam, redacting %s: %s", pdu.event_id, pdu.get_pdu_json(), ) return prune_event(pdu) return pdu