Add unstable federation API for MSC4370 GET /extremities (#19314)

MSC (recommended reading):
https://github.com/matrix-org/matrix-spec-proposals/pull/4370

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: turt2live <1190097+turt2live@users.noreply.github.com>
Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>
This commit is contained in:
Travis Ralston
2026-03-05 11:30:52 -07:00
committed by GitHub
parent 699a898b30
commit 6e21f9c12b
6 changed files with 181 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
Add experimental support for the [MSC4370](https://github.com/matrix-org/matrix-spec-proposals/pull/4370) Federation API `GET /extremities` endpoint.
+5
View File
@@ -3,6 +3,7 @@
#
# Copyright 2021 The Matrix.org Foundation C.I.C.
# Copyright (C) 2023 New Vector, Ltd
# Copyright (C) 2025 Element Creations 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
@@ -517,6 +518,10 @@ class ExperimentalConfig(Config):
"msc4108_delegation_endpoint", None
)
# MSC4370: Get extremities federation endpoint
# See https://github.com/element-hq/synapse/issues/19524
self.msc4370_enabled = experimental.get("msc4370_enabled", False)
auth_delegated = self.msc3861.enabled or (
config.get("matrix_authentication_service") or {}
).get("enabled", False)
+13
View File
@@ -4,6 +4,7 @@
# Copyright 2019-2021 Matrix.org Federation C.I.C
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright (C) 2023 New Vector, Ltd
# Copyright (C) 2025 Element Creations 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
@@ -683,6 +684,18 @@ class FederationServer(FederationBase):
resp = await self.registry.on_query(query_type, args)
return 200, resp
async def on_get_extremities_request(self, origin: str, room_id: str) -> JsonDict:
# Assert host in room first to hide contents of the ACL from the caller
await self._event_auth_handler.assert_host_in_room(room_id, origin)
origin_host, _ = parse_server_name(origin)
await self.check_server_matches_acl(origin_host, room_id)
extremities = await self.store.get_forward_extremities_for_room(room_id)
prev_event_ids = [event_id for event_id, _, _, _ in extremities]
if len(prev_event_ids) == 0:
raise SynapseError(500, "Room has no forward extremities")
return {"prev_events": prev_event_ids}
async def on_make_join_request(
self, origin: str, room_id: str, user_id: str, supported_versions: list[str]
) -> dict[str, Any]:
@@ -4,6 +4,7 @@
# Copyright 2020 Sorunome
# Copyright 2014-2021 The Matrix.org Foundation C.I.C.
# Copyright (C) 2023 New Vector, Ltd
# Copyright (C) 2025 Element Creations 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
@@ -33,6 +34,7 @@ from synapse.federation.transport.server.federation import (
FederationMediaDownloadServlet,
FederationMediaThumbnailServlet,
FederationUnstableClientKeysClaimServlet,
FederationUnstableGetExtremitiesServlet,
)
from synapse.http.server import HttpServer, JsonResource
from synapse.http.servlet import (
@@ -326,6 +328,12 @@ def register_servlets(
if not hs.config.media.can_load_media_repo:
continue
if (
servletclass == FederationUnstableGetExtremitiesServlet
and not hs.config.experimental.msc4370_enabled
):
continue
servletclass(
hs=hs,
authenticator=authenticator,
@@ -1,8 +1,9 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright 2021 The Matrix.org Foundation C.I.C.
# Copyright 2021 The Matrix.org Foundation C.I.C.
# Copyright (C) 2023 New Vector, Ltd
# Copyright (C) 2025 Element Creations 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
@@ -273,6 +274,22 @@ class FederationQueryServlet(BaseFederationServerServlet):
return await self.handler.on_query_request(query_type, args)
class FederationUnstableGetExtremitiesServlet(BaseFederationServerServlet):
PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc4370"
PATH = "/extremities/(?P<room_id>[^/]*)"
CATEGORY = "Federation requests"
async def on_GET(
self,
origin: str,
content: Literal[None],
query: dict[bytes, list[bytes]],
room_id: str,
) -> tuple[int, JsonDict]:
result = await self.handler.on_get_extremities_request(origin, room_id)
return 200, result
class FederationMakeJoinServlet(BaseFederationServerServlet):
PATH = "/make_join/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)"
CATEGORY = "Federation requests"
@@ -884,6 +901,7 @@ FEDERATION_SERVLET_CLASSES: tuple[type[BaseFederationServlet], ...] = (
FederationBackfillServlet,
FederationTimestampLookupServlet,
FederationQueryServlet,
FederationUnstableGetExtremitiesServlet,
FederationMakeJoinServlet,
FederationMakeLeaveServlet,
FederationEventServlet,
+135
View File
@@ -324,6 +324,141 @@ class StateQueryTests(unittest.FederatingHomeserverTestCase):
self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
class UnstableGetExtremitiesTests(unittest.FederatingHomeserverTestCase):
servlets = [
admin.register_servlets,
room.register_servlets,
login.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
super().prepare(reactor, clock, hs)
self._storage_controllers = hs.get_storage_controllers()
def _make_endpoint_path(self, room_id: str) -> str:
return f"/_matrix/federation/unstable/org.matrix.msc4370/extremities/{room_id}"
def _remote_join(self, room_id: str, room_version: str) -> str:
# Note: other tests ensure the called endpoints in this function return useful
# and proper data.
# make_join first
joining_user = "@misspiggy:" + self.OTHER_SERVER_NAME
channel = self.make_signed_federation_request(
"GET",
f"/_matrix/federation/v1/make_join/{room_id}/{joining_user}?ver={room_version}",
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
join_result = channel.json_body
# Sign/populate the join
join_event_dict = join_result["event"]
self.add_hashes_and_signatures_from_other_server(
join_event_dict,
KNOWN_ROOM_VERSIONS[room_version],
)
if room_version in ["1", "2"]:
add_hashes_and_signatures(
KNOWN_ROOM_VERSIONS[room_version],
join_event_dict,
signature_name=self.hs.hostname,
signing_key=self.hs.signing_key,
)
# Send the join
channel = self.make_signed_federation_request(
"PUT",
f"/_matrix/federation/v2/send_join/{room_id}/x",
content=join_event_dict,
)
# Check that things went okay so the test doesn't become a total train wreck
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
r = self.get_success(self._storage_controllers.state.get_current_state(room_id))
self.assertEqual(r[("m.room.member", joining_user)].membership, "join")
return r[("m.room.member", joining_user)].event_id
def _test_get_extremities_common(self, room_version: str) -> None:
# Create a room to test with
creator_user_id = self.register_user("kermit", "test")
tok = self.login("kermit", "test")
room_id = self.helper.create_room_as(
room_creator=creator_user_id,
tok=tok,
room_version=room_version,
extra_content={
# Public preset uses `shared` history visibility, but makes joins
# easier in our tests.
# https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3createroom
"preset": "public_chat"
},
)
# At this stage we should fail to get the extremities because we're not joined
# and therefore can't see the events (`shared` history visibility).
channel = self.make_signed_federation_request(
"GET", self._make_endpoint_path(room_id)
)
self.assertEqual(channel.code, HTTPStatus.FORBIDDEN, channel.json_body)
self.assertEqual(channel.json_body["error"], "Host not in room.")
self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
# Now join the room and try again
# Note: there should be just one extremity: the join
join_event_id = self._remote_join(room_id, room_version)
channel = self.make_signed_federation_request(
"GET", self._make_endpoint_path(room_id)
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
self.assertEqual(channel.json_body["prev_events"], [join_event_id])
# ACL the calling server and try again. This should cause an error getting extremities.
self.helper.send_state(
room_id,
"m.room.server_acl",
{
"allow": ["*"],
"allow_ip_literals": False,
"deny": [self.OTHER_SERVER_NAME],
},
tok=tok,
expect_code=HTTPStatus.OK,
)
channel = self.make_signed_federation_request(
"GET", self._make_endpoint_path(room_id)
)
self.assertEqual(channel.code, HTTPStatus.FORBIDDEN, channel.json_body)
self.assertEqual(channel.json_body["error"], "Server is banned from room")
self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
@parameterized.expand([(k,) for k in KNOWN_ROOM_VERSIONS.keys()])
@override_config(
{"use_frozen_dicts": True, "experimental_features": {"msc4370_enabled": True}}
)
def test_get_extremities_with_frozen_dicts(self, room_version: str) -> None:
"""Test GET /extremities with USE_FROZEN_DICTS=True"""
self._test_get_extremities_common(room_version)
@parameterized.expand([(k,) for k in KNOWN_ROOM_VERSIONS.keys()])
@override_config(
{"use_frozen_dicts": False, "experimental_features": {"msc4370_enabled": True}}
)
def test_get_extremities_without_frozen_dicts(self, room_version: str) -> None:
"""Test GET /extremities with USE_FROZEN_DICTS=False"""
self._test_get_extremities_common(room_version)
# note the lack of config-setting stuff on this test.
def test_get_extremities_unstable_not_enabled(self) -> None:
"""Test that GET /extremities returns M_UNRECOGNIZED when MSC4370 is not enabled"""
# We shouldn't even have to create a room - the endpoint should just fail.
channel = self.make_signed_federation_request(
"GET", self._make_endpoint_path("!room:example.org")
)
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND, channel.json_body)
self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED")
class SendJoinFederationTests(unittest.FederatingHomeserverTestCase):
servlets = [
admin.register_servlets,