tuwunel_service/membership/
stripped_state.rs1use ruma::{
2 CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, RoomId, RoomVersionId,
3 api::federation::membership::RawStrippedState,
4 events::AnyStrippedStateEvent,
5 room_version_rules::RoomIdFormatVersion,
6 serde::{JsonObject, Raw},
7};
8use tuwunel_core::{Event, PduEvent, Result, implement, matrix::event::gen_event_id};
9
10use super::Service;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum StrippedCreateVerdict {
15 Valid,
17
18 Missing,
20
21 NotPdu,
23
24 WrongRoom,
26
27 BadSignature,
29}
30
31#[must_use]
34pub fn enforce_stripped_create(
35 verdict: StrippedCreateVerdict,
36 v12_room_ids: bool,
37 enforce: bool,
38) -> bool {
39 use StrippedCreateVerdict::*;
40
41 match verdict {
42 | Valid => false,
43 | WrongRoom => v12_room_ids || enforce,
46 | Missing | NotPdu | BadSignature => enforce,
47 }
48}
49
50#[must_use]
54pub fn v12_room_ids(room_version: &RoomVersionId) -> bool {
55 room_version
56 .rules()
57 .is_some_and(|rules| matches!(rules.room_id_format, RoomIdFormatVersion::V2))
58}
59
60#[expect(
63 deprecated,
64 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
65 compatibility."
66)]
67#[must_use]
68pub fn into_client_stripped(
69 room_id: &RoomId,
70 state: RawStrippedState,
71) -> Option<Raw<AnyStrippedStateEvent>> {
72 match state {
73 | RawStrippedState::Stripped(raw) => Some(raw),
74 | RawStrippedState::Pdu(raw) => {
75 let mut event: JsonObject = serde_json::from_str(raw.get()).ok()?;
76
77 event.insert("event_id".into(), "$placeholder".into());
80 event
81 .entry("room_id")
82 .or_insert_with(|| room_id.as_str().into());
83
84 let pdu: PduEvent = serde_json::from_value(event.into()).ok()?;
85
86 Some(pdu.to_format())
87 },
88 }
89}
90
91#[implement(Service)]
95#[expect(
96 deprecated,
97 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
98 compatibility."
99)]
100#[tracing::instrument(level = "debug", skip_all, fields(%room_id))]
101pub async fn validate_stripped_create(
102 &self,
103 state: &[RawStrippedState],
104 room_id: &RoomId,
105 room_version_id: &RoomVersionId,
106) -> Result<StrippedCreateVerdict> {
107 let create = state.iter().find_map(|event| match event {
108 | RawStrippedState::Pdu(raw) => serde_json::from_str::<CanonicalJsonObject>(raw.get())
109 .ok()
110 .filter(is_create),
111 | RawStrippedState::Stripped(_) => None,
112 });
113
114 let Some(mut create) = create else {
115 let stripped = state.iter().any(|event| match event {
116 | RawStrippedState::Stripped(raw) =>
117 serde_json::from_str::<CanonicalJsonObject>(raw.json().get())
118 .is_ok_and(|json| is_create(&json)),
119 | RawStrippedState::Pdu(_) => false,
120 });
121
122 return Ok(match stripped {
123 | true => StrippedCreateVerdict::NotPdu,
124 | false => StrippedCreateVerdict::Missing,
125 });
126 };
127
128 create.remove("unsigned");
129
130 let bound = if v12_room_ids(room_version_id) {
133 gen_event_id(&create, room_version_id)
134 .ok()
135 .and_then(|event_id| OwnedRoomId::from_parts('!', event_id.localpart(), None).ok())
136 .is_some_and(|expected| expected == room_id)
137 } else {
138 create
139 .get("room_id")
140 .and_then(CanonicalJsonValue::as_str)
141 .is_some_and(|id| id == room_id.as_str())
142 };
143
144 if !bound {
145 return Ok(StrippedCreateVerdict::WrongRoom);
146 }
147
148 if self
149 .services
150 .server_keys
151 .verify_event(&create, Some(room_version_id))
152 .await
153 .is_err()
154 {
155 return Ok(StrippedCreateVerdict::BadSignature);
156 }
157
158 Ok(StrippedCreateVerdict::Valid)
159}
160
161fn is_create(json: &CanonicalJsonObject) -> bool {
162 let field = |key| json.get(key).and_then(CanonicalJsonValue::as_str);
163
164 field("type") == Some("m.room.create") && field("state_key") == Some("")
165}