Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19557.removal
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove support for undocumented variants of `/send` and `/redact` without a transaction ID, and `/createRoom`, `/forget` and the membership endpoints (join, invite, leave, ban, unban, kick) with a transaction ID. Contributed by @tulir @ Beeper.
189 changes: 26 additions & 163 deletions synapse/rest/client/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,32 +165,15 @@ def __init__(self, hs: "HomeServer"):

class RoomCreateRestServlet(TransactionRestServlet):
CATEGORY = "Client API requests"
PATTERNS = client_patterns("/createRoom$", v1=True)

def __init__(self, hs: "HomeServer"):
super().__init__(hs)
self._room_creation_handler = hs.get_room_creation_handler()
self.auth = hs.get_auth()

def register(self, http_server: HttpServer) -> None:
PATTERNS = "/createRoom"
register_txn_path(self, PATTERNS, http_server)

async def on_PUT(
self, request: SynapseRequest, txn_id: str
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request)
set_tag("txn_id", txn_id)
return await self.txns.fetch_or_execute_request(
request, requester, self._do, request, requester
)

async def on_POST(self, request: SynapseRequest) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request)
return await self._do(request, requester)

async def _do(
self, request: SynapseRequest, requester: Requester
) -> tuple[int, JsonDict]:
room_id, _, _ = await self._room_creation_handler.create_room(
requester, self.get_room_config(request)
)
Expand Down Expand Up @@ -409,6 +392,10 @@ async def on_PUT(
# TODO: Needs unit testing for generic events + feedback
class RoomSendEventRestServlet(TransactionRestServlet):
CATEGORY = "Event sending requests"
PATTERNS = client_patterns(
Copy link
Contributor

@reivilibre reivilibre Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sad to say, but after discussing with @erikjohnston, we need to preserve /rooms/.../send/... without the txnId on r0 and api/v1 as well.

They've been recently used on m.org and it's going to be hard to just turn these off.

Should comment that although api/v1 is genuine (legacy/historical), r0 was an accidental mistake that is widely used in practice.

I'd be somewhat keen on somehow cracking down on it (e.g. restrict based on user registration date?) but that'd definitely be follow-up PR territory.

"/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)/(?P<txn_id>[^/]*)$",
v1=True,
)

def __init__(self, hs: "HomeServer"):
super().__init__(hs)
Expand All @@ -418,11 +405,6 @@ def __init__(self, hs: "HomeServer"):
self._max_event_delay_ms = hs.config.server.max_event_delay_ms
self._msc4354_enabled = hs.config.experimental.msc4354_enabled

def register(self, http_server: HttpServer) -> None:
# /rooms/$roomid/send/$event_type[/$txn_id]
PATTERNS = "/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)"
register_txn_path(self, PATTERNS, http_server)

async def _do(
self,
request: SynapseRequest,
Expand Down Expand Up @@ -487,15 +469,6 @@ async def _do(
set_tag("event_id", event_id)
return 200, {"event_id": event_id}

async def on_POST(
self,
request: SynapseRequest,
room_id: str,
event_type: str,
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
return await self._do(request, requester, room_id, event_type, None)

async def on_PUT(
self, request: SynapseRequest, room_id: str, event_type: str, txn_id: str
) -> tuple[int, JsonDict]:
Expand Down Expand Up @@ -560,24 +533,19 @@ def _parse_request_delay(
# TODO: Needs unit testing for room ID + alias joins
class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
CATEGORY = "Event sending requests"
PATTERNS = client_patterns("/join/(?P<room_identifier>[^/]*)", v1=True)

def __init__(self, hs: "HomeServer"):
super().__init__(hs)
super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up
self.auth = hs.get_auth()

def register(self, http_server: HttpServer) -> None:
# /join/$room_identifier[/$txn_id]
PATTERNS = "/join/(?P<room_identifier>[^/]*)"
register_txn_path(self, PATTERNS, http_server)

async def _do(
async def on_POST(
self,
request: SynapseRequest,
requester: Requester,
room_identifier: str,
txn_id: str | None,
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
content = parse_json_object_from_request(request, allow_empty_body=True)

# twisted.web.server.Request.args is incorrectly defined as Any | None
Expand All @@ -598,32 +566,14 @@ async def _do(
target=requester.user,
room_id=room_id,
action="join",
txn_id=txn_id,
txn_id=None,
remote_room_hosts=remote_room_hosts,
content=content,
third_party_signed=content.get("third_party_signed", None),
)

return 200, {"room_id": room_id}

async def on_POST(
self,
request: SynapseRequest,
room_identifier: str,
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
return await self._do(request, requester, room_identifier, None)

async def on_PUT(
self, request: SynapseRequest, room_identifier: str, txn_id: str
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
set_tag("txn_id", txn_id)

return await self.txns.fetch_or_execute_request(
request, requester, self._do, request, requester, room_identifier, txn_id
)


# TODO: Needs unit testing
class PublicRoomListRestServlet(RestServlet):
Expand Down Expand Up @@ -1184,63 +1134,45 @@ async def on_GET(


class RoomForgetRestServlet(TransactionRestServlet):
PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/forget$", v1=True)

def __init__(self, hs: "HomeServer"):
super().__init__(hs)
self.room_member_handler = hs.get_room_member_handler()
self.auth = hs.get_auth()

def register(self, http_server: HttpServer) -> None:
PATTERNS = "/rooms/(?P<room_id>[^/]*)/forget"
register_txn_path(self, PATTERNS, http_server)

async def _do(self, requester: Requester, room_id: str) -> tuple[int, JsonDict]:
await self.room_member_handler.forget(user=requester.user, room_id=room_id)

return 200, {}

async def on_POST(
self, request: SynapseRequest, room_id: str
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=False)
return await self._do(requester, room_id)

async def on_PUT(
self, request: SynapseRequest, room_id: str, txn_id: str
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=False)
set_tag("txn_id", txn_id)
await self.room_member_handler.forget(user=requester.user, room_id=room_id)

return await self.txns.fetch_or_execute_request(
request, requester, self._do, requester, room_id
)
return 200, {}


# TODO: Needs unit testing
class RoomMembershipRestServlet(TransactionRestServlet):
CATEGORY = "Event sending requests"
PATTERNS = client_patterns(
"/rooms/(?P<room_id>[^/]*)/"
"(?P<membership_action>join|invite|leave|ban|unban|kick)",
v1=True,
)

def __init__(self, hs: "HomeServer"):
super().__init__(hs)
self.room_member_handler = hs.get_room_member_handler()
self.auth = hs.get_auth()
self.config = hs.config

def register(self, http_server: HttpServer) -> None:
# /rooms/$roomid/[join|invite|leave|ban|unban|kick]
PATTERNS = (
"/rooms/(?P<room_id>[^/]*)/"
"(?P<membership_action>join|invite|leave|ban|unban|kick)"
)
register_txn_path(self, PATTERNS, http_server)

async def _do(
async def on_POST(
self,
request: SynapseRequest,
requester: Requester,
room_id: str,
membership_action: str,
txn_id: str | None,
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
if requester.is_guest and membership_action not in {
Membership.JOIN,
Membership.LEAVE,
Expand All @@ -1267,7 +1199,7 @@ async def _do(
request_body["address"],
request_body["id_server"],
requester,
txn_id,
None,
request_body["id_access_token"],
)
except ShadowBanError:
Expand Down Expand Up @@ -1297,7 +1229,7 @@ async def _do(
target=target,
room_id=room_id,
action=membership_action,
txn_id=txn_id,
txn_id=None,
third_party_signed=request_body.get("third_party_signed", None),
content=event_content,
)
Expand All @@ -1312,35 +1244,13 @@ async def _do(

return 200, return_value

async def on_POST(
self,
request: SynapseRequest,
room_id: str,
membership_action: str,
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
return await self._do(request, requester, room_id, membership_action, None)

async def on_PUT(
self, request: SynapseRequest, room_id: str, membership_action: str, txn_id: str
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
set_tag("txn_id", txn_id)

return await self.txns.fetch_or_execute_request(
request,
requester,
self._do,
request,
requester,
room_id,
membership_action,
txn_id,
)


class RoomRedactEventRestServlet(TransactionRestServlet):
CATEGORY = "Event sending requests"
PATTERNS = client_patterns(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to say we need to keep the non-txnId form on r0 only for now, for Element iOS Classic (stopping people from redacting is not a fun bug).


Happy to throw away the other versions though.

We should comment that this is out of spec for ECiOS and link to the ticket element-hq/element-ios#8011

In a follow-up PR I would not be against adding a user-agent check to ensure it's only ECiOS that can use this, to stop anyone else depending on it. (Needs further consideration though, I don't know if this would also backfire on ECiOS forks. Should probably give some grace time for a fix anyway.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also note in the comments that this can be removed once Element Classic iOS is shutdown in favor of Element X in the near-ish future

"/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)/(?P<txn_id>[^/]*)$",
v1=True,
)

def __init__(self, hs: "HomeServer"):
super().__init__(hs)
Expand All @@ -1352,10 +1262,6 @@ def __init__(self, hs: "HomeServer"):
self._relation_handler = hs.get_relations_handler()
self._msc3912_enabled = hs.config.experimental.msc3912_enabled

def register(self, http_server: HttpServer) -> None:
PATTERNS = "/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)"
register_txn_path(self, PATTERNS, http_server)

async def _do(
self,
request: SynapseRequest,
Expand Down Expand Up @@ -1444,15 +1350,6 @@ async def _do(
set_tag("event_id", event_id)
return 200, {"event_id": event_id}

async def on_POST(
self,
request: SynapseRequest,
room_id: str,
event_id: str,
) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request)
return await self._do(request, requester, room_id, event_id, None)

async def on_PUT(
self, request: SynapseRequest, room_id: str, event_id: str, txn_id: str
) -> tuple[int, JsonDict]:
Expand Down Expand Up @@ -1586,40 +1483,6 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
return 200, {"joined_rooms": list(room_ids)}


def register_txn_path(
servlet: RestServlet,
regex_string: str,
http_server: HttpServer,
) -> None:
"""Registers a transaction-based path.

This registers two paths:
PUT regex_string/$txnid
POST regex_string

Args:
regex_string: The regex string to register. Must NOT have a
trailing $ as this string will be appended to.
http_server: The http_server to register paths with.
"""
on_POST = getattr(servlet, "on_POST", None)
on_PUT = getattr(servlet, "on_PUT", None)
if on_POST is None or on_PUT is None:
raise RuntimeError("on_POST and on_PUT must exist when using register_txn_path")
http_server.register_paths(
"POST",
client_patterns(regex_string + "$", v1=True),
on_POST,
servlet.__class__.__name__,
)
http_server.register_paths(
"PUT",
client_patterns(regex_string + "/(?P<txn_id>[^/]*)$", v1=True),
on_PUT,
servlet.__class__.__name__,
)


class TimestampLookupRestServlet(RestServlet):
"""
API endpoint to fetch the `event_id` of the closest event to the given
Expand Down
30 changes: 19 additions & 11 deletions tests/rest/client/test_delayed_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,12 @@ def test_cancel_delayed_state_event(self, action_in_path: bool) -> None:
)
def test_cancel_delayed_event_ratelimit(self, action_in_path: bool) -> None:
delay_ids = []
for _ in range(2):
for i in range(2):
channel = self.make_request(
"POST",
_get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000),
"PUT",
_get_path_for_delayed_send(
self.room_id, _EVENT_TYPE, 100000, f"txn{i}"
),
{},
self.user1_access_token,
)
Expand Down Expand Up @@ -331,10 +333,12 @@ def test_send_delayed_state_event(
@unittest.override_config({"rc_message": {"per_second": 2.5, "burst_count": 3}})
def test_send_delayed_event_ratelimit(self, action_in_path: bool) -> None:
delay_ids = []
for _ in range(2):
for i in range(2):
channel = self.make_request(
"POST",
_get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000),
"PUT",
_get_path_for_delayed_send(
self.room_id, _EVENT_TYPE, 100000, f"txn{i}"
),
{},
self.user1_access_token,
)
Expand Down Expand Up @@ -412,10 +416,12 @@ def test_restart_delayed_state_event(self, action_in_path: bool) -> None:
)
def test_restart_delayed_event_ratelimit(self, action_in_path: bool) -> None:
delay_ids = []
for _ in range(2):
for i in range(2):
channel = self.make_request(
"POST",
_get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000),
"PUT",
_get_path_for_delayed_send(
self.room_id, _EVENT_TYPE, 100000, f"txn{i}"
),
{},
self.user1_access_token,
)
Expand Down Expand Up @@ -556,5 +562,7 @@ def _get_path_for_delayed_state(
return f"rooms/{room_id}/state/{event_type}/{state_key}?org.matrix.msc4140.delay={delay_ms}"


def _get_path_for_delayed_send(room_id: str, event_type: str, delay_ms: int) -> str:
return f"rooms/{room_id}/send/{event_type}?org.matrix.msc4140.delay={delay_ms}"
def _get_path_for_delayed_send(
room_id: str, event_type: str, delay_ms: int, txn_id: str
) -> str:
return f"rooms/{room_id}/send/{event_type}/{txn_id}?org.matrix.msc4140.delay={delay_ms}"
Loading
Loading