Files
continuwuity/src/service/federation/execute.rs
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

342 lines
9.3 KiB
Rust
Raw Normal View History

use std::{borrow::Cow, fmt::Debug, mem, time::Instant};
use bytes::Bytes;
2024-12-14 21:58:01 -05:00
use conduwuit::{
Err, Error, Result, debug, debug_error, debug_info, debug_warn, err, trace,
utils::response::LimitReadExt,
};
use ipaddress::IPAddress;
2024-04-22 10:35:12 -07:00
use reqwest::{Client, Method, Request, Response, Url};
use resolvematrix::resolution::Resolution;
use ruma::{
2026-04-07 12:17:35 -04:00
ServerName,
api::{
2026-07-26 05:03:55 +00:00
EndpointError, IncomingResponse, OutgoingRequest, OutgoingRequestExt, SupportedVersions,
2026-04-07 12:17:35 -04:00
auth_scheme::{AuthScheme, NoAuthentication},
error::Error as RumaError,
2026-04-07 12:17:35 -04:00
federation::authentication::{ServerSignatures, ServerSignaturesInput},
path_builder::PathBuilder,
},
};
use crate::SUPPORTED_VERSIONS;
2024-04-16 20:54:16 -07:00
2026-06-26 05:24:42 +01:00
impl super::Service {
/// Sends a signed request to a remote server over federation.
2026-06-26 05:24:42 +01:00
#[tracing::instrument(skip_all, name = "request", level = "debug")]
pub async fn execute<'i, T>(
&self,
dest: &ServerName,
request: T,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest<
Authentication = ServerSignatures,
PathBuilder: PathBuilder<Input<'i>: FederationPathBuilderInput>,
> + Debug
+ Send,
{
let client = &self.services.client.federation;
self.execute_signed(client, dest, request).await
}
/// Sends a signed request to a remote server over federation, with a
/// significantly higher timeout to account for slow endpoints.
#[tracing::instrument(skip_all, name = "request_slow", level = "debug")]
pub async fn execute_slow<'i, T>(
2026-06-26 05:24:42 +01:00
&self,
dest: &ServerName,
request: T,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest<
Authentication = ServerSignatures,
PathBuilder: PathBuilder<Input<'i>: FederationPathBuilderInput>,
> + Debug
+ Send,
{
let client = &self.services.client.federation_slow;
2026-06-26 05:24:42 +01:00
self.execute_signed(client, dest, request).await
}
/// Sends an unsigned (unauthenticated) request to a remote server over
/// federation.
2026-06-26 05:24:42 +01:00
pub async fn execute_unauthenticated<'i, T>(
&self,
dest: &ServerName,
request: T,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest<
Authentication = NoAuthentication,
PathBuilder: PathBuilder<Input<'i>: FederationPathBuilderInput>,
> + Debug
+ Send,
{
let client = &self.services.client.federation;
2026-06-26 05:24:42 +01:00
self.execute_on(client, dest, request, ()).await
}
/// Sends a signed request to a remote server over federation, given the
/// specific client.
2026-06-26 05:24:42 +01:00
pub async fn execute_signed<'i, T>(
&self,
client: &Client,
dest: &ServerName,
request: T,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest<
Authentication = ServerSignatures,
PathBuilder: PathBuilder<Input<'i>: FederationPathBuilderInput>,
> + Send,
{
let authentication = ServerSignaturesInput::new(
self.services.server.name.clone(),
dest.to_owned(),
self.services.server_keys.keypair(),
);
self.execute_on(client, dest, request, authentication).await
}
/// Prepares and sends a federation request with the given client.
/// If federation is disabled, an error is returned here. Likewise, if the
/// destination is forbidden.
/// Preparation includes resolving the server before sending the request.
2026-06-26 05:24:42 +01:00
#[tracing::instrument(
name = "fed",
level = "info",
skip(self, client, request, authentication)
)]
pub async fn execute_on<'i, T, PathBuilderInput>(
&self,
client: &Client,
dest: &ServerName,
request: T,
authentication: <T::Authentication as AuthScheme>::Input<'_>,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest<PathBuilder: PathBuilder<Input<'i> = PathBuilderInput>> + Send,
PathBuilderInput: FederationPathBuilderInput,
{
if !self.services.server.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled."));
}
if self.services.moderation.is_remote_server_forbidden(dest) {
return Err!(Request(Forbidden(debug_warn!(
"Federation with {dest} is not allowed."
))));
}
let actual = self
2026-06-30 21:22:35 +02:00
.services
.client
.matrix_resolver
2026-06-30 21:22:35 +02:00
.resolve_server(dest.as_str())
.await?;
2026-06-26 05:24:42 +01:00
let request = Request::try_from(request.try_into_http_request::<Vec<u8>>(
actual.base_url().as_str(),
2026-06-26 05:24:42 +01:00
authentication,
PathBuilderInput::create(),
)?)?;
self.validate_url(request.url())?;
self.services.server.check_running()?;
self.perform::<T>(dest, &actual, request, client).await
}
/// Actually sends the federation request, handling the response before
/// returning.
///
/// Timing info and request logs are available when debug logs are enabled.
2026-06-26 05:24:42 +01:00
async fn perform<T>(
&self,
dest: &ServerName,
actual: &Resolution,
2026-06-26 05:24:42 +01:00
request: Request,
client: &Client,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest + Send,
{
let url = request.url().clone();
let method = request.method().clone();
debug_info!(
"Sending request: {} matrix-federation://{dest}{}",
method.as_str(),
url.path(),
);
let start = Instant::now();
let response = client.execute(request).await;
debug_info!(
"Request finished in {:?}: {} matrix-federation://{dest}{}",
start.elapsed(),
method.as_str(),
url.path(),
);
match response {
2026-06-26 05:24:42 +01:00
| Ok(response) =>
self.handle_response::<T>(dest, actual, &method, &url, response)
.await,
| Err(error) =>
Err(handle_error(actual, &method, &url, error).expect_err("always returns error")),
2024-07-18 06:37:47 +00:00
}
2024-04-16 20:54:16 -07:00
}
2026-06-26 05:24:42 +01:00
fn validate_url(&self, url: &Url) -> Result<()> {
if let Some(url_host) = url.host_str() {
if let Ok(ip) = IPAddress::parse(url_host) {
trace!("Checking request URL IP {ip:?}");
if !self.services.client.valid_cidr_range(&ip) {
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
}
2026-06-26 05:24:42 +01:00
}
}
2024-04-23 15:31:40 -07:00
2026-06-26 05:24:42 +01:00
Ok(())
}
/// Handles a successful response from a remote server, reading and
/// deserializing the response body (if any).
2026-06-26 05:24:42 +01:00
async fn handle_response<T>(
&self,
dest: &ServerName,
actual: &Resolution,
2026-06-26 05:24:42 +01:00
method: &Method,
url: &Url,
response: Response,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest + Send,
{
const HUGE_ENDPOINTS: [&str; 2] =
["/_matrix/federation/v2/send_join/", "/_matrix/federation/v2/state/"];
let size_limit: u64 = if HUGE_ENDPOINTS.iter().any(|e| url.path().starts_with(e)) {
// Some federation endpoints can return huge response bodies, so we'll bump the
// limit for those endpoints specifically.
self.services
.server
.config
.max_request_size
.saturating_mul(10)
} else {
self.services.server.config.max_request_size
}
.try_into()
.expect("size_limit (usize) should fit within a u64");
let response =
into_http_response(dest, actual, method, url, response, size_limit).await?;
T::IncomingResponse::try_from_http_response(response)
.map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}")))
}
}
async fn into_http_response(
dest: &ServerName,
actual: &Resolution,
method: &Method,
url: &Url,
mut response: Response,
max_size: u64,
) -> Result<http::Response<Bytes>> {
2024-04-16 20:54:16 -07:00
let status = response.status();
2024-07-18 06:37:47 +00:00
trace!(
2026-01-04 03:04:37 +00:00
%status, %method,
request_url = %url,
response_url = %response.url(),
2024-07-18 06:37:47 +00:00
"Received response from {}",
actual.base_url(),
2024-07-18 06:37:47 +00:00
);
2024-04-16 20:54:16 -07:00
let mut http_response_builder = http::Response::builder()
.status(status)
.version(response.version());
2024-04-16 20:54:16 -07:00
mem::swap(
response.headers_mut(),
http_response_builder
.headers_mut()
.expect("http::response::Builder is usable"),
);
2024-07-18 06:37:47 +00:00
trace!("Waiting for response body...");
2024-04-16 20:54:16 -07:00
let http_response = http_response_builder
.body(
response
.limit_read(max_size)
.await
.unwrap_or_default()
.into(),
)
2024-04-16 20:54:16 -07:00
.expect("reqwest body is valid http body");
debug!("Got {status:?} for {method} {url}");
if !status.is_success() {
return Err(Error::Federation(
dest.to_owned(),
RumaError::from_http_response(http_response),
));
}
Ok(http_response)
2024-04-16 20:54:16 -07:00
}
fn handle_error(
actual: &Resolution,
method: &Method,
url: &Url,
mut e: reqwest::Error,
) -> Result {
2024-04-22 10:35:12 -07:00
if e.is_timeout() || e.is_connect() {
e = e.without_url();
debug_warn!("{e:?}");
2024-04-16 20:54:16 -07:00
} else if e.is_redirect() {
debug_error!(
2026-01-04 03:04:37 +00:00
%method,
%url,
final_url = e.url().map(tracing::field::display),
"Redirect loop {}: {}",
actual.host,
2024-04-16 20:54:16 -07:00
e,
);
} else {
2024-04-22 10:35:12 -07:00
debug_error!("{e:?}");
2024-04-16 20:54:16 -07:00
}
Err(e.into())
}
/// A trait for the input types of acceptable path builders for outgoing
/// federation requests.
///
/// Ruma uses Rust's type system to encode the versioning scheme of endpoints in
/// the Matrix spec. Every endpoint has a `PathBuilder` associated type, which
/// has an `Input` associated type. Endpoints with multiple versions have
/// `VersionHistory` as their `PathBuilder`, which has `SupportedVersions`
/// as its `Input` type. Endpoints with no version have `SinglePath` as their
/// `PathBuilder`, which has `()` as its `Input` type. Both `SupportedVersions`
/// and `()` can be created out of thin air using static data (or no data at
/// all). This property is what the `FederationPathBuilderInput` trait
/// represents.
///
/// This trait allows the federation sender service's functions to accept
/// requests for either versioned or unversioned endpoints, by requiring that
/// the `Input` of the `PathBuilder` of the endpoint implements
/// `FederationPathBuilderInput`.
2026-04-13 17:31:52 -04:00
pub trait FederationPathBuilderInput {
fn create() -> Self;
2024-04-16 20:54:16 -07:00
}
impl FederationPathBuilderInput for () {
fn create() -> Self {}
}
impl FederationPathBuilderInput for Cow<'_, SupportedVersions> {
fn create() -> Self { Cow::Borrowed(&SUPPORTED_VERSIONS) }
}