Skip to main content

tuwunel_api/client/
state.rs

1use axum::extract::State;
2use futures::{FutureExt, TryFutureExt, TryStreamExt};
3use ruma::{
4	CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomAliasId, RoomId,
5	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	matrix::{
27		Event,
28		pdu::{PduBuilder, PduEvent},
29	},
30	result::NotFound,
31	utils::{BoolExt, stream::TryBroadbandExt},
32};
33use tuwunel_service::Services;
34
35use crate::{Ruma, RumaResponse, client::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/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}`
65///
66/// Sends a state event into the room.
67pub(crate) async fn send_state_event_for_empty_key_route(
68	State(services): State<crate::State>,
69	body: Ruma<send_state_event::v3::Request>,
70) -> Result<RumaResponse<send_state_event::v3::Response>> {
71	send_state_event_for_key_route(State(services), body)
72		.boxed()
73		.await
74		.map(RumaResponse)
75}
76
77/// # `GET /_matrix/client/v3/rooms/{roomid}/state`
78///
79/// Get all state events for a room.
80///
81/// - If not joined: Only works if current room history visibility is world
82///   readable
83pub(crate) async fn get_state_events_route(
84	State(services): State<crate::State>,
85	body: Ruma<get_state_events::v3::Request>,
86) -> Result<get_state_events::v3::Response> {
87	let sender_user = body.sender_user();
88
89	if !services
90		.state_accessor
91		.user_can_see_state_events(sender_user, &body.room_id)
92		.await
93	{
94		return Err!(Request(Forbidden("You don't have permission to view the room state.")));
95	}
96
97	let encrypted = services
98		.state_accessor
99		.is_encrypted_room(&body.room_id)
100		.await;
101
102	let room_state = services
103		.state_accessor
104		.room_state_full_pdus(&body.room_id)
105		.map_ok(Event::into_pdu)
106		.broad_and_then(async |pdu| {
107			Ok(with_membership(&services, pdu, sender_user, encrypted).await)
108		})
109		.map_ok(Event::into_format)
110		.try_collect()
111		.await?;
112
113	Ok(get_state_events::v3::Response { room_state })
114}
115
116/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}/{stateKey}`
117///
118/// Get single state event of a room with the specified state key.
119/// The optional query parameter `?format=event|content` allows returning the
120/// full room state event or just the state event's content (default behaviour)
121///
122/// - If not joined: Only works if current room history visibility is world
123///   readable
124pub(crate) async fn get_state_events_for_key_route(
125	State(services): State<crate::State>,
126	body: Ruma<get_state_event_for_key::v3::Request>,
127) -> Result<get_state_event_for_key::v3::Response> {
128	let sender_user = body.sender_user();
129
130	if !services
131		.state_accessor
132		.user_can_see_state_events(sender_user, &body.room_id)
133		.await
134	{
135		return Err!(Request(NotFound(debug_warn!(
136			"You don't have permission to view the room state."
137		))));
138	}
139
140	let event = services
141		.state_accessor
142		.room_state_get(&body.room_id, &body.event_type, &body.state_key)
143		.await
144		.map_err(|e| {
145			err!(Request(NotFound(debug_warn!(
146				room_id = ?body.room_id,
147				event_type = ?body.event_type,
148				"Failed to get state event: {e}.",
149			))))
150		})?;
151
152	let event_or_content = match body.format {
153		| StateEventFormat::Event => json!({
154			"content": event.content(),
155			"event_id": event.event_id(),
156			"origin_server_ts": event.origin_server_ts(),
157			"room_id": event.room_id(),
158			"sender": event.sender(),
159			"state_key": event.state_key(),
160			"type": event.kind(),
161			"unsigned": event.unsigned(),
162		}),
163
164		| _ => event.get_content_as_value(),
165	};
166
167	let event_or_content = to_raw_value(&event_or_content).expect("serializable JSON value");
168
169	Ok(get_state_event_for_key::v3::Response::new(event_or_content))
170}
171
172/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}`
173///
174/// Get single state event of a room.
175/// The optional query parameter `?format=event|content` allows returning the
176/// full room state event or just the state event's content (default behaviour)
177///
178/// - If not joined: Only works if current room history visibility is world
179///   readable
180pub(crate) async fn get_state_events_for_empty_key_route(
181	State(services): State<crate::State>,
182	body: Ruma<get_state_event_for_key::v3::Request>,
183) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
184	get_state_events_for_key_route(State(services), body)
185		.await
186		.map(RumaResponse)
187}
188
189async fn send_state_event_for_key_helper(
190	services: &Services,
191	sender: &UserId,
192	room_id: &RoomId,
193	event_type: &StateEventType,
194	json: &Raw<AnyStateEventContent>,
195	state_key: &str,
196	timestamp: Option<MilliSecondsSinceUnixEpoch>,
197) -> Result<OwnedEventId> {
198	allowed_to_send_state_event(services, sender, room_id, event_type, state_key, json).await?;
199	let state_lock = services.state.mutex.lock(room_id).await;
200
201	let current = match state_dedup_eligible(event_type, timestamp.as_ref()) {
202		| false => None,
203		| true => services
204			.state_accessor
205			.room_state_get(room_id, event_type, state_key)
206			.await
207			.optional()?,
208	};
209
210	if let Some(current) = current
211		&& current.sender() == sender
212	{
213		let content = json.deserialize_as_unchecked::<CanonicalJsonObject>()?;
214
215		if is_duplicate_state(event_type, sender, &content, &current)?
216			&& services
217				.state_cache
218				.is_joined(sender, room_id)
219				.await
220		{
221			return Ok(current.event_id().to_owned());
222		}
223	}
224
225	let event_id = services
226		.timeline
227		.build_and_append_pdu(
228			PduBuilder {
229				event_type: event_type.to_string().into(),
230				content: serde_json::from_str(json.json().get())?,
231				state_key: Some(state_key.into()),
232				timestamp,
233				..Default::default()
234			},
235			sender,
236			room_id,
237			&state_lock,
238		)
239		.boxed()
240		.await?;
241
242	Ok(event_id)
243}
244
245fn state_dedup_eligible(
246	event_type: &StateEventType,
247	timestamp: Option<&MilliSecondsSinceUnixEpoch>,
248) -> bool {
249	timestamp.is_none() && !matches!(event_type, StateEventType::RoomMember)
250}
251
252/// Whether an incoming state event is a content-identical resend by its own
253/// author.
254///
255/// The caller's guard returns before `state_res::auth_check` runs, so every
256/// conjunct gating that early return must be a version-invariant fact that can
257/// only suppress a dedup, never permit one. Membership class qualifies; power
258/// levels and per-type rules do not, and wanting an exact status code there is
259/// a reason to move the guard after authorization rather than to add a
260/// conjunct.
261fn is_duplicate_state(
262	event_type: &StateEventType,
263	sender: &UserId,
264	content: &CanonicalJsonObject,
265	current: &PduEvent,
266) -> Result<bool> {
267	if matches!(event_type, StateEventType::RoomMember) || current.sender() != sender {
268		return Ok(false);
269	}
270
271	let current_content = current.content.deserialize()?;
272
273	Ok(current_content == *content)
274}
275
276async fn allowed_to_send_state_event(
277	services: &Services,
278	sender: &UserId,
279	room_id: &RoomId,
280	event_type: &StateEventType,
281	state_key: &str,
282	json: &Raw<AnyStateEventContent>,
283) -> Result {
284	let suspended = services.users.is_suspended(sender).await;
285
286	if suspended && !matches!(event_type, StateEventType::RoomMember) {
287		return Err!(Request(UserSuspended("Account is suspended.")));
288	}
289
290	match event_type {
291		| StateEventType::RoomCreate => Err!(Request(BadJson(debug_warn!(
292			?room_id,
293			"You cannot update m.room.create after a room has been created."
294		)))),
295		| StateEventType::RoomServerAcl => validate_server_acl(services, room_id, json),
296		| StateEventType::RoomEncryption => validate_encryption(services),
297		| StateEventType::RoomJoinRules => validate_join_rules(services, room_id, json).await,
298		| StateEventType::RoomHistoryVisibility =>
299			validate_history_visibility(services, room_id, json).await,
300		| StateEventType::RoomCanonicalAlias =>
301			validate_canonical_alias(services, room_id, json).await,
302		| StateEventType::RoomMember =>
303			validate_member(services, sender, room_id, state_key, json, suspended).await,
304		| _ => Ok(()),
305	}
306}
307
308fn validate_encryption(services: &Services) -> Result {
309	services
310		.config
311		.allow_encryption
312		.then_some(())
313		.ok_or_else(|| err!(Request(Forbidden("Encryption is disabled on this homeserver."))))
314}
315
316fn validate_server_acl(
317	services: &Services,
318	room_id: &RoomId,
319	json: &Raw<AnyStateEventContent>,
320) -> Result {
321	let acl_content = json
322		.deserialize_as_unchecked::<RoomServerAclEventContent>()
323		.map_err(|e| {
324			err!(Request(BadJson(debug_warn!("Room server ACL event is invalid: {e}"))))
325		})?;
326
327	if acl_content.allow_is_empty() {
328		return Err!(Request(BadJson(debug_warn!(
329			?room_id,
330			"Sending an ACL event with an empty allow key will permanently brick the room for \
331			 non-tuwunel's as this equates to no servers being allowed to participate in this \
332			 room."
333		))));
334	}
335
336	if acl_content.deny_contains("*") && acl_content.allow_contains("*") {
337		return Err!(Request(BadJson(debug_warn!(
338			?room_id,
339			"Sending an ACL event with a deny and allow key value of \"*\" will permanently \
340			 brick the room for non-tuwunel's as this equates to no servers being allowed to \
341			 participate in this room."
342		))));
343	}
344
345	let server_name = services.globals.server_name();
346	let self_allowed =
347		acl_content.is_allowed(server_name) || acl_content.allow_contains(server_name.as_str());
348
349	if acl_content.deny_contains("*") && !self_allowed {
350		return Err!(Request(BadJson(debug_warn!(
351			?room_id,
352			"Sending an ACL event with a deny key value of \"*\" and without your own server \
353			 name in the allow key will result in you being unable to participate in this room."
354		))));
355	}
356
357	if !acl_content.allow_contains("*") && !self_allowed {
358		return Err!(Request(BadJson(debug_warn!(
359			?room_id,
360			"Sending an ACL event for an allow key without \"*\" and without your own server \
361			 name in the allow key will result in you being unable to participate in this room."
362		))));
363	}
364
365	Ok(())
366}
367
368async fn validate_join_rules(
369	services: &Services,
370	room_id: &RoomId,
371	json: &Raw<AnyStateEventContent>,
372) -> Result {
373	let Ok(admin_room_id) = services.admin.get_admin_room().await else {
374		return Ok(());
375	};
376
377	if admin_room_id != room_id {
378		return Ok(());
379	}
380
381	let join_rule = json
382		.deserialize_as_unchecked::<RoomJoinRulesEventContent>()
383		.map_err(|e| {
384			err!(Request(BadJson(debug_warn!("Room join rules event is invalid: {e}"))))
385		})?;
386
387	if join_rule.join_rule == JoinRule::Public {
388		return Err!(Request(Forbidden(
389			"Admin room is a sensitive room, it cannot be made public"
390		)));
391	}
392
393	Ok(())
394}
395
396async fn validate_history_visibility(
397	services: &Services,
398	room_id: &RoomId,
399	json: &Raw<AnyStateEventContent>,
400) -> Result {
401	let Ok(admin_room_id) = services.admin.get_admin_room().await else {
402		return Ok(());
403	};
404
405	let visibility_content = json
406		.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>()
407		.map_err(|e| {
408			err!(Request(BadJson(debug_warn!("Room history visibility event is invalid: {e}"))))
409		})?;
410
411	if admin_room_id == room_id
412		&& visibility_content.history_visibility == HistoryVisibility::WorldReadable
413	{
414		return Err!(Request(Forbidden(
415			"Admin room is a sensitive room, it cannot be made world readable (public room \
416			 history)."
417		)));
418	}
419
420	Ok(())
421}
422
423async fn validate_canonical_alias(
424	services: &Services,
425	room_id: &RoomId,
426	json: &Raw<AnyStateEventContent>,
427) -> Result {
428	let canonical_alias_content = json
429		.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>()
430		.map_err(|e| {
431			err!(Request(InvalidParam(debug_warn!("Room canonical alias event is invalid: {e}"))))
432		})?;
433
434	let current_aliases: Vec<OwnedRoomAliasId> = services
435		.state_accessor
436		.room_state_get_content::<RoomCanonicalAliasEventContent>(
437			room_id,
438			&StateEventType::RoomCanonicalAlias,
439			"",
440		)
441		.await
442		.ok()
443		.map(|content| content.aliases().cloned().collect())
444		.unwrap_or_default();
445
446	let new_aliases = canonical_alias_content
447		.aliases()
448		.filter(|alias| !current_aliases.contains(alias));
449
450	for alias in new_aliases {
451		let (alias_room_id, _servers) = services
452			.alias
453			.resolve_alias(alias)
454			.await
455			.map_err(|e| err!(Request(BadAlias("Failed resolving alias \"{alias}\": {e}"))))?;
456
457		if alias_room_id != room_id {
458			return Err!(Request(BadAlias(
459				"Room alias {alias} does not belong to room {room_id}"
460			)));
461		}
462	}
463
464	Ok(())
465}
466
467async fn validate_member(
468	services: &Services,
469	sender: &UserId,
470	room_id: &RoomId,
471	state_key: &str,
472	json: &Raw<AnyStateEventContent>,
473	suspended: bool,
474) -> Result {
475	let membership_content = json
476		.deserialize_as_unchecked::<RoomMemberEventContent>()
477		.map_err(|e| {
478			err!(Request(BadJson(
479				"Membership content must have a valid JSON body with at least a valid \
480				 membership state: {e}"
481			)))
482		})?;
483
484	let Ok(target_user) = UserId::parse(state_key) else {
485		return Err!(Request(BadJson("Membership event has invalid or non-existent state key")));
486	};
487
488	if suspended
489		&& (membership_content.membership != MembershipState::Leave || target_user != sender)
490	{
491		return Err!(Request(UserSuspended("Account is suspended.")));
492	}
493
494	if membership_content.membership == MembershipState::Invite
495		&& services.globals.user_is_local(&target_user)
496		&& services
497			.users
498			.invite_permission(sender, &target_user)
499			.await
500			.eq(&InvitePermission::Block)
501	{
502		return Err!(Request(InviteBlocked("{target_user} has blocked this invite.")));
503	}
504
505	let Some(authorising_user) = membership_content.join_authorized_via_users_server else {
506		return Ok(());
507	};
508
509	if membership_content.membership != MembershipState::Join {
510		return Err!(Request(BadJson(
511			"join_authorised_via_users_server is only for member joins"
512		)));
513	}
514
515	// Already joined or invited: no restricted-join authorisation needed.
516	if services
517		.state_cache
518		.user_membership(&target_user, room_id)
519		.await
520		.is_some_and(|m| matches!(m, MembershipState::Join | MembershipState::Invite))
521	{
522		return Ok(());
523	}
524
525	if !services.globals.user_is_local(&authorising_user) {
526		return Err!(Request(InvalidParam(
527			"Authorising user {authorising_user} does not belong to this homeserver"
528		)));
529	}
530
531	services
532		.state_cache
533		.is_joined(&authorising_user, room_id)
534		.map(is_false!())
535		.map(BoolExt::into_result)
536		.map_err(|()| {
537			err!(Request(InvalidParam(
538				"Authorising user {authorising_user} is not in the room. They cannot authorise \
539				 the join."
540			)))
541		})
542		.await
543}
544
545#[cfg(test)]
546mod tests {
547	use ruma::user_id;
548	use serde_json::{Value as JsonValue, from_str, from_value};
549
550	use super::*;
551
552	fn current_state(sender: &str, content: &JsonValue) -> PduEvent {
553		from_value(json!({
554			"type": "m.room.history_visibility",
555			"content": content,
556			"state_key": "",
557			"event_id": "$event:example.com",
558			"room_id": "!room:example.com",
559			"sender": sender,
560			"prev_events": [],
561			"auth_events": [],
562			"origin_server_ts": 1,
563			"depth": 1,
564			"hashes": { "sha256": "thishashcoversallfieldsincasethisisredacted" },
565		}))
566		.expect("valid pdu")
567	}
568
569	#[test]
570	fn identical_state_content_is_duplicate() {
571		let sender = user_id!("@alice:example.com");
572		let current = current_state(
573			sender.as_str(),
574			&json!({ "history_visibility": "shared", "extra": true }),
575		);
576
577		let content = from_str::<CanonicalJsonObject>(
578			r#"{ "extra": true, "history_visibility": "shared" }"#,
579		)
580		.expect("canonical content");
581
582		assert!(
583			is_duplicate_state(
584				&StateEventType::RoomHistoryVisibility,
585				sender,
586				&content,
587				&current,
588			)
589			.expect("comparison")
590		);
591	}
592
593	#[test]
594	fn changed_state_content_is_not_duplicate() {
595		let sender = user_id!("@alice:example.com");
596		let current = current_state(sender.as_str(), &json!({ "history_visibility": "shared" }));
597		let content =
598			from_str(r#"{ "history_visibility": "world_readable" }"#).expect("canonical content");
599
600		assert!(
601			!is_duplicate_state(
602				&StateEventType::RoomHistoryVisibility,
603				sender,
604				&content,
605				&current,
606			)
607			.expect("comparison")
608		);
609	}
610
611	#[test]
612	fn different_sender_is_not_duplicate() {
613		let current =
614			current_state("@alice:example.com", &json!({ "history_visibility": "shared" }));
615
616		let content =
617			from_str(r#"{ "history_visibility": "shared" }"#).expect("canonical content");
618
619		assert!(
620			!is_duplicate_state(
621				&StateEventType::RoomHistoryVisibility,
622				user_id!("@bob:example.com"),
623				&content,
624				&current,
625			)
626			.expect("comparison")
627		);
628	}
629
630	#[test]
631	fn member_state_is_not_duplicate() {
632		let sender = user_id!("@alice:example.com");
633		let current = current_state(sender.as_str(), &json!({ "membership": "join" }));
634		let content = from_str(r#"{ "membership": "join" }"#).expect("canonical content");
635
636		assert!(
637			!is_duplicate_state(&StateEventType::RoomMember, sender, &content, &current)
638				.expect("comparison")
639		);
640	}
641
642	#[test]
643	fn timestamped_state_is_not_eligible_for_dedup() {
644		let event_type = StateEventType::RoomHistoryVisibility;
645		let timestamp = MilliSecondsSinceUnixEpoch::now();
646
647		assert!(state_dedup_eligible(&event_type, None));
648		assert!(!state_dedup_eligible(&event_type, Some(&timestamp)));
649	}
650}