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