tuwunel_service/membership/
stripped_state.rs1use std::{
2 borrow::Cow,
3 collections::{HashMap, hash_map::Entry},
4};
5
6use ruma::{
7 CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, RoomId, RoomVersionId, UserId,
8 api::federation::membership::RawStrippedState,
9 events::{AnyStrippedStateEvent, StateEventType, StateKey},
10 room_version_rules::RoomIdFormatVersion,
11 serde::{JsonObject, Raw},
12};
13use serde::Deserialize;
14use tuwunel_core::{Event, PduEvent, Result, implement, matrix::event::gen_event_id};
15
16use super::Service;
17
18type StateCell = (StateEventType, StateKey);
20
21type Accumulator = (HashMap<StateCell, usize>, Vec<RawStrippedState>);
23
24#[derive(Deserialize)]
26struct Cell<'a> {
27 #[serde(rename = "type", borrow)]
28 kind: Cow<'a, str>,
29
30 #[serde(borrow)]
31 state_key: Cow<'a, str>,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum StrippedCreateVerdict {
37 Valid,
39
40 Missing,
42
43 NotPdu,
45
46 WrongRoom,
48
49 BadSignature,
51}
52
53#[must_use]
56pub fn enforce_stripped_create(
57 verdict: StrippedCreateVerdict,
58 v12_room_ids: bool,
59 enforce: bool,
60) -> bool {
61 use StrippedCreateVerdict::*;
62
63 match verdict {
64 | Valid => false,
65 | WrongRoom => v12_room_ids || enforce,
68 | Missing | NotPdu | BadSignature => enforce,
69 }
70}
71
72#[must_use]
76pub fn v12_room_ids(room_version: &RoomVersionId) -> bool {
77 room_version
78 .rules()
79 .is_some_and(|rules| matches!(rules.room_id_format, RoomIdFormatVersion::V2))
80}
81
82#[must_use]
93pub fn dedup_stripped_state(state: Vec<RawStrippedState>) -> Vec<RawStrippedState> {
94 let (_, kept) = state
95 .into_iter()
96 .filter_map(|entry| state_cell(&entry).map(|cell| (cell, entry)))
97 .fold(Accumulator::default(), |(mut chosen, mut kept), (cell, entry)| {
98 match chosen.entry(cell) {
99 | Entry::Vacant(vacant) => {
100 vacant.insert(kept.len());
101 kept.push(entry);
102 },
103 | Entry::Occupied(occupied) => {
104 let held = &mut kept[*occupied.get()];
105
106 if is_legacy(held) && !is_legacy(&entry) {
107 *held = entry;
108 }
109 },
110 }
111
112 (chosen, kept)
113 });
114
115 kept
116}
117
118pub fn without_member(
124 state: Vec<RawStrippedState>,
125 user_id: &UserId,
126) -> impl Iterator<Item = RawStrippedState> {
127 state
128 .into_iter()
129 .filter(move |entry| !occupies_member_cell(entry, user_id))
130}
131
132fn state_cell(state: &RawStrippedState) -> Option<StateCell> {
133 cell(state).map(|cell| (cell.kind.as_ref().into(), cell.state_key.as_ref().into()))
134}
135
136fn occupies_member_cell(state: &RawStrippedState, user_id: &UserId) -> bool {
137 cell(state).is_some_and(|cell| {
138 StateEventType::from(cell.kind.as_ref()) == StateEventType::RoomMember
139 && cell.state_key == user_id.as_str()
140 })
141}
142
143fn cell(state: &RawStrippedState) -> Option<Cell<'_>> {
149 serde_json::from_str(entry_json(state)).ok()
150}
151
152#[expect(
153 deprecated,
154 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
155 compatibility."
156)]
157fn entry_json(state: &RawStrippedState) -> &str {
158 match state {
159 | RawStrippedState::Stripped(raw) => raw.json().get(),
160 | RawStrippedState::Pdu(raw) => raw.get(),
161 }
162}
163
164#[expect(
165 deprecated,
166 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
167 compatibility."
168)]
169fn is_legacy(state: &RawStrippedState) -> bool { matches!(state, RawStrippedState::Stripped(_)) }
170
171#[expect(
174 deprecated,
175 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
176 compatibility."
177)]
178#[must_use]
179pub fn into_client_stripped(
180 room_id: &RoomId,
181 state: RawStrippedState,
182) -> Option<Raw<AnyStrippedStateEvent>> {
183 match state {
184 | RawStrippedState::Stripped(raw) => Some(raw),
185 | RawStrippedState::Pdu(raw) => {
186 let mut event: JsonObject = serde_json::from_str(raw.get()).ok()?;
187
188 event.insert("event_id".into(), "$placeholder".into());
191 event
192 .entry("room_id")
193 .or_insert_with(|| room_id.as_str().into());
194
195 let pdu: PduEvent = serde_json::from_value(event.into()).ok()?;
196
197 Some(pdu.to_format())
198 },
199 }
200}
201
202#[implement(Service)]
206#[expect(
207 deprecated,
208 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
209 compatibility."
210)]
211#[tracing::instrument(level = "debug", skip_all, fields(%room_id))]
212pub async fn validate_stripped_create(
213 &self,
214 state: &[RawStrippedState],
215 room_id: &RoomId,
216 room_version_id: &RoomVersionId,
217) -> Result<StrippedCreateVerdict> {
218 let create = state.iter().find_map(|event| match event {
219 | RawStrippedState::Pdu(raw) => serde_json::from_str::<CanonicalJsonObject>(raw.get())
220 .ok()
221 .filter(is_create),
222 | RawStrippedState::Stripped(_) => None,
223 });
224
225 let Some(mut create) = create else {
226 let stripped = state.iter().any(|event| match event {
227 | RawStrippedState::Stripped(raw) =>
228 serde_json::from_str::<CanonicalJsonObject>(raw.json().get())
229 .is_ok_and(|json| is_create(&json)),
230 | RawStrippedState::Pdu(_) => false,
231 });
232
233 return Ok(match stripped {
234 | true => StrippedCreateVerdict::NotPdu,
235 | false => StrippedCreateVerdict::Missing,
236 });
237 };
238
239 create.remove("unsigned");
240
241 let bound = if v12_room_ids(room_version_id) {
244 gen_event_id(&create, room_version_id)
245 .ok()
246 .and_then(|event_id| OwnedRoomId::from_parts('!', event_id.localpart(), None).ok())
247 .is_some_and(|expected| expected == room_id)
248 } else {
249 create
250 .get("room_id")
251 .and_then(CanonicalJsonValue::as_str)
252 .is_some_and(|id| id == room_id.as_str())
253 };
254
255 if !bound {
256 return Ok(StrippedCreateVerdict::WrongRoom);
257 }
258
259 if self
260 .services
261 .server_keys
262 .verify_event(&create, Some(room_version_id))
263 .await
264 .is_err()
265 {
266 return Ok(StrippedCreateVerdict::BadSignature);
267 }
268
269 Ok(StrippedCreateVerdict::Valid)
270}
271
272fn is_create(json: &CanonicalJsonObject) -> bool {
273 let field = |key| json.get(key).and_then(CanonicalJsonValue::as_str);
274
275 field("type") == Some("m.room.create") && field("state_key") == Some("")
276}
277
278#[cfg(test)]
279#[expect(
280 deprecated,
281 reason = "Matrix 1.16 still permits receiving the legacy stripped variant for backwards \
282 compatibility."
283)]
284mod tests {
285 use ruma::{
286 api::federation::membership::RawStrippedState, events::StateEventType, serde::Raw,
287 user_id,
288 };
289 use serde_json::{Value as JsonValue, json, value::RawValue as RawJsonValue};
290
291 use super::{
292 dedup_stripped_state, entry_json, is_legacy, occupies_member_cell, state_cell,
293 without_member,
294 };
295
296 #[test]
297 fn a_cells_first_pdu_wins_over_an_earlier_legacy_entry() {
298 let deduped = dedup_stripped_state(vec![
299 legacy(&create("@forged:example.org")),
300 pdu(&create("@genuine:example.org")),
301 ]);
302
303 assert_eq!(senders(&deduped), ["@genuine:example.org"]);
304 }
305
306 #[test]
307 fn a_cell_holding_no_pdu_keeps_its_first_legacy_entry() {
308 let deduped = dedup_stripped_state(vec![
309 legacy(&create("@first:example.org")),
310 legacy(&create("@second:example.org")),
311 ]);
312
313 assert_eq!(senders(&deduped), ["@first:example.org"]);
314 }
315
316 #[test]
317 fn distinct_cells_all_survive_in_order() {
318 let deduped = dedup_stripped_state(vec![
319 pdu(&create("@creator:example.org")),
320 pdu(&member("@alice:example.org", "@alice:example.org")),
321 pdu(&member("@bob:example.org", "@alice:example.org")),
322 ]);
323
324 assert_eq!(deduped.len(), 3);
325 assert_eq!(state_cell(&deduped[0]).expect("a cell").0, StateEventType::RoomCreate);
326 }
327
328 #[test]
329 fn an_entry_without_a_readable_cell_drops() {
330 let deduped = dedup_stripped_state(vec![
331 pdu(&json!({"sender": "@alice:example.org", "content": {}})),
332 pdu(&create("@creator:example.org")),
333 ]);
334
335 assert_eq!(senders(&deduped), ["@creator:example.org"]);
336 }
337
338 #[test]
339 fn the_invitees_membership_cell_never_survives() {
340 let state = vec![
341 pdu(&member("@invitee:example.org", "@forged:example.org")),
342 pdu(&member("@other:example.org", "@alice:example.org")),
343 pdu(&create("@creator:example.org")),
344 ];
345
346 let kept: Vec<_> = without_member(state, user_id!("@invitee:example.org")).collect();
347
348 assert_eq!(senders(&kept), ["@alice:example.org", "@creator:example.org"]);
349 }
350
351 #[test]
352 fn an_all_legacy_array_survives_with_every_cell_intact() {
353 let name = json!({
354 "type": "m.room.name",
355 "state_key": "",
356 "sender": "@creator:example.org",
357 "content": {"name": "a room"},
358 });
359
360 let deduped = dedup_stripped_state(vec![
361 legacy(&create("@creator:example.org")),
362 legacy(&member("@alice:example.org", "@alice:example.org")),
363 legacy(&name),
364 ]);
365
366 assert_eq!(deduped.len(), 3);
367 assert!(deduped.iter().all(is_legacy));
368 assert_eq!(senders(&deduped), [
369 "@creator:example.org",
370 "@alice:example.org",
371 "@creator:example.org"
372 ]);
373 }
374
375 #[test]
376 fn a_cell_spelled_with_escapes_still_reads() {
377 let escaped = raw_pdu(
380 r#"{"type":"m.room.\u006dember","state_key":"@invitee:example.org",
381 "sender":"@forged:example.org","content":{"membership":"invite"}}"#,
382 );
383
384 assert!(occupies_member_cell(&escaped, user_id!("@invitee:example.org")));
385
386 let mut kept = without_member(vec![escaped], user_id!("@invitee:example.org"));
387
388 assert!(kept.next().is_none());
389 }
390
391 fn legacy(event: &JsonValue) -> RawStrippedState {
392 RawStrippedState::Stripped(
393 Raw::new(event)
394 .expect("valid json")
395 .cast_unchecked(),
396 )
397 }
398
399 fn raw_pdu(json: &str) -> RawStrippedState {
400 RawStrippedState::Pdu(RawJsonValue::from_string(json.to_owned()).expect("valid json"))
401 }
402
403 fn pdu(event: &JsonValue) -> RawStrippedState {
404 RawStrippedState::Pdu(
405 Raw::<JsonValue>::new(event)
406 .expect("valid json")
407 .into_json(),
408 )
409 }
410
411 fn create(sender: &str) -> JsonValue {
412 json!({"type": "m.room.create", "state_key": "", "sender": sender, "content": {}})
413 }
414
415 fn member(user_id: &str, sender: &str) -> JsonValue {
416 json!({
417 "type": "m.room.member",
418 "state_key": user_id,
419 "sender": sender,
420 "content": {"membership": "invite"},
421 })
422 }
423
424 fn senders(state: &[RawStrippedState]) -> Vec<String> {
425 state
426 .iter()
427 .map(|entry| {
428 let value: JsonValue =
429 serde_json::from_str(entry_json(entry)).expect("valid json");
430
431 value["sender"]
432 .as_str()
433 .expect("a sender")
434 .to_owned()
435 })
436 .collect()
437 }
438}