mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-25 11:05:49 +03:00
23740eaa3d
During the migration the automated script to update the copyright headers accidentally got rid of some of the existing copyright lines. Reinstate them.
323 lines
12 KiB
Python
323 lines
12 KiB
Python
#
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright 2020 Matrix.org Federation C.I.C
|
|
# Copyright (C) 2023 New Vector, Ltd
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# See the GNU Affero General Public License for more details:
|
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
#
|
|
# Originally licensed under the Apache License, Version 2.0:
|
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# [This file includes modifications made by New Vector Limited]
|
|
#
|
|
#
|
|
from collections import OrderedDict
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from twisted.test.proto_helpers import MemoryReactor
|
|
|
|
from synapse.api.constants import EventTypes, JoinRules, Membership
|
|
from synapse.api.room_versions import RoomVersion, RoomVersions
|
|
from synapse.events import EventBase, builder
|
|
from synapse.events.snapshot import EventContext
|
|
from synapse.rest import admin
|
|
from synapse.rest.client import login, room
|
|
from synapse.server import HomeServer
|
|
from synapse.types import RoomAlias
|
|
from synapse.util import Clock
|
|
|
|
from tests.test_utils import event_injection
|
|
from tests.unittest import FederatingHomeserverTestCase, HomeserverTestCase
|
|
|
|
|
|
class KnockingStrippedStateEventHelperMixin(HomeserverTestCase):
|
|
def send_example_state_events_to_room(
|
|
self,
|
|
hs: "HomeServer",
|
|
room_id: str,
|
|
sender: str,
|
|
) -> OrderedDict:
|
|
"""Adds some state to a room. State events are those that should be sent to a knocking
|
|
user after they knock on the room, as well as some state that *shouldn't* be sent
|
|
to the knocking user.
|
|
|
|
Args:
|
|
hs: The homeserver of the sender.
|
|
room_id: The ID of the room to send state into.
|
|
sender: The ID of the user to send state as. Must be in the room.
|
|
|
|
Returns:
|
|
The OrderedDict of event types and content that a user is expected to see
|
|
after knocking on a room.
|
|
"""
|
|
# To set a canonical alias, we'll need to point an alias at the room first.
|
|
canonical_alias = "#fancy_alias:test"
|
|
self.get_success(
|
|
self.hs.get_datastores().main.create_room_alias_association(
|
|
RoomAlias.from_string(canonical_alias), room_id, ["test"]
|
|
)
|
|
)
|
|
|
|
# Send some state that we *don't* expect to be given to knocking users
|
|
self.get_success(
|
|
event_injection.inject_event(
|
|
hs,
|
|
room_version=RoomVersions.V7.identifier,
|
|
room_id=room_id,
|
|
sender=sender,
|
|
type="com.example.secret",
|
|
state_key="",
|
|
content={"secret": "password"},
|
|
)
|
|
)
|
|
|
|
# We use an OrderedDict here to ensure that the knock membership appears last.
|
|
# Note that order only matters when sending stripped state to clients, not federated
|
|
# homeservers.
|
|
room_state = OrderedDict(
|
|
[
|
|
# We need to set the room's join rules to allow knocking
|
|
(
|
|
EventTypes.JoinRules,
|
|
{"content": {"join_rule": JoinRules.KNOCK}, "state_key": ""},
|
|
),
|
|
# Below are state events that are to be stripped and sent to clients
|
|
(
|
|
EventTypes.Name,
|
|
{"content": {"name": "A cool room"}, "state_key": ""},
|
|
),
|
|
(
|
|
EventTypes.RoomAvatar,
|
|
{
|
|
"content": {
|
|
"info": {
|
|
"h": 398,
|
|
"mimetype": "image/jpeg",
|
|
"size": 31037,
|
|
"w": 394,
|
|
},
|
|
"url": "mxc://example.org/JWEIFJgwEIhweiWJE",
|
|
},
|
|
"state_key": "",
|
|
},
|
|
),
|
|
(
|
|
EventTypes.RoomEncryption,
|
|
{"content": {"algorithm": "m.megolm.v1.aes-sha2"}, "state_key": ""},
|
|
),
|
|
(
|
|
EventTypes.CanonicalAlias,
|
|
{
|
|
"content": {"alias": canonical_alias, "alt_aliases": []},
|
|
"state_key": "",
|
|
},
|
|
),
|
|
(
|
|
EventTypes.Topic,
|
|
{
|
|
"content": {
|
|
"topic": "A really cool room",
|
|
},
|
|
"state_key": "",
|
|
},
|
|
),
|
|
]
|
|
)
|
|
|
|
for event_type, event_dict in room_state.items():
|
|
event_content = event_dict["content"]
|
|
state_key = event_dict["state_key"]
|
|
|
|
self.get_success(
|
|
event_injection.inject_event(
|
|
hs,
|
|
room_version=RoomVersions.V7.identifier,
|
|
room_id=room_id,
|
|
sender=sender,
|
|
type=event_type,
|
|
state_key=state_key,
|
|
content=event_content,
|
|
)
|
|
)
|
|
|
|
# Finally, we expect to see the m.room.create event of the room as part of the
|
|
# stripped state. We don't need to inject this event though.
|
|
room_state[EventTypes.Create] = {
|
|
"content": {
|
|
"creator": sender,
|
|
"room_version": RoomVersions.V7.identifier,
|
|
},
|
|
"state_key": "",
|
|
}
|
|
|
|
return room_state
|
|
|
|
def check_knock_room_state_against_room_state(
|
|
self,
|
|
knock_room_state: List[Dict],
|
|
expected_room_state: Dict,
|
|
) -> None:
|
|
"""Test a list of stripped room state events received over federation against a
|
|
dict of expected state events.
|
|
|
|
Args:
|
|
knock_room_state: The list of room state that was received over federation.
|
|
expected_room_state: A dict containing the room state we expect to see in
|
|
`knock_room_state`.
|
|
"""
|
|
for event in knock_room_state:
|
|
event_type = event["type"]
|
|
|
|
# Check that this event type is one of those that we expected.
|
|
# Note: This will also check that no excess state was included
|
|
self.assertIn(event_type, expected_room_state)
|
|
|
|
# Check the state content matches
|
|
self.assertEqual(
|
|
expected_room_state[event_type]["content"], event["content"]
|
|
)
|
|
|
|
# Check the state key is correct
|
|
self.assertEqual(
|
|
expected_room_state[event_type]["state_key"], event["state_key"]
|
|
)
|
|
|
|
# Ensure the event has been stripped
|
|
self.assertNotIn("signatures", event)
|
|
|
|
# Pop once we've found and processed a state event
|
|
expected_room_state.pop(event_type)
|
|
|
|
# Check that all expected state events were accounted for
|
|
self.assertEqual(len(expected_room_state), 0)
|
|
|
|
|
|
class FederationKnockingTestCase(
|
|
FederatingHomeserverTestCase, KnockingStrippedStateEventHelperMixin
|
|
):
|
|
servlets = [
|
|
admin.register_servlets,
|
|
room.register_servlets,
|
|
login.register_servlets,
|
|
]
|
|
|
|
def prepare(
|
|
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
|
|
) -> None:
|
|
self.store = homeserver.get_datastores().main
|
|
|
|
# We're not going to be properly signing events as our remote homeserver is fake,
|
|
# therefore disable event signature checks.
|
|
# Note that these checks are not relevant to this test case.
|
|
|
|
# Have this homeserver auto-approve all event signature checking.
|
|
async def approve_all_signature_checking(
|
|
room_version: RoomVersion,
|
|
pdu: EventBase,
|
|
record_failure_callback: Any = None,
|
|
) -> EventBase:
|
|
return pdu
|
|
|
|
homeserver.get_federation_server()._check_sigs_and_hash = ( # type: ignore[method-assign]
|
|
approve_all_signature_checking
|
|
)
|
|
|
|
# Have this homeserver skip event auth checks. This is necessary due to
|
|
# event auth checks ensuring that events were signed by the sender's homeserver.
|
|
async def _check_event_auth(
|
|
origin: Optional[str], event: EventBase, context: EventContext
|
|
) -> None:
|
|
pass
|
|
|
|
homeserver.get_federation_event_handler()._check_event_auth = _check_event_auth # type: ignore[method-assign]
|
|
|
|
return super().prepare(reactor, clock, homeserver)
|
|
|
|
def test_room_state_returned_when_knocking(self) -> None:
|
|
"""
|
|
Tests that specific, stripped state events from a room are returned after
|
|
a remote homeserver successfully knocks on a local room.
|
|
"""
|
|
user_id = self.register_user("u1", "you the one")
|
|
user_token = self.login("u1", "you the one")
|
|
|
|
fake_knocking_user_id = "@user:other.example.com"
|
|
|
|
# Create a room with a room version that includes knocking
|
|
room_id = self.helper.create_room_as(
|
|
"u1",
|
|
is_public=False,
|
|
room_version=RoomVersions.V7.identifier,
|
|
tok=user_token,
|
|
)
|
|
|
|
# Update the join rules and add additional state to the room to check for later
|
|
expected_room_state = self.send_example_state_events_to_room(
|
|
self.hs, room_id, user_id
|
|
)
|
|
|
|
channel = self.make_signed_federation_request(
|
|
"GET",
|
|
"/_matrix/federation/v1/make_knock/%s/%s?ver=%s"
|
|
% (
|
|
room_id,
|
|
fake_knocking_user_id,
|
|
# Inform the remote that we support the room version of the room we're
|
|
# knocking on
|
|
RoomVersions.V7.identifier,
|
|
),
|
|
)
|
|
self.assertEqual(200, channel.code, channel.result)
|
|
|
|
# Note: We don't expect the knock membership event to be sent over federation as
|
|
# part of the stripped room state, as the knocking homeserver already has that
|
|
# event. It is only done for clients during /sync
|
|
|
|
# Extract the generated knock event json
|
|
knock_event = channel.json_body["event"]
|
|
|
|
# Check that the event has things we expect in it
|
|
self.assertEqual(knock_event["room_id"], room_id)
|
|
self.assertEqual(knock_event["sender"], fake_knocking_user_id)
|
|
self.assertEqual(knock_event["state_key"], fake_knocking_user_id)
|
|
self.assertEqual(knock_event["type"], EventTypes.Member)
|
|
self.assertEqual(knock_event["content"]["membership"], Membership.KNOCK)
|
|
|
|
# Turn the event json dict into a proper event.
|
|
# We won't sign it properly, but that's OK as we stub out event auth in `prepare`
|
|
signed_knock_event = builder.create_local_event_from_event_dict(
|
|
self.clock,
|
|
self.hs.hostname,
|
|
self.hs.signing_key,
|
|
room_version=RoomVersions.V7,
|
|
event_dict=knock_event,
|
|
)
|
|
|
|
# Convert our proper event back to json dict format
|
|
signed_knock_event_json = signed_knock_event.get_pdu_json(
|
|
self.clock.time_msec()
|
|
)
|
|
|
|
# Send the signed knock event into the room
|
|
channel = self.make_signed_federation_request(
|
|
"PUT",
|
|
"/_matrix/federation/v1/send_knock/%s/%s"
|
|
% (room_id, signed_knock_event.event_id),
|
|
signed_knock_event_json,
|
|
)
|
|
self.assertEqual(200, channel.code, channel.result)
|
|
|
|
# Check that we got the stripped room state in return
|
|
room_state_events = channel.json_body["knock_room_state"]
|
|
|
|
# Validate the stripped room state events
|
|
self.check_knock_room_state_against_room_state(
|
|
room_state_events, expected_room_state
|
|
)
|