diff --git a/src/api/client_server/membership.rs b/src/api/client_server/membership.rs index 5496299c..95aefcf4 100644 --- a/src/api/client_server/membership.rs +++ b/src/api/client_server/membership.rs @@ -1032,8 +1032,9 @@ async fn join_room_by_id_helper( info!("Running send_join auth check"); let authenticated = state_res::event_auth::auth_check( - &state_res::RoomVersion::new(&room_version_id) - .expect("room version is supported"), + &state_res::RoomVersion::new(&room_version_id).map_err(|_| { + Error::UnsupportedRoomVersion(room_version_id.clone()) + })?, &parsed_join_pdu, // TODO: third party invite None::, diff --git a/src/api/client_server/room.rs b/src/api/client_server/room.rs index 835a57f4..c2488464 100644 --- a/src/api/client_server/room.rs +++ b/src/api/client_server/room.rs @@ -24,14 +24,14 @@ use ruma::{ }, int, serde::JsonObject, - CanonicalJsonObject, OwnedRoomAliasId, RoomAliasId, RoomId, RoomVersionId, + CanonicalJsonObject, OwnedRoomAliasId, RoomAliasId, RoomId, }; use serde_json::{json, value::to_raw_value}; use tracing::{info, warn}; use crate::{ - api::client_server::invite_helper, service::pdu::PduBuilder, services, Ar, - Error, Ra, Result, + api::client_server::invite_helper, service::pdu::PduBuilder, services, + utils::room_version::RoomVersion, Ar, Error, Ra, Result, }; /// # `POST /_matrix/client/r0/createRoom` @@ -114,7 +114,7 @@ pub(crate) async fn create_room_route( } } - let room_version = match body.room_version.clone() { + let room_version_id = match body.room_version.clone() { Some(room_version) => { if services() .globals @@ -131,6 +131,7 @@ pub(crate) async fn create_room_route( } None => services().globals.default_room_version(), }; + let room_version = RoomVersion::try_from(&room_version_id)?; let content = match &body.creation_content { Some(content) => { @@ -138,30 +139,20 @@ pub(crate) async fn create_room_route( .deserialize_as::() .expect("Invalid creation content"); - match &room_version { - room_version if *room_version < RoomVersionId::V11 => { - content.insert( - "creator".into(), - json!(&sender_user).try_into().map_err(|_| { - Error::BadRequest( - ErrorKind::BadJson, - "Invalid creation content", - ) - })?, - ); - } - // V11 removed the "creator" key - RoomVersionId::V11 => {} - _ => { - return Err(Error::BadServerResponse( - "Unsupported room version.", - )) - } + if room_version.create_event_creator_prop { + content.insert( + "creator".into(), + json!(&sender_user).try_into().map_err(|_| { + Error::BadRequest( + ErrorKind::BadJson, + "Invalid creation content", + ) + })?, + ); } - content.insert( "room_version".into(), - json!(room_version.as_str()).try_into().map_err(|_| { + json!(room_version_id.as_str()).try_into().map_err(|_| { Error::BadRequest( ErrorKind::BadJson, "Invalid creation content", @@ -171,16 +162,10 @@ pub(crate) async fn create_room_route( content } None => { - let content = match &room_version { - room_version if *room_version < RoomVersionId::V11 => { - RoomCreateEventContent::new_v1(sender_user.to_owned()) - } - RoomVersionId::V11 => RoomCreateEventContent::new_v11(), - _ => { - return Err(Error::BadServerResponse( - "Unsupported room version.", - )) - } + let content = if room_version.create_event_creator_prop { + RoomCreateEventContent::new_v1(sender_user.to_owned()) + } else { + RoomCreateEventContent::new_v11() }; let mut content = serde_json::from_str::( to_raw_value(&content) @@ -195,7 +180,7 @@ pub(crate) async fn create_room_route( .unwrap(); content.insert( "room_version".into(), - json!(room_version.as_str()).try_into().map_err(|_| { + json!(room_version_id.as_str()).try_into().map_err(|_| { Error::BadRequest( ErrorKind::BadJson, "Invalid creation content", @@ -598,6 +583,7 @@ pub(crate) async fn upgrade_room_route( "This server does not support that room version.", )); } + let new_version = RoomVersion::try_from(&body.new_version)?; // Create a replacement room let replacement_room = RoomId::new(services().globals.server_name()); @@ -661,23 +647,18 @@ pub(crate) async fn upgrade_room_route( // Send a m.room.create event containing a predecessor field and the // applicable room_version - match &body.new_version { - room_version if *room_version < RoomVersionId::V11 => { - create_event_content.insert( - "creator".into(), - json!(&sender_user).try_into().map_err(|_| { - Error::BadRequest( - ErrorKind::BadJson, - "Error forming creation event", - ) - })?, - ); - } - RoomVersionId::V11 => { - // "creator" key no longer exists in V11 rooms - create_event_content.remove("creator"); - } - _ => return Err(Error::BadServerResponse("Unsupported room version.")), + if new_version.create_event_creator_prop { + create_event_content.insert( + "creator".into(), + json!(&sender_user).try_into().map_err(|_| { + Error::BadRequest( + ErrorKind::BadJson, + "Error forming creation event", + ) + })?, + ); + } else { + create_event_content.remove("creator"); } create_event_content.insert( "room_version".into(), diff --git a/src/service/admin.rs b/src/service/admin.rs index cf8ab5db..e46368ed 100644 --- a/src/service/admin.rs +++ b/src/service/admin.rs @@ -34,7 +34,7 @@ use super::pdu::PduBuilder; use crate::{ api::client_server::{leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH}, services, - utils::{self, dbg_truncate_str}, + utils::{self, dbg_truncate_str, room_version::RoomVersion}, Error, PduEvent, Result, }; @@ -1301,23 +1301,18 @@ impl Service { services().users.create(&services().globals.admin_bot_user_id, None)?; - let room_version = services().globals.default_room_version(); - let mut content = match &room_version { - room_version if *room_version < RoomVersionId::V11 => { - RoomCreateEventContent::new_v1( - services().globals.admin_bot_user_id.clone(), - ) - } - RoomVersionId::V11 => RoomCreateEventContent::new_v11(), - _ => { - return Err(Error::BadServerResponse( - "Unsupported room version.", - )) - } + let room_version_id = services().globals.default_room_version(); + let room_version = RoomVersion::try_from(&room_version_id)?; + let mut content = if room_version.create_event_creator_prop { + RoomCreateEventContent::new_v1( + services().globals.admin_bot_user_id.clone(), + ) + } else { + RoomCreateEventContent::new_v11() }; content.federate = true; content.predecessor = None; - content.room_version = room_version; + content.room_version = room_version_id; // 1. The room create event services() diff --git a/src/service/rooms/event_handler.rs b/src/service/rooms/event_handler.rs index 23b08bf7..d5e7a6a4 100644 --- a/src/service/rooms/event_handler.rs +++ b/src/service/rooms/event_handler.rs @@ -27,7 +27,7 @@ use ruma::{ StateEventType, TimelineEventType, }, int, - state_res::{self, RoomVersion, StateMap}, + state_res::{self, StateMap}, uint, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedServerName, OwnedServerSigningKeyId, RoomId, RoomVersionId, ServerName, @@ -44,7 +44,7 @@ use super::{ use crate::{ service::{globals::SigningKeys, pdu}, services, - utils::debug_slice_truncated, + utils::{debug_slice_truncated, room_version::RoomVersion}, Error, PduEvent, Result, }; @@ -339,8 +339,10 @@ impl Service { )?; let room_version_id = &create_event_content.room_version; - let room_version = RoomVersion::new(room_version_id) - .expect("room version is supported"); + let ruma_room_version = + state_res::RoomVersion::new(room_version_id).map_err(|_| { + Error::UnsupportedRoomVersion(room_version_id.clone()) + })?; // TODO: For RoomVersion6 we must check that Raw<..> is canonical, // do we anywhere? @@ -527,7 +529,7 @@ impl Service { } if !state_res::event_auth::auth_check( - &room_version, + &ruma_room_version, &incoming_pdu, // TODO: third party invite None::, @@ -601,8 +603,11 @@ impl Service { )?; let room_version_id = &create_event_content.room_version; - let room_version = RoomVersion::new(room_version_id) - .expect("room version is supported"); + let room_version = RoomVersion::try_from(room_version_id)?; + let ruma_room_version = state_res::RoomVersion::new(room_version_id) + .map_err(|_| { + Error::UnsupportedRoomVersion(room_version_id.clone()) + })?; // 10. Fetch missing state and auth chain events by calling /state_ids // at backwards extremities doing all the checks in this list @@ -885,7 +890,7 @@ impl Service { // 11. Check the auth of the event passes based on the state of the // event let check_result = state_res::event_auth::auth_check( - &room_version, + &ruma_room_version, &incoming_pdu, // TODO: third party invite None::, @@ -930,7 +935,7 @@ impl Service { )?; let soft_fail = !state_res::event_auth::auth_check( - &room_version, + &ruma_room_version, &incoming_pdu, None::, |k, s| auth_events.get(&(k.clone(), s.to_owned())), @@ -939,45 +944,34 @@ impl Service { Error::BadRequest(ErrorKind::InvalidParam, "Auth check failed.") })? || incoming_pdu.kind == TimelineEventType::RoomRedaction - && match room_version_id { - room_version if *room_version < RoomVersionId::V11 => { - if let Some(redact_id) = &incoming_pdu.redacts { - !services().rooms.state_accessor.user_can_redact( - redact_id, - &incoming_pdu.sender, - &incoming_pdu.room_id, - true, - )? - } else { - false - } - } - RoomVersionId::V11 => { - let content = serde_json::from_str::< - RoomRedactionEventContent, - >( - incoming_pdu.content.get() + && if room_version.redaction_event_redacts_in_content { + let content = + serde_json::from_str::( + incoming_pdu.content.get(), ) .map_err(|_| { Error::bad_database("Invalid content in redaction pdu.") })?; - if let Some(redact_id) = &content.redacts { - !services().rooms.state_accessor.user_can_redact( - redact_id, - &incoming_pdu.sender, - &incoming_pdu.room_id, - true, - )? - } else { - false - } - } - _ => { - return Err(Error::BadServerResponse( - "Unsupported room version.", - )) + if let Some(redact_id) = &content.redacts { + !services().rooms.state_accessor.user_can_redact( + redact_id, + &incoming_pdu.sender, + &incoming_pdu.room_id, + true, + )? + } else { + false } + } else if let Some(redact_id) = &incoming_pdu.redacts { + !services().rooms.state_accessor.user_can_redact( + redact_id, + &incoming_pdu.sender, + &incoming_pdu.room_id, + true, + )? + } else { + false }; // 13. Use state resolution to find new room state diff --git a/src/service/rooms/timeline.rs b/src/service/rooms/timeline.rs index 257b2fac..53802841 100644 --- a/src/service/rooms/timeline.rs +++ b/src/service/rooms/timeline.rs @@ -21,10 +21,9 @@ use ruma::{ GlobalAccountDataEventType, StateEventType, TimelineEventType, }, push::{Action, Ruleset, Tweak}, - state_res::{self, Event, RoomVersion}, + state_res::{self, Event}, uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, - OwnedEventId, OwnedRoomId, OwnedServerName, RoomId, RoomVersionId, - ServerName, UserId, + OwnedEventId, OwnedRoomId, OwnedServerName, RoomId, ServerName, UserId, }; use serde::Deserialize; use serde_json::value::{to_raw_value, RawValue as RawJsonValue}; @@ -40,7 +39,7 @@ use crate::{ pdu::{EventHash, PduBuilder}, }, services, - utils::{self, on_demand_hashmap::KeyToken}, + utils::{self, on_demand_hashmap::KeyToken, room_version::RoomVersion}, Error, PduEvent, Result, }; @@ -410,44 +409,32 @@ impl Service { TimelineEventType::RoomRedaction => { let room_version_id = services().rooms.state.get_room_version(&pdu.room_id)?; - match &room_version_id { - room_version if *room_version < RoomVersionId::V11 => { - if let Some(redact_id) = &pdu.redacts { - if services().rooms.state_accessor.user_can_redact( - redact_id, - &pdu.sender, - &pdu.room_id, - false, - )? { - self.redact_pdu(redact_id, pdu, shortroomid)?; - } + let room_version = RoomVersion::try_from(&room_version_id)?; + if room_version.redaction_event_redacts_in_content { + let content = serde_json::from_str::< + RoomRedactionEventContent, + >(pdu.content.get()) + .map_err(|_| { + Error::bad_database("Invalid content in redaction pdu.") + })?; + if let Some(redact_id) = &content.redacts { + if services().rooms.state_accessor.user_can_redact( + redact_id, + &pdu.sender, + &pdu.room_id, + false, + )? { + self.redact_pdu(redact_id, pdu, shortroomid)?; } } - RoomVersionId::V11 => { - let content = - serde_json::from_str::( - pdu.content.get(), - ) - .map_err(|_| { - Error::bad_database( - "Invalid content in redaction pdu.", - ) - })?; - if let Some(redact_id) = &content.redacts { - if services().rooms.state_accessor.user_can_redact( - redact_id, - &pdu.sender, - &pdu.room_id, - false, - )? { - self.redact_pdu(redact_id, pdu, shortroomid)?; - } - } - } - _ => { - return Err(Error::BadServerResponse( - "Unsupported room version.", - )); + } else if let Some(redact_id) = &pdu.redacts { + if services().rooms.state_accessor.user_can_redact( + redact_id, + &pdu.sender, + &pdu.room_id, + false, + )? { + self.redact_pdu(redact_id, pdu, shortroomid)?; } }; } @@ -737,8 +724,10 @@ impl Service { } })?; - let room_version = RoomVersion::new(&room_version_id) - .expect("room version is supported"); + let ruma_room_version = state_res::RoomVersion::new(&room_version_id) + .map_err(|_| { + Error::UnsupportedRoomVersion(room_version_id.clone()) + })?; let auth_events = services().rooms.state.get_auth_events( room_id, @@ -810,7 +799,7 @@ impl Service { }; let auth_check = state_res::auth_check( - &room_version, + &ruma_room_version, &pdu, // TODO: third_party_invite None::, @@ -1008,57 +997,41 @@ impl Service { // If redaction event is not authorized, do not append it to the // timeline if pdu.kind == TimelineEventType::RoomRedaction { - match services().rooms.state.get_room_version(&pdu.room_id)? { - RoomVersionId::V1 - | RoomVersionId::V2 - | RoomVersionId::V3 - | RoomVersionId::V4 - | RoomVersionId::V5 - | RoomVersionId::V6 - | RoomVersionId::V7 - | RoomVersionId::V8 - | RoomVersionId::V9 - | RoomVersionId::V10 => { - if let Some(redact_id) = &pdu.redacts { - if !services().rooms.state_accessor.user_can_redact( - redact_id, - &pdu.sender, - &pdu.room_id, - false, - )? { - return Err(Error::BadRequest( - ErrorKind::forbidden(), - "User cannot redact this event.", - )); - } - }; - } - RoomVersionId::V11 => { - let content = serde_json::from_str::< - RoomRedactionEventContent, - >(pdu.content.get()) + let room_version_id = + services().rooms.state.get_room_version(&pdu.room_id)?; + let room_version = RoomVersion::try_from(&room_version_id)?; + if room_version.redaction_event_redacts_in_content { + let content = + serde_json::from_str::( + pdu.content.get(), + ) .map_err(|_| { Error::bad_database("Invalid content in redaction pdu.") })?; - if let Some(redact_id) = &content.redacts { - if !services().rooms.state_accessor.user_can_redact( - redact_id, - &pdu.sender, - &pdu.room_id, - false, - )? { - return Err(Error::BadRequest( - ErrorKind::forbidden(), - "User cannot redact this event.", - )); - } + if let Some(redact_id) = &content.redacts { + if !services().rooms.state_accessor.user_can_redact( + redact_id, + &pdu.sender, + &pdu.room_id, + false, + )? { + return Err(Error::BadRequest( + ErrorKind::forbidden(), + "User cannot redact this event.", + )); } } - _ => { + } else if let Some(redact_id) = &pdu.redacts { + if !services().rooms.state_accessor.user_can_redact( + redact_id, + &pdu.sender, + &pdu.room_id, + false, + )? { return Err(Error::BadRequest( - ErrorKind::UnsupportedRoomVersion, - "Unsupported room version", + ErrorKind::forbidden(), + "User cannot redact this event.", )); } } diff --git a/src/utils.rs b/src/utils.rs index e0d62063..c9cafa56 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ pub(crate) mod error; pub(crate) mod on_demand_hashmap; +pub(crate) mod room_version; use std::{ borrow::Cow, diff --git a/src/utils/error.rs b/src/utils/error.rs index 7b87b5f5..49b5ab15 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -80,6 +80,8 @@ pub(crate) enum Error { AdminCommand(&'static str), #[error("from {0}: {1}")] Redaction(OwnedServerName, ruma::canonical_json::RedactionError), + #[error("unsupported room version {0}")] + UnsupportedRoomVersion(ruma::RoomVersionId), #[error("{0} in {1}")] InconsistentRoomState(&'static str, ruma::OwnedRoomId), } @@ -146,6 +148,10 @@ impl Error { _ => StatusCode::BAD_REQUEST, }, ), + Self::UnsupportedRoomVersion(_) => ( + ErrorKind::UnsupportedRoomVersion, + StatusCode::INTERNAL_SERVER_ERROR, + ), Self::Conflict(_) => (Unknown, StatusCode::CONFLICT), _ => (Unknown, StatusCode::INTERNAL_SERVER_ERROR), }; diff --git a/src/utils/room_version.rs b/src/utils/room_version.rs new file mode 100644 index 00000000..389dc367 --- /dev/null +++ b/src/utils/room_version.rs @@ -0,0 +1,87 @@ +use ruma::RoomVersionId; + +use crate::Error; + +/// Properties that we care about in grapevine that differ between supported +/// room versions. +/// +/// This is similar to [`ruma::state_res::RoomVersion`], except that it has +/// properties that are relevant to us instead of only properties relevant to +/// ruma state resolution. +/// +/// When branching for different room versions, prefer constructing a +/// `RoomVersion` and branching on it's properties over branching based on the +/// [`RoomVersionId`] directly. If there is a relevant property that is not +/// already included in `RoomVersion`, add it. In particular, comparisons like +/// `room_version_id < RoomVersionId::V11` do not work, because room versions +/// do not have a defined order. Ruma implements `PartialOrd` on `RoomVersionId` +/// as a lexicographic string comparison, which is almost certainly not what you +/// want. +pub(crate) struct RoomVersion { + /// Whether `m.room.create` state events have a `creator` property. + /// + /// The `creator` property is always equivalent to the event `sender`, and + /// was [removed][spec] in v11. + /// + /// [spec]: https://spec.matrix.org/v1.11/rooms/v11/#remove-the-creator-property-of-mroomcreate-events + pub(crate) create_event_creator_prop: bool, + + /// Whether the `redacts` property of `m.room.redaction` events is a + /// property in the event `content` or a top-level property of the event. + /// + /// This property was [moved][spec] from top-level to `content` in v11. + /// + /// [spec]: https://spec.matrix.org/v1.11/rooms/v11/#moving-the-redacts-property-of-mroomredaction-events-to-a-content-property + pub(crate) redaction_event_redacts_in_content: bool, +} + +// Allowed so that we can use struct-update syntax for incremental changes +// between versions even when all fields change. +#[allow(clippy::needless_update)] +mod known_versions { + use super::RoomVersion; + + pub(super) const V6: RoomVersion = RoomVersion { + create_event_creator_prop: true, + redaction_event_redacts_in_content: false, + }; + + pub(super) const V7: RoomVersion = RoomVersion { + ..V6 + }; + + pub(super) const V8: RoomVersion = RoomVersion { + ..V7 + }; + + pub(super) const V9: RoomVersion = RoomVersion { + ..V8 + }; + + pub(super) const V10: RoomVersion = RoomVersion { + ..V9 + }; + + pub(super) const V11: RoomVersion = RoomVersion { + create_event_creator_prop: false, + redaction_event_redacts_in_content: true, + ..V10 + }; +} + +/// May return an error for unsupported room versions. +impl TryFrom<&RoomVersionId> for RoomVersion { + type Error = Error; + + fn try_from(version: &RoomVersionId) -> Result { + match version { + RoomVersionId::V6 => Ok(known_versions::V6), + RoomVersionId::V7 => Ok(known_versions::V7), + RoomVersionId::V8 => Ok(known_versions::V8), + RoomVersionId::V9 => Ok(known_versions::V9), + RoomVersionId::V10 => Ok(known_versions::V10), + RoomVersionId::V11 => Ok(known_versions::V11), + _ => Err(Error::UnsupportedRoomVersion(version.clone())), + } + } +}