-
Notifications
You must be signed in to change notification settings - Fork 592
MSC4311: Use full PDU's in stripped state (like invite_room_state) over federation and always include m.room.create event
#19723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 50 commits
bdfeb90
43a11f5
7f25120
6026aaa
e0eb224
3464ec8
f96c008
a088aa8
336b686
22f4f20
cce5dc7
7e379e7
92d0d8b
76b4905
5103f1b
5c1f4ca
374c4c5
ffe5c4b
2b900b4
5e12cb8
ff533df
de78d9e
4598a43
fbff685
75a53ef
178dd89
1926534
17c3763
75d1935
3888387
259f151
c289e93
ec572c6
4e7f3fc
bf9ab2f
c54c93b
17a4ef6
1cf8282
ce17c93
5cc8753
53a68d7
7488a24
3870034
40d1316
3fc94ba
26bf739
2822f3c
3ce6900
19d8a7a
ecab104
eed3071
f751b5f
e06b311
e301cdd
bc73bb0
0baa67b
4f70688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Remove flawed [MSC4311](https://github.com/matrix-org/matrix-spec-proposals/pull/4311) partial implementation: Client-side API's like `/sync` should still use stripped events. | ||
|
MadLittleMods marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,23 @@ pub struct RoomVersion { | |
| /// This is similar to how doubly-linked lists can potentially not refer to previous items correctly | ||
| /// without verifying the list's integrity, but doing it on every insert is too expensive. | ||
| pub msc4242_state_dags: bool, | ||
| /// Determines whether a room version *SHOULD* rather than *MAY* reject invites/knocks | ||
| /// with invalid stripped state events. | ||
| /// | ||
| /// According to MSC4311: | ||
| /// > If any of the [stripped state] events are not a PDU, not for the room ID specified, or fail | ||
| /// > signature checks, or the `m.room.create` event is missing, the receiving | ||
| /// > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix | ||
| /// > error (new to the endpoint). For invites to room version 12+ rooms, servers | ||
| /// > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`. | ||
| /// | ||
| /// Regardless of room version (we should always do these things): | ||
| /// 1. The `m.room.create` event *MUST* be included in | ||
| /// `invite_room_state`/`knock_room_state` when sending invites/knocks over the | ||
| /// federation API's. | ||
| /// 2. Use full PDU's in the `invite_room_state`/`knock_room_state` in the federation | ||
| /// API. The client API still uses stripped state. | ||
| msc4311_stripped_state: bool, | ||
| } | ||
|
|
||
| impl RoomVersion { | ||
|
|
@@ -187,6 +204,7 @@ impl RoomVersion { | |
| msc4291_room_ids_as_hashes: false, | ||
| strict_event_byte_limits_room_versions: false, | ||
| msc4242_state_dags: false, | ||
| msc4311_stripped_state: false, | ||
| }; | ||
|
|
||
| pub const V2: RoomVersion = RoomVersion { | ||
|
|
@@ -304,6 +322,7 @@ impl RoomVersion { | |
| state_res: StateResolutionVersions::V2_1, | ||
| msc4289_creator_power_enabled: true, | ||
| msc4291_room_ids_as_hashes: true, | ||
| msc4311_stripped_state: true, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this not break invites across new and old Synapses for v12 rooms?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How so? We don't strictly validate anything (just warn when it fails validation in v12 rooms). See "What does this PR do?" in the PR description.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, my bad. Feels very odd to have an option that merely causes things to log. Especially given that initially this will log for almost every invite in v12. I wonder if its worth it for now?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's house-keeping for the room versions. We use it today and will use it in the future. |
||
| ..Self::V11 | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,7 +37,6 @@ | |
| CANONICALJSON_MAX_INT, | ||
| CANONICALJSON_MIN_INT, | ||
| MAX_PDU_SIZE, | ||
| EventTypes, | ||
| ) | ||
| from synapse.api.errors import Codes, SynapseError | ||
| from synapse.logging.opentracing import SynapseTags, set_tag, trace | ||
|
|
@@ -55,7 +54,7 @@ | |
| from synapse.synapse_rust.types import Requester | ||
| from synapse.types import JsonDict | ||
|
|
||
| from . import EventBase, StrippedStateEvent | ||
| from . import EventBase | ||
|
|
||
| # These are imported only to re-export them (callers import them from this | ||
| # module); listing them in __all__ stops the unused-import lint flagging them | ||
|
|
@@ -549,45 +548,9 @@ def strip_event(event: EventBase) -> JsonDict: | |
| Stripped state events can only have the `sender`, `type`, `state_key` and `content` | ||
| properties present. | ||
| """ | ||
| # MSC4311: Ensure the create event is available on invites and knocks. | ||
| # TODO: Implement the rest of MSC4311 | ||
| if ( | ||
| event.room_version.msc4291_room_ids_as_hashes | ||
| and event.type == EventTypes.Create | ||
| and event.get_state_key() == "" | ||
| ): | ||
| return event.get_pdu_json() | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| return { | ||
| "type": event.type, | ||
| "state_key": event.state_key, | ||
| "content": dict(event.content), | ||
| "sender": event.sender, | ||
| } | ||
|
|
||
|
|
||
| def parse_stripped_state_event(raw_stripped_event: Any) -> StrippedStateEvent | None: | ||
| """ | ||
| Given a raw value from an event's `unsigned` field, attempt to parse it into a | ||
| `StrippedStateEvent`. | ||
| """ | ||
| if isinstance(raw_stripped_event, dict): | ||
| # All of these fields are required | ||
| type = raw_stripped_event.get("type") | ||
| state_key = raw_stripped_event.get("state_key") | ||
| sender = raw_stripped_event.get("sender") | ||
| content = raw_stripped_event.get("content") | ||
| if ( | ||
| isinstance(type, str) | ||
| and isinstance(state_key, str) | ||
| and isinstance(sender, str) | ||
| and isinstance(content, dict) | ||
| ): | ||
| return StrippedStateEvent( | ||
| type=type, | ||
| state_key=state_key, | ||
| sender=sender, | ||
| content=content, | ||
| ) | ||
|
|
||
| return None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
|
|
||
| import itertools | ||
| import logging | ||
| from http import HTTPStatus | ||
| from typing import ( | ||
| TYPE_CHECKING, | ||
| AbstractSet, | ||
|
|
@@ -59,6 +60,7 @@ | |
| RoomVersions, | ||
| ) | ||
| from synapse.events import EventBase, builder, make_event_from_dict | ||
| from synapse.events.snapshot import EventContext | ||
| from synapse.federation.federation_base import ( | ||
| FederationBase, | ||
| InvalidEventSignatureError, | ||
|
|
@@ -70,7 +72,12 @@ | |
| from synapse.http.types import QueryParams | ||
| from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace | ||
| from synapse.metrics import SERVER_NAME_LABEL | ||
| from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id | ||
| from synapse.types import ( | ||
| JsonDict, | ||
| StrCollection, | ||
| UserID, | ||
| get_domain_from_id, | ||
| ) | ||
| from synapse.util.async_helpers import concurrently_execute | ||
| from synapse.util.caches.expiringcache import ExpiringCache | ||
| from synapse.util.duration import Duration | ||
|
|
@@ -1303,12 +1310,12 @@ async def send_invite( | |
| self, | ||
| destination: str, | ||
| room_id: str, | ||
| event_id: str, | ||
| pdu: EventBase, | ||
| context: EventContext, | ||
| ) -> EventBase: | ||
| room_version = await self.store.get_room_version(room_id) | ||
|
|
||
| content = await self._do_send_invite(destination, pdu, room_version) | ||
| content = await self._do_send_invite(destination, pdu, context, room_version) | ||
|
|
||
| pdu_dict = content["event"] | ||
|
|
||
|
|
@@ -1329,11 +1336,21 @@ async def send_invite( | |
| return pdu | ||
|
|
||
| async def _do_send_invite( | ||
| self, destination: str, pdu: EventBase, room_version: RoomVersion | ||
| self, | ||
| destination: str, | ||
| pdu: EventBase, | ||
| context: EventContext, | ||
| room_version: RoomVersion, | ||
| ) -> JsonDict: | ||
| """Actually sends the invite, first trying v2 API and falling back to | ||
| v1 API if necessary. | ||
|
|
||
| Args: | ||
| destination: | ||
| pdu: Invite event | ||
| context: | ||
| room_version: | ||
|
|
||
| Returns: | ||
| The event as a dict as returned by the remote server | ||
|
|
||
|
|
@@ -1344,6 +1361,19 @@ async def _do_send_invite( | |
| """ | ||
| time_now = self._clock.time_msec() | ||
|
|
||
| # MSC4311: For the federation API, format events in `invite_room_state` as full | ||
| # PDU's | ||
| # | ||
| # Find the full events based on the state at the time of the invite | ||
| state_ids = await self.store.get_stripped_room_state_ids_from_event_context( | ||
| pdu, | ||
| context, | ||
| ) | ||
| state_events = await self.store.get_events(state_ids) | ||
| assert set(state_ids) == set(state_events.keys()), ( | ||
| "We should have all events available that were set as stripped state." | ||
| ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, this is technically a behaviour change where we are recalculating the stripped state here rather than using the stripped state we calculated at event creation. Generally, I'm wondering if we should change the format of what we store as
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤷 Future plans |
||
|
|
||
| try: | ||
| return await self.transport_layer.send_invite_v2( | ||
| destination=destination, | ||
|
|
@@ -1352,7 +1382,11 @@ async def _do_send_invite( | |
| content={ | ||
| "event": pdu.get_pdu_json(time_now), | ||
| "room_version": room_version.identifier, | ||
| "invite_room_state": pdu.unsigned.get("invite_room_state", []), | ||
| "invite_room_state": [ | ||
| # Use full PDU's according to MSC4311 | ||
| state_event.get_pdu_json(time_now) | ||
| for state_event in state_events.values() | ||
| ], | ||
| }, | ||
| ) | ||
| except HttpResponseException as e: | ||
|
|
@@ -1367,18 +1401,74 @@ async def _do_send_invite( | |
| "User's homeserver does not support this room version", | ||
| Codes.UNSUPPORTED_ROOM_VERSION, | ||
| ) | ||
| # MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx | ||
| # error by the sending server over the Client-Server API. This is done | ||
| # because there's nothing the client can materially do differently to make | ||
| # the request succeed. | ||
| elif ( | ||
| err.code == HTTPStatus.BAD_REQUEST | ||
| and err.errcode == Codes.MISSING_PARAM | ||
| ): | ||
| raise SynapseError( | ||
| 500, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're not doing a 4xx response then we shouldn't use 500 as it's not an internal server error. I'm not really sure what the closest HTTP code for "this server can't perform the action requested", it's half a 4xx and half a 5xx. TBH it feels quite similar to the 400 above, "User's homeserver does not support this room version".
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the comment above states, the MSC tells us this should be a 5xx error. I chose And the description sounds perfect:
In other words, our homeserver implementation is preventing the invite from going through. Nothing the user can do.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, bleurgh. The problem is that in practice returning a 500 is surprising behaviour for almost everyone: they expect a 500 to indicate there is a bug/misconfiguration in the server. Clients/users will retry when they hit a 500 as they assume it may be a transient problem. Generally, regardless of what the web specs imply, clients view 5xx as possibly transient server issues, while 4xx as a final failure. Most 4xx are actually things the user can't do anything about, e.g. 403 or 404. Concretely: returning a 500 will cause confusion for server operators (and us) as that should be reserved for bugs, and clients will keep retrying this endpoint. Intermediate proxies do sometimes infer uptime via 5xx errors (e.g. returning 502 often causes this). Have taken this up wit the Spec Core Team.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MSC is up at: matrix-org/matrix-spec-proposals#4528 |
||
| f"Received {HTTPStatus.BAD_REQUEST} {Codes.MISSING_PARAM} response from remote homeserver " | ||
| "while trying to send the invite over federation. This indicates a compatibility problem " | ||
| "between your homeserver and the homeserver you're trying to send the invite to " | ||
| "(either one could be at fault).", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, missed this last time but I don't think this is a useful user facing error, it's too verbose and technical. I think one of the following maybe:
We want something that the user can understand the net result: namely the invite failed for some reason.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as a user, not knowing why the invite failed (beyond "computer said no") would be more frustrating than the confusion caused by a technical error that I can ask for help with. As a server admin, it'd be even more annoying because I would then have to open a terminal and scrape through logs to try and find out more information.
would make sense. It's simple, concise, but still explains what the problem was, and a possible solution. It's also similar to the error returned when the remote server doesn't support the room version, just above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm in agreeance on providing enough info so that the error is useful to understand and for others (like us when people ask for help). I've updated the error message to start off more plainly:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair that we should communicate that it is likely an incompatibility issue.
I think this is confusing to people who aren't technical, how can an invite be not "understood"? The problem is that these are user visible and therefore affect the UX. In practice giving "complicated" error messages simply causes more confusion for the vast majority of users with little benefit (even to technical users). Especially in this case where nothing we're telling the user in the error messages is actually useful information. The fact that the remote server rejected it with a The overarching problem here is that we simply don't know why the other side rejected the invite, and so we can't give a useful error message to the user, just an unsatisfactory "something went went wrong". I think the information that is useful for the user is:
So perhaps something like:
It's not great, but I think says all that is useful to convey in a more friendly language. It's also unique enough that if more information is required server admins can scrape the logs easily.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better phrasing is good but we're losing too much information in my opinion. The problem needs to be clear. Forcing a random user to get their homeserver admin to log dive is not nice. It's undue burden on a homeserver admin and not possible in unresponsive admin scenarios. If someone posts that error message in Synapse Admins and asks why, my next question is what did the other server respond with? If I looked specifically at the Synapse code, I can deduce it in the this case because it corresponds to one specific scenario but it still requires me to dig. That's just not necessary. Then the next step to look at is the two homeserver versions and see what they're running. These are the same questions I would have as a user if this happened to me. Knowing that the other server responded with I've updated to this (separated the simple explanation from the details with f"Invite was rejected by the recipient's server.\n\n"
f"The remote homeserver ({destination}) returned {HTTPStatus.BAD_REQUEST} {Codes.MISSING_PARAM} "
"which indicates a compatibility problem between your homeserver and the "
"homeserver you're trying to send the invite to (either one could be at fault).",To better illustrate, this is a Hulu error. This drives me crazy
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do agree that we shouldn't return a completely vacuous error. However, we do need to balance utility between super-power users and UX for normal users. To that end: I don't really see what specifying I wouldn't be that adverse to adding extra information if our error messages had an extra collapsed-by-default section for "Advanced Information", but we don't have that. Plus, generally we're reluctant to give out too much information to users (like stack traces), as they can too easily leak information we'd rather wasn't. At which point for most cases you're going to need to look at the logs anyway. (Not to mention that any remedies here require involvement by the server admins)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From your three point criteria: the invite failed, it's not the user's fault, retrying won't help. A vague message asserts those but a specific message demonstrates them. Reassurance without specifics isn't very believable. Everyone has been told "sorry, something went wrong" by a system that was in fact blaming them or hiding the fact that they could have changed the outcome by trying something different. The details help close those loops and let the person stop second-guessing themselves. The I think we've struck a good balance with the last iteration. |
||
| Codes.UNKNOWN, | ||
| additional_fields={ | ||
| "cause": err.msg, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we have to worry about leaking anything by passing this error message along. Calling this out so the reviewer can sanity check this assumption
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Err, there's little point returning this to clients as nothing will read it. It can also be confusing if devs interpret it as the what this server is complaining about to the client
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #19723 (comment) in regards to providing enough info. If you can think of a better way to present it, that would be good. |
||
| "destination_server": destination, | ||
| }, | ||
| ) | ||
| else: | ||
| raise err | ||
|
|
||
| # Didn't work, try v1 API. | ||
| # Note the v1 API returns a tuple of `(200, content)` | ||
|
|
||
| _, content = await self.transport_layer.send_invite_v1( | ||
| destination=destination, | ||
| room_id=pdu.room_id, | ||
| event_id=pdu.event_id, | ||
| content=pdu.get_pdu_json(time_now), | ||
| ) | ||
| try: | ||
| # Use full PDU's for `invite_room_state` according to MSC4311 | ||
| # | ||
| # With the v1 invite API, `invite_room_state` is carried inside the event | ||
| # instead of a separate field like in v2 so we must munge it in ourselves | ||
| event_json = pdu.get_pdu_json(time_now) | ||
| event_json.setdefault("unsigned", {})["invite_room_state"] = [ | ||
| # Use full PDU's according to MSC4311 | ||
| state_event.get_pdu_json(time_now) | ||
| for state_event in state_events.values() | ||
| ] | ||
|
|
||
| _, content = await self.transport_layer.send_invite_v1( | ||
| destination=destination, | ||
| room_id=pdu.room_id, | ||
| event_id=pdu.event_id, | ||
| content=event_json, | ||
| ) | ||
| except HttpResponseException as e: | ||
| # MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx | ||
| # error by the sending server over the Client-Server API. This is done | ||
| # because there's nothing the client can materially do differently to make | ||
| # the request succeed. | ||
| err = e.to_synapse_error() | ||
| if ( | ||
| err.code == HTTPStatus.BAD_REQUEST | ||
| and err.errcode == Codes.MISSING_PARAM | ||
| ): | ||
| raise SynapseError( | ||
| 500, | ||
| f"Received {HTTPStatus.BAD_REQUEST} {Codes.MISSING_PARAM} response from remote homeserver " | ||
| "while trying to send the invite over federation. This indicates a compatibility problem " | ||
| "between your homeserver and the homeserver you're trying to send the invite to " | ||
| "(either one could be at fault).", | ||
| Codes.UNKNOWN, | ||
| additional_fields={ | ||
| "cause": err.msg, | ||
| "destination_server": destination, | ||
| }, | ||
| ) | ||
| else: | ||
| raise err | ||
| return content | ||
|
|
||
| async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None: | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Complement tests are expected to fail ❌ as we removed the flawed partial implementation in this PR.
The Complement tests have been updated in matrix-org/complement#796 and pass locally. We will merge both PRs at the same time.