From 7a45c25d7a82389042bd169eee9c8b47a9bece6a Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Fri, 15 Aug 2025 23:57:11 +0200 Subject: [PATCH 1/4] fix: don't lookup create event when converting stripped state by making the caller pass the room version rules, fixing stripped state conversion for invites over federation (cherry picked from commit 03dfa72b8f0380bc29ce779f1bf5af00d686bb95) --- src/api/client_server/membership.rs | 2 +- src/api/server_server.rs | 2 +- src/utils/mod.rs | 12 +++--------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/api/client_server/membership.rs b/src/api/client_server/membership.rs index 91ae3ef6..522b44e7 100644 --- a/src/api/client_server/membership.rs +++ b/src/api/client_server/membership.rs @@ -231,7 +231,7 @@ pub async fn knock_room_route( .to_stripped_state_event() .into(), ); - let stripped_state = utils::convert_stripped_state(stripped_state, &room_id)?; + let stripped_state = utils::convert_stripped_state(stripped_state, &rules)?; services().rooms.state_cache.update_membership( &room_id, diff --git a/src/api/server_server.rs b/src/api/server_server.rs index a1d50a3d..24f96176 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -2169,7 +2169,7 @@ pub async fn create_invite_route( })?; invite_state.push(pdu.to_stripped_state_event().into()); - let invite_state = utils::convert_stripped_state(invite_state, &room_id)?; + let invite_state = utils::convert_stripped_state(invite_state, &rules)?; // If we are active in the room, the remote server will notify us about the join via /send if !services() diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 12ba3d51..f1411016 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -22,7 +22,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use crate::{service::pdu::gen_event_id_canonical_json, services, Result}; +use crate::{service::pdu::gen_event_id_canonical_json, Result}; pub fn millis_since_unix_epoch() -> u64 { SystemTime::now() @@ -202,20 +202,14 @@ impl fmt::Display for HtmlEscape<'_> { /// Converts `RawStrippedState` (federation format) into `Raw` (client format) pub fn convert_stripped_state( stripped_state: Vec, - room_id: &RoomId, + rules: &RoomVersionRules, ) -> Result>> { stripped_state .into_iter() .map(|stripped_state| match stripped_state { RawStrippedState::Stripped(state) => Ok(state.cast()), RawStrippedState::Pdu(state) => { - let rules = services() - .rooms - .state - .get_room_version(room_id)? - .rules() - .expect("Supported room version must have rules."); - let (event_id, mut event) = gen_event_id_canonical_json(&state, &rules)?; + let (event_id, mut event) = gen_event_id_canonical_json(&state, rules)?; event.retain(|k, _| { matches!( From 47745daa64fbe9f09f04ee86ef919a8552d408df Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Wed, 10 Sep 2025 18:41:06 +0100 Subject: [PATCH 2/4] feat: updated MSC4311 support (cherry picked from commit 1c6b2e0016f26f26254f8f63426caa08152302b7) --- Cargo.lock | 22 +-- src/api/client_server/membership.rs | 7 +- src/api/server_server.rs | 4 +- src/database/key_value/rooms/state_cache.rs | 17 +- src/service/rooms/event_handler/mod.rs | 79 +++++---- src/service/rooms/state/mod.rs | 43 ++--- src/service/rooms/state_cache/data.rs | 17 +- src/service/rooms/state_cache/mod.rs | 16 +- src/service/rooms/timeline/mod.rs | 2 +- src/utils/mod.rs | 185 ++++++++++++-------- 10 files changed, 216 insertions(+), 176 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb9631ec..46cbcea4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2519,7 +2519,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.6" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "assign", "js_int", @@ -2538,7 +2538,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.12.2" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "js_int", "ruma-common", @@ -2550,7 +2550,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.4" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "as_variant", "assign", @@ -2573,7 +2573,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.4" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "as_variant", "base64 0.22.1", @@ -2605,7 +2605,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.5" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "as_variant", "indexmap 2.9.0", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.2" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "bytes", "headers", @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.1" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "js_int", "thiserror 2.0.12", @@ -2660,7 +2660,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.15.2" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "cfg-if", "proc-macro-crate", @@ -2675,7 +2675,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.11.0" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "js_int", "ruma-common", @@ -2687,7 +2687,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.17.1" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "base64 0.22.1", "ed25519-dalek", @@ -2703,7 +2703,7 @@ dependencies = [ [[package]] name = "ruma-state-res" version = "0.13.0" -source = "git+https://github.com/ruma/ruma.git#547efbf24831066ae3199dc51b93f6b3a30ea8e7" +source = "git+https://github.com/ruma/ruma.git#d879f7df16ba9928a73649f8149dabeee939691e" dependencies = [ "js_int", "ruma-common", diff --git a/src/api/client_server/membership.rs b/src/api/client_server/membership.rs index 522b44e7..bc45e3bb 100644 --- a/src/api/client_server/membership.rs +++ b/src/api/client_server/membership.rs @@ -188,9 +188,6 @@ pub async fn knock_room_route( } _ => return Err(Error::BadServerResponse("Room version is not supported")), }; - let rules = room_version_id - .rules() - .expect("Supported room version has rules"); let (event_id, knock_event, _) = services().rooms.helpers.populate_membership_template( &knock_template.event, @@ -215,8 +212,6 @@ pub async fn knock_room_route( ) .await?; - utils::check_stripped_state(&send_kock_response.knock_room_state, &room_id, &rules)?; - info!("send_knock finished"); let mut stripped_state = send_kock_response.knock_room_state; @@ -231,7 +226,7 @@ pub async fn knock_room_route( .to_stripped_state_event() .into(), ); - let stripped_state = utils::convert_stripped_state(stripped_state, &rules)?; + let stripped_state = utils::convert_stripped_state(stripped_state)?; services().rooms.state_cache.update_membership( &room_id, diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 24f96176..5c83c56b 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -2105,7 +2105,7 @@ pub async fn create_invite_route( .rules() .expect("Supported room version has rules"); - utils::check_stripped_state(&invite_room_state, &room_id, &rules)?; + utils::check_stripped_state(&invite_room_state, &room_id, &rules).await?; let mut signed_event = utils::to_canonical_object(&event) .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invite event is invalid."))?; @@ -2169,7 +2169,7 @@ pub async fn create_invite_route( })?; invite_state.push(pdu.to_stripped_state_event().into()); - let invite_state = utils::convert_stripped_state(invite_state, &rules)?; + let invite_state = utils::convert_stripped_state(invite_state)?; // If we are active in the room, the remote server will notify us about the join via /send if !services() diff --git a/src/database/key_value/rooms/state_cache.rs b/src/database/key_value/rooms/state_cache.rs index a758322b..f439f5f3 100644 --- a/src/database/key_value/rooms/state_cache.rs +++ b/src/database/key_value/rooms/state_cache.rs @@ -1,7 +1,8 @@ use std::{collections::HashSet, sync::Arc}; use ruma::{ - api::client::sync::sync_events::StrippedState, events::AnySyncStateEvent, serde::Raw, + events::{AnyStrippedStateEvent, AnySyncStateEvent}, + serde::Raw, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId, }; @@ -38,7 +39,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { &self, user_id: &UserId, room_id: &RoomId, - last_state: Option>>, + last_state: Option>>, ) -> Result<()> { let (roomuser_id, userroom_id) = get_room_and_user_byte_ids(room_id, user_id); @@ -65,7 +66,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { &self, user_id: &UserId, room_id: &RoomId, - last_state: Option>>, + last_state: Option>>, ) -> Result<()> { let (roomuser_id, userroom_id) = get_room_and_user_byte_ids(room_id, user_id); @@ -482,7 +483,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { fn rooms_invited<'a>( &'a self, user_id: &UserId, - ) -> Box>)>> + 'a> { + ) -> Box>)>> + 'a> { scan_userroom_id_memberstate_tree(user_id, &self.userroomid_invitestate) } @@ -492,7 +493,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { fn rooms_knocked<'a>( &'a self, user_id: &UserId, - ) -> Box>)>> + 'a> { + ) -> Box>)>> + 'a> { scan_userroom_id_memberstate_tree(user_id, &self.userroomid_knockstate) } @@ -501,7 +502,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>> { + ) -> Result>>> { let mut key = user_id.as_bytes().to_vec(); key.push(0xff); key.extend_from_slice(room_id.as_bytes()); @@ -522,7 +523,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>> { + ) -> Result>>> { let mut key = user_id.as_bytes().to_vec(); key.push(0xff); key.extend_from_slice(room_id.as_bytes()); @@ -543,7 +544,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>> { + ) -> Result>>> { let mut key = user_id.as_bytes().to_vec(); key.push(0xff); key.extend_from_slice(room_id.as_bytes()); diff --git a/src/service/rooms/event_handler/mod.rs b/src/service/rooms/event_handler/mod.rs index 47a479d6..4f6b8369 100644 --- a/src/service/rooms/event_handler/mod.rs +++ b/src/service/rooms/event_handler/mod.rs @@ -32,6 +32,7 @@ use ruma::{ }, int, room_version_rules::{AuthorizationRules, RoomVersionRules, StateResolutionV2Rules}, + serde::Base64, state_res::{self, StateMap}, uint, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedServerName, OwnedServerSigningKeyId, RoomId, ServerName, @@ -338,43 +339,14 @@ impl Service { } // TODO: For RoomVersion6 we must check that Raw<..> is canonical do we anywhere?: https://matrix.org/docs/spec/rooms/v6#canonical-json - // We go through all the signatures we see on the value and fetch the corresponding signing // keys self.fetch_required_signing_keys(&value, pub_key_map) .await?; - let origin_server_ts = value.get("origin_server_ts").ok_or_else(|| { - error!("Invalid PDU, no origin_server_ts field"); - Error::BadRequest( - ErrorKind::MissingParam, - "Invalid PDU, no origin_server_ts field", - ) - })?; - - let origin_server_ts: MilliSecondsSinceUnixEpoch = { - let ts = origin_server_ts.as_integer().ok_or_else(|| { - Error::BadRequest( - ErrorKind::InvalidParam, - "origin_server_ts must be an integer", - ) - })?; - - MilliSecondsSinceUnixEpoch(i64::from(ts).try_into().map_err(|_| { - Error::BadRequest(ErrorKind::InvalidParam, "Time must be after the unix epoch") - })?) - }; - - let guard = pub_key_map.read().await; - - let pkey_map = (*guard).clone(); - - // Removing all the expired keys, unless the room version allows stale keys - let filtered_keys = services().globals.filter_keys_server_map( - pkey_map, - origin_server_ts, - &room_version_rules, - ); + let filtered_keys = self + .filter_required_signing_keys(&value, pub_key_map, &room_version_rules) + .await?; let mut val = match ruma::signatures::verify_event(&filtered_keys, &value, &room_version_rules) { @@ -416,8 +388,6 @@ impl Service { Ok(ruma::signatures::Verified::All) => value, }; - drop(guard); - // Now that we have checked the signature and hashes we can add the eventID and convert // to our PduEvent type val.insert( @@ -1451,6 +1421,47 @@ impl Service { Ok((sorted, eventid_info)) } + /// Filters down the given signing keys, only keeping those which could be valid for this event. + #[tracing::instrument(skip_all)] + pub async fn filter_required_signing_keys( + &self, + event: &BTreeMap, + pub_key_map: &RwLock>, + room_version_rules: &RoomVersionRules, + ) -> Result>> { + let origin_server_ts = event.get("origin_server_ts").ok_or_else(|| { + error!("Invalid PDU, no origin_server_ts field"); + Error::BadRequest( + ErrorKind::MissingParam, + "Invalid PDU, no origin_server_ts field", + ) + })?; + + let origin_server_ts: MilliSecondsSinceUnixEpoch = { + let ts = origin_server_ts.as_integer().ok_or_else(|| { + Error::BadRequest( + ErrorKind::InvalidParam, + "origin_server_ts must be an integer", + ) + })?; + + MilliSecondsSinceUnixEpoch(i64::from(ts).try_into().map_err(|_| { + Error::BadRequest(ErrorKind::InvalidParam, "Time must be after the unix epoch") + })?) + }; + + let guard = pub_key_map.write().await; + + let pkey_map = (*guard).clone(); + + // Removing all the expired keys, unless the room version allows stale keys + Ok(services().globals.filter_keys_server_map( + pkey_map, + origin_server_ts, + room_version_rules, + )) + } + #[tracing::instrument(skip_all)] pub(crate) async fn fetch_required_signing_keys( &self, diff --git a/src/service/rooms/state/mod.rs b/src/service/rooms/state/mod.rs index d903188b..c9b8685f 100644 --- a/src/service/rooms/state/mod.rs +++ b/src/service/rooms/state/mod.rs @@ -6,13 +6,11 @@ use std::{ pub use data::Data; use ruma::{ - api::{ - client::{error::ErrorKind, sync::sync_events::StrippedState}, - federation::membership::RawStrippedState, - }, + api::{client::error::ErrorKind, federation::membership::RawStrippedState}, events::{ room::{create::RoomCreateEventContent, member::MembershipState}, - StateEventType, TimelineEventType, RECOMMENDED_STRIPPED_STATE_EVENT_TYPES, + AnyStrippedStateEvent, StateEventType, TimelineEventType, + RECOMMENDED_STRIPPED_STATE_EVENT_TYPES, }, room_version_rules::AuthorizationRules, serde::Raw, @@ -273,31 +271,28 @@ impl Service { services() .rooms .state_accessor - .room_state_get(room_id, state_event_type, "") + .room_state_get_id(room_id, state_event_type, "") .transpose() }) .map(|e| { - if e.as_ref() - .is_ok_and(|e| e.kind == TimelineEventType::RoomCreate) - { - e.and_then(|e| { - services() - .rooms - .timeline - .get_pdu_json(&e.event_id) - .transpose() - .expect("Event must be present for it to make up the current state") - .map(PduEvent::convert_to_outgoing_federation_event) - .map(RawStrippedState::Pdu) - }) - } else { - e.map(|e| RawStrippedState::Stripped(e.to_stripped_state_event())) - } + e.and_then(|e| { + services() + .rooms + .timeline + .get_pdu_json(&e) + .transpose() + .expect("Event must be present for it to make up the current state") + .map(PduEvent::convert_to_outgoing_federation_event) + .map(RawStrippedState::Pdu) + }) }) .collect::>>() } - pub fn stripped_state_client(&self, room_id: &RoomId) -> Result>> { + pub fn stripped_state_client( + &self, + room_id: &RoomId, + ) -> Result>> { RECOMMENDED_STRIPPED_STATE_EVENT_TYPES .iter() .filter_map(|state_event_type| { @@ -307,7 +302,7 @@ impl Service { .room_state_get(room_id, state_event_type, "") .transpose() }) - .map(|e| e.map(|e| e.to_stripped_state_event().cast())) + .map(|e| e.map(|e| e.to_stripped_state_event())) .collect::>>() } diff --git a/src/service/rooms/state_cache/data.rs b/src/service/rooms/state_cache/data.rs index 65ba16ff..3ee73dfa 100644 --- a/src/service/rooms/state_cache/data.rs +++ b/src/service/rooms/state_cache/data.rs @@ -2,7 +2,8 @@ use std::{collections::HashSet, sync::Arc}; use crate::{service::appservice::RegistrationInfo, Result}; use ruma::{ - api::client::sync::sync_events::StrippedState, events::AnySyncStateEvent, serde::Raw, + events::{AnyStrippedStateEvent, AnySyncStateEvent}, + serde::Raw, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId, }; @@ -13,13 +14,13 @@ pub trait Data: Send + Sync { &self, user_id: &UserId, room_id: &RoomId, - last_state: Option>>, + last_state: Option>>, ) -> Result<()>; fn mark_as_knocked( &self, user_id: &UserId, room_id: &RoomId, - last_state: Option>>, + last_state: Option>>, ) -> Result<()>; fn mark_as_left(&self, user_id: &UserId, room_id: &RoomId) -> Result<()>; @@ -85,32 +86,32 @@ pub trait Data: Send + Sync { fn rooms_invited<'a>( &'a self, user_id: &UserId, - ) -> Box>)>> + 'a>; + ) -> Box>)>> + 'a>; /// Returns an iterator over all rooms a user has knocked on. #[allow(clippy::type_complexity)] fn rooms_knocked<'a>( &'a self, user_id: &UserId, - ) -> Box>)>> + 'a>; + ) -> Box>)>> + 'a>; fn invite_state( &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>>; + ) -> Result>>>; fn knock_state( &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>>; + ) -> Result>>>; fn left_state( &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>>; + ) -> Result>>>; /// Returns an iterator over all rooms a user left. #[allow(clippy::type_complexity)] diff --git a/src/service/rooms/state_cache/mod.rs b/src/service/rooms/state_cache/mod.rs index 132355cd..d5c43ea5 100644 --- a/src/service/rooms/state_cache/mod.rs +++ b/src/service/rooms/state_cache/mod.rs @@ -4,12 +4,12 @@ use std::{collections::HashSet, sync::Arc}; pub use data::Data; use ruma::{ - api::client::sync::sync_events::StrippedState, events::{ direct::DirectEvent, ignored_user_list::IgnoredUserListEvent, room::{create::RoomCreateEventContent, member::MembershipState}, - AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, + AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, + RoomAccountDataEventType, StateEventType, }, serde::Raw, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId, @@ -31,7 +31,7 @@ impl Service { user_id: &UserId, membership: MembershipState, sender: &UserId, - last_state: Option>>, + last_state: Option>>, update_joined_count: bool, ) -> Result<()> { // Keep track what remote users exist by adding them as "deactivated" users @@ -317,7 +317,7 @@ impl Service { pub fn rooms_invited<'a>( &'a self, user_id: &UserId, - ) -> impl Iterator>)>> + 'a { + ) -> impl Iterator>)>> + 'a { self.db.rooms_invited(user_id) } @@ -326,7 +326,7 @@ impl Service { pub fn rooms_knocked<'a>( &'a self, user_id: &UserId, - ) -> impl Iterator>)>> + 'a { + ) -> impl Iterator>)>> + 'a { self.db.rooms_knocked(user_id) } @@ -335,7 +335,7 @@ impl Service { &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>> { + ) -> Result>>> { self.db.invite_state(user_id, room_id) } @@ -344,7 +344,7 @@ impl Service { &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>> { + ) -> Result>>> { self.db.knock_state(user_id, room_id) } @@ -353,7 +353,7 @@ impl Service { &self, user_id: &UserId, room_id: &RoomId, - ) -> Result>>> { + ) -> Result>>> { self.db.left_state(user_id, room_id) } diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index 1558eff3..e6eabc74 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -455,7 +455,7 @@ impl Service { .state .stripped_state_client(&pdu.room_id())?; // So that clients can get info about who invitied them (not relevant for knocking), the reason, when, etc. - state.push(pdu.to_stripped_state_event().cast()); + state.push(pdu.to_stripped_state_event()); Some(state) } _ => None, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f1411016..bca81b15 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,24 +5,26 @@ use cmp::Ordering; use rand::prelude::*; use ring::digest; use ruma::{ - api::{ - client::{error::ErrorKind, sync::sync_events::StrippedState}, - federation::membership::RawStrippedState, - }, + api::{client::error::ErrorKind, federation::membership::RawStrippedState}, canonical_json::try_from_json_map, - events::{AnyStateEvent, StateEventType}, + events::AnyStrippedStateEvent, room_version_rules::RoomVersionRules, serde::Raw, + signatures::Verified, CanonicalJsonError, CanonicalJsonObject, CanonicalJsonValue, RoomId, }; use serde_json::value::to_raw_value; use std::{ - cmp, fmt, + cmp, + collections::BTreeMap, + fmt, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; +use tokio::sync::RwLock; +use tracing::warn; -use crate::{service::pdu::gen_event_id_canonical_json, Result}; +use crate::{service::pdu::gen_event_id_canonical_json, services, Error, Result}; pub fn millis_since_unix_epoch() -> u64 { SystemTime::now() @@ -199,45 +201,39 @@ impl fmt::Display for HtmlEscape<'_> { } } -/// Converts `RawStrippedState` (federation format) into `Raw` (client format) +/// Converts `RawStrippedState` (federation format) into `Raw` (client format) pub fn convert_stripped_state( stripped_state: Vec, - rules: &RoomVersionRules, -) -> Result>> { +) -> Result>> { stripped_state .into_iter() .map(|stripped_state| match stripped_state { - RawStrippedState::Stripped(state) => Ok(state.cast()), + RawStrippedState::Stripped(state) => Ok(state), RawStrippedState::Pdu(state) => { - let (event_id, mut event) = gen_event_id_canonical_json(&state, rules)?; + let mut event: CanonicalJsonObject = + serde_json::from_str(state.get()).map_err(|e| { + warn!("Error parsing incoming event {:?}: {:?}", state, e); + Error::BadServerResponse("Invalid PDU in server response") + })?; event.retain(|k, _| { - matches!( - k.as_str(), - "content" - | "event_id" - | "origin_server_ts" - | "room_id" - | "sender" - | "state_key" - | "type" - | "unsigned" - ) + matches!(k.as_str(), "content" | "sender" | "state_key" | "type") }); - event.insert("event_id".to_owned(), event_id.as_str().into()); - let raw_value = to_raw_value(&CanonicalJsonValue::Object(event)) .expect("To raw json should not fail since only change was adding signature"); - Ok(Raw::::from_json(raw_value).cast()) + Ok(Raw::::from_json(raw_value)) } }) .collect() } -pub fn check_stripped_state( - stripped_state: &Vec, +/// Performs checks on incoming stripped state, as per [MSC4311] +/// +/// [MSC4311]: https://github.com/matrix-org/matrix-spec-proposals/pull/4311 +pub async fn check_stripped_state( + stripped_state: &[RawStrippedState], room_id: &RoomId, rules: &RoomVersionRules, ) -> Result<()> { @@ -246,65 +242,106 @@ pub fn check_stripped_state( return Ok(()); } + #[cfg(feature = "enforce_msc4311")] let mut seen_create_event = false; #[cfg(feature = "enforce_msc4311")] - let mut seen_valid_create_event = false; + if !stripped_state.iter().all(|state| match state { + RawStrippedState::Pdu(_) => true, + RawStrippedState::Stripped(_) => false, + }) { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Non-pdu found in stripped state", + )); + } - for state in stripped_state { - match state { - RawStrippedState::Pdu(pdu) => { - let Ok((event_id, value)) = gen_event_id_canonical_json(pdu, rules) else { - continue; - }; - let Some(event_type) = value.get("type").and_then(|t| t.as_str()) else { - continue; - }; - if event_type != "m.room.create" { - continue; - } - if seen_create_event { - return Err(error::Error::BadRequest( - ErrorKind::InvalidParam, - "Stripped state has multiple create events", - )); - } - if event_id.localpart() != room_id.strip_sigil() { - return Err(error::Error::BadRequest( - ErrorKind::InvalidParam, - "Room ID generated from create event does not match that from the request", - )); - } - - seen_create_event = true; - #[cfg(feature = "enforce_msc4311")] - { - seen_valid_create_event = true; - } + let stripped_state = stripped_state + .iter() + .filter_map(|event| { + if let RawStrippedState::Pdu(pdu) = event { + Some(pdu) + } else { + None } - RawStrippedState::Stripped(event) => { - let Ok(event) = event.deserialize() else { - continue; - }; + }) + .map(|pdu| gen_event_id_canonical_json(pdu, rules)) + .collect::>>()?; - if event.event_type() != StateEventType::RoomCreate { - continue; - } + let pub_key_map = RwLock::new(BTreeMap::new()); - if seen_create_event { - return Err(error::Error::BadRequest( - ErrorKind::InvalidParam, - "Stripped state has multiple create events", - )); - } + for (_, pdu) in &stripped_state { + services() + .rooms + .event_handler + .fetch_required_signing_keys(pdu, &pub_key_map) + .await?; + } + for (event_id, pdu) in stripped_state { + let filtered_keys = services() + .rooms + .event_handler + .filter_required_signing_keys(&pdu, &pub_key_map, rules) + .await?; + + if !ruma::signatures::verify_event(&filtered_keys, &pdu, rules) + .is_ok_and(|verified| verified == Verified::All) + { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Signature check on stripped state failed", + )); + } + + let Some(event_type) = pdu.get("type").and_then(|t| t.as_str()) else { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Event with no type returned", + )); + }; + + if !(event_type == "m.room.create" && rules.authorization.room_create_event_id_as_room_id) { + let pdu_room_id = pdu + .get("room_id") + .ok_or_else(|| Error::BadRequest(ErrorKind::InvalidParam, "Event missing room ID")) + .map(|v| v.as_str())? + .ok_or_else(|| { + Error::BadRequest(ErrorKind::InvalidParam, "Event has non-string room id") + }) + .map(RoomId::parse)? + .map_err(|_| { + Error::BadRequest(ErrorKind::InvalidParam, "Event has invalid room ID") + })?; + + if pdu_room_id != room_id { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Stripped state room ID does not match the one of the request", + )); + } + } + + if event_type == "m.room.create" { + #[allow(clippy::collapsible_if)] + if event_id.localpart() != room_id.strip_sigil() + && rules.authorization.room_create_event_id_as_room_id + { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Room ID generated from create event does not match that from the request", + )); + } + + #[cfg(feature = "enforce_msc4311")] + { seen_create_event = true; } } } #[cfg(feature = "enforce_msc4311")] - if !seen_valid_create_event { - return Err(error::Error::BadRequest( + if !seen_create_event { + return Err(Error::BadRequest( ErrorKind::InvalidParam, "Stripped state contained no valid create PDUs", )); From 4ca72efdc46004196d9f1952136f58da516de1b5 Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Sun, 17 Aug 2025 13:20:30 +0200 Subject: [PATCH 3/4] fix: set previous creators to max power level if "upgraded" room doesn't support creator power level (cherry picked from commit e757a98e10b080323c253e0cd24349e562853a96) --- src/api/client_server/room.rs | 78 +++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/api/client_server/room.rs b/src/api/client_server/room.rs index 2efe2e7e..4435256e 100644 --- a/src/api/client_server/room.rs +++ b/src/api/client_server/room.rs @@ -23,13 +23,15 @@ use ruma::{ }, int, serde::JsonObject, - CanonicalJsonObject, CanonicalJsonValue, OwnedRoomAliasId, OwnedUserId, RoomAliasId, + CanonicalJsonObject, CanonicalJsonValue, Int, OwnedRoomAliasId, OwnedUserId, RoomAliasId, + RoomVersionId, }; use serde::Deserialize; use serde_json::{json, value::to_raw_value}; use std::{ cmp::max, collections::{BTreeMap, HashSet}, + str::FromStr, sync::Arc, }; use tracing::{error, info, warn}; @@ -600,16 +602,29 @@ pub async fn upgrade_room_route( .authorization; // Get the old room creation event - let mut create_event_content = serde_json::from_str::( - services() - .rooms - .state_accessor - .room_state_get(&body.room_id, &StateEventType::RoomCreate, "")? - .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))? - .content - .get(), - ) - .map_err(|_| Error::bad_database("Invalid room event in database."))?; + let create_event = services() + .rooms + .state_accessor + .room_state_get(&body.room_id, &StateEventType::RoomCreate, "")? + .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?; + + let mut create_event_content = + serde_json::from_str::(create_event.content.get()) + .map_err(|_| Error::bad_database("Invalid room event in database."))?; + + let old_rules = if let Some(CanonicalJsonValue::String(old_version)) = + create_event_content.get("room_version") + { + RoomVersionId::from_str(old_version) + .map_err(|_| Error::BadDatabase("Create event must be a valid room version ID"))? + .rules() + .expect("Supported room version must have rules") + .authorization + } else { + return Err(Error::BadDatabase( + "Room create event does not have content", + )); + }; // Use the m.room.tombstone event as the predecessor let predecessor = Some(ruma::events::room::create::PreviousRoom::new( @@ -772,8 +787,7 @@ pub async fn upgrade_room_route( None => continue, // Skipping missing events. }; - if event_type == StateEventType::RoomPowerLevels && rules.explicitly_privilege_room_creators - { + if event_type == StateEventType::RoomPowerLevels { let mut pl_event_content: CanonicalJsonObject = serde_json::from_str(event_content.get()).map_err(|e| { error!( @@ -783,13 +797,41 @@ pub async fn upgrade_room_route( Error::BadDatabase("Invalid m.room.power_levels event content in room") })?; - if let Some(CanonicalJsonValue::Object(users)) = pl_event_content.get_mut("users") { - users.remove(sender_user.as_str()); + let mut users_was_empty = false; - if rules.additional_room_creators { - for user in &body.additional_creators { - users.remove(user.as_str()); + if let CanonicalJsonValue::Object(users) = pl_event_content + .entry("users".to_owned()) + .or_insert_with(|| { + users_was_empty = true; + CanonicalJsonValue::Object(BTreeMap::new()) + }) + { + if rules.explicitly_privilege_room_creators { + users.remove(sender_user.as_str()); + + if rules.additional_room_creators { + for user in &body.additional_creators { + users.remove(user.as_str()); + } } + } else if old_rules.explicitly_privilege_room_creators { + users.insert(create_event.sender.to_string(), Int::MAX.into()); + + if old_rules.additional_room_creators { + if let Some(CanonicalJsonValue::Array(creators)) = + create_event_content.get("additional_creators") + { + for creator in creators { + if let CanonicalJsonValue::String(creator) = creator { + users.insert(creator.to_owned(), Int::MAX.into()); + } + } + } + } + } + + if users.is_empty() && users_was_empty { + pl_event_content.remove("users"); } } From 4624df0998e88aed5b752f097f619a77ffa77a8e Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Fri, 12 Sep 2025 17:54:26 +0100 Subject: [PATCH 4/4] chore(release): 0.10.9 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46cbcea4..624cec6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,7 +492,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "conduit" -version = "0.10.8" +version = "0.10.9" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index 970ea33e..e1fa54ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ license = "Apache-2.0" name = "conduit" readme = "README.md" repository = "https://gitlab.com/famedly/conduit" -version = "0.10.8" +version = "0.10.9" # See also `rust-toolchain.toml` rust-version = "1.85.0"