Skip to main content

tuwunel_service/federation/
execute.rs

1use std::{fmt::Debug, mem, time::Duration};
2
3use bytes::Bytes;
4use ipaddress::IPAddress;
5use reqwest::{Client, Method, Request, Response, Url};
6use ruma::{
7	ServerName,
8	api::{
9		EndpointError, IncomingResponse, MatrixVersion, OutgoingRequest, OutgoingRequestExt,
10		SupportedVersions,
11		error::{Error as RumaError, ErrorBody},
12	},
13};
14use tokio::time::timeout;
15use tuwunel_core::{
16	Err, Error, Result, debug, debug::INFO_SPAN_LEVEL, debug_error, debug_warn, err, implement,
17	trace,
18};
19
20use super::{
21	ShouldAttempt,
22	peer::classify_error,
23	scheme::{FedAuth, FedPath},
24};
25use crate::{client::read_response_capped, resolver::actual::ActualDest};
26
27/// Sends a request to a federation server
28#[implement(super::Service)]
29#[tracing::instrument(skip_all, name = "request", level = "debug")]
30pub async fn execute<T>(&self, dest: &ServerName, request: T) -> Result<T::IncomingResponse>
31where
32	T: OutgoingRequest + Debug + Send,
33	T::Authentication: FedAuth,
34	T::PathBuilder: FedPath,
35{
36	let client = &self.services.client.federation;
37	self.execute_on(client, dest, request).await
38}
39
40/// Client-initiated key lookup (`/keys/query`, `/keys/claim`) over federation:
41/// skips servers already in backoff and bounds the request by
42/// `federation_keys_timeout` so a waiting client is not held past its own send
43/// deadline. Honors peer-status but does not record into it; a slow key lookup
44/// must not suppress unrelated outbound traffic to the server.
45#[implement(super::Service)]
46#[tracing::instrument(skip_all, name = "keys", level = "debug")]
47pub async fn execute_keys<T>(&self, dest: &ServerName, request: T) -> Result<T::IncomingResponse>
48where
49	T: OutgoingRequest + Debug + Send,
50	T::Authentication: FedAuth,
51	T::PathBuilder: FedPath,
52{
53	if matches!(self.should_attempt(dest).await, ShouldAttempt::No { .. }) {
54		return Err!("{dest} is in federation backoff; skipping key lookup");
55	}
56
57	let timeout_dur = Duration::from_secs(
58		self.services
59			.server
60			.config
61			.federation_keys_timeout,
62	);
63
64	let client = &self.services.client.federation;
65
66	match timeout(timeout_dur, self.execute_uncounted(client, dest, request)).await {
67		| Ok(result) => result,
68		| Err(_elapsed) => Err!("{dest} key lookup exceeded {}s", timeout_dur.as_secs()),
69	}
70}
71
72/// Like execute() but with a very large timeout
73#[implement(super::Service)]
74#[tracing::instrument(skip_all, name = "synapse", level = "debug")]
75pub async fn execute_synapse<T>(
76	&self,
77	dest: &ServerName,
78	request: T,
79) -> Result<T::IncomingResponse>
80where
81	T: OutgoingRequest + Debug + Send,
82	T::Authentication: FedAuth,
83	T::PathBuilder: FedPath,
84{
85	let client = &self.services.client.synapse;
86	self.execute_on(client, dest, request).await
87}
88
89#[implement(super::Service)]
90pub async fn execute_on<T>(
91	&self,
92	client: &Client,
93	dest: &ServerName,
94	request: T,
95) -> Result<T::IncomingResponse>
96where
97	T: OutgoingRequest + Send,
98	T::Authentication: FedAuth,
99	T::PathBuilder: FedPath,
100{
101	let result = self
102		.execute_uncounted(client, dest, request)
103		.await;
104
105	match &result {
106		| Ok(_) => self.record_success(dest).await,
107		| Err(error) =>
108			if let Some(class) = classify_error(error) {
109				self.record_failure(dest, class);
110			},
111	}
112
113	result
114}
115
116/// Like [`execute_on`] but leaves peer-status untouched, for callers that
117/// must honor backoff without contributing to it.
118#[implement(super::Service)]
119#[tracing::instrument(
120	name = "fed",
121	level = INFO_SPAN_LEVEL,
122	skip(self, client, request),
123)]
124async fn execute_uncounted<T>(
125	&self,
126	client: &Client,
127	dest: &ServerName,
128	request: T,
129) -> Result<T::IncomingResponse>
130where
131	T: OutgoingRequest + Send,
132	T::Authentication: FedAuth,
133	T::PathBuilder: FedPath,
134{
135	if !self.services.server.config.allow_federation {
136		return Err!(Config("allow_federation", "Federation is disabled."));
137	}
138
139	if self
140		.services
141		.server
142		.config
143		.is_forbidden_remote_server_name(dest)
144	{
145		return Err!(Request(Forbidden(debug_warn!("Federation with {dest} is not allowed."))));
146	}
147
148	let actual = self
149		.services
150		.resolver
151		.get_actual_dest(dest)
152		.await?;
153
154	let request = self.prepare(&actual, dest, request)?;
155
156	self.perform::<T>(&actual, dest, request, client)
157		.await
158}
159
160#[implement(super::Service)]
161async fn perform<T>(
162	&self,
163	actual: &ActualDest,
164	dest: &ServerName,
165	request: Request,
166	client: &Client,
167) -> Result<T::IncomingResponse>
168where
169	T: OutgoingRequest + Send,
170	T::Authentication: FedAuth,
171	T::PathBuilder: FedPath,
172{
173	let url = request.url().clone();
174	let method = request.method().clone();
175
176	debug!(?method, ?url, "Sending request");
177	let limit = self.services.server.config.max_response_size;
178
179	match client.execute(request).await {
180		| Ok(response) => handle_response::<T>(actual, dest, &method, &url, response, limit)
181			.await
182			.inspect_err(|error| self.evict_misrouted(dest, actual, error)),
183		| Err(error) => Err(self
184			.handle_error(dest, actual, &method, &url, error)
185			.expect_err("always returns error")),
186	}
187}
188
189#[implement(super::Service)]
190fn prepare<T>(&self, actual: &ActualDest, dest: &ServerName, request: T) -> Result<Request>
191where
192	T: OutgoingRequest + Send,
193	T::Authentication: FedAuth,
194	T::PathBuilder: FedPath,
195{
196	let request = self.to_http_request::<T>(actual, dest, request)?;
197	let request = Request::try_from(request)?;
198	self.validate_url(request.url())?;
199	self.services.server.check_running()?;
200
201	Ok(request)
202}
203
204#[implement(super::Service)]
205fn validate_url(&self, url: &Url) -> Result {
206	if let Some(url_host) = url.host_str()
207		&& let Ok(ip) = IPAddress::parse(url_host)
208	{
209		trace!("Checking request URL IP {ip:?}");
210		self.services.resolver.validate_ip(&ip)?;
211	}
212
213	Ok(())
214}
215
216async fn handle_response<T>(
217	actual: &ActualDest,
218	dest: &ServerName,
219	method: &Method,
220	url: &Url,
221	response: Response,
222	limit: usize,
223) -> Result<T::IncomingResponse>
224where
225	T: OutgoingRequest + Send,
226	T::Authentication: FedAuth,
227	T::PathBuilder: FedPath,
228{
229	let response = into_http_response(dest, actual, method, url, response, limit).await?;
230
231	T::IncomingResponse::try_from_http_response(response)
232		.map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}")))
233}
234
235async fn into_http_response(
236	dest: &ServerName,
237	actual: &ActualDest,
238	method: &Method,
239	url: &Url,
240	mut response: Response,
241	limit: usize,
242) -> Result<http::Response<Bytes>> {
243	let status = response.status();
244	trace!(
245		?status, ?method,
246		request_url = ?url,
247		response_url = ?response.url(),
248		"Received response from {}",
249		actual.to_string(),
250	);
251
252	let mut http_response_builder = http::Response::builder()
253		.status(status)
254		.version(response.version());
255
256	mem::swap(
257		response.headers_mut(),
258		http_response_builder
259			.headers_mut()
260			.expect("http::response::Builder is usable"),
261	);
262
263	// TODO: handle timeout
264	trace!("Waiting for response body...");
265	let body = read_response_capped(response, limit).await?;
266
267	let http_response = http_response_builder
268		.body(body)
269		.expect("reqwest body is valid http body");
270
271	debug!("Got {status:?} for {method} {url}");
272	if !status.is_success() {
273		return Err(Error::Federation(
274			dest.to_owned(),
275			RumaError::from_http_response(http_response),
276		));
277	}
278
279	Ok(http_response)
280}
281
282#[implement(super::Service)]
283fn handle_error(
284	&self,
285	dest: &ServerName,
286	actual: &ActualDest,
287	method: &Method,
288	url: &Url,
289	mut e: reqwest::Error,
290) -> Result {
291	if e.is_timeout() || e.is_connect() {
292		e = e.without_url();
293		debug_warn!("{e:?}");
294	} else if e.is_redirect() {
295		debug_error!(
296			method = ?method,
297			url = ?url,
298			final_url = ?e.url(),
299			"Redirect loop {}: {}",
300			actual.host,
301			e,
302		);
303	} else {
304		debug_error!("{e:?}");
305	}
306
307	self.evict_route(dest, actual);
308
309	Err(e.into())
310}
311
312// A non-JSON federation response means a proxy or CDN answered, not the
313// homeserver, so the cached route is stale; evict it as transport errors do.
314#[implement(super::Service)]
315fn evict_misrouted(&self, dest: &ServerName, actual: &ActualDest, error: &Error) {
316	let Error::Federation(_, response) = error else {
317		return;
318	};
319
320	if matches!(response.body, ErrorBody::NotJson { .. }) {
321		self.evict_route(dest, actual);
322	}
323}
324
325// Overrides are keyed by the resolved (delegated/SRV) hostname, so evict under
326// the key resolution wrote (`actual.dest.hostname()`), not the origin name.
327#[implement(super::Service)]
328fn evict_route(&self, dest: &ServerName, actual: &ActualDest) {
329	self.services.resolver.cache.del_destination(dest);
330	self.services
331		.resolver
332		.cache
333		.del_override(&actual.dest.hostname());
334}
335
336#[implement(super::Service)]
337fn to_http_request<T>(
338	&self,
339	actual: &ActualDest,
340	dest: &ServerName,
341	request: T,
342) -> Result<http::Request<Vec<u8>>>
343where
344	T: OutgoingRequest + Send,
345	T::Authentication: FedAuth,
346	T::PathBuilder: FedPath,
347{
348	const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_11];
349	let supported = SupportedVersions {
350		versions: VERSIONS.into(),
351		features: Default::default(),
352	};
353
354	let auth = T::Authentication::input(
355		self.services.server.name.clone(),
356		dest.to_owned(),
357		self.services.server_keys.keypair(),
358	);
359	let path = T::PathBuilder::input(&supported);
360
361	request
362		.try_into_http_request::<Vec<u8>>(actual.to_string().as_str(), auth, path)
363		.map_err(|e| err!(BadServerResponse("Invalid destination: {e:?}")))
364}