mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-11 18:38:49 +00:00
fix: Merge nex/fix/incoming-federated-invites
This commit is contained in:
+276
-110
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user