fix: Merge nex/fix/incoming-federated-invites

This commit is contained in:
Jacob Taylor
2026-07-11 06:36:22 -07:00
parent 883b7acb39
commit e6dd5abdff
11 changed files with 522 additions and 451 deletions
+276 -110
View File
@@ -1,23 +1,29 @@
use std::collections::{HashMap, hash_map::Entry};
use axum::extract::State;
use axum_client_ip::ClientIp;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, PduEvent, Result, err, error,
matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256},
Err, Error, EventTypeExt, PduEvent, Result, err, error,
matrix::{Event, StateKey},
result::FlatOk,
state_res,
utils::hash::sha256,
warn,
};
use ruma::{
CanonicalJsonValue, UserId,
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, ServerName,
UserId,
api::{
error::{ErrorKind, IncompatibleRoomVersionErrorData},
federation::membership::{RawStrippedState, create_invite},
},
events::room::member::{MembershipState, RoomMemberEventContent},
serde::JsonObject,
events::{StateEventType, room::member::MembershipState},
room_version_rules::RoomVersionRules,
};
use serde::Deserialize;
use crate::Ruma;
use crate::{Ruma, server::utils::validate_any_membership_event};
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
///
@@ -28,30 +34,16 @@ pub(crate) async fn create_invite_route(
ClientIp(client): ClientIp,
body: Ruma<create_invite::v2::Request>,
) -> Result<create_invite::v2::Response> {
// ACL check origin
services
.rooms
.event_handler
.acl_check(&body.identity, &body.room_id)
.await?;
if !services.server.supported_room_version(&body.room_version) {
return Err(Error::BadRequest(
ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData::new(
body.room_version.clone(),
)),
"Server does not support this room version.",
"This server does not support that room version",
));
}
let room_version_rules = body.room_version.rules().unwrap();
if let Some(server) = body.room_id.server_name() {
if services.moderation.is_remote_server_forbidden(server) {
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
}
if services
.moderation
.is_remote_server_forbidden(&body.identity)
@@ -61,90 +53,54 @@ pub(crate) async fn create_invite_route(
body.identity, body.room_id
);
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
return Err!(Request(Forbidden("Federation denied with {}", body.identity)));
}
let mut signed_event = utils::to_canonical_object(&body.event)
.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
// 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, state) = 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?;
let create_event_json = state
.get(&StateEventType::RoomCreate.with_state_key(""))
.expect("must have create event in invite state by this point");
// 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
// We can now perform the banned remote server check with the create event.
// N.B. this checks the sender field, which is technically incorrect for rooms
// v10 and below. This usually isn't the case though so sue me
let creator = create_event_json
.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())
.flat_ok()
.expect("must have valid sender in create event");
if services
.moderation
.is_remote_server_forbidden(creator.server_name())
{
return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
// Make sure we're not ACL'ed from their room.
services
.rooms
.event_handler
.acl_check(recipient_user.server_name(), &body.room_id)
.await?;
services
.server_keys
.hash_and_sign_event(&mut signed_event, &room_version_rules)
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
// Generate event id
let event_id = gen_event_id(&signed_event, &room_version_rules)?;
// Add event_id back
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
// And then we can validate the member event itself
let (mut signed_event, sender_user, recipient_user) = validate_invite_membership_event(
&services,
&body.event,
&room_version_rules,
&body.identity,
create_event_id.clone(),
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
{
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
return Err!(Request(Forbidden("That room is banned on this homeserver.")));
}
if services.config.block_non_admin_invites && !services.users.is_admin(&recipient_user).await
@@ -161,30 +117,57 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
}
// If we're already in the room, ensure that neither the origin nor ourselves
// are ACL'd.
let resident = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await;
if resident {
services
.rooms
.event_handler
.acl_check(&body.identity, &body.room_id)
.await?;
services
.rooms
.event_handler
.acl_check(recipient_user.server_name(), &body.room_id)
.await
.map_err(|_| err!(Request(Forbidden("This server is ACL'd from that room"))))?;
}
services
.server_keys
.hash_and_sign_event(&mut signed_event, &room_version_rules)
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
// Add event_id back
signed_event
.insert("event_id".to_owned(), CanonicalJsonValue::String(body.event_id.to_string()));
let mut invite_state = body.invite_room_state.clone();
let mut event: JsonObject = serde_json::from_str(body.event.get())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
event.insert("event_id".to_owned(), "$placeholder".into());
let pdu: PduEvent = serde_json::from_value(event.into())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
invite_state.push(RawStrippedState::Pdu(
serde_json::value::to_raw_value(&pdu).expect("PDU was just created, it must be valid"),
));
let pdu = PduEvent::from_id_val(&body.event_id, signed_event.clone())
.expect("must be able to create PDU object");
invite_state.push(RawStrippedState::Pdu(serde_json::value::to_raw_value(&signed_event)?));
// If we are active in the room, the remote server will notify us about the
// join/invite through /send. If we are not in the room, we need to manually
// record the invited state for client /sync through update_membership(), and
// send the invite PDU to the relevant appservices.
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &body.room_id)
.await
{
if !resident {
// We will start by recording the room's create event as an outlier.
// This will allow us to recognise it later in case the sender revokes the
// invite over federation later. We could store more state from the invite
// request, but we will get that during send_join anyway.
// This is safe to just add directly as an outlier as we already auth checked it
// during validation.
services
.rooms
.outlier
.add_pdu_outlier(&create_event_id, create_event_json);
services
.rooms
.state_cache
@@ -240,3 +223,186 @@ 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_invite_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 (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
if sender_user.server_name() != origin {
return Err!(Request(Forbidden("Sender belongs to a different server")));
}
// Ensure the target user belongs to this server
if !services
.globals
.server_is_ours(recipient_user.server_name())
{
return Err!(Request(InvalidParam("Recipient does not belong to this homeserver")));
}
if target_membership != MembershipState::Invite {
return Err!(Request(BadJson("Invalid membership (expected `invite`)")));
}
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<OwnedEventId> = 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 {
// Ensure this is a legal create event.
let pdu_event =
PduEvent::from_id_val(&state_event_id, state_event_json.clone())
.expect("must be able to create pdu event from event json");
validate_invite_create_event(&pdu_event, room_version_rules).await?;
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))
}
#[derive(Deserialize)]
struct MFederate {
#[serde(rename = "m.federate")]
mfederate: Option<bool>,
}
/// Validates that a create event is suitable for the invite, namely:
///
/// 1. It passes auth checks (aka is valid)
/// 2. The room is federated (there's no point persisting unfederated rooms)
async fn validate_invite_create_event(
pdu: &PduEvent,
room_version_rules: &RoomVersionRules,
) -> Result {
if !state_res::auth_check(
room_version_rules,
pdu,
None,
|_, _| async {
unreachable!("No state should be fetched when processing a lone create event");
},
pdu,
)
.await
.unwrap_or_default()
{
return Err!(Request(InvalidParam("m.room.create event fails auth check")));
}
let can_federate = pdu.get_content::<MFederate>()?.mfederate;
if !can_federate.unwrap_or(true) {
return Err!(Request(InvalidParam(
"Cannot receive invites to a room with m.federate=false"
)));
}
Ok(())
}
-8
View File
@@ -75,14 +75,6 @@ pub(crate) async fn create_join_event_template_route(
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) {
return Err!(Request(Forbidden(warn!(
"Room ID server name {server} is banned on this homeserver."
))));
}
}
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
let room_version_rules = room_version.rules().unwrap();
if !body.ver.contains(&room_version) {
-6
View File
@@ -58,12 +58,6 @@ pub(crate) async fn create_knock_event_template_route(
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) {
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
}
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
let room_version_rules = room_version.rules().unwrap();
+1
View File
@@ -27,6 +27,7 @@ pub(crate) async fn create_leave_event_template_route(
{
info!(
origin = body.identity.as_str(),
room_id = %body.room_id,
"Refusing to serve make_leave for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
+10 -6
View File
@@ -296,12 +296,16 @@ async fn handle_room(
// Try to sort PDUs by their dependencies, but fall back to arbitrary order on
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's
// responsibility.
let sorted_event_ids = build_local_dag(&pdu_map, DagBuilderTree::PrevEvents)
.await
.unwrap_or_else(|e| {
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
pdu_map.keys().cloned().collect()
});
let sorted_event_ids = if pdu_map.len() >= 2 {
build_local_dag(&pdu_map, DagBuilderTree::PrevEvents)
.await
.unwrap_or_else(|e| {
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
pdu_map.keys().cloned().collect()
})
} else {
pdu_map.keys().cloned().collect()
};
let mut results = Vec::with_capacity(sorted_event_ids.len());
for event_id in sorted_event_ids {
let value = pdu_map
+65 -114
View File
@@ -2,26 +2,25 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at, debug, err, info,
matrix::event::gen_event_id_canonical_json,
trace,
Err, Event, Result, at, debug, err, info, trace,
utils::stream::{BroadbandExt, IterStream, TryBroadbandExt},
warn,
};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName,
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName, UserId,
api::federation::membership::create_join_event,
assign,
events::{
StateEventType, TimelineEventType,
TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
room_version_rules::RoomVersionRules,
};
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
use serde_json::value::to_raw_value;
use crate::Ruma;
use crate::{Ruma, server::utils::validate_any_membership_event};
/// helper method for /send_join v1 and v2
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()), level = "info")]
@@ -29,7 +28,9 @@ async fn create_join_event(
services: &Services,
origin: &ServerName,
room_id: &RoomId,
pdu: &RawJsonValue,
event_id: &EventId,
room_version_rules: &RoomVersionRules,
mut pdu: CanonicalJsonObject,
omit_members: bool,
) -> Result<create_join_event::v2::RoomState> {
if !services.rooms.metadata.exists(room_id).await {
@@ -43,6 +44,7 @@ async fn create_join_event(
{
info!(
origin = origin.as_str(),
room_id = %room_id,
"Refusing to serve send_join for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
@@ -64,94 +66,25 @@ async fn create_join_event(
.await
.map_err(|e| err!(Request(NotFound(error!("Room has no state: {e}")))))?;
// We do not add the event_id field to the pdu here because of signature and
// hashes checks
trace!("Getting room version");
let room_version = services.rooms.state.get_room_version(room_id).await?;
let room_version_rules = room_version.rules().unwrap();
trace!("Generating event ID and converting to canonical json");
let Ok((event_id, mut value)) = gen_event_id_canonical_json(pdu, &room_version_rules) else {
// Event could not be converted to canonical json
return Err!(Request(BadJson("Could not convert event to canonical json.")));
};
let event_room_id: OwnedRoomId = serde_json::from_value(
value
.get("room_id")
.ok_or_else(|| err!(Request(BadJson("Event missing room_id property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("room_id field is not a valid room ID: {e}")))))?;
if event_room_id != room_id {
return Err!(Request(BadJson("Event room_id does not match request path room ID.")));
}
let event_type: StateEventType = serde_json::from_value(
value
.get("type")
.ok_or_else(|| err!(Request(BadJson("Event missing type property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("Event has invalid state event type: {e}")))))?;
if event_type != StateEventType::RoomMember {
return Err!(Request(BadJson(
"Not allowed to send non-membership state event to join endpoint."
)));
}
let sender = pdu
.get("sender")
.and_then(|v| v.as_str())
.map(UserId::parse)
.and_then(Result::ok)
.expect("sender was already validated");
let content: RoomMemberEventContent = serde_json::from_value(
value
.get("content")
pdu.get("content")
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
if content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"Not allowed to send a non-join membership event to join endpoint."
)));
}
let sender: OwnedUserId = serde_json::from_value(
value
.get("sender")
.ok_or_else(|| err!(Request(BadJson("Event missing sender property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("sender property is not a valid user ID: {e}")))))?;
// check if origin server is trying to send for another server
if sender.server_name() != origin {
return Err!(Request(Forbidden("Not allowed to join on behalf of another server.")));
}
let state_key: OwnedUserId = serde_json::from_value(
value
.get("state_key")
.ok_or_else(|| err!(Request(BadJson("Event missing state_key property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("State key is not a valid user ID: {e}")))))?;
if state_key != sender {
return Err!(Request(BadJson("State key does not match sender user.")));
}
if let Some(authorising_user) = content.join_authorized_via_users_server {
use ruma::RoomVersionId::*;
if matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
if !room_version_rules.authorization.restricted_join_rule {
return Err!(Request(InvalidParam(
"Room version {room_version} does not support restricted rooms but \
"Room version does not support restricted rooms but \
join_authorised_via_users_server ({authorising_user}) was found in the event."
)));
}
@@ -174,8 +107,7 @@ async fn create_join_event(
they cannot authorise your join."
)));
}
if !super::user_can_perform_restricted_join(services, &state_key, room_id).await? {
if !super::user_can_perform_restricted_join(services, &sender, room_id).await? {
return Err!(Request(UnableToAuthorizeJoin(
"Joining user did not pass restricted room's rules."
)));
@@ -183,7 +115,7 @@ async fn create_join_event(
services
.server_keys
.hash_and_sign_event(&mut value, &room_version_rules)
.hash_and_sign_event(&mut pdu, room_version_rules)
.map_err(|e| {
err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}"))))
})?;
@@ -200,7 +132,7 @@ async fn create_join_event(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(sender.server_name(), room_id, &event_id, value.clone(), false)
.handle_incoming_pdu(sender.server_name(), room_id, event_id, pdu.clone(), false)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
@@ -220,14 +152,12 @@ async fn create_join_event(
.iter()
.try_stream()
.broad_filter_map(|event_id| async move {
if omit_members {
if let Ok(e) = event_id.as_ref() {
let pdu = services.rooms.timeline.get_pdu(e).await;
if pdu.is_ok_and(|p| *p.kind() == TimelineEventType::RoomMember) {
trace!("omitting member event {e:?} from returned state");
// skip members
return None;
}
if omit_members && let Ok(e) = event_id.as_ref() {
let pdu = services.rooms.timeline.get_pdu(e).await;
if pdu.is_ok_and(|p| *p.kind() == TimelineEventType::RoomMember) {
trace!("omitting member event {e:?} from returned state");
// skip members
return None;
}
}
Some(event_id)
@@ -289,7 +219,7 @@ async fn create_join_event(
Ok(assign!(create_join_event::v2::RoomState::new(), {
auth_chain,
state,
event: to_raw_value(&CanonicalJsonValue::Object(value)).ok(),
event: to_raw_value(&CanonicalJsonValue::Object(pdu)).ok(),
members_omitted: omit_members,
servers_in_room,
}))
@@ -309,24 +239,45 @@ pub(crate) async fn create_join_event_v2_route(
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 joining room ID {} through us which has a server name that is \
globally forbidden. Rejecting.",
body.identity, &body.room_id,
);
return Err!(Request(Forbidden(warn!(
"Room ID server name {server} is banned on this homeserver."
))));
}
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();
let (value, membership, sender, target) = validate_any_membership_event(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id.clone(),
body.room_id.clone(),
body.event_id.clone(),
)
.await?;
if membership != MembershipState::Join {
return Err!(Request(InvalidParam("Invalid membership (expected `join`)")));
}
if sender.server_name() != body.identity {
return Err!(Request(InvalidParam("Sender belongs to a different server")));
}
if sender != target {
return Err!(Request(InvalidParam("Sender does not match state key")));
}
let now = Instant::now();
let room_state =
create_join_event(&services, &body.identity, &body.room_id, &body.pdu, body.omit_members)
.boxed()
.await?;
let room_state = create_join_event(
&services,
&body.identity,
&body.room_id,
&body.event_id,
&room_version_rules,
value,
body.omit_members,
)
.boxed()
.await?;
info!(
"Finished sending a join for {} in {} in {:?}",
body.identity,
+28 -96
View File
@@ -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."))))?;
+31 -104
View File
@@ -1,18 +1,11 @@
use axum::extract::State;
use conduwuit::{Err, Result, err, info, matrix::event::gen_event_id_canonical_json};
use conduwuit_service::Services;
use conduwuit::{Err, Result, debug_info, err};
use futures::FutureExt;
use ruma::{
OwnedRoomId, OwnedUserId, RoomId, ServerName,
api::federation::membership::create_leave_event,
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
api::federation::membership::create_leave_event, events::room::member::MembershipState,
};
use serde_json::value::RawValue as RawJsonValue;
use crate::Ruma;
use crate::{Ruma, server::utils::validate_any_membership_event};
/// # `PUT /_matrix/federation/v2/send_leave/{roomId}/{eventId}`
///
@@ -21,17 +14,8 @@ pub(crate) async fn create_leave_event_v2_route(
State(services): State<crate::State>,
body: Ruma<create_leave_event::v2::Request>,
) -> Result<create_leave_event::v2::Response> {
create_leave_event(&services, &body.identity, &body.room_id, &body.pdu).await?;
Ok(create_leave_event::v2::Response::new())
}
async fn create_leave_event(
services: &Services,
origin: &ServerName,
room_id: &RoomId,
pdu: &RawJsonValue,
) -> Result {
let room_id = body.room_id.as_ref();
let origin = &body.identity;
if !services.rooms.metadata.exists(room_id).await {
return Err!(Request(NotFound("Room is unknown to this server.")));
}
@@ -42,9 +26,10 @@ async fn create_leave_event(
.server_in_room(services.globals.server_name(), room_id)
.await
{
info!(
debug_info!(
origin = origin.as_str(),
"Refusing to serve backfill for room we aren't participating in"
room_id = %room_id,
"Refusing to send_leave for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
@@ -56,91 +41,31 @@ async fn create_leave_event(
.acl_check(origin, room_id)
.await?;
// We do not add the event_id field to the pdu here because of signature and
// hashes checks
let room_version = services.rooms.state.get_room_version(room_id).await?;
let create_event = services
.rooms
.state_accessor
.get_room_create_event(room_id)
.await;
let room_version_rules = room_version.rules().unwrap();
let Ok((event_id, value)) = gen_event_id_canonical_json(pdu, &room_version_rules) else {
// Event could not be converted to canonical json
return Err!(Request(BadJson("Could not convert event to canonical json.")));
};
let event_room_id: OwnedRoomId = serde_json::from_value(
serde_json::to_value(
value
.get("room_id")
.ok_or_else(|| err!(Request(BadJson("Event missing room_id property."))))?,
)
.expect("CanonicalJson is valid json value"),
let (value, membership, sender, target) = validate_any_membership_event(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id.clone(),
body.room_id.clone(),
body.event_id.clone(),
)
.map_err(|e| err!(Request(BadJson(warn!("room_id field is not a valid room ID: {e}")))))?;
if event_room_id != room_id {
return Err!(Request(BadJson("Event room_id does not match request path room ID.")));
.await?;
if membership != MembershipState::Leave {
return Err!(Request(InvalidParam("Invalid membership (expected `leave`)")));
}
let content: RoomMemberEventContent = serde_json::from_value(
value
.get("content")
.ok_or_else(|| err!(Request(BadJson("Event missing content property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
if content.membership != MembershipState::Leave {
return Err!(Request(BadJson(
"Not allowed to send a non-leave membership event to leave endpoint."
)));
if sender.server_name() != body.identity {
return Err!(Request(InvalidParam("Sender belongs to a different server")));
}
let event_type: StateEventType = serde_json::from_value(
value
.get("type")
.ok_or_else(|| err!(Request(BadJson("Event missing type property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("Event has invalid state event type: {e}")))))?;
if event_type != StateEventType::RoomMember {
return Err!(Request(BadJson(
"Not allowed to send non-membership state event to leave endpoint."
)));
}
// ACL check sender server name
let sender: OwnedUserId = serde_json::from_value(
value
.get("sender")
.ok_or_else(|| err!(Request(BadJson("Event missing sender property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("sender property is not a valid user ID: {e}")))))?;
services
.rooms
.event_handler
.acl_check(sender.server_name(), room_id)
.await?;
if sender.server_name() != origin {
return Err!(Request(BadJson("Not allowed to leave on behalf of another server/user.")));
}
let state_key: OwnedUserId = serde_json::from_value(
value
.get("state_key")
.ok_or_else(|| err!(Request(BadJson("Event missing state_key property."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("State key is not a valid user ID: {e}")))))?;
if state_key != sender {
return Err!(Request(BadJson("State key does not match sender user.")));
if sender != target {
return Err!(Request(InvalidParam("Sender does not match state key")));
}
let mutex_lock = services
@@ -153,7 +78,7 @@ async fn create_leave_event(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(origin, room_id, &event_id, value, false)
.handle_incoming_pdu(origin, room_id, &body.event_id, value, false)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
@@ -164,5 +89,7 @@ async fn create_leave_event(
.sending
.send_pdu_room(room_id, &pdu_id)
.boxed()
.await
.await?;
Ok(create_leave_event::v2::Response::new())
}
+103 -2
View File
@@ -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))
}
@@ -14,10 +14,12 @@ impl super::Service {
/// This performs steps 1 through 4 of [S-S section 5.1][spec], returning
/// the parsed PDU and modified JSON object.
///
/// **External callers likely want `handle_incoming_pdu` instead.**
///
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(name = "handle_outlier", skip_all)]
pub(super) async fn handle_outlier_pdu<'a, Pdu>(
pub async fn handle_outlier_pdu<'a, Pdu>(
&self,
origin: &'a ServerName,
create_event: &'a Pdu,
@@ -54,7 +56,6 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
})?;
value.remove("unsigned");
let room_version_rules = get_room_version_rules(create_event)?;
// 2. Check signatures, otherwise drop.
// 3. Check content hash, redacting the event if it fails.
@@ -18,7 +18,7 @@ impl super::Service {
/// Checks that the PDU conforms to the PDU format (check 1). This is
/// already mostly done during deserialisation, so this function just checks
/// that the PDU isn't a too large.
pub(super) fn pdu_format_check_1(
pub fn pdu_format_check_1(
pdu_json: &CanonicalJsonObject,
room_version_rules: &RoomVersionRules,
create_event_id: &EventId,
@@ -45,6 +45,8 @@ pub(super) fn pdu_format_check_1(
let create_event_in_auth_events = auth_events.iter().any(|id| id == create_event_id);
if !event_format.allow_room_create_in_auth_events && create_event_in_auth_events {
return Err!(Request(BadJson("PDU references a create event")));
} else if event_format.allow_room_create_in_auth_events && !create_event_in_auth_events {
return Err!(Request(BadJson("PDU does not reference the room create event")));
}
let prev_events = expect_event_id_array(pdu_json, "prev_events")?;
@@ -59,7 +61,7 @@ pub(super) fn pdu_format_check_1(
/// the content hash verification fails (check 3), returning the
/// potentially modified JSON. Returns an error if the PDU cannot be
/// redacted, or fails signature verification.
pub(super) async fn signature_hash_check_2_3(
pub async fn signature_hash_check_2_3(
&self,
pdu_json: CanonicalJsonObject,
room_version_rules: &RoomVersionRules,
@@ -92,7 +94,7 @@ pub(super) async fn signature_hash_check_2_3(
/// If the auth check fails, false is returned, otherwise true.
///
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu
pub(super) async fn auth_state_check_4(
pub async fn auth_state_check_4(
&self,
pdu: &PduEvent,
room_version_rules: &RoomVersionRules,