Skip to main content

tuwunel_api_federation/
utils.rs

1use std::{borrow::Borrow, iter::once};
2
3use futures::{
4	FutureExt, Stream, StreamExt,
5	future::{join, join3, try_join},
6};
7use itertools::Itertools;
8use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, ServerName};
9use serde::Deserialize;
10use serde_json::value::RawValue as RawJsonValue;
11use tuwunel_core::{
12	Err, Result, at, err, implement, is_false,
13	result::NotFound,
14	utils::{FutureBoolExt, IterStream, ReadyExt, future::ReadyBoolExt, option::OptionExt},
15	warn,
16};
17use tuwunel_service::{Services, rooms::short::ShortEventId};
18
19type StateAtEvent = (RoomVersionId, Vec<OwnedEventId>);
20
21pub(super) struct AccessCheck<'a> {
22	pub(super) services: &'a Services,
23	pub(super) origin: &'a ServerName,
24	pub(super) room_id: &'a RoomId,
25	pub(super) event_id: Option<&'a EventId>,
26}
27
28#[implement(AccessCheck, params = "<'_>")]
29pub(super) async fn check(&self) -> Result {
30	let acl_check = self
31		.services
32		.event_handler
33		.acl_check(self.origin, self.room_id)
34		.map(|result| result.is_ok());
35
36	let server_in_room = self
37		.services
38		.state_cache
39		.server_in_room(self.origin, self.room_id);
40
41	let world_readable = self
42		.services
43		.state_accessor
44		.is_world_readable(self.room_id);
45
46	let server_can_see = self.event_id.map_async(|event_id| {
47		self.services
48			.state_accessor
49			.server_can_see_event(self.origin, self.room_id, event_id)
50	});
51
52	// The cheap membership probe leads; a hit there elides the other read.
53	let room_unreachable = server_in_room
54		.is_false()
55		.and(world_readable.is_false());
56
57	let (acl_check, room_unreachable, server_can_see) =
58		join3(acl_check, room_unreachable, server_can_see).await;
59
60	if !acl_check {
61		return Err!(Request(Forbidden("Server access denied.")));
62	}
63
64	if room_unreachable {
65		return Err!(Request(Forbidden("Server is not in room.")));
66	}
67
68	if server_can_see.is_some_and(is_false!()) {
69		return Err!(Request(Forbidden("Server is not allowed to see event.")));
70	}
71
72	Ok(())
73}
74
75pub(super) async fn require_known_room(
76	services: &Services,
77	room_id: &RoomId,
78	origin: &ServerName,
79) -> Result {
80	if !services.metadata.exists(room_id).await {
81		return Err!(Request(NotFound("Room is unknown to this server.")));
82	}
83
84	services
85		.event_handler
86		.acl_check(origin, room_id)
87		.await
88}
89
90/// Reads the state of a room at an event for a federation response.
91///
92/// The origin must pass the room's access check and the event must belong to
93/// the room. The state comes back as event IDs, beside the room's version.
94pub(super) async fn state_at_event(
95	services: &Services,
96	origin: &ServerName,
97	room_id: &RoomId,
98	event_id: &EventId,
99) -> Result<StateAtEvent> {
100	AccessCheck {
101		services,
102		origin,
103		room_id,
104		event_id: None,
105	}
106	.check()
107	.await?;
108
109	require_event_in_room(services, event_id, room_id).await?;
110
111	let shortstatehash = services
112		.state
113		.pdu_shortstatehash(event_id)
114		.await
115		.map_err(|_| err!(Request(NotFound("PDU state not found."))))?;
116
117	let state_ids = services
118		.state_accessor
119		.state_full_ids(shortstatehash)
120		.map(at!(1))
121		.collect()
122		.map(Ok);
123
124	let room_version = services.state.get_room_version(room_id);
125
126	try_join(room_version, state_ids).await
127}
128
129pub(super) async fn require_event_in_room(
130	services: &Services,
131	event_id: &EventId,
132	room_id: &RoomId,
133) -> Result {
134	#[derive(Deserialize)]
135	struct PduRoomId {
136		room_id: OwnedRoomId,
137	}
138
139	services
140		.timeline
141		.get::<PduRoomId>(event_id)
142		.await
143		.is_ok_and(|pdu| pdu.room_id == room_id)
144		.then_some(())
145		.ok_or_else(|| err!(Request(NotFound("Event not found."))))
146}
147
148/// Streams the auth chain of the state at an event and of the event itself.
149///
150/// Once the room has forked, the event's own auth events can fall outside the
151/// state's chain, and a peer validating the event needs them too. The two are
152/// walked separately and merged, so requests at the same state can share its
153/// cached walk.
154pub(super) fn walk_state_auth_chain<'a>(
155	services: &'a Services,
156	room_id: &'a RoomId,
157	room_version: &'a RoomVersionId,
158	state: &'a [OwnedEventId],
159	event_id: &'a EventId,
160) -> impl Stream<Item = OwnedEventId> + Send + 'a {
161	let state_heads = state.iter().map(Borrow::borrow);
162	let state_chain = short_auth_chain(services, room_id, room_version, state_heads);
163	let event_chain = short_auth_chain(services, room_id, room_version, once(event_id));
164
165	let chain = join(state_chain, event_chain)
166		.map(|(state_chain, event_chain)| state_chain.into_iter().merge(event_chain).dedup());
167
168	auth_chain_ids(services, chain)
169}
170
171/// Streams the auth chain of the state a federation response serves.
172///
173/// A walk that fails is logged and yields no events, so the response goes out
174/// without the chain rather than failing whole.
175pub(super) fn walk_auth_chain<'a, I>(
176	services: &'a Services,
177	room_id: &'a RoomId,
178	room_version: &'a RoomVersionId,
179	heads: I,
180) -> impl Stream<Item = OwnedEventId> + Send + 'a
181where
182	I: Iterator<Item = &'a EventId> + Clone + ExactSizeIterator + Send + 'a,
183{
184	let chain = short_auth_chain(services, room_id, room_version, heads);
185
186	auth_chain_ids(services, chain)
187}
188
189async fn short_auth_chain<'a, I>(
190	services: &'a Services,
191	room_id: &RoomId,
192	room_version: &RoomVersionId,
193	heads: I,
194) -> Vec<ShortEventId>
195where
196	I: Iterator<Item = &'a EventId> + Clone + ExactSizeIterator + Send + 'a,
197{
198	services
199		.auth_chain
200		.get_auth_chain(room_id, room_version, heads)
201		.await
202		.inspect_err(|error| warn!(%room_id, %error, "Omitting the auth chain of a failed walk."))
203		.unwrap_or_default()
204}
205
206fn auth_chain_ids<'a, F>(
207	services: &'a Services,
208	chain: F,
209) -> impl Stream<Item = OwnedEventId> + Send + 'a
210where
211	F: Future + Send + 'a,
212	F::Output: IntoIterator<Item = ShortEventId> + Send + 'a,
213	<F::Output as IntoIterator>::IntoIter: Send + 'a,
214{
215	chain
216		.map(|chain| {
217			services
218				.short
219				.multi_get_eventid_from_short(chain.stream())
220		})
221		.flatten_stream()
222		.ready_filter_map(Result::ok)
223}
224
225/// Reads an auth chain event for a federation response, or omits it.
226///
227/// An ancestor missing from storage is a normal backfill gap and is omitted
228/// quietly, while any other read error is logged first. The rest of the chain
229/// still goes out, so one unreadable event never fails the response.
230pub(super) async fn auth_chain_pdu(
231	services: &Services,
232	event_id: &EventId,
233	room_version: &RoomVersionId,
234) -> Option<Box<RawJsonValue>> {
235	services
236		.federation
237		.get_pdu_into(event_id, Some(room_version))
238		.await
239		.present()
240		.inspect_err(|error| warn!(%event_id, %error, "Omitting unreadable auth event."))
241		.ok()
242		.flatten()
243}
244
245/// Reads a state event for a federation response, or omits it.
246///
247/// Every event the state names is expected in storage, so any read error, a
248/// missing event included, is logged before the event is omitted. The rest of
249/// the state still goes out, so one unreadable event never fails the response.
250pub(super) async fn state_pdu(
251	services: &Services,
252	event_id: &EventId,
253	room_version: &RoomVersionId,
254) -> Option<Box<RawJsonValue>> {
255	services
256		.federation
257		.get_pdu_into(event_id, Some(room_version))
258		.await
259		.inspect_err(|error| warn!(%event_id, %error, "Omitting unreadable state event."))
260		.ok()
261}