Skip to content

Commit 6e21f9c

Browse files
Add unstable federation API for MSC4370 GET /extremities (#19314)
MSC (recommended reading): matrix-org/matrix-spec-proposals#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>
1 parent 699a898 commit 6e21f9c

File tree

6 files changed

+181
-1
lines changed

6 files changed

+181
-1
lines changed

changelog.d/19314.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add experimental support for the [MSC4370](https://github.com/matrix-org/matrix-spec-proposals/pull/4370) Federation API `GET /extremities` endpoint.

synapse/config/experimental.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
# Copyright 2021 The Matrix.org Foundation C.I.C.
55
# Copyright (C) 2023 New Vector, Ltd
6+
# Copyright (C) 2025 Element Creations Ltd
67
#
78
# This program is free software: you can redistribute it and/or modify
89
# it under the terms of the GNU Affero General Public License as
@@ -517,6 +518,10 @@ def read_config(
517518
"msc4108_delegation_endpoint", None
518519
)
519520

521+
# MSC4370: Get extremities federation endpoint
522+
# See https://github.com/element-hq/synapse/issues/19524
523+
self.msc4370_enabled = experimental.get("msc4370_enabled", False)
524+
520525
auth_delegated = self.msc3861.enabled or (
521526
config.get("matrix_authentication_service") or {}
522527
).get("enabled", False)

synapse/federation/federation_server.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Copyright 2019-2021 Matrix.org Federation C.I.C
55
# Copyright 2015, 2016 OpenMarket Ltd
66
# Copyright (C) 2023 New Vector, Ltd
7+
# Copyright (C) 2025 Element Creations Ltd
78
#
89
# This program is free software: you can redistribute it and/or modify
910
# it under the terms of the GNU Affero General Public License as
@@ -683,6 +684,18 @@ async def on_query_request(
683684
resp = await self.registry.on_query(query_type, args)
684685
return 200, resp
685686

687+
async def on_get_extremities_request(self, origin: str, room_id: str) -> JsonDict:
688+
# Assert host in room first to hide contents of the ACL from the caller
689+
await self._event_auth_handler.assert_host_in_room(room_id, origin)
690+
origin_host, _ = parse_server_name(origin)
691+
await self.check_server_matches_acl(origin_host, room_id)
692+
693+
extremities = await self.store.get_forward_extremities_for_room(room_id)
694+
prev_event_ids = [event_id for event_id, _, _, _ in extremities]
695+
if len(prev_event_ids) == 0:
696+
raise SynapseError(500, "Room has no forward extremities")
697+
return {"prev_events": prev_event_ids}
698+
686699
async def on_make_join_request(
687700
self, origin: str, room_id: str, user_id: str, supported_versions: list[str]
688701
) -> dict[str, Any]:

synapse/federation/transport/server/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Copyright 2020 Sorunome
55
# Copyright 2014-2021 The Matrix.org Foundation C.I.C.
66
# Copyright (C) 2023 New Vector, Ltd
7+
# Copyright (C) 2025 Element Creations Ltd
78
#
89
# This program is free software: you can redistribute it and/or modify
910
# it under the terms of the GNU Affero General Public License as
@@ -33,6 +34,7 @@
3334
FederationMediaDownloadServlet,
3435
FederationMediaThumbnailServlet,
3536
FederationUnstableClientKeysClaimServlet,
37+
FederationUnstableGetExtremitiesServlet,
3638
)
3739
from synapse.http.server import HttpServer, JsonResource
3840
from synapse.http.servlet import (
@@ -326,6 +328,12 @@ def register_servlets(
326328
if not hs.config.media.can_load_media_repo:
327329
continue
328330

331+
if (
332+
servletclass == FederationUnstableGetExtremitiesServlet
333+
and not hs.config.experimental.msc4370_enabled
334+
):
335+
continue
336+
329337
servletclass(
330338
hs=hs,
331339
authenticator=authenticator,

synapse/federation/transport/server/federation.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#
22
# This file is licensed under the Affero General Public License (AGPL) version 3.
33
#
4-
# Copyright 2021 The Matrix.org Foundation C.I.C.
4+
# Copyright 2021 The Matrix.org Foundation C.I.C.
55
# Copyright (C) 2023 New Vector, Ltd
6+
# Copyright (C) 2025 Element Creations Ltd
67
#
78
# This program is free software: you can redistribute it and/or modify
89
# it under the terms of the GNU Affero General Public License as
@@ -273,6 +274,22 @@ async def on_GET(
273274
return await self.handler.on_query_request(query_type, args)
274275

275276

277+
class FederationUnstableGetExtremitiesServlet(BaseFederationServerServlet):
278+
PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc4370"
279+
PATH = "/extremities/(?P<room_id>[^/]*)"
280+
CATEGORY = "Federation requests"
281+
282+
async def on_GET(
283+
self,
284+
origin: str,
285+
content: Literal[None],
286+
query: dict[bytes, list[bytes]],
287+
room_id: str,
288+
) -> tuple[int, JsonDict]:
289+
result = await self.handler.on_get_extremities_request(origin, room_id)
290+
return 200, result
291+
292+
276293
class FederationMakeJoinServlet(BaseFederationServerServlet):
277294
PATH = "/make_join/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)"
278295
CATEGORY = "Federation requests"
@@ -884,6 +901,7 @@ async def on_GET(
884901
FederationBackfillServlet,
885902
FederationTimestampLookupServlet,
886903
FederationQueryServlet,
904+
FederationUnstableGetExtremitiesServlet,
887905
FederationMakeJoinServlet,
888906
FederationMakeLeaveServlet,
889907
FederationEventServlet,

tests/federation/test_federation_server.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,141 @@ def test_needs_to_be_in_room(self) -> None:
324324
self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
325325

326326

327+
class UnstableGetExtremitiesTests(unittest.FederatingHomeserverTestCase):
328+
servlets = [
329+
admin.register_servlets,
330+
room.register_servlets,
331+
login.register_servlets,
332+
]
333+
334+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
335+
super().prepare(reactor, clock, hs)
336+
self._storage_controllers = hs.get_storage_controllers()
337+
338+
def _make_endpoint_path(self, room_id: str) -> str:
339+
return f"/_matrix/federation/unstable/org.matrix.msc4370/extremities/{room_id}"
340+
341+
def _remote_join(self, room_id: str, room_version: str) -> str:
342+
# Note: other tests ensure the called endpoints in this function return useful
343+
# and proper data.
344+
345+
# make_join first
346+
joining_user = "@misspiggy:" + self.OTHER_SERVER_NAME
347+
channel = self.make_signed_federation_request(
348+
"GET",
349+
f"/_matrix/federation/v1/make_join/{room_id}/{joining_user}?ver={room_version}",
350+
)
351+
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
352+
join_result = channel.json_body
353+
354+
# Sign/populate the join
355+
join_event_dict = join_result["event"]
356+
self.add_hashes_and_signatures_from_other_server(
357+
join_event_dict,
358+
KNOWN_ROOM_VERSIONS[room_version],
359+
)
360+
if room_version in ["1", "2"]:
361+
add_hashes_and_signatures(
362+
KNOWN_ROOM_VERSIONS[room_version],
363+
join_event_dict,
364+
signature_name=self.hs.hostname,
365+
signing_key=self.hs.signing_key,
366+
)
367+
368+
# Send the join
369+
channel = self.make_signed_federation_request(
370+
"PUT",
371+
f"/_matrix/federation/v2/send_join/{room_id}/x",
372+
content=join_event_dict,
373+
)
374+
375+
# Check that things went okay so the test doesn't become a total train wreck
376+
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
377+
r = self.get_success(self._storage_controllers.state.get_current_state(room_id))
378+
self.assertEqual(r[("m.room.member", joining_user)].membership, "join")
379+
380+
return r[("m.room.member", joining_user)].event_id
381+
382+
def _test_get_extremities_common(self, room_version: str) -> None:
383+
# Create a room to test with
384+
creator_user_id = self.register_user("kermit", "test")
385+
tok = self.login("kermit", "test")
386+
room_id = self.helper.create_room_as(
387+
room_creator=creator_user_id,
388+
tok=tok,
389+
room_version=room_version,
390+
extra_content={
391+
# Public preset uses `shared` history visibility, but makes joins
392+
# easier in our tests.
393+
# https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3createroom
394+
"preset": "public_chat"
395+
},
396+
)
397+
398+
# At this stage we should fail to get the extremities because we're not joined
399+
# and therefore can't see the events (`shared` history visibility).
400+
channel = self.make_signed_federation_request(
401+
"GET", self._make_endpoint_path(room_id)
402+
)
403+
self.assertEqual(channel.code, HTTPStatus.FORBIDDEN, channel.json_body)
404+
self.assertEqual(channel.json_body["error"], "Host not in room.")
405+
self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
406+
407+
# Now join the room and try again
408+
# Note: there should be just one extremity: the join
409+
join_event_id = self._remote_join(room_id, room_version)
410+
channel = self.make_signed_federation_request(
411+
"GET", self._make_endpoint_path(room_id)
412+
)
413+
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
414+
self.assertEqual(channel.json_body["prev_events"], [join_event_id])
415+
416+
# ACL the calling server and try again. This should cause an error getting extremities.
417+
self.helper.send_state(
418+
room_id,
419+
"m.room.server_acl",
420+
{
421+
"allow": ["*"],
422+
"allow_ip_literals": False,
423+
"deny": [self.OTHER_SERVER_NAME],
424+
},
425+
tok=tok,
426+
expect_code=HTTPStatus.OK,
427+
)
428+
channel = self.make_signed_federation_request(
429+
"GET", self._make_endpoint_path(room_id)
430+
)
431+
self.assertEqual(channel.code, HTTPStatus.FORBIDDEN, channel.json_body)
432+
self.assertEqual(channel.json_body["error"], "Server is banned from room")
433+
self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
434+
435+
@parameterized.expand([(k,) for k in KNOWN_ROOM_VERSIONS.keys()])
436+
@override_config(
437+
{"use_frozen_dicts": True, "experimental_features": {"msc4370_enabled": True}}
438+
)
439+
def test_get_extremities_with_frozen_dicts(self, room_version: str) -> None:
440+
"""Test GET /extremities with USE_FROZEN_DICTS=True"""
441+
self._test_get_extremities_common(room_version)
442+
443+
@parameterized.expand([(k,) for k in KNOWN_ROOM_VERSIONS.keys()])
444+
@override_config(
445+
{"use_frozen_dicts": False, "experimental_features": {"msc4370_enabled": True}}
446+
)
447+
def test_get_extremities_without_frozen_dicts(self, room_version: str) -> None:
448+
"""Test GET /extremities with USE_FROZEN_DICTS=False"""
449+
self._test_get_extremities_common(room_version)
450+
451+
# note the lack of config-setting stuff on this test.
452+
def test_get_extremities_unstable_not_enabled(self) -> None:
453+
"""Test that GET /extremities returns M_UNRECOGNIZED when MSC4370 is not enabled"""
454+
# We shouldn't even have to create a room - the endpoint should just fail.
455+
channel = self.make_signed_federation_request(
456+
"GET", self._make_endpoint_path("!room:example.org")
457+
)
458+
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND, channel.json_body)
459+
self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED")
460+
461+
327462
class SendJoinFederationTests(unittest.FederatingHomeserverTestCase):
328463
servlets = [
329464
admin.register_servlets,

0 commit comments

Comments
 (0)