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
35pub(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
62pub(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
75pub(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
114pub(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
170pub(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, ¤t)?
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
251fn 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 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 ¤t,
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 ¤t,
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 ¤t,
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, ¤t)
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(×tamp)));
644 }
645}