Skip to main content

tuwunel_service/rooms/event_handler/
handle_incoming_pdu.rs

1use std::collections::HashMap;
2
3use futures::{FutureExt, TryStreamExt, future::try_join5};
4use ruma::{
5	CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId,
6	RoomId, RoomVersionId, ServerName, UserId,
7	events::{
8		AnyStrippedStateEvent, StateEventType,
9		room::member::{MembershipState, RoomMemberEventContent},
10	},
11};
12use tuwunel_core::{
13	Err, Result, async_noinline, debug,
14	debug::INFO_SPAN_LEVEL,
15	debug_warn, err, implement,
16	matrix::{Event, PduCount, PduEvent, pdu::MAX_PREV_EVENTS, room_version::from_create_event},
17	smallvec::SmallVec,
18	trace,
19	utils::{
20		BoolExt,
21		stream::{IterStream, TryWidebandExt},
22	},
23	warn,
24};
25
26use super::backoff::{Context, Disposition};
27use crate::rooms::{state_cache::MembershipUpdate, timeline::RawPduId};
28
29type PrevResultsHandled = SmallVec<[PrevHandled; MAX_PREV_EVENTS]>;
30type PrevHandled = (OwnedEventId, Handled);
31type PrevSplit = SmallVec<[OwnedEventId; MAX_PREV_EVENTS]>;
32
33type Handled = Option<(RawPduId, bool)>;
34
35/// When receiving an event one needs to:
36/// 0. Check the server is in the room
37/// 1. Skip the PDU if we already know about it
38/// 1.1. Remove unsigned field
39/// 2. Check signatures, otherwise drop
40/// 3. Check content hash, redact if doesn't match
41/// 4. Fetch any missing auth events doing all checks listed here starting at 1.
42///    These are not timeline events
43/// 5. Reject "due to auth events" if can't get all the auth events or some of
44///    the auth events are also rejected "due to auth events"
45/// 6. Reject "due to auth events" if the event doesn't pass auth based on the
46///    auth events
47/// 7. Persist this event as an outlier
48/// 8. If not timeline event: stop
49/// 9. Fetch any missing prev events doing all checks listed here starting at 1.
50///    These are timeline events
51/// 10. Fetch missing state and auth chain events by calling `/state_ids` at
52///     backwards extremities doing all the checks in this list starting at
53///     1. These are not timeline events
54/// 11. Check the auth of the event passes based on the state of the event
55/// 12. Ensure that the state is derived from the previous current state (i.e.
56///     we calculated by doing state res where one of the inputs was a
57///     previously trusted set of state, don't just trust a set of state we got
58///     from a remote)
59/// 13. Use state resolution to find new room state
60/// 14. Check if the event passes auth based on the "current state" of the room,
61///     if not soft fail it
62#[implement(super::Service)]
63#[async_noinline]
64#[tracing::instrument(
65	name = "pdu",
66	level = INFO_SPAN_LEVEL,
67	skip_all,
68	fields(%room_id, %event_id),
69	ret(level = "debug"),
70)]
71pub async fn handle_incoming_pdu<'a>(
72	&'a self,
73	origin: &'a ServerName,
74	room_id: &'a RoomId,
75	event_id: &'a EventId,
76	pdu: CanonicalJsonObject,
77	is_timeline_event: bool,
78) -> Result<Handled> {
79	// 1. Skip the PDU if we already have it as a timeline event
80	if let Ok(pdu_id) = self.services.timeline.get_pdu_id(event_id).await {
81		debug!(?pdu_id, "Exists.");
82		return Ok(Some((pdu_id, false)));
83	}
84
85	// 1.1 Check the server is in the room
86	let meta_exists = self.services.metadata.exists(room_id).map(Ok);
87
88	// 1.2 Check if the room is disabled
89	let is_disabled = self
90		.services
91		.metadata
92		.is_disabled(room_id)
93		.map(Ok);
94
95	// 1.3.1 Check room ACL on origin field/server
96	let origin_acl_check = self.acl_check(origin, room_id);
97
98	// 1.3.2 Check room ACL on sender's server name
99	let sender: &UserId = pdu
100		.get("sender")
101		.try_into()
102		.map_err(|e| err!(Request(InvalidParam("PDU does not have a valid sender key: {e}"))))?;
103
104	let sender_acl_check = sender
105		.server_name()
106		.ne(origin)
107		.then_async(|| self.acl_check(sender.server_name(), room_id));
108
109	// Fetch create event; absent when we are not resident in the room.
110	let create_event = self
111		.services
112		.state_accessor
113		.room_state_get(room_id, &StateEventType::RoomCreate, "")
114		.map(|result| Ok(result.ok()));
115
116	let (meta_exists, is_disabled, (), (), create_event) = try_join5(
117		meta_exists,
118		is_disabled,
119		origin_acl_check,
120		sender_acl_check.map(|o| o.unwrap_or(Ok(()))),
121		create_event,
122	)
123	.await?;
124
125	// When not resident, the only event we can act on is a leave rescinding an
126	// out-of-band invite we hold for a local user.
127	if !meta_exists {
128		return if self
129			.handle_rescinded_invite(room_id, &pdu)
130			.await?
131		{
132			Ok(None)
133		} else {
134			Err!(Request(NotFound("Room is unknown to this server")))
135		};
136	}
137
138	if is_disabled {
139		return Err!(Request(Forbidden("Federation of this room is disabled by this server.")));
140	}
141
142	let create_event =
143		create_event.ok_or_else(|| err!(Request(NotFound("Room is unknown to this server"))))?;
144
145	let room_version = from_create_event(&create_event)?;
146	let recursion_level = 0;
147
148	let (incoming_pdu, pdu) = self
149		.handle_outlier_pdu(origin, room_id, event_id, pdu, &room_version, recursion_level, false)
150		.await?;
151
152	// 8. if not timeline event: stop
153	if !is_timeline_event {
154		debug!(
155			kind = ?incoming_pdu.event_type(),
156			"Not a timeline event.",
157		);
158		return Ok(None);
159	}
160
161	// Skip old events
162	let first_ts_in_room = self
163		.services
164		.timeline
165		.first_pdu_in_room(room_id)
166		.await?
167		.origin_server_ts();
168
169	if incoming_pdu.origin_server_ts() < first_ts_in_room {
170		debug!(
171			origin_server_ts = ?incoming_pdu.origin_server_ts(),
172			?first_ts_in_room,
173			"Skipping old event."
174		);
175		return Ok(None);
176	}
177
178	// 9. Fetch any missing prev events doing all checks listed here starting at 1.
179	//    These are timeline events
180	let (sorted_prev_events, eventid_info) = self
181		.fetch_prev(
182			origin,
183			room_id,
184			event_id,
185			incoming_pdu.prev_events(),
186			&room_version,
187			recursion_level,
188			first_ts_in_room,
189		)
190		.await?;
191
192	self.handle_prev_events(
193		origin,
194		room_id,
195		event_id,
196		sorted_prev_events,
197		eventid_info,
198		&room_version,
199		recursion_level,
200		first_ts_in_room,
201		create_event.event_id(),
202	)
203	.boxed()
204	.await?;
205
206	// Done with prev events, now handling the incoming event
207	self.upgrade_outlier_to_timeline_pdu(
208		origin,
209		room_id,
210		incoming_pdu,
211		pdu,
212		&room_version,
213		recursion_level,
214		create_event.event_id(),
215	)
216	.boxed()
217	.await
218}
219
220/// Apply a federated leave that rescinds an out-of-band invite for a local
221/// user.
222///
223/// We are not resident in the room, so the kick cannot be processed as a normal
224/// timeline event for lack of room state; but it must still clear the invite so
225/// the invited user's `/sync` reflects the rescission. Mirrors Synapse's
226/// out-of-band membership handling: only a kick from the original inviter is
227/// honored, since without the room state we cannot judge any other sender's
228/// authority. Returns `true` when a rescission was applied.
229#[implement(super::Service)]
230#[tracing::instrument(skip_all, level = "debug", fields(%room_id))]
231async fn handle_rescinded_invite(
232	&self,
233	room_id: &RoomId,
234	pdu: &CanonicalJsonObject,
235) -> Result<bool> {
236	if pdu
237		.get("type")
238		.and_then(CanonicalJsonValue::as_str)
239		!= Some("m.room.member")
240	{
241		return Ok(false);
242	}
243
244	let Some(target) = pdu
245		.get("state_key")
246		.and_then(CanonicalJsonValue::as_str)
247		.and_then(|state_key| UserId::parse(state_key).ok())
248	else {
249		return Ok(false);
250	};
251
252	let Some(sender) = pdu
253		.get("sender")
254		.and_then(CanonicalJsonValue::as_str)
255		.and_then(|sender| UserId::parse(sender).ok())
256	else {
257		return Ok(false);
258	};
259
260	if sender == target || !self.services.globals.user_is_local(&target) {
261		return Ok(false);
262	}
263
264	let Some(content) = pdu
265		.get("content")
266		.cloned()
267		.map(Into::into)
268		.and_then(|content| serde_json::from_value::<RoomMemberEventContent>(content).ok())
269	else {
270		return Ok(false);
271	};
272
273	if content.membership != MembershipState::Leave {
274		return Ok(false);
275	}
276
277	if self
278		.services
279		.state_cache
280		.user_membership(&target, room_id)
281		.await != Some(MembershipState::Invite)
282	{
283		return Ok(false);
284	}
285
286	// Recover the inviter and the room version from the stored stripped state.
287	let invite_state = self
288		.services
289		.state_cache
290		.invite_state(&target, room_id)
291		.await?;
292
293	let inviter = invite_state
294		.iter()
295		.find_map(|event| match event.deserialize() {
296			| Ok(AnyStrippedStateEvent::RoomMember(member)) if member.state_key == target =>
297				Some(member.sender),
298			| _ => None,
299		});
300
301	// Honor the rescission only from the original inviter.
302	if inviter.as_ref() != Some(&sender) {
303		return Ok(false);
304	}
305
306	let Some(room_version_id) = super::room_version_of(&invite_state) else {
307		return Ok(false);
308	};
309
310	// Verify the kick is signed by the sender's server before acting on it.
311	self.services
312		.server_keys
313		.verify_event(pdu, Some(&room_version_id))
314		.await
315		.map_err(|e| {
316			err!(Request(InvalidParam("Invite rescission signature is invalid: {e}")))
317		})?;
318
319	let count = self.services.globals.next_count();
320	self.services
321		.state_cache
322		.update_membership(MembershipUpdate {
323			room_id,
324			user_id: &target,
325			membership_event: RoomMemberEventContent::new(MembershipState::Leave),
326			sender: &sender,
327			last_state: None,
328			invite_via: None,
329			update_joined_count: false,
330			count: PduCount::Normal(*count),
331		})
332		.await?;
333
334	debug!(%room_id, %target, %sender, "Applied a federated invite rescission.");
335
336	Ok(true)
337}
338
339/// Upgrade an incoming PDU's previous events, walking interior events after
340/// their parents so each derives state locally instead of refetching it.
341#[implement(super::Service)]
342#[expect(clippy::too_many_arguments)]
343async fn handle_prev_events(
344	&self,
345	origin: &ServerName,
346	room_id: &RoomId,
347	event_id: &EventId,
348	sorted_prev_events: Vec<OwnedEventId>,
349	mut eventid_info: HashMap<OwnedEventId, (PduEvent, CanonicalJsonObject)>,
350	room_version: &RoomVersionId,
351	recursion_level: usize,
352	first_ts_in_room: MilliSecondsSinceUnixEpoch,
353	create_event_id: &EventId,
354) -> Result<()> {
355	trace!(
356		events = sorted_prev_events.len(),
357		event_ids = ?sorted_prev_events,
358		"Handling previous events"
359	);
360
361	let (interior, extremities): (PrevSplit, PrevSplit) = sorted_prev_events
362		.into_iter()
363		.partition(|prev_id| {
364			eventid_info.get(prev_id).is_some_and(|(pdu, _)| {
365				pdu.prev_events()
366					.any(|prev| eventid_info.contains_key(prev))
367			})
368		});
369
370	extremities
371		.into_iter()
372		.try_stream()
373		.map_ok(|prev_id| (eventid_info.remove(&prev_id), prev_id))
374		.widen_and_then(MAX_PREV_EVENTS, async |(info, prev_id)| {
375			self.upgrade_prev_event(
376				origin,
377				room_id,
378				event_id,
379				info,
380				room_version,
381				recursion_level,
382				first_ts_in_room,
383				prev_id,
384				create_event_id,
385			)
386			.await
387		})
388		.try_collect::<PrevResultsHandled>()
389		.boxed()
390		.await?;
391
392	// Walk interior events forward so each parent commits before its children.
393	interior
394		.into_iter()
395		.try_stream()
396		.map_ok(|prev_id| (eventid_info.remove(&prev_id), prev_id))
397		.try_for_each(async |(info, prev_id)| {
398			self.upgrade_prev_event(
399				origin,
400				room_id,
401				event_id,
402				info,
403				room_version,
404				recursion_level,
405				first_ts_in_room,
406				prev_id,
407				create_event_id,
408			)
409			.await?;
410
411			Ok(())
412		})
413		.boxed()
414		.await
415}
416
417/// Upgrade one previous event, folding a transient failure into a no-op so a
418/// single bad prev does not abort the batch; a shutdown still propagates.
419#[implement(super::Service)]
420#[expect(clippy::too_many_arguments)]
421async fn upgrade_prev_event(
422	&self,
423	origin: &ServerName,
424	room_id: &RoomId,
425	event_id: &EventId,
426	info: Option<(PduEvent, CanonicalJsonObject)>,
427	room_version: &RoomVersionId,
428	recursion_level: usize,
429	first_ts_in_room: MilliSecondsSinceUnixEpoch,
430	prev_id: OwnedEventId,
431	create_event_id: &EventId,
432) -> Result<PrevHandled> {
433	self.services.server.check_running()?;
434	match self
435		.handle_prev_pdu(
436			origin,
437			room_id,
438			event_id,
439			info,
440			room_version,
441			recursion_level,
442			first_ts_in_room,
443			&prev_id,
444			create_event_id,
445		)
446		.await
447	{
448		| Ok(handled) => {
449			if handled.is_some() {
450				self.record_success(Context::Upgrade, &prev_id)
451					.await;
452				debug!(?prev_id, ?handled, "Prev event processed.");
453			} else {
454				debug_warn!(?prev_id, "Prev event not processed.");
455			}
456
457			Ok((prev_id, handled))
458		},
459		| Err(e) => {
460			self.record_outcome(Context::Upgrade, &prev_id, Disposition::Transient);
461			warn!(?prev_id, ?event_id, ?room_id, "Prev event processing failed: {e}");
462
463			Ok((prev_id, None))
464		},
465	}
466}