Skip to content

Add support for matrix-federation:// scheme to be compatible with new versions of Synapse #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 15, 2025
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Matrix Hyper Federation Client

A hyper client for connecting over Matrix federation.
A hyper client for handling internal Synapse routing of outbound federation traffic.

- `matrix-federation://`: Used in Synapse >= [1.87.0rc1][synapse-1.87.0rc1-changelog]
(2023-06-27)
- `matrix://`: Used in Synapse < [1.87.0rc1][synapse-1.87.0rc1-changelog] (2023-06-27)

[synapse-1.87.0rc1-changelog]: https://github.com/element-hq/synapse/blob/develop/docs/changelogs/CHANGES-2023.md#synapse-1870rc1-2023-06-27

## Example

Expand All @@ -10,9 +15,10 @@ use ed25519_dalek::SigningKey;
use matrix_hyper_federation_client::SigningFederationClient;

async fn run(secret_key: SigningKey) -> Result<(), anyhow::Error> {
let client = SigningFederationClient::new("local_server", "ed25519:sg5Sa", secret_key).await?;
let client = SigningFederationClient::new("local_server", "ed25519:sg5Sa", secret_key)?;

let resp = client.get("matrix://matrix.org/_matrix/federation/v1/version".parse()?).await?;
let resp = client.get("matrix-federation://matrix.org/_matrix/federation/v1/version".parse()?).await?;
// let resp = client.get("matrix://matrix.org/_matrix/federation/v1/version".parse()?).await?;

assert_eq!(resp.status(), 200);

Expand Down
47 changes: 26 additions & 21 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ use signed_json::{Canonical, Signed};

use crate::server_resolver::{handle_delegated_server, MatrixConnector};

/// A [`hyper::Client`] that routes `matrix://` URIs correctly, but does not
/// sign the requests.
/// A [`hyper::Client`] that routes `matrix://` (Synapse <1.87.0rc1 (2023-06-27)) and
/// `matrix-federation://` (Synapse >=1.87.0rc1 (2023-06-27)) URIs correctly, but does
/// not sign the requests.
///
/// Either use [`SigningFederationClient`] if you want requests to be automatically
/// signed, or [`sign_and_build_json_request`] to sign the requests.
Expand All @@ -34,26 +35,26 @@ impl FederationClient {
FederationClient { client }
}

/// Helper function to build a [`FederationClient`].
pub fn new_with_default_resolver() -> Result<FederationClient, Error> {
let connector = MatrixConnector::with_default_resolver()?;

Ok(FederationClient {
client: Client::builder().build(connector),
})
}

pub async fn request(&self, mut req: Request<Body>) -> Result<Response<Body>, Error> {
req = handle_delegated_server(&self.client, req).await?;

Ok(self.client.request(req).await?)
}
}

/// Helper function to build a [`FederationClient`].
pub async fn new_federation_client() -> Result<FederationClient, Error> {
let connector = MatrixConnector::with_default_resolver().await?;

Ok(FederationClient {
client: Client::builder().build(connector),
})
}

/// A HTTP client that correctly resolves `matrix://` URIs and signs the
/// A HTTP client that correctly resolves `matrix://` and `matrix-federation://` URIs and signs the
/// requests.
///
/// This will fail for requests to a `matrix://` URI that have a non-JSON body.
/// This will fail for requests to a `matrix://` or `matrix-federation://` URI that have a non-JSON body.
///
/// **Note**: Using this is less efficient than using a [`Client`] with a
/// [`MatrixConnector`] and manually signing the requests, as the implementation
Expand All @@ -69,12 +70,12 @@ pub struct SigningFederationClient<C = MatrixConnector> {

impl SigningFederationClient<MatrixConnector> {
/// Create a new client with the default resolver.
pub async fn new(
pub fn new(
server_name: impl ToString,
key_id: impl ToString,
secret_key: SigningKey,
) -> Result<Self, Error> {
let connector = MatrixConnector::with_default_resolver().await?;
let connector = MatrixConnector::with_default_resolver()?;

Ok(SigningFederationClient {
client: Client::builder().build(connector),
Expand All @@ -88,8 +89,8 @@ impl SigningFederationClient<MatrixConnector> {
impl<C> SigningFederationClient<C> {
/// Create a new [`SigningFederationClient`] using the given [`Client`].
///
/// Note, the connector used by the [`Client`] must support `matrix://`
/// URIs.
/// Note, the connector used by the [`Client`] must support `matrix://` and
/// `matrix-federation://` URIs.
pub fn with_client(
client: Client<C>,
server_name: String,
Expand Down Expand Up @@ -122,14 +123,18 @@ where

/// Send the request.
///
/// For `matrix://` URIs the request body must be JSON (if not empty) and
/// the request will be signed.
/// For `matrix://` or `matrix-federation://` URIs the request body must be JSON (if
/// not empty) and the request will be signed.
pub async fn request(&self, mut req: Request<Body>) -> Result<Response<Body>, Error> {
req = handle_delegated_server(&self.client, req).await?;

if req.uri().scheme() != Some(&"matrix".parse()?) {
return Ok(self.client.request(req).await?);
// Return-early and make a normal request if the URI scheme is not `matrix://`
// or `matrix-federation://`.
match req.uri().scheme_str() {
Some("matrix") | Some("matrix-federation") => {}
_ => return Ok(self.client.request(req).await?),
}

if !req.body().is_end_stream()
&& req.headers().get(CONTENT_TYPE)
!= Some(&HeaderValue::from_static("application/json"))
Expand Down
18 changes: 10 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//!
//! # [`SigningFederationClient`]
//!
//! The [`SigningFederationClient`] correctly routes `matrix://` URIs and
//! The [`SigningFederationClient`] correctly routes `matrix://` (Synapse <1.87.0rc1
//! (2023-06-27)) and `matrix-federation://` (Synapse >=1.87.0rc1 (2023-06-27)) URIs and
//! automatically signs such requests:
//!
//! ```no_run
Expand All @@ -11,9 +12,9 @@
//! #
//! # async fn run(secret_key: SigningKey) -> Result<(), anyhow::Error> {
//! #
//! let client = SigningFederationClient::new("local_server", "ed25519:sg5Sa", secret_key).await?;
//! let client = SigningFederationClient::new("local_server", "ed25519:sg5Sa", secret_key)?;
//!
//! let uri = "matrix://matrix.org/_matrix/federation/v1/version".parse()?;
//! let uri = "matrix-federation://matrix.org/_matrix/federation/v1/version".parse()?;
//! let resp = client.get(uri).await?;
//!
//! assert_eq!(resp.status(), 200);
Expand All @@ -28,22 +29,23 @@
//! # [`FederationClient`]
//!
//! The [`FederationClient`] is just a standard [`hyper::Client`] with a
//! [`MatrixConnector`] that can route `matrix://` URIs, but does *not* sign the
//! requests automatically:
//! [`MatrixConnector`] that can route `matrix://` and `matrix-federation://` URIs, but
//! does *not* sign the requests automatically:
//!
//! ```no_run
//! # use matrix_hyper_federation_client::client::{new_federation_client, sign_and_build_json_request};
//! # use matrix_hyper_federation_client::FederationClient;
//! # use hyper::Request;
//! use matrix_hyper_federation_client::SignedRequestBuilderExt;
//! # use ed25519_dalek::SigningKey;
//! #
//! # async fn run(secret_key: &SigningKey) -> Result<(), anyhow::Error> {
//! #
//! let client = new_federation_client().await?;
//! let client = FederationClient::new_with_default_resolver()
//! .expect("failed to build federation client");
//!
//! let request = Request::builder()
//! .method("GET")
//! .uri("matrix://matrix.org/_matrix/federation/v1/version")
//! .uri("matrix-federation://matrix.org/_matrix/federation/v1/version")
//! .signed("localhost", "ed25519:sg5Sa", &secret_key)?;
//!
//! let resp = client.request(request).await?;
Expand Down
67 changes: 33 additions & 34 deletions src/server_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::task::{self, Poll};
use anyhow::{format_err, Context, Error};
use futures::FutureExt;
use futures_util::stream::StreamExt;
use hickory_resolver::error::ResolveErrorKind;
use http::header::{HOST, LOCATION};
use http::{Request, Uri};
use hyper::client::connect::Connect;
Expand All @@ -20,7 +21,6 @@ use log::{debug, trace, warn};
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use tokio_rustls::rustls::ClientConfig;
use hickory_resolver::error::ResolveErrorKind;
use url::Url;

/// A resolved host for a Matrix server.
Expand Down Expand Up @@ -53,7 +53,7 @@ pub struct MatrixResolver {

impl MatrixResolver {
/// Create a new [`MatrixResolver`]
pub async fn new() -> Result<MatrixResolver, Error> {
pub fn new() -> Result<MatrixResolver, Error> {
let resolver = hickory_resolver::TokioAsyncResolver::tokio_from_system_conf()?;

Ok(MatrixResolver { resolver })
Expand Down Expand Up @@ -120,17 +120,6 @@ impl MatrixResolver {
host.to_string()
};

// If a literal IP or includes port then we shortcircuit.
if host.parse::<IpAddr>().is_ok() || port.is_some() {
return Ok(vec![Endpoint {
host: host.to_string(),
port: port.unwrap_or(8448),

host_header: authority.to_string(),
tls_name: host.to_string(),
}]);
}

// If a literal IP or includes port then we short circuit.
if host.parse::<IpAddr>().is_ok() || port.is_some() {
debug!("Host is IP or port is set");
Expand Down Expand Up @@ -262,10 +251,13 @@ where
C: Connect + Clone + Sync + Send + 'static,
{
debug!("URI: {:?}", req.uri());
if req.uri().scheme_str() != Some("matrix") {
debug!("Got scheme: {:?}", req.uri().scheme_str());
return Ok(req);
}
let matrix_url_scheme: &str = match req.uri().scheme_str() {
Some(scheme @ ("matrix" | "matrix-federation")) => scheme,
_ => {
debug!("Got scheme: {:?}", req.uri().scheme_str());
return Ok(req);
}
};

let host = req.uri().host().context("missing host")?;
let port = req.uri().port();
Expand All @@ -280,7 +272,9 @@ where
debug!("Found well-known: {}", &w.server);

let a = http::uri::Authority::from_str(&w.server)?;
let mut builder = Uri::builder().scheme("matrix").authority(a);
// When building the new URL, use whatever scheme that was used in the
// original request.
let mut builder = Uri::builder().scheme(matrix_url_scheme).authority(a);
if let Some(p) = req.uri().path_and_query() {
builder = builder.path_and_query(p.clone());
}
Expand Down Expand Up @@ -309,7 +303,7 @@ pub struct WellKnownServer {
}

/// A connector that can be used with a [`hyper::Client`] that correctly
/// resolves and connects to `matrix://` URIs.
/// resolves and connects to `matrix://` and `matrix-federation://` URIs.
#[derive(Debug, Clone)]
pub struct MatrixConnector {
resolver: MatrixResolver,
Expand All @@ -331,8 +325,8 @@ impl MatrixConnector {
}

/// Create new [`MatrixConnector`] with a default [`MatrixResolver`].
pub async fn with_default_resolver() -> Result<MatrixConnector, Error> {
let resolver = MatrixResolver::new().await?;
pub fn with_default_resolver() -> Result<MatrixConnector, Error> {
let resolver = MatrixResolver::new()?;

Ok(MatrixConnector::with_resolver(resolver))
}
Expand All @@ -356,19 +350,24 @@ impl Service<Uri> for MatrixConnector {
let client_config = self.client_config.clone();

async move {
if dst.scheme_str() != Some("matrix") {
let mut https = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(client_config)
.https_only()
.enable_http1()
.build();

let r = https.call(dst).await;

return match r {
Ok(r) => Ok(r),
Err(e) => Err(format_err!("{}", e)),
};
// Return-early and make a normal request if the URI scheme is not
// `matrix://` or `matrix-federation://`.
match dst.scheme_str() {
Some("matrix") | Some("matrix-federation") => {}
_ => {
let mut https = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(client_config)
.https_only()
.enable_http1()
.build();

let r = https.call(dst).await;

return match r {
Ok(r) => Ok(r),
Err(e) => Err(format_err!("{}", e)),
};
}
}

let endpoints = resolver
Expand Down
Loading