diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 720a6518d..083c86d6d 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -23,7 +23,7 @@ }; use serde::Deserialize; -use crate::Ruma; +use crate::{Ruma, server::utils::validate_any_membership_event}; /// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}` /// @@ -86,7 +86,7 @@ pub(crate) async fn create_invite_route( } // And then we can validate the member event itself - let (mut signed_event, sender_user, recipient_user) = validate_membership_event( + let (mut signed_event, sender_user, recipient_user) = validate_invite_membership_event( &services, &body.event, &room_version_rules, @@ -234,7 +234,7 @@ pub(crate) async fn create_invite_route( /// invite event itself. /// /// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid -async fn validate_membership_event( +async fn validate_invite_membership_event( services: &crate::State, body: &serde_json::value::RawValue, room_version_rules: &RoomVersionRules, @@ -243,80 +243,22 @@ async fn validate_membership_event( room_id: OwnedRoomId, event_id: OwnedEventId, ) -> Result<(CanonicalJsonObject, OwnedUserId, OwnedUserId)> { - let (template_room_id, template_event_id, pdu) = services - .rooms - .event_handler - .parse_incoming_pdu(body) - .await - .map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?; - - if template_room_id != room_id { - return Err!(Request(InvalidParam("Membership event does not belong to requested room"))); - } - if template_event_id != event_id { - return Err!(Request(InvalidParam( - "Membership event ID does not match provided event ID" - ))); - } - - services - .server_keys - .verify_event(&pdu, room_version_rules) - .await - .map_err(|e| { - err!(Request(InvalidParam("Signature verification failed on invite event: {e}"))) - })?; - - // Ensure this is a membership event - if pdu - .get("type") - .expect("event must have a type") - .as_str() - .expect("type must be a string") - != "m.room.member" - { - return Err!(Request(BadJson( - "Not allowed to send non-membership event to invite endpoint" - ))); - } - - // Ensure it is an invite event - // My Huge Chain (avoids deser) - let membership = pdu - .get("content") - .ok_or_else(|| err!(Request(BadJson("Event missing content property"))))? - .as_object() - .ok_or_else(|| err!(Request(BadJson("Event content is not an object"))))? - .get("membership") - .ok_or_else(|| err!(Request(BadJson("Event missing membership property"))))? - .as_str() - .ok_or_else(|| err!(Request(BadJson("Event is not a string"))))?; - if MembershipState::Invite != membership.into() { - return Err!(Request(BadJson( - "Not allowed to send non-invite membership event to invite endpoint" - ))); - } + let (pdu, target_membership, sender_user, recipient_user) = validate_any_membership_event( + services, + body, + room_version_rules, + create_event_id, + room_id, + event_id, + ) + .await?; // Ensure the sender belongs to the remote that is sending the invite - let sender_user = pdu - .get("sender") - .and_then(|v| v.as_str()) - .map(UserId::parse) - .and_then(Result::ok) - .ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?; - if sender_user.server_name() != origin { return Err!(Request(Forbidden("Sender belongs to a different server"))); } // Ensure the target user belongs to this server - let recipient_user = pdu - .get("state_key") - .and_then(|v| v.as_str()) - .map(UserId::parse) - .and_then(Result::ok) - .ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?; - if !services .globals .server_is_ours(recipient_user.server_name()) @@ -324,18 +266,9 @@ async fn validate_membership_event( return Err!(Request(InvalidParam("Recipient does not belong to this homeserver"))); } - // Do a quick format check. The spec doesn't suggest this, but it's probably - // a good idea nonetheless. - service::rooms::event_handler::Service::pdu_format_check_1( - &pdu, - room_version_rules, - &create_event_id, - ) - .map_err(|e| { - err!(Request(InvalidParam( - "Invite membership event violates the room event format: {e}" - ))) - })?; + if target_membership != MembershipState::Invite { + return Err!(Request(BadJson("Invalid membership (expected `invite`)"))); + } Ok((pdu, sender_user, recipient_user)) } diff --git a/src/api/server/send_knock.rs b/src/api/server/send_knock.rs index 377a8792d..905b73a65 100644 --- a/src/api/server/send_knock.rs +++ b/src/api/server/send_knock.rs @@ -1,21 +1,11 @@ use axum::extract::State; -use conduwuit::{ - Err, Result, err, info, - matrix::{event::gen_event_id_canonical_json, pdu::PduEvent}, - warn, -}; +use conduwuit::{Err, Event, Result, debug_info, err, matrix::pdu::PduEvent, warn}; use futures::FutureExt; use ruma::{ - OwnedUserId, - api::federation::membership::create_knock_event, - events::{ - StateEventType, - room::member::{MembershipState, RoomMemberEventContent}, - }, - serde::JsonObject, + api::federation::membership::create_knock_event, events::room::member::MembershipState, }; -use crate::Ruma; +use crate::{Ruma, server::utils::validate_any_membership_event}; /// # `PUT /_matrix/federation/v1/send_knock/{roomId}/{eventId}` /// @@ -33,18 +23,7 @@ pub(crate) async fn create_knock_event_v1_route( forbidden. Rejecting.", body.identity, &body.room_id, ); - return Err!(Request(Forbidden("Server is banned on this homeserver."))); - } - - if let Some(server) = body.room_id.server_name() { - if services.moderation.is_remote_server_forbidden(server) { - warn!( - "Server {} tried knocking room ID {} which has a server name that is globally \ - forbidden. Rejecting.", - body.identity, &body.room_id, - ); - return Err!(Request(Forbidden("Server is banned on this homeserver."))); - } + return Err!(Request(Forbidden("Federation denied with {}", body.identity))); } if !services.rooms.metadata.exists(&body.room_id).await { @@ -57,8 +36,9 @@ pub(crate) async fn create_knock_event_v1_route( .server_in_room(services.globals.server_name(), &body.room_id) .await { - info!( + debug_info!( origin = body.identity.as_str(), + room_id = %body.room_id, "Refusing to serve send_knock for room we aren't participating in" ); return Err!(Request(NotFound("This server is not participating in that room."))); @@ -72,88 +52,40 @@ pub(crate) async fn create_knock_event_v1_route( .await?; let room_version = services.rooms.state.get_room_version(&body.room_id).await?; + let create_event = services + .rooms + .state_accessor + .get_room_create_event(&body.room_id) + .await; let room_version_rules = room_version.rules().unwrap(); if !room_version_rules.authorization.knocking { return Err!(Request(Forbidden("Room version does not support knocking."))); } - let Ok((event_id, value)) = gen_event_id_canonical_json(&body.pdu, &room_version_rules) - else { - // Event could not be converted to canonical json - return Err!(Request(InvalidParam("Could not convert event to canonical json."))); - }; - - let event_type: StateEventType = serde_json::from_value( - value - .get("type") - .ok_or_else(|| err!(Request(InvalidParam("Event has no event type."))))? - .clone() - .into(), + let (mut event, target_membership, sender, target) = validate_any_membership_event( + &services, + &body.pdu, + &room_version_rules, + create_event.event_id().to_owned(), + body.room_id.clone(), + body.event_id.clone(), ) - .map_err(|e| err!(Request(InvalidParam("Event has invalid event type: {e}"))))?; + .await?; - if event_type != StateEventType::RoomMember { - return Err!(Request(InvalidParam( - "Not allowed to send non-membership state event to knock endpoint.", - ))); + if target_membership != MembershipState::Knock { + return Err!(Request(InvalidParam("Invalid membership (expected `knock`)"))); } - - let content: RoomMemberEventContent = serde_json::from_value( - value - .get("content") - .ok_or_else(|| err!(Request(InvalidParam("Membership event has no content"))))? - .clone() - .into(), - ) - .map_err(|e| err!(Request(InvalidParam("Event has invalid membership content: {e}"))))?; - - if content.membership != MembershipState::Knock { - return Err!(Request(InvalidParam( - "Not allowed to send a non-knock membership event to knock endpoint." - ))); - } - - // ACL check sender server name - let sender: OwnedUserId = serde_json::from_value( - value - .get("sender") - .ok_or_else(|| err!(Request(InvalidParam("Event has no sender user ID."))))? - .clone() - .into(), - ) - .map_err(|e| err!(Request(BadJson("Event sender is not a valid user ID: {e}"))))?; - - services - .rooms - .event_handler - .acl_check(sender.server_name(), &body.room_id) - .await?; - - // check if origin server is trying to send for another server if sender.server_name() != body.identity { - return Err!(Request(BadJson("Not allowed to knock on behalf of another server/user."))); + return Err!(Request(InvalidParam("Sender belongs to a different server"))); + } + if sender != target { + return Err!(Request(InvalidParam("Sender does not match state key"))); } - let state_key: OwnedUserId = serde_json::from_value( - value - .get("state_key") - .ok_or_else(|| err!(Request(InvalidParam("Event does not have a state_key"))))? - .clone() - .into(), - ) - .map_err(|e| err!(Request(BadJson("Event does not have a valid state_key: {e}"))))?; + event.insert("event_id".to_owned(), body.event_id.as_str().into()); - if state_key != sender { - return Err!(Request(InvalidParam("state_key does not match sender user of event."))); - } - - let mut event: JsonObject = serde_json::from_str(body.pdu.get()) - .map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?; - - event.insert("event_id".to_owned(), "$placeholder".into()); - - let pdu: PduEvent = serde_json::from_value(event.into()) + let pdu = PduEvent::from_id_val(&body.event_id, event.clone()) .map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?; let mutex_lock = services @@ -166,7 +98,7 @@ pub(crate) async fn create_knock_event_v1_route( let pdu_id = services .rooms .event_handler - .handle_incoming_pdu(sender.server_name(), &body.room_id, &event_id, value.clone(), false) + .handle_incoming_pdu(sender.server_name(), &body.room_id, &body.event_id, event, false) .boxed() .await? .ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?; diff --git a/src/api/server/utils.rs b/src/api/server/utils.rs index 52912dfdc..b420c59a9 100644 --- a/src/api/server/utils.rs +++ b/src/api/server/utils.rs @@ -1,7 +1,10 @@ -use conduwuit::{Err, Result, is_false}; +use conduwuit::{Err, Result, err, is_false}; use conduwuit_service::Services; use futures::{FutureExt, future::OptionFuture, join}; -use ruma::{EventId, RoomId, ServerName}; +use ruma::{ + CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, + UserId, events::room::member::MembershipState, room_version_rules::RoomVersionRules, +}; pub(super) struct AccessCheck<'a> { pub(super) services: &'a Services, @@ -63,3 +66,101 @@ pub(super) async fn assert(&self) -> Result { Ok(()) } } + +/// Performs validation on a membership event that should be run on any event a +/// remote is trying to send via us. +/// +/// ## Checks performed +/// +/// 1. PDU room ID matches request path room ID +/// 2. PDU event ID matches request path event ID +/// 3. Signature check +/// 4. Event type check +/// 5. `sender` field presence (and parsing) +/// 6. `state_key` field presence (and parsing) +/// 7. PDU room format check (PDU check 1) +/// +/// ## Returns +/// +/// A resulting tuple of (PDU JSON, target membership state, sender, recipient). +pub(crate) async fn validate_any_membership_event( + services: &crate::State, + body: &serde_json::value::RawValue, + room_version_rules: &RoomVersionRules, + create_event_id: OwnedEventId, + expected_room_id: OwnedRoomId, + expected_event_id: OwnedEventId, +) -> Result<(CanonicalJsonObject, MembershipState, OwnedUserId, OwnedUserId)> { + let (template_room_id, template_event_id, pdu) = services + .rooms + .event_handler + .parse_incoming_pdu(body) + .await + .map_err(|e| err!(Request(BadJson("Invalid membership PDU: {e}"))))?; + + if template_room_id != expected_room_id { + return Err!(Request(InvalidParam("Membership event does not belong to requested room"))); + } + if template_event_id != expected_event_id { + return Err!(Request(InvalidParam( + "Membership event ID does not match provided event ID" + ))); + } + + services + .server_keys + .verify_event(&pdu, room_version_rules) + .await + .map_err(|e| { + err!(Request(InvalidParam("Signature verification failed on membership event: {e}"))) + })?; + + // Ensure this is a membership event + if pdu + .get("type") + .expect("event must have a type") + .as_str() + .expect("type must be a string") + != "m.room.member" + { + return Err!(Request(BadJson( + "Not allowed to send non-membership event to this endpoint" + ))); + } + let membership = pdu + .get("content") + .ok_or_else(|| err!(Request(BadJson("Event missing content property"))))? + .as_object() + .ok_or_else(|| err!(Request(BadJson("Event content is not an object"))))? + .get("membership") + .ok_or_else(|| err!(Request(BadJson("Event missing membership property"))))? + .as_str() + .ok_or_else(|| err!(Request(BadJson("Event is not a string"))))? + .to_owned(); + + let sender_user = pdu + .get("sender") + .and_then(|v| v.as_str()) + .map(UserId::parse) + .and_then(Result::ok) + .ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?; + let recipient_user = pdu + .get("state_key") + .and_then(|v| v.as_str()) + .map(UserId::parse) + .and_then(Result::ok) + .ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?; + + // Do a quick format check. The spec doesn't suggest this, but it's probably + // a good idea nonetheless. + service::rooms::event_handler::Service::pdu_format_check_1( + &pdu, + room_version_rules, + &create_event_id, + ) + .map_err(|e| { + err!(Request(InvalidParam("Membership event violates the room event format: {e}"))) + })?; + + Ok((pdu, membership.into(), sender_user, recipient_user)) +}