diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 078511ec7..b81aa4f96 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -6,19 +6,18 @@ use conduwuit::{ Err, Error, EventTypeExt, PduEvent, Result, err, error, matrix::{Event, StateKey, event::gen_event_id}, - utils::{self, hash::sha256}, + utils::hash::sha256, warn, }; use ruma::{ - CanonicalJsonValue, OwnedEventId, UserId, + CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, ServerName, + UserId, api::{ error::{ErrorKind, IncompatibleRoomVersionErrorData}, federation::membership::{RawStrippedState, create_invite}, }, - events::{ - StateEventType, - room::member::{MembershipState, RoomMemberEventContent}, - }, + events::{StateEventType, room::member::MembershipState}, + room_version_rules::RoomVersionRules, serde::JsonObject, }; @@ -69,150 +68,27 @@ pub(crate) async fn create_invite_route( return Err!(Request(Forbidden("Server is banned on this homeserver."))); } - let mut signed_event = utils::to_canonical_object(&body.event) - .map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?; - - services - .server_keys - .verify_event(&signed_event, &room_version_rules) - .await - .map_err(|e| { - err!(Request(InvalidParam("Signature verification failed on invite event: {e}"))) - })?; - - // Ensure this is a membership event - if signed_event - .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." - ))); - } - - let content: RoomMemberEventContent = serde_json::from_value( - signed_event - .get("content") - .ok_or_else(|| err!(Request(BadJson("Event missing content property"))))? - .clone() - .into(), + // First, validate the invite room state, so we can compare with the create + // event. + let (create_event_id, _) = validate_invite_state( + &services, + &body.invite_room_state, + &room_version_rules, + body.room_id.clone(), ) - .map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?; + .await?; - // Ensure this is an invite membership event - if content.membership != MembershipState::Invite { - return Err!(Request(BadJson( - "Not allowed to send a non-invite membership event to invite endpoint." - ))); - } - - // Ensure the sending user isn't a lying bozo - let sender_user = signed_event - .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() != body.identity { - return Err!(Request(Forbidden("Sender's server does not match the origin server.",))); - } - - // Ensure the target user belongs to this server - let recipient_user = signed_event - .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()) - { - return Err!(Request(InvalidParam("User does not belong to this homeserver."))); - } - - // Validate the invite room state. - let mut invite_state_map: HashMap<(StateEventType, StateKey), _> = - HashMap::with_capacity(body.invite_room_state.len()); - let mut create_event_id: Option = None; - for (idx, invite_state_event) in body.invite_room_state.iter().cloned().enumerate() { - // Stripped state hasn't been sent over federation since v1.16. - let RawStrippedState::Pdu(raw_pdu) = invite_state_event else { - return Err!(Request(InvalidParam( - "PDU in invite state (index {idx}) violates the room event format" - ))); - }; - let (state_event_room_id, state_event_id, state_event_json) = services - .rooms - .event_handler - .parse_incoming_pdu(&raw_pdu) - .await - .map_err(|e| err!(Request(InvalidParam("Invalid PDU in invite state: {e}"))))?; - - if state_event_room_id != body.room_id { - return Err!(Request(InvalidParam( - "PDU in invite state ({state_event_id}) belongs to the wrong room" - ))); - } - - services - .server_keys - .verify_event(&signed_event, &room_version_rules) - .await - .map_err(|e| { - err!(Request(InvalidParam("Signature verification failed on invite event: {e}"))) - })?; - - let Some(state_key) = state_event_json.get("state_key").and_then(|k| k.as_str()) else { - return Err!(Request(InvalidParam( - "PDU in invite state ({state_event_id}) is not a state event" - ))); - }; - let Some(event_type) = state_event_json.get("event_type").and_then(|k| k.as_str()) else { - return Err!(Request(InvalidParam( - "PDU in invite state ({state_event_id}) is not an event?" - ))); - }; - - let key = StateEventType::from(event_type).with_state_key(state_key); - match invite_state_map.entry(key) { - | Entry::Occupied(entry) => - return Err!(Request(InvalidParam( - "Duplicate state events in invite state for state key: {:?}", - entry.key(), - ))), - | Entry::Vacant(entry) => { - if entry.key().0 == StateEventType::RoomCreate { - let event_id = gen_event_id(&state_event_json, &room_version_rules) - .expect("must be able to generate room ID"); - create_event_id = Some(event_id); - } - entry.insert(state_event_json); - }, - } - } - let Some(create_event_id) = create_event_id else { - return Err!(Request(InvalidParam( - "Invite state does not contain the m.room.create event" - ))); - }; - invite_state_map.iter().try_for_each(|(key, event_json)| { - service::rooms::event_handler::Service::pdu_format_check_1( - event_json, - &room_version_rules, - &create_event_id, - ) - .map_err(|e| { - err!(Request(InvalidParam( - "PDU in invite state for {key:?} violates the room event format: {e}" - ))) - }) - })?; + // And then we can validate the member event itself + let (mut signed_event, sender_user, recipient_user) = validate_membership_event( + &services, + &body.event, + &room_version_rules, + &body.identity, + create_event_id, + body.room_id.clone(), + body.event_id.clone(), + ) + .await?; if services.rooms.metadata.is_banned(&body.room_id).await && !services.users.is_admin(&recipient_user).await @@ -331,3 +207,209 @@ pub(crate) async fn create_invite_route( .await, )) } + +/// Validates the *membership event* in the invite request, per the steps listed +/// under the invite endpoint's [spec]. +/// +/// Returns the validated JSON body, sender user ID, and recipient user ID. +/// +/// Since this function performs a PDU format check, the create event must be +/// known ahead of time. This implies validating the invite state before the +/// invite event itself. +/// +/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid +async fn validate_membership_event( + services: &crate::State, + body: &serde_json::value::RawValue, + room_version_rules: &RoomVersionRules, + origin: &ServerName, + create_event_id: OwnedEventId, + 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" + ))); + } + + // 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()) + { + 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}" + ))) + })?; + + Ok((pdu, sender_user, recipient_user)) +} + +/// Validates the *invite state* of an invite request, per the steps listed +/// under the endpoint's [spec]. +/// +/// Returns the create event's event ID, and the partial state map. +/// +/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid +async fn validate_invite_state( + services: &crate::State, + invite_state: &[RawStrippedState], + room_version_rules: &RoomVersionRules, + room_id: OwnedRoomId, +) -> Result<(OwnedEventId, HashMap<(StateEventType, StateKey), CanonicalJsonObject>)> { + let mut invite_state_map: HashMap<(StateEventType, StateKey), _> = + HashMap::with_capacity(invite_state.len()); + let mut create_event_id: Option = None; + for (idx, invite_state_event) in invite_state.iter().cloned().enumerate() { + // Stripped state hasn't been sent over federation since v1.16. + let RawStrippedState::Pdu(raw_pdu) = invite_state_event else { + return Err!(Request(InvalidParam( + "PDU in invite state (index {idx}) violates the room event format" + ))); + }; + let (state_event_room_id, state_event_id, state_event_json) = services + .rooms + .event_handler + .parse_incoming_pdu(&raw_pdu) + .await + .map_err(|e| err!(Request(InvalidParam("Invalid PDU in invite state: {e}"))))?; + + if state_event_room_id != room_id { + return Err!(Request(InvalidParam( + "PDU in invite state ({state_event_id}) belongs to the wrong room" + ))); + } + + services + .server_keys + .verify_event(&state_event_json, room_version_rules) + .await + .map_err(|e| { + err!(Request(InvalidParam("Signature verification failed on invite event: {e}"))) + })?; + + let Some(state_key) = state_event_json.get("state_key").and_then(|k| k.as_str()) else { + return Err!(Request(InvalidParam( + "PDU in invite state ({state_event_id}) is not a state event" + ))); + }; + let Some(event_type) = state_event_json.get("event_type").and_then(|k| k.as_str()) else { + return Err!(Request(InvalidParam( + "PDU in invite state ({state_event_id}) is not an event?" + ))); + }; + + let key = StateEventType::from(event_type).with_state_key(state_key); + match invite_state_map.entry(key) { + | Entry::Occupied(entry) => + return Err!(Request(InvalidParam( + "Duplicate state events in invite state for state key: {:?}", + entry.key(), + ))), + | Entry::Vacant(entry) => { + if entry.key().0 == StateEventType::RoomCreate { + create_event_id = Some(state_event_id); + } + entry.insert(state_event_json); + }, + } + } + let Some(create_event_id) = create_event_id else { + return Err!(Request(InvalidParam( + "Invite state does not contain the m.room.create event" + ))); + }; + invite_state_map.iter().try_for_each(|(key, event_json)| { + service::rooms::event_handler::Service::pdu_format_check_1( + event_json, + room_version_rules, + &create_event_id, + ) + .map_err(|e| { + err!(Request(InvalidParam( + "PDU in invite state for {key:?} violates the room event format: {e}" + ))) + }) + })?; + + Ok((create_event_id, invite_state_map)) +}