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
37pub(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
64pub(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
77pub(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
116pub(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
172pub(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, ¤t)?
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
252fn 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 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 ¤t,
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 ¤t,
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 ¤t,
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, ¤t)
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(×tamp)));
649 }
650}