Skip to main content

tuwunel_core/matrix/pdu/format/
check.rs

1use ruma::{
2	CanonicalJsonObject, CanonicalJsonValue, ID_MAX_BYTES, RoomId, int,
3	room_version_rules::EventFormatRules,
4};
5use serde_json::to_string as to_json_string;
6
7use super::super::{MAX_AUTH_EVENTS, MAX_PDU_BYTES, MAX_PREV_EVENTS, Pdu};
8use crate::{Err, Result, err};
9
10pub fn check_room_id(pdu: &Pdu, room_id: &RoomId) -> Result {
11	if pdu.room_id != room_id {
12		return Err!(Request(InvalidParam(error!(
13			pdu_event_id = ?pdu.event_id,
14			pdu_room_id = ?pdu.room_id,
15			?room_id,
16			"Event in wrong room",
17		))));
18	}
19
20	Ok(())
21}
22
23/// Check that the given canonicalized PDU respects the event format of the room
24/// version and the [size limits] from the Matrix specification.
25///
26/// This is part of the [checks performed on receipt of a PDU].
27///
28/// This checks the following and enforces their size limits:
29///
30/// * Full PDU
31/// * `sender`
32/// * `room_id`
33/// * `type`
34/// * `event_id`
35/// * `state_key`
36/// * `prev_events`
37/// * `auth_events`
38/// * `depth`
39///
40/// Returns an `Err(_)` if the JSON is malformed or if the PDU doesn't pass the
41/// checks.
42///
43/// [size limits]: https://spec.matrix.org/latest/client-server-api/#size-limits
44/// [checks performed on receipt of a PDU]: https://spec.matrix.org/latest/server-server-api/#checks-performed-on-receipt-of-a-pdu
45pub fn check_rules(pdu: &CanonicalJsonObject, rules: &EventFormatRules) -> Result {
46	// Check the PDU size, it must occur on the full PDU with signatures.
47	let json = to_json_string(&pdu)
48		.map_err(|e| err!(Request(BadJson("Failed to serialize canonical JSON: {e}"))))?;
49
50	if json.len() > MAX_PDU_BYTES {
51		return Err!(Request(TooLarge("PDU is larger than maximum of {MAX_PDU_BYTES} bytes")));
52	}
53
54	// Check the presence, type and length of the `type` field.
55	let event_type = extract_required_string_field(pdu, "type")?;
56
57	// Check the presence, type and length of the `sender` field.
58	extract_required_string_field(pdu, "sender")?;
59
60	// Check the presence, type and length of the `room_id` field.
61	let room_id = (event_type != "m.room.create" || rules.require_room_create_room_id)
62		.then(|| extract_required_string_field(pdu, "room_id"))
63		.transpose()?;
64
65	// Check the presence, type and length of the `event_id` field.
66	if rules.require_event_id {
67		extract_required_string_field(pdu, "event_id")?;
68	}
69
70	// Check the type and length of the `state_key` field.
71	extract_optional_string_field(pdu, "state_key")?;
72
73	// Check the presence, type and length of the `prev_events` field.
74	extract_required_array_field(pdu, "prev_events", MAX_PREV_EVENTS)?;
75
76	// Check the presence, type and length of the `auth_events` field.
77	let auth_events = extract_required_array_field(pdu, "auth_events", MAX_AUTH_EVENTS)?;
78
79	if !rules.allow_room_create_in_auth_events {
80		// The only case where the room ID should be missing is for m.room.create which
81		// shouldn't have any auth_events.
82		if let Some(room_id) = room_id {
83			let room_create_event_reference_hash = <&RoomId>::try_from(room_id.as_str())
84				.map_err(|e| err!("invalid `room_id` field in PDU: {e}"))?
85				.strip_sigil();
86
87			for event_id in auth_events {
88				let CanonicalJsonValue::String(event_id) = event_id else {
89					return Err!(Request(InvalidParam(
90						"unexpected format of array item in `auth_events` field in PDU: \
91						 expected string, got {event_id:?}"
92					)));
93				};
94
95				let reference_hash =
96					event_id
97						.strip_prefix('$')
98						.ok_or(err!(Request(InvalidParam(
99							"unexpected format of array item in `auth_events` field in PDU: \
100							 string not beginning with the `$` sigil"
101						))))?;
102
103				if reference_hash == room_create_event_reference_hash {
104					return Err!(Request(InvalidParam(
105						"invalid `auth_events` field in PDU: cannot contain the `m.room.create` \
106						 event ID"
107					)));
108				}
109			}
110		}
111	}
112
113	// Check the presence, type and value of the `depth` field.
114	match pdu.get("depth") {
115		| Some(CanonicalJsonValue::Integer(value)) =>
116			if *value < int!(0) {
117				return Err!(Request(InvalidParam(
118					"invalid `depth` field in PDU: cannot be a negative integer"
119				)));
120			},
121		| Some(value) => {
122			return Err!(Request(InvalidParam(
123				"unexpected format of `depth` field in PDU: expected integer, got {value:?}"
124			)));
125		},
126		| None => return Err!(Request(InvalidParam("missing `depth` field in PDU"))),
127	}
128
129	Ok(())
130}
131
132/// Extract the optional string field with the given name from the given
133/// canonical JSON object.
134///
135/// Returns `Ok(Some(value))` if the field is present and a valid string,
136/// `Ok(None)` if the field is missing and `Err(_)` if the field is not a string
137/// or its length is bigger than [`ID_MAX_BYTES`].
138fn extract_optional_string_field<'a>(
139	object: &'a CanonicalJsonObject,
140	field: &'a str,
141) -> Result<Option<&'a String>> {
142	match object.get(field) {
143		| Some(CanonicalJsonValue::String(value)) =>
144			if value.len() > ID_MAX_BYTES {
145				Err!(Request(TooLarge(
146					"invalid `{field}` field in PDU: string length is larger than maximum of \
147					 {ID_MAX_BYTES} bytes"
148				)))
149			} else {
150				Ok(Some(value))
151			},
152
153		| Some(value) => Err!(Request(InvalidParam(
154			"unexpected format of `{field}` field in PDU: expected string, got {value:?}"
155		))),
156
157		| None => Ok(None),
158	}
159}
160
161/// Extract the required string field with the given name from the given
162/// canonical JSON object.
163///
164/// Returns `Ok(value)` if the field is present and a valid string and `Err(_)`
165/// if the field is missing, not a string or its length is bigger than
166/// [`ID_MAX_BYTES`].
167fn extract_required_string_field<'a>(
168	object: &'a CanonicalJsonObject,
169	field: &'a str,
170) -> Result<&'a String> {
171	extract_optional_string_field(object, field)?
172		.ok_or_else(|| err!(Request(InvalidParam("missing `{field}` field in PDU"))))
173}
174
175/// Extract the required array field with the given name from the given
176/// canonical JSON object.
177///
178/// Returns `Ok(value)` if the field is present and a valid array or `Err(_)` if
179/// the field is missing, not an array or its length is bigger than the given
180/// value.
181fn extract_required_array_field<'a>(
182	object: &'a CanonicalJsonObject,
183	field: &'a str,
184	max_len: usize,
185) -> Result<&'a [CanonicalJsonValue]> {
186	match object.get(field) {
187		| Some(CanonicalJsonValue::Array(value)) =>
188			if value.len() > max_len {
189				Err!(Request(TooLarge(
190					"invalid `{field}` field in PDU: array length is larger than maximum of \
191					 {max_len}"
192				)))
193			} else {
194				Ok(value)
195			},
196
197		| Some(value) => Err!(Request(InvalidParam(
198			"unexpected format of `{field}` field in PDU: expected array, got {value:?}"
199		))),
200
201		| None => Err!(Request(InvalidParam("missing `{field}` field in PDU"))),
202	}
203}
204
205#[cfg(test)]
206mod tests {
207	use std::iter::repeat_n;
208
209	use ruma::{
210		CanonicalJsonObject, CanonicalJsonValue, int, room_version_rules::EventFormatRules,
211	};
212	use serde_json::{from_value as from_json_value, json};
213
214	use super::check_rules as check_pdu_format;
215
216	/// Construct a PDU valid for the event format of room v1.
217	fn pdu_v1() -> CanonicalJsonObject {
218		let pdu = json!({
219			"auth_events": [
220				[
221					"$af232176:example.org",
222					{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" },
223				],
224			],
225			"content": {
226				"key": "value",
227			},
228			"depth": 12,
229			"event_id": "$a4ecee13e2accdadf56c1025:example.com",
230			"hashes": {
231				"sha256": "thishashcoversallfieldsincasethisisredacted"
232			},
233			"origin_server_ts": 1_838_188_000,
234			"prev_events": [
235				[
236					"$af232176:example.org",
237					{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" }
238				],
239			],
240			"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
241			"sender": "@alice:example.com",
242			"signatures": {
243				"example.com": {
244					"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
245				},
246			},
247			"type": "m.room.message",
248			"unsigned": {
249				"age": 4612,
250			},
251		});
252
253		from_json_value(pdu).unwrap()
254	}
255
256	/// Construct a PDU valid for the event format of room v3.
257	fn pdu_v3() -> CanonicalJsonObject {
258		let pdu = json!({
259			"auth_events": [
260				"$base64encodedeventid",
261				"$adifferenteventid",
262			],
263			"content": {
264				"key": "value",
265			},
266			"depth": 12,
267			"hashes": {
268				"sha256": "thishashcoversallfieldsincasethisisredacted",
269			},
270			"origin_server_ts": 1_838_188_000,
271			"prev_events": [
272				"$base64encodedeventid",
273				"$adifferenteventid",
274			],
275			"redacts": "$some/old+event",
276			"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
277			"sender": "@alice:example.com",
278			"signatures": {
279				"example.com": {
280					"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
281				},
282			},
283			"type": "m.room.message",
284			"unsigned": {
285				"age": 4612,
286			}
287		});
288
289		from_json_value(pdu).unwrap()
290	}
291
292	/// Construct an `m.room.create` PDU valid for the event format of
293	/// `org.matrix.hydra.11`.
294	fn room_create_hydra() -> CanonicalJsonObject {
295		let pdu = json!({
296			"auth_events": [],
297			"content": {
298				"room_version": "org.matrix.hydra.11",
299			},
300			"depth": 1,
301			"hashes": {
302				"sha256": "thishashcoversallfieldsincasethisisredacted",
303			},
304			"origin_server_ts": 1_838_188_000,
305			"prev_events": [],
306			"sender": "@alice:example.com",
307			"signatures": {
308				"example.com": {
309					"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
310				},
311			},
312			"type": "m.room.create",
313			"unsigned": {
314				"age": 4612,
315			}
316		});
317
318		from_json_value(pdu).unwrap()
319	}
320
321	/// Construct a PDU valid for the event format of `org.matrix.hydra.11`.
322	fn pdu_hydra() -> CanonicalJsonObject {
323		let pdu = json!({
324			"auth_events": [
325				"$base64encodedeventid",
326				"$adifferenteventid",
327			],
328			"content": {
329				"key": "value",
330			},
331			"depth": 12,
332			"hashes": {
333				"sha256": "thishashcoversallfieldsincasethisisredacted",
334			},
335			"origin_server_ts": 1_838_188_000,
336			"prev_events": [
337				"$base64encodedeventid",
338			],
339			"room_id": "!roomcreatereferencehash",
340			"sender": "@alice:example.com",
341			"signatures": {
342				"example.com": {
343					"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
344				},
345			},
346			"type": "m.room.message",
347			"unsigned": {
348				"age": 4612,
349			}
350		});
351
352		from_json_value(pdu).unwrap()
353	}
354
355	#[test]
356	fn check_pdu_format_valid_v1() {
357		check_pdu_format(&pdu_v1(), &EventFormatRules::V1).unwrap();
358	}
359
360	#[test]
361	fn check_pdu_format_valid_v3() {
362		check_pdu_format(&pdu_v3(), &EventFormatRules::V3).unwrap();
363	}
364
365	#[test]
366	fn check_pdu_format_pdu_too_big() {
367		// Add a lot of data in the content to reach MAX_PDU_SIZE.
368		let mut pdu = pdu_v3();
369		let content = pdu
370			.get_mut("content")
371			.unwrap()
372			.as_object_mut()
373			.unwrap();
374
375		let long_string = repeat_n('a', 66_000).collect::<String>();
376		content.insert("big_data".into(), long_string.into());
377		check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
378	}
379
380	#[test]
381	fn check_pdu_format_fields_missing() {
382		for field in
383			&["event_id", "sender", "room_id", "type", "prev_events", "auth_events", "depth"]
384		{
385			let mut pdu = pdu_v1();
386			pdu.remove(*field).unwrap();
387			check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
388		}
389	}
390
391	#[test]
392	fn check_pdu_format_strings_too_big() {
393		for field in &["event_id", "sender", "room_id", "type", "state_key"] {
394			let mut pdu = pdu_v1();
395			let value = repeat_n('a', 300).collect::<String>();
396			pdu.insert((*field).into(), value.into());
397			check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
398		}
399	}
400
401	#[test]
402	fn check_pdu_format_strings_wrong_format() {
403		for field in &["event_id", "sender", "room_id", "type", "state_key"] {
404			let mut pdu = pdu_v1();
405			pdu.insert((*field).into(), true.into());
406			check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
407		}
408	}
409
410	#[test]
411	fn check_pdu_format_arrays_too_big() {
412		for field in &["prev_events", "auth_events"] {
413			let mut pdu = pdu_v3();
414			let value: Vec<_> =
415				repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect();
416
417			pdu.insert((*field).into(), value.into());
418			check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
419		}
420	}
421
422	#[test]
423	fn check_pdu_format_arrays_wrong_format() {
424		for field in &["prev_events", "auth_events"] {
425			let mut pdu = pdu_v3();
426			pdu.insert((*field).into(), true.into());
427			check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
428		}
429	}
430
431	#[test]
432	fn check_pdu_format_negative_depth() {
433		let mut pdu = pdu_v3();
434		pdu.insert("depth".into(), int!(-1).into())
435			.unwrap();
436
437		check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
438	}
439
440	#[test]
441	fn check_pdu_format_depth_wrong_format() {
442		let mut pdu = pdu_v3();
443		pdu.insert("depth".into(), true.into());
444		check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
445	}
446
447	#[test]
448	fn check_pdu_format_valid_room_create_hydra() {
449		let pdu = room_create_hydra();
450		check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
451	}
452
453	#[test]
454	fn check_pdu_format_valid_hydra() {
455		let pdu = pdu_hydra();
456		check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
457	}
458
459	#[test]
460	fn check_pdu_format_hydra_with_room_create() {
461		let mut pdu = pdu_hydra();
462		pdu.get_mut("auth_events")
463			.unwrap()
464			.as_array_mut()
465			.unwrap()
466			.push("$roomcreatereferencehash".to_owned().into());
467
468		check_pdu_format(&pdu, &EventFormatRules::V12).unwrap_err();
469	}
470}