Skip to main content

tuwunel_api_client/
state.rs

1use axum::extract::State;
2use futures::{FutureExt, StreamExt, TryFutureExt};
3use ruma::{
4	CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedEventId,
5	OwnedRoomAliasId, RoomId, UserId,
6	api::client::state::{
7		get_state_event_for_key::{self, v3::StateEventFormat},
8		get_state_events, send_state_event,
9	},
10	events::{
11		AnyStateEventContent, StateEventType,
12		invite_permission_config::InvitePermission,
13		room::{
14			canonical_alias::RoomCanonicalAliasEventContent,
15			history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
16			join_rules::{JoinRule, RoomJoinRulesEventContent},
17			member::{MembershipState, RoomMemberEventContent},
18			server_acl::RoomServerAclEventContent,
19		},
20	},
21	serde::Raw,
22};
23use serde_json::{json, value::to_raw_value};
24use tuwunel_core::{
25	Err, Result, err, is_false,
26	result::NotFound,
27	utils::{BoolExt, json::serialized_len, stream::BroadbandExt},
28};
29use tuwunel_matrix::{
30	Event,
31	pdu::{PduBuilder, PduEvent},
32};
33use tuwunel_service::{Services, profile::check_member_content};
34
35use crate::{Ruma, RumaResponse, with_membership};
36
37/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
38///
39/// Sends a state event into the room.
40pub(crate) async fn send_state_event_for_key_route(
41	State(services): State<crate::State>,
42	body: Ruma<send_state_event::v3::Request>,
43) -> Result<send_state_event::v3::Response> {
44	let sender_user = body.sender_user();
45
46	Ok(send_state_event::v3::Response {
47		event_id: send_state_event_for_key_helper(
48			&services,
49			sender_user,
50			&body.room_id,
51			&body.event_type,
52			&body.body.body,
53			&body.state_key,
54			if body.appservice_info.is_some() {
55				body.timestamp
56			} else {
57				None
58			},
59		)
60		.await?,
61	})
62}
63
64/// # `GET /_matrix/client/v3/rooms/{roomid}/state`
65///
66/// Get all state events for a room.
67///
68/// A caller who is not joined reads the state as of their departure if they
69/// left the room, and otherwise only while its history visibility admits them.
70pub(crate) async fn get_state_events_route(
71	State(services): State<crate::State>,
72	body: Ruma<get_state_events::v3::Request>,
73) -> Result<get_state_events::v3::Response> {
74	let sender_user = body.sender_user();
75
76	if !services
77		.state_accessor
78		.user_can_see_state_events(sender_user, &body.room_id)
79		.await
80	{
81		return Err!(Request(Forbidden("You don't have permission to view the room state.")));
82	}
83
84	let encrypted = services
85		.state_accessor
86		.is_encrypted_room(&body.room_id)
87		.await;
88
89	let shortstatehash = services
90		.state_accessor
91		.user_visible_shortstatehash(sender_user, &body.room_id, None)
92		.map(|result| {
93			result?.ok_or_else(|| {
94				err!(Request(Forbidden("You don't have permission to view the room state.")))
95			})
96		})
97		.await?;
98
99	let room_state = services
100		.state_accessor
101		.state_full_pdus(shortstatehash)
102		.map(Event::into_pdu)
103		.map(|pdu| pdu.without_private_unless_sender(Some(sender_user)))
104		.broad_then(async |pdu| with_membership(&services, pdu, sender_user, encrypted).await)
105		.map(Event::into_format)
106		.collect()
107		.await;
108
109	Ok(get_state_events::v3::Response { room_state })
110}
111
112/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}/{stateKey}`
113///
114/// Get single state event of a room with the specified state key.
115///
116/// The optional query parameter `?format=event|content` allows returning the
117/// full room state event or just the state event's content (default behaviour).
118/// A caller who is not joined reads the state as of their departure if they
119/// left the room, and otherwise only while its history visibility admits them.
120pub(crate) async fn get_state_events_for_key_route(
121	State(services): State<crate::State>,
122	body: Ruma<get_state_event_for_key::v3::Request>,
123) -> Result<get_state_event_for_key::v3::Response> {
124	let sender_user = body.sender_user();
125
126	if !services
127		.state_accessor
128		.user_can_see_state_events(sender_user, &body.room_id)
129		.await
130	{
131		return Err!(Request(NotFound(debug_warn!(
132			"You don't have permission to view the room state."
133		))));
134	}
135
136	let failed = |e| {
137		err!(Request(NotFound(debug_warn!(
138			room_id = ?body.room_id,
139			event_type = ?body.event_type,
140			"Failed to get state event: {e}.",
141		))))
142	};
143
144	let shortstatehash = services
145		.state_accessor
146		.user_visible_shortstatehash(sender_user, &body.room_id, None)
147		.map_err(failed)
148		.map(|result| {
149			result?.ok_or_else(|| {
150				err!(Request(NotFound(debug_warn!(
151					"You don't have permission to view the room state."
152				))))
153			})
154		})
155		.await?;
156
157	let event = services
158		.state_accessor
159		.state_get(shortstatehash, &body.event_type, &body.state_key)
160		.map_err(failed)
161		.map_ok(|event| event.without_private_unless_sender(Some(sender_user)))
162		.await?;
163
164	let event_or_content = match body.format {
165		| StateEventFormat::Event => json!({
166			"content": event.content(),
167			"event_id": event.event_id(),
168			"origin_server_ts": event.origin_server_ts(),
169			"room_id": event.room_id(),
170			"sender": event.sender(),
171			"state_key": event.state_key(),
172			"type": event.kind(),
173			"unsigned": event.unsigned(),
174		}),
175
176		| _ => event.get_content_as_value(),
177	};
178
179	let event_or_content = to_raw_value(&event_or_content).expect("serializable JSON value");
180
181	Ok(get_state_event_for_key::v3::Response::new(event_or_content))
182}
183
184/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}`
185///
186/// Get single state event of a room.
187///
188/// The optional query parameter `?format=event|content` allows returning the
189/// full room state event or just the state event's content (default behaviour).
190/// A caller who is not joined reads the state as of their departure if they
191/// left the room, and otherwise only while its history visibility admits them.
192pub(crate) async fn get_state_events_for_empty_key_route(
193	State(services): State<crate::State>,
194	body: Ruma<get_state_event_for_key::v3::Request>,
195) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
196	get_state_events_for_key_route(State(services), body)
197		.await
198		.map(RumaResponse)
199}
200
201async fn send_state_event_for_key_helper(
202	services: &Services,
203	sender: &UserId,
204	room_id: &RoomId,
205	event_type: &StateEventType,
206	json: &Raw<AnyStateEventContent>,
207	state_key: &str,
208	timestamp: Option<MilliSecondsSinceUnixEpoch>,
209) -> Result<OwnedEventId> {
210	allowed_to_send_state_event(services, sender, room_id, event_type, state_key, json).await?;
211
212	let state_lock = services.state.mutex.lock(room_id).await;
213
214	if *event_type == StateEventType::RoomMember {
215		check_member_content_size(services, sender, room_id, state_key, json).await?;
216	}
217
218	let current = match state_dedup_eligible(event_type, timestamp.as_ref()) {
219		| false => None,
220		| true => services
221			.state_accessor
222			.room_state_get(room_id, event_type, state_key)
223			.await
224			.optional()?,
225	};
226
227	if let Some(current) = current
228		&& current.sender() == sender
229	{
230		let content = json.deserialize_as_unchecked::<CanonicalJsonObject>()?;
231
232		if is_duplicate_state(event_type, sender, &content, &current)?
233			&& services
234				.state_cache
235				.is_joined(sender, room_id)
236				.await
237		{
238			return Ok(current.event_id().to_owned());
239		}
240	}
241
242	let event_id = services
243		.timeline
244		.build_and_append_pdu(
245			PduBuilder {
246				event_type: event_type.to_string().into(),
247				content: serde_json::from_str(json.json().get())?,
248				state_key: Some(state_key.into()),
249				timestamp,
250				..Default::default()
251			},
252			sender,
253			room_id,
254			&state_lock,
255		)
256		.await?;
257
258	Ok(event_id)
259}
260
261/// Refuses a client's member event that is over the configured budget, or that
262/// newly stores a display name or avatar URL past its limit.
263///
264/// A stored name or avatar the write leaves unchanged is not bounded again, as long
265/// as the whole content fits the byte cap. A missing or unreadable stored event
266/// checks every value, as a first write does.
267#[tracing::instrument(level = "debug", skip_all)]
268pub(crate) async fn check_member_content_size(
269	services: &Services,
270	sender: &UserId,
271	room_id: &RoomId,
272	state_key: &str,
273	json: &Raw<AnyStateEventContent>,
274) -> Result {
275	let content = json.deserialize_as_unchecked::<CanonicalJsonObject>()?;
276	let max_bytes = services.config.profile.member_event_max_bytes;
277	let length = serialized_len(&content)?;
278
279	// A non-string value is not bounded here; the typed content rejects it.
280	let string_field = |field: &str| {
281		content
282			.get(field)
283			.and_then(CanonicalJsonValue::as_str)
284	};
285
286	let check = |displayname, avatar_url| {
287		check_member_content(max_bytes, length, displayname, avatar_url)
288	};
289
290	if check(string_field("displayname"), string_field("avatar_url")).is_ok() {
291		return Ok(());
292	}
293
294	let stored = services
295		.state_accessor
296		.room_state_get_content::<CanonicalJsonObject>(
297			room_id,
298			&StateEventType::RoomMember,
299			state_key,
300		)
301		.map(Result::ok)
302		.await;
303
304	// The owner re-sending its own stored content stores nothing new.
305	if sender.as_str() == state_key && stored.as_ref() == Some(&content) {
306		return Ok(());
307	}
308
309	let is_new = |field: &str| {
310		stored
311			.as_ref()
312			.and_then(|stored| stored.get(field))
313			.ne(&content.get(field))
314	};
315
316	let changed = |field: &str| string_field(field).filter(|_| is_new(field));
317
318	check(changed("displayname"), changed("avatar_url"))
319}
320
321fn state_dedup_eligible(
322	event_type: &StateEventType,
323	timestamp: Option<&MilliSecondsSinceUnixEpoch>,
324) -> bool {
325	timestamp.is_none() && !matches!(event_type, StateEventType::RoomMember)
326}
327
328/// Whether an incoming state event is a content-identical resend by its own
329/// author.
330///
331/// The caller's guard returns before `tuwunel_matrix::room::state::resolution::auth_check` runs, so every
332/// conjunct gating that early return must be a version-invariant fact that can
333/// only suppress a dedup, never permit one. Membership class qualifies; power
334/// levels and per-type rules do not, and wanting an exact status code there is
335/// a reason to move the guard after authorization rather than to add a
336/// conjunct.
337fn is_duplicate_state(
338	event_type: &StateEventType,
339	sender: &UserId,
340	content: &CanonicalJsonObject,
341	current: &PduEvent,
342) -> Result<bool> {
343	if matches!(event_type, StateEventType::RoomMember) || current.sender() != sender {
344		return Ok(false);
345	}
346
347	let current_content = current.content.deserialize()?;
348
349	Ok(current_content == *content)
350}
351
352/// Applies the server's policy for sending a state event, before the room
353/// lock.
354///
355/// Refuses what room auth alone would allow but this server forbids: changes
356/// to `m.room.create`, a server ACL that would lock this server out, admin
357/// room exposure, foreign aliases, and membership changes the sender is
358/// barred from.
359pub(crate) async fn allowed_to_send_state_event(
360	services: &Services,
361	sender: &UserId,
362	room_id: &RoomId,
363	event_type: &StateEventType,
364	state_key: &str,
365	json: &Raw<AnyStateEventContent>,
366) -> Result {
367	let suspended = services.users.is_suspended(sender).await;
368
369	if suspended && !matches!(event_type, StateEventType::RoomMember) {
370		return Err!(Request(UserSuspended("Account is suspended.")));
371	}
372
373	match event_type {
374		| StateEventType::RoomCreate => Err!(Request(BadJson(debug_warn!(
375			?room_id,
376			"You cannot update m.room.create after a room has been created."
377		)))),
378		| StateEventType::RoomServerAcl => validate_server_acl(services, room_id, json),
379		| StateEventType::RoomEncryption => validate_encryption(services),
380		| StateEventType::RoomJoinRules => validate_join_rules(services, room_id, json).await,
381		| StateEventType::RoomHistoryVisibility =>
382			validate_history_visibility(services, room_id, json).await,
383		| StateEventType::RoomCanonicalAlias =>
384			validate_canonical_alias(services, room_id, json).await,
385		| StateEventType::RoomMember =>
386			validate_member(services, sender, room_id, state_key, json, suspended).await,
387		| _ => Ok(()),
388	}
389}
390
391fn validate_encryption(services: &Services) -> Result {
392	services
393		.config
394		.encryption
395		.allow
396		.then_some(())
397		.ok_or_else(|| err!(Request(Forbidden("Encryption is disabled on this homeserver."))))
398}
399
400fn validate_server_acl(
401	services: &Services,
402	room_id: &RoomId,
403	json: &Raw<AnyStateEventContent>,
404) -> Result {
405	let acl_content = json
406		.deserialize_as_unchecked::<RoomServerAclEventContent>()
407		.map_err(|e| {
408			err!(Request(BadJson(debug_warn!("Room server ACL event is invalid: {e}"))))
409		})?;
410
411	if acl_content.allow_is_empty() {
412		return Err!(Request(BadJson(debug_warn!(
413			?room_id,
414			"Sending an ACL event with an empty allow key will permanently brick the room for \
415			 non-tuwunel's as this equates to no servers being allowed to participate in this \
416			 room."
417		))));
418	}
419
420	if acl_content.deny_contains("*") && acl_content.allow_contains("*") {
421		return Err!(Request(BadJson(debug_warn!(
422			?room_id,
423			"Sending an ACL event with a deny and allow key value of \"*\" will permanently \
424			 brick the room for non-tuwunel's as this equates to no servers being allowed to \
425			 participate in this room."
426		))));
427	}
428
429	let server_name = services.globals.server_name();
430	let self_allowed =
431		acl_content.is_allowed(server_name) || acl_content.allow_contains(server_name.as_str());
432
433	if acl_content.deny_contains("*") && !self_allowed {
434		return Err!(Request(BadJson(debug_warn!(
435			?room_id,
436			"Sending an ACL event with a deny key value of \"*\" and without your own server \
437			 name in the allow key will result in you being unable to participate in this room."
438		))));
439	}
440
441	if !acl_content.allow_contains("*") && !self_allowed {
442		return Err!(Request(BadJson(debug_warn!(
443			?room_id,
444			"Sending an ACL event for an allow key without \"*\" and without your own server \
445			 name in the allow key will result in you being unable to participate in this room."
446		))));
447	}
448
449	Ok(())
450}
451
452async fn validate_join_rules(
453	services: &Services,
454	room_id: &RoomId,
455	json: &Raw<AnyStateEventContent>,
456) -> Result {
457	let Ok(admin_room_id) = services.admin.get_admin_room().await else {
458		return Ok(());
459	};
460
461	if admin_room_id != room_id {
462		return Ok(());
463	}
464
465	let join_rule = json
466		.deserialize_as_unchecked::<RoomJoinRulesEventContent>()
467		.map_err(|e| {
468			err!(Request(BadJson(debug_warn!("Room join rules event is invalid: {e}"))))
469		})?;
470
471	if join_rule.join_rule == JoinRule::Public {
472		return Err!(Request(Forbidden(
473			"Admin room is a sensitive room, it cannot be made public"
474		)));
475	}
476
477	Ok(())
478}
479
480async fn validate_history_visibility(
481	services: &Services,
482	room_id: &RoomId,
483	json: &Raw<AnyStateEventContent>,
484) -> Result {
485	let Ok(admin_room_id) = services.admin.get_admin_room().await else {
486		return Ok(());
487	};
488
489	let visibility_content = json
490		.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>()
491		.map_err(|e| {
492			err!(Request(BadJson(debug_warn!("Room history visibility event is invalid: {e}"))))
493		})?;
494
495	if admin_room_id == room_id
496		&& visibility_content.history_visibility == HistoryVisibility::WorldReadable
497	{
498		return Err!(Request(Forbidden(
499			"Admin room is a sensitive room, it cannot be made world readable (public room \
500			 history)."
501		)));
502	}
503
504	Ok(())
505}
506
507async fn validate_canonical_alias(
508	services: &Services,
509	room_id: &RoomId,
510	json: &Raw<AnyStateEventContent>,
511) -> Result {
512	let canonical_alias_content = json
513		.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>()
514		.map_err(|e| {
515			err!(Request(InvalidParam(debug_warn!("Room canonical alias event is invalid: {e}"))))
516		})?;
517
518	let current_aliases: Vec<OwnedRoomAliasId> = services
519		.state_accessor
520		.room_state_get_content::<RoomCanonicalAliasEventContent>(
521			room_id,
522			&StateEventType::RoomCanonicalAlias,
523			"",
524		)
525		.await
526		.ok()
527		.map(|content| content.aliases().cloned().collect())
528		.unwrap_or_default();
529
530	let new_aliases = canonical_alias_content
531		.aliases()
532		.filter(|alias| !current_aliases.contains(alias));
533
534	for alias in new_aliases {
535		let (alias_room_id, _servers) = services
536			.alias
537			.resolve_alias(alias)
538			.await
539			.map_err(|e| err!(Request(BadAlias("Failed resolving alias \"{alias}\": {e}"))))?;
540
541		if alias_room_id != room_id {
542			return Err!(Request(BadAlias(
543				"Room alias {alias} does not belong to room {room_id}"
544			)));
545		}
546	}
547
548	Ok(())
549}
550
551async fn validate_member(
552	services: &Services,
553	sender: &UserId,
554	room_id: &RoomId,
555	state_key: &str,
556	json: &Raw<AnyStateEventContent>,
557	suspended: bool,
558) -> Result {
559	let membership_content = json
560		.deserialize_as_unchecked::<RoomMemberEventContent>()
561		.map_err(|e| {
562			err!(Request(BadJson(
563				"Membership content must have a valid JSON body with at least a valid \
564				 membership state: {e}"
565			)))
566		})?;
567
568	let Ok(target_user) = UserId::parse(state_key) else {
569		return Err!(Request(BadJson("Membership event has invalid or non-existent state key")));
570	};
571
572	if suspended
573		&& (membership_content.membership != MembershipState::Leave || target_user != sender)
574	{
575		return Err!(Request(UserSuspended("Account is suspended.")));
576	}
577
578	if !matches!(membership_content.membership, MembershipState::Leave | MembershipState::Ban)
579		&& services.metadata.is_banned(room_id).await
580		&& !services.admin.user_is_admin(sender).await
581	{
582		return Err!(Request(Forbidden("This room is banned on this homeserver.")));
583	}
584
585	if membership_content.membership == MembershipState::Invite
586		&& services.globals.user_is_local(&target_user)
587		&& services
588			.users
589			.invite_permission(sender, &target_user)
590			.await
591			.eq(&InvitePermission::Block)
592	{
593		return Err!(Request(InviteBlocked("{target_user} has blocked this invite.")));
594	}
595
596	let Some(authorising_user) = membership_content.join_authorized_via_users_server else {
597		return Ok(());
598	};
599
600	if membership_content.membership != MembershipState::Join {
601		return Err!(Request(BadJson(
602			"join_authorised_via_users_server is only for member joins"
603		)));
604	}
605
606	// Already joined or invited: no restricted-join authorisation needed.
607	if services
608		.state_cache
609		.user_membership(&target_user, room_id)
610		.await
611		.is_some_and(|m| matches!(m, MembershipState::Join | MembershipState::Invite))
612	{
613		return Ok(());
614	}
615
616	if !services.globals.user_is_local(&authorising_user) {
617		return Err!(Request(InvalidParam(
618			"Authorising user {authorising_user} does not belong to this homeserver"
619		)));
620	}
621
622	services
623		.state_cache
624		.is_joined(&authorising_user, room_id)
625		.map(is_false!())
626		.map(BoolExt::into_result)
627		.map_err(|()| {
628			err!(Request(InvalidParam(
629				"Authorising user {authorising_user} is not in the room. They cannot authorise \
630				 the join."
631			)))
632		})
633		.await
634}
635
636#[cfg(test)]
637mod tests {
638	use ruma::user_id;
639	use serde_json::{Value as JsonValue, from_str, from_value};
640
641	use super::*;
642
643	fn current_state(sender: &str, content: &JsonValue) -> PduEvent {
644		from_value(json!({
645			"type": "m.room.history_visibility",
646			"content": content,
647			"state_key": "",
648			"event_id": "$event:example.com",
649			"room_id": "!room:example.com",
650			"sender": sender,
651			"prev_events": [],
652			"auth_events": [],
653			"origin_server_ts": 1,
654			"depth": 1,
655			"hashes": { "sha256": "thishashcoversallfieldsincasethisisredacted" },
656		}))
657		.expect("valid pdu")
658	}
659
660	#[test]
661	fn identical_state_content_is_duplicate() {
662		let sender = user_id!("@alice:example.com");
663		let current = current_state(
664			sender.as_str(),
665			&json!({ "history_visibility": "shared", "extra": true }),
666		);
667
668		let content = from_str::<CanonicalJsonObject>(
669			r#"{ "extra": true, "history_visibility": "shared" }"#,
670		)
671		.expect("canonical content");
672
673		assert!(
674			is_duplicate_state(
675				&StateEventType::RoomHistoryVisibility,
676				sender,
677				&content,
678				&current,
679			)
680			.expect("comparison")
681		);
682	}
683
684	#[test]
685	fn changed_state_content_is_not_duplicate() {
686		let sender = user_id!("@alice:example.com");
687		let current = current_state(sender.as_str(), &json!({ "history_visibility": "shared" }));
688		let content =
689			from_str(r#"{ "history_visibility": "world_readable" }"#).expect("canonical content");
690
691		assert!(
692			!is_duplicate_state(
693				&StateEventType::RoomHistoryVisibility,
694				sender,
695				&content,
696				&current,
697			)
698			.expect("comparison")
699		);
700	}
701
702	#[test]
703	fn different_sender_is_not_duplicate() {
704		let current =
705			current_state("@alice:example.com", &json!({ "history_visibility": "shared" }));
706
707		let content =
708			from_str(r#"{ "history_visibility": "shared" }"#).expect("canonical content");
709
710		assert!(
711			!is_duplicate_state(
712				&StateEventType::RoomHistoryVisibility,
713				user_id!("@bob:example.com"),
714				&content,
715				&current,
716			)
717			.expect("comparison")
718		);
719	}
720
721	#[test]
722	fn member_state_is_not_duplicate() {
723		let sender = user_id!("@alice:example.com");
724		let current = current_state(sender.as_str(), &json!({ "membership": "join" }));
725		let content = from_str(r#"{ "membership": "join" }"#).expect("canonical content");
726
727		assert!(
728			!is_duplicate_state(&StateEventType::RoomMember, sender, &content, &current)
729				.expect("comparison")
730		);
731	}
732
733	#[test]
734	fn timestamped_state_is_not_eligible_for_dedup() {
735		let event_type = StateEventType::RoomHistoryVisibility;
736		let timestamp = MilliSecondsSinceUnixEpoch::now();
737
738		assert!(state_dedup_eligible(&event_type, None));
739		assert!(!state_dedup_eligible(&event_type, Some(&timestamp)));
740	}
741}