mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-15 13:16:56 +00:00
232 lines
7.4 KiB
Rust
232 lines
7.4 KiB
Rust
use std::collections::{BTreeMap, HashMap, hash_map};
|
|
|
|
use conduwuit::{
|
|
Err, Event, EventTypeExt, PduEvent, Result, debug, debug_info, debug_warn, err, info, trace,
|
|
warn,
|
|
};
|
|
use ruma::{
|
|
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
|
|
canonical_json::redact,
|
|
};
|
|
|
|
use super::{check_room_id, get_room_version_rules};
|
|
use crate::rooms::timeline::pdu_fits;
|
|
|
|
impl super::Service {
|
|
/// Handles a PDU as an outlier, performing basic checks like signatures and
|
|
/// hashes, proclaimed event auth, and then adding it to the outlier tree.
|
|
///
|
|
/// This performs steps 1 through 4 of [S-S section 5.1][spec], returning
|
|
/// the parsed PDU and modified JSON object.
|
|
///
|
|
/// [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>(
|
|
&self,
|
|
origin: &'a ServerName,
|
|
create_event: &'a Pdu,
|
|
event_id: &'a EventId,
|
|
room_id: &'a RoomId,
|
|
mut value: CanonicalJsonObject,
|
|
) -> Result<(PduEvent, BTreeMap<String, CanonicalJsonValue>)>
|
|
where
|
|
Pdu: Event + Send + Sync,
|
|
{
|
|
// Skip outlier handling if we already have this event as either a timeline or
|
|
// outlier PDU.
|
|
if let Ok(pdu_event) = self.services.timeline.get_pdu(event_id).await {
|
|
debug!(
|
|
"Already have event {event_id} as an outlier or timeline event, not \
|
|
re-processing"
|
|
);
|
|
value.insert(
|
|
"event_id".to_owned(),
|
|
CanonicalJsonValue::String(event_id.as_str().to_owned()),
|
|
);
|
|
check_room_id(room_id, &pdu_event)?;
|
|
return Ok((pdu_event, value));
|
|
}
|
|
|
|
// 1. Check that the PDU follows the format for the room version
|
|
// (in this case, just size check)
|
|
if !Self::pdu_format_check_1(&value) {
|
|
warn!(
|
|
"dropping incoming PDU {event_id} in room {room_id} from {origin} because it \
|
|
exceeds 65535 bytes or is otherwise too large."
|
|
);
|
|
return Err!(Request(TooLarge("PDU is too large")));
|
|
}
|
|
|
|
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.
|
|
let mut incoming_pdu = self
|
|
.signature_hash_check_2_3(value, &room_version_rules)
|
|
.await?;
|
|
|
|
// Now that we have checked the signature and hashes we can add the eventID and
|
|
// convert to our PduEvent type
|
|
incoming_pdu.insert(
|
|
"event_id".to_owned(),
|
|
CanonicalJsonValue::String(event_id.as_str().to_owned()),
|
|
);
|
|
let pdu_event = serde_json::from_value::<PduEvent>(
|
|
serde_json::to_value(&incoming_pdu).expect("CanonicalJsonObj is a valid JsonValue"),
|
|
)
|
|
.map_err(|e| err!(Request(BadJson(debug_warn!("Event is not a valid PDU: {e}")))))?;
|
|
|
|
// TODO(nex): From hereon the event is not dropped, and thus always added as an
|
|
// outlier. However, we only do that at the end of this function, which means
|
|
// several duplicated calls to add_pdu_outlier. Shouldn't we just do it here
|
|
// instead, since we know it's going to be persisted as an outlier no matter
|
|
// what? the rest of this function is basically just to check PDU check 4.
|
|
|
|
// NOTE^: Technically, persisting the event before knowing if it's rejected
|
|
// introduces a race condition in fetch_and_persist_event_auth, where we have
|
|
// the event locally, but haven't yet flagged it as rejected, which the
|
|
// fetcher perceives as "accepted". I'm not sure if that's practically possible
|
|
// though.
|
|
|
|
// Fetch all auth events
|
|
let mut auth_events: HashMap<OwnedEventId, PduEvent> = HashMap::new();
|
|
|
|
for auth_event_id in pdu_event.auth_events() {
|
|
if let Ok(auth_event) = self.services.timeline.get_pdu(auth_event_id).await {
|
|
trace!("Found auth event {auth_event_id} for outlier event {event_id} locally");
|
|
auth_events.insert(auth_event_id.to_owned(), auth_event);
|
|
} else {
|
|
debug_warn!(
|
|
"Could not find auth event {auth_event_id} for outlier event {event_id} \
|
|
locally"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Fetch any missing ones & reject invalid ones
|
|
if auth_events.len() != pdu_event.auth_events().count() {
|
|
info!("Missing some auth events, asking remote for auth chain");
|
|
let auth_chain_map = self
|
|
.fetch_and_persist_event_auth(
|
|
&pdu_event,
|
|
origin,
|
|
&room_version_rules,
|
|
create_event,
|
|
)
|
|
.await?;
|
|
for auth_event_id in pdu_event.auth_events() {
|
|
if auth_events.contains_key(auth_event_id) {
|
|
continue;
|
|
}
|
|
if let Some(auth_event) = auth_chain_map.get(auth_event_id) {
|
|
auth_events.insert(auth_event_id.to_owned(), auth_event.clone());
|
|
} else {
|
|
return Err!(Request(Forbidden(
|
|
"Remote server is not divulging incoming event's auth events (missing: \
|
|
{auth_event_id})"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure none of the auth events are rejected - if they are, reject too.
|
|
for (auth_event_id, auth_event) in &auth_events {
|
|
if self
|
|
.services
|
|
.pdu_metadata
|
|
.is_event_rejected(auth_event_id)
|
|
.await
|
|
{
|
|
debug_warn!(
|
|
"Rejecting incoming event {} which depends on rejected auth event \
|
|
{auth_event_id}",
|
|
event_id,
|
|
);
|
|
self.reject_and_persist(event_id, &incoming_pdu);
|
|
return Err!(Request(Forbidden(
|
|
"Event has rejected auth event: {auth_event_id}"
|
|
)));
|
|
}
|
|
if auth_event.room_id_or_hash() != room_id {
|
|
debug_warn!(
|
|
%auth_event_id,
|
|
auth_event_room_id=%auth_event.room_id_or_hash(),
|
|
expected_room_id=%room_id,
|
|
"Rejecting incoming event which depends on an auth event in another room.",
|
|
);
|
|
self.reject_and_persist(event_id, &incoming_pdu);
|
|
return Err!(Request(Forbidden(
|
|
"Event depends on a cross-room auth event: {auth_event_id}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
// 4. Reject "due to auth events" if the event doesn't pass auth based on the
|
|
// claimed auth events
|
|
debug!("Checking based on auth events");
|
|
let mut auth_events_by_key: HashMap<_, _> = HashMap::with_capacity(auth_events.len());
|
|
// Build map of auth events
|
|
for id in pdu_event.auth_events() {
|
|
let auth_event = auth_events
|
|
.get(id)
|
|
.expect("we just checked that we have all auth events")
|
|
.to_owned();
|
|
|
|
let key = auth_event.kind().with_state_key(
|
|
auth_event
|
|
.state_key
|
|
.clone()
|
|
.expect("all auth events must have state keys"),
|
|
);
|
|
match auth_events_by_key.entry(key) {
|
|
| hash_map::Entry::Vacant(v) => {
|
|
v.insert(auth_event);
|
|
},
|
|
| hash_map::Entry::Occupied(_) => {
|
|
self.reject_and_persist(event_id, &incoming_pdu);
|
|
return Err!(Request(Forbidden(
|
|
"Auth event's type and state_key combination exists multiple times: {}, \
|
|
{}",
|
|
auth_event.kind,
|
|
auth_event.state_key().unwrap_or("")
|
|
)));
|
|
},
|
|
}
|
|
}
|
|
|
|
if !self
|
|
.auth_state_check_4(
|
|
&pdu_event,
|
|
&room_version_rules,
|
|
create_event.as_pdu(),
|
|
&auth_events_by_key,
|
|
)
|
|
.await?
|
|
{
|
|
self.reject_and_persist(event_id, &incoming_pdu);
|
|
return Err!(Request(Forbidden(
|
|
"Event authorisation fails based on event's claimed auth events"
|
|
)));
|
|
}
|
|
|
|
trace!("Validation successful.");
|
|
|
|
// 7. Persist the event as an outlier.
|
|
self.services
|
|
.outlier
|
|
.add_pdu_outlier(pdu_event.event_id(), &incoming_pdu);
|
|
|
|
trace!("Added pdu as outlier.");
|
|
|
|
Ok((pdu_event, incoming_pdu))
|
|
}
|
|
|
|
/// Marks the event as rejected and then saves it as an outlier.
|
|
pub(super) fn reject_and_persist(&self, event_id: &EventId, pdu: &CanonicalJsonObject) {
|
|
self.services.pdu_metadata.mark_event_rejected(event_id);
|
|
self.services.outlier.add_pdu_outlier(event_id, pdu);
|
|
}
|
|
}
|