mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-08-28 13:14:05 +00:00
feat: Give send_knock the invite treatment
This commit is contained in:
+15
-82
@@ -23,7 +23,7 @@
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::Ruma;
|
||||
use crate::{Ruma, server::utils::validate_any_membership_event};
|
||||
|
||||
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
|
||||
///
|
||||
@@ -86,7 +86,7 @@ pub(crate) async fn create_invite_route(
|
||||
}
|
||||
|
||||
// And then we can validate the member event itself
|
||||
let (mut signed_event, sender_user, recipient_user) = validate_membership_event(
|
||||
let (mut signed_event, sender_user, recipient_user) = validate_invite_membership_event(
|
||||
&services,
|
||||
&body.event,
|
||||
&room_version_rules,
|
||||
@@ -234,7 +234,7 @@ pub(crate) async fn create_invite_route(
|
||||
/// invite event itself.
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid
|
||||
async fn validate_membership_event(
|
||||
async fn validate_invite_membership_event(
|
||||
services: &crate::State,
|
||||
body: &serde_json::value::RawValue,
|
||||
room_version_rules: &RoomVersionRules,
|
||||
@@ -243,80 +243,22 @@ async fn validate_membership_event(
|
||||
room_id: OwnedRoomId,
|
||||
event_id: OwnedEventId,
|
||||
) -> Result<(CanonicalJsonObject, OwnedUserId, OwnedUserId)> {
|
||||
let (template_room_id, template_event_id, pdu) = services
|
||||
.rooms
|
||||
.event_handler
|
||||
.parse_incoming_pdu(body)
|
||||
.await
|
||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
||||
|
||||
if template_room_id != room_id {
|
||||
return Err!(Request(InvalidParam("Membership event does not belong to requested room")));
|
||||
}
|
||||
if template_event_id != event_id {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Membership event ID does not match provided event ID"
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.server_keys
|
||||
.verify_event(&pdu, room_version_rules)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
|
||||
})?;
|
||||
|
||||
// Ensure this is a membership event
|
||||
if pdu
|
||||
.get("type")
|
||||
.expect("event must have a type")
|
||||
.as_str()
|
||||
.expect("type must be a string")
|
||||
!= "m.room.member"
|
||||
{
|
||||
return Err!(Request(BadJson(
|
||||
"Not allowed to send non-membership event to invite endpoint"
|
||||
)));
|
||||
}
|
||||
|
||||
// Ensure it is an invite event
|
||||
// My Huge Chain (avoids deser)
|
||||
let membership = pdu
|
||||
.get("content")
|
||||
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
|
||||
.as_object()
|
||||
.ok_or_else(|| err!(Request(BadJson("Event content is not an object"))))?
|
||||
.get("membership")
|
||||
.ok_or_else(|| err!(Request(BadJson("Event missing membership property"))))?
|
||||
.as_str()
|
||||
.ok_or_else(|| err!(Request(BadJson("Event is not a string"))))?;
|
||||
if MembershipState::Invite != membership.into() {
|
||||
return Err!(Request(BadJson(
|
||||
"Not allowed to send non-invite membership event to invite endpoint"
|
||||
)));
|
||||
}
|
||||
let (pdu, target_membership, sender_user, recipient_user) = validate_any_membership_event(
|
||||
services,
|
||||
body,
|
||||
room_version_rules,
|
||||
create_event_id,
|
||||
room_id,
|
||||
event_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Ensure the sender belongs to the remote that is sending the invite
|
||||
let sender_user = pdu
|
||||
.get("sender")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(UserId::parse)
|
||||
.and_then(Result::ok)
|
||||
.ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?;
|
||||
|
||||
if sender_user.server_name() != origin {
|
||||
return Err!(Request(Forbidden("Sender belongs to a different server")));
|
||||
}
|
||||
|
||||
// Ensure the target user belongs to this server
|
||||
let recipient_user = pdu
|
||||
.get("state_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(UserId::parse)
|
||||
.and_then(Result::ok)
|
||||
.ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?;
|
||||
|
||||
if !services
|
||||
.globals
|
||||
.server_is_ours(recipient_user.server_name())
|
||||
@@ -324,18 +266,9 @@ async fn validate_membership_event(
|
||||
return Err!(Request(InvalidParam("Recipient does not belong to this homeserver")));
|
||||
}
|
||||
|
||||
// Do a quick format check. The spec doesn't suggest this, but it's probably
|
||||
// a good idea nonetheless.
|
||||
service::rooms::event_handler::Service::pdu_format_check_1(
|
||||
&pdu,
|
||||
room_version_rules,
|
||||
&create_event_id,
|
||||
)
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam(
|
||||
"Invite membership event violates the room event format: {e}"
|
||||
)))
|
||||
})?;
|
||||
if target_membership != MembershipState::Invite {
|
||||
return Err!(Request(BadJson("Invalid membership (expected `invite`)")));
|
||||
}
|
||||
|
||||
Ok((pdu, sender_user, recipient_user))
|
||||
}
|
||||
|
||||
@@ -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."))))?;
|
||||
|
||||
+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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user