Repository: matrix-org/synapse

Issue:
When knocking, Synapse returns and accepts a list of stripped state events in an incorrectly named field
When a homeserver knocks on a room over federation, it calls the [` PUT /_matrix/federation/v1/send_knock/{roomId}/{eventId}`](https://spec.matrix.org/v1.4/server-server-api/#put_matrixfederationv1send_knockroomideventid) endpoint on the remote homeserver. The remote homeserver can then return a set of [stripped state events](https://spec.matrix.org/v1.4/client-server-api/#stripped-state) that describe the room, which are passed subsequently to the client.

The response field for a knock's stripped state events is defined in the spec as `knock_room_state`. It looks like it was implemented as `knock_state_events` in https://github.com/matrix-org/synapse/pull/6739/files#diff-996dafa2cabdf043e51a444e5d3c5ba8389d48e6cdab0d469677974bd470d58fR686. A subtle but important difference.

As far as I can tell, this is a problem that's exclusive to Synapse. [Conduit](https://gitlab.com/famedly/conduit) does not implement knocking, [Dendrite](https://github.com/matrix-org/dendrite/) supports knock membership but not initiating a knock.

Assuming we want to support older Synapse's knocking through a newer Synapse and vice versa, a backwards-compatible fix would involve:

* Supporting both field names when extracting stripped state events from a response to [` PUT /_matrix/federation/v1/send_knock/{roomId}/{eventId}`](https://spec.matrix.org/v1.4/server-server-api/#put_matrixfederationv1send_knockroomideventid)
* Including the room's stripped state events under both field names when responding to the above endpoint.

If we didn't implement backwards-compatibility, a user on an old Synapse (or knocking on a new Synapse through an old Synapse) would (assuming knocking was implemented in any clients) see only a room ID in their "pending knocks" list, instead of a room name/avatar/any other metadata.

Alternatively we could opt for changing the field name in the spec and leave Synapse as is, but I'd be adverse as `knock_room_state` currently lines up nicely with `invite_room_state` for [`PUT /_matrix/federation/v2/invite/{roomId}/{eventId} `](https://spec.matrix.org/v1.4/server-server-api/#put_matrixfederationv2inviteroomideventid). It'd also require an MSC.


Discussion:

Resolving patch:
diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py
--- a/synapse/federation/federation_client.py
+++ b/synapse/federation/federation_client.py
@@ -1294,7 +1294,7 @@ async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict:
         return resp[1]
 
     async def send_knock(self, destinations: List[str], pdu: EventBase) -> JsonDict:
-        """Attempts to send a knock event to given a list of servers. Iterates
+        """Attempts to send a knock event to a given list of servers. Iterates
         through the list until one attempt succeeds.
 
         Doing so will cause the remote server to add the event to the graph,
diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py
--- a/synapse/federation/federation_server.py
+++ b/synapse/federation/federation_server.py
@@ -824,7 +824,14 @@ async def on_send_knock_request(
                 context, self._room_prejoin_state_types
             )
         )
-        return {"knock_state_events": stripped_room_state}
+        return {
+            "knock_room_state": stripped_room_state,
+            # Since v1.37, Synapse incorrectly used "knock_state_events" for this field.
+            # Thus, we also populate a 'knock_state_events' with the same content to
+            # support old instances.
+            # See https://github.com/matrix-org/synapse/issues/14088.
+            "knock_state_events": stripped_room_state,
+        }
 
     async def _on_send_membership_event(
         self, origin: str, content: JsonDict, membership_type: str, room_id: str
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -781,15 +781,27 @@ async def do_knock(
 
         # Send the signed event back to the room, and potentially receive some
         # further information about the room in the form of partial state events
-        stripped_room_state = await self.federation_client.send_knock(
-            target_hosts, event
-        )
+        knock_response = await self.federation_client.send_knock(target_hosts, event)
 
         # Store any stripped room state events in the "unsigned" key of the event.
         # This is a bit of a hack and is cribbing off of invites. Basically we
         # store the room state here and retrieve it again when this event appears
         # in the invitee's sync stream. It is stripped out for all other local users.
-        event.unsigned["knock_room_state"] = stripped_room_state["knock_state_events"]
+        stripped_room_state = (
+            knock_response.get("knock_room_state")
+            # Since v1.37, Synapse incorrectly used "knock_state_events" for this field.
+            # Thus, we also check for a 'knock_state_events' to support old instances.
+            # See https://github.com/matrix-org/synapse/issues/14088.
+            or knock_response.get("knock_state_events")
+        )
+
+        if stripped_room_state is None:
+            raise KeyError(
+                "Missing 'knock_room_state' (or legacy 'knock_state_events') field in "
+                "send_knock response"
+            )
+
+        event.unsigned["knock_room_state"] = stripped_room_state
 
         context = EventContext.for_outlier(self._storage_controllers)
         stream_id = await self._federation_event_handler.persist_events_and_notify(


Test patch: