mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-04-25 09:02:19 +00:00
refactor: Fix errors in api/router/
This commit is contained in:
@@ -735,8 +735,7 @@ async fn join_room_by_id_helper_local(
|
||||
// This is a restricted room, check if we can complete the join requirements
|
||||
// locally.
|
||||
let needs_auth_user =
|
||||
user_can_perform_restricted_join(services, sender_user, room_id, &room_version)
|
||||
.await;
|
||||
user_can_perform_restricted_join(services, sender_user, room_id).await;
|
||||
if needs_auth_user.is_ok_and(is_true!()) {
|
||||
// If there was an error or the value is false, we'll try joining over
|
||||
// federation. Since it's Ok(true), we can authorise this locally.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mod args;
|
||||
mod auth;
|
||||
mod handler;
|
||||
mod request;
|
||||
mod response;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
use std::{mem, ops::Deref};
|
||||
use std::ops::Deref;
|
||||
|
||||
use axum::{body::Body, extract::FromRequest};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use conduwuit::{Error, Result, debug, debug_warn, err, trace};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
||||
OwnedUserId, ServerName, UserId, api::IncomingRequest,
|
||||
use axum::{
|
||||
RequestExt, RequestPartsExt,
|
||||
body::Body,
|
||||
extract::{FromRequest, Path, Query},
|
||||
};
|
||||
use conduwuit::{Error, Result, err};
|
||||
use ruma::{
|
||||
CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, ServerName,
|
||||
UserId, api::IncomingRequest,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{auth, request, request::Request};
|
||||
use crate::{State, service::appservice::RegistrationInfo};
|
||||
use crate::{State, router::auth::CheckAuth, service::appservice::RegistrationInfo};
|
||||
|
||||
/// Query parameters needed to authenticate requests
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct AuthQueryParams {
|
||||
pub(super) access_token: Option<String>,
|
||||
pub(super) user_id: Option<String>,
|
||||
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
|
||||
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
|
||||
#[serde(alias = "org.matrix.msc3202.device_id")]
|
||||
pub(super) device_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Extractor for Ruma request structs
|
||||
pub(crate) struct Args<T> {
|
||||
@@ -77,9 +91,9 @@ impl<T> Deref for Args<T>
|
||||
fn deref(&self) -> &Self::Target { &self.body }
|
||||
}
|
||||
|
||||
impl<T> FromRequest<State, Body> for Args<T>
|
||||
impl<R> FromRequest<State, Body> for Args<R>
|
||||
where
|
||||
T: IncomingRequest + Send + Sync + 'static,
|
||||
R: IncomingRequest<Authentication: CheckAuth> + Send + Sync + 'static,
|
||||
{
|
||||
type Rejection = Error;
|
||||
|
||||
@@ -87,27 +101,53 @@ async fn from_request(
|
||||
request: hyper::Request<Body>,
|
||||
services: &State,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let mut request = request::from(services, request).await?;
|
||||
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&request.body).ok();
|
||||
let limited = request.with_limited_body();
|
||||
|
||||
let (mut parts, body) = limited.into_parts();
|
||||
|
||||
// Read the body
|
||||
let body = {
|
||||
let max_body_size = services.server.config.max_request_size;
|
||||
|
||||
// Check if the Content-Length header is present and valid, saves us streaming
|
||||
// the response into memory
|
||||
if let Some(content_length) = parts.headers.get(http::header::CONTENT_LENGTH) {
|
||||
if let Ok(content_length) = content_length
|
||||
.to_str()
|
||||
.map(|s| s.parse::<usize>().unwrap_or_default())
|
||||
{
|
||||
if content_length > max_body_size {
|
||||
return Err(err!(Request(TooLarge("Request body too large"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
axum::body::to_bytes(body, max_body_size)
|
||||
.await
|
||||
.map_err(|e| err!(Request(TooLarge("Request body too large: {e}"))))?
|
||||
};
|
||||
|
||||
// Make a JSON copy of the body for use in handlers
|
||||
let json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
|
||||
|
||||
// Extract the query parameters and path
|
||||
let Path(path): Path<Vec<String>> = parts.extract().await?;
|
||||
let Query(auth_query): Query<AuthQueryParams> = parts.extract().await?;
|
||||
|
||||
// Assemble a new request from the read body and parts
|
||||
let request = hyper::Request::from_parts(parts, body);
|
||||
|
||||
// Check authentication
|
||||
let auth =
|
||||
R::Authentication::authenticate::<R, bytes::Bytes>(services, &request, auth_query)
|
||||
.await?;
|
||||
|
||||
// Deserialize the body
|
||||
let body = R::try_from_http_request(request, &path)
|
||||
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))?;
|
||||
|
||||
// while very unusual and really shouldn't be recommended, Synapse accepts POST
|
||||
// requests with a completely empty body. very old clients, libraries, and some
|
||||
// appservices still call APIs like /join like this. so let's just default to
|
||||
// empty object `{}` to copy synapse's behaviour
|
||||
if json_body.is_none()
|
||||
&& request.parts.method == http::Method::POST
|
||||
&& !request.parts.uri.path().contains("/media/")
|
||||
{
|
||||
trace!("json_body from_request: {:?}", json_body.clone());
|
||||
debug_warn!(
|
||||
"received a POST request with an empty body, defaulting/assuming to {{}} like \
|
||||
Synapse does"
|
||||
);
|
||||
json_body = Some(CanonicalJsonValue::Object(CanonicalJsonObject::new()));
|
||||
}
|
||||
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
|
||||
Ok(Self {
|
||||
body: make_body::<T>(&mut request, json_body.as_mut())?,
|
||||
body,
|
||||
origin: auth.origin,
|
||||
sender_user: auth.sender_user,
|
||||
sender_device: auth.sender_device,
|
||||
@@ -116,41 +156,3 @@ async fn from_request(
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn make_body<T>(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Result<T>
|
||||
where
|
||||
T: IncomingRequest,
|
||||
{
|
||||
let body = take_body(request, json_body);
|
||||
let http_request = into_http_request(request, body);
|
||||
T::try_from_http_request(http_request, &request.path)
|
||||
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))
|
||||
}
|
||||
|
||||
fn into_http_request(request: &Request, body: Bytes) -> hyper::Request<Bytes> {
|
||||
let mut http_request = hyper::Request::builder()
|
||||
.uri(request.parts.uri.clone())
|
||||
.method(request.parts.method.clone());
|
||||
|
||||
*http_request.headers_mut().expect("mutable http headers") = request.parts.headers.clone();
|
||||
|
||||
let http_request = http_request.body(body).expect("http request body");
|
||||
|
||||
let headers = http_request.headers();
|
||||
let method = http_request.method();
|
||||
let uri = http_request.uri();
|
||||
debug!("{method:?} {uri:?} {headers:?}");
|
||||
|
||||
http_request
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn take_body(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Bytes {
|
||||
let Some(CanonicalJsonValue::Object(json_body)) = json_body else {
|
||||
return mem::take(&mut request.body);
|
||||
};
|
||||
|
||||
let mut buf = BytesMut::new().writer();
|
||||
serde_json::to_writer(&mut buf, &json_body).expect("value serialization can't fail");
|
||||
buf.into_inner().freeze()
|
||||
}
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
use axum::RequestPartsExt;
|
||||
use axum_extra::{
|
||||
TypedHeader,
|
||||
headers::{Authorization, authorization::Bearer},
|
||||
typed_header::TypedHeaderRejectionReason,
|
||||
};
|
||||
use conduwuit::{Err, Error, Result, debug_error, err, warn};
|
||||
use futures::{
|
||||
TryFutureExt,
|
||||
future::{
|
||||
Either::{Left, Right},
|
||||
select_ok,
|
||||
},
|
||||
pin_mut,
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
use conduwuit::{Err, Result, err};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
||||
OwnedUserId, UserId,
|
||||
OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
||||
api::{
|
||||
AuthScheme, IncomingRequest, Metadata,
|
||||
client::{
|
||||
directory::get_public_rooms,
|
||||
error::ErrorKind,
|
||||
profile::{get_avatar_url, get_display_name, get_profile, get_profile_key},
|
||||
voip::get_turn_server_info,
|
||||
IncomingRequest,
|
||||
auth_scheme::{
|
||||
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
|
||||
AuthScheme, NoAccessToken, NoAuthentication,
|
||||
},
|
||||
federation::{authentication::XMatrix, openid::get_openid_userinfo},
|
||||
federation::authentication::ServerSignatures,
|
||||
},
|
||||
};
|
||||
use service::{
|
||||
@@ -32,8 +17,7 @@
|
||||
server_keys::{PubKeyMap, PubKeys},
|
||||
};
|
||||
|
||||
use super::request::Request;
|
||||
use crate::service::appservice::RegistrationInfo;
|
||||
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
|
||||
|
||||
enum Token {
|
||||
Appservice(Box<RegistrationInfo>),
|
||||
@@ -42,6 +26,7 @@ enum Token {
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Auth {
|
||||
pub(super) origin: Option<OwnedServerName>,
|
||||
pub(super) sender_user: Option<OwnedUserId>,
|
||||
@@ -49,350 +34,227 @@ pub(super) struct Auth {
|
||||
pub(super) appservice_info: Option<RegistrationInfo>,
|
||||
}
|
||||
|
||||
pub(super) async fn auth(
|
||||
services: &Services,
|
||||
request: &mut Request,
|
||||
json_body: Option<&CanonicalJsonValue>,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Auth> {
|
||||
let bearer: Option<TypedHeader<Authorization<Bearer>>> =
|
||||
request.parts.extract().await.unwrap_or(None);
|
||||
let token = match &bearer {
|
||||
| Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
|
||||
| None => request.query.access_token.as_deref(),
|
||||
};
|
||||
pub(crate) trait CheckAuth: AuthScheme {
|
||||
fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
incoming_request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
) -> impl Future<Output = Result<Auth>> + Send {
|
||||
async move {
|
||||
let route = TypeId::of::<R>();
|
||||
|
||||
let token = find_token(services, token).await?;
|
||||
let output = Self::extract_authentication(&incoming_request).map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!(
|
||||
"Failed to extract authorization: {}",
|
||||
err.into()
|
||||
))))
|
||||
})?;
|
||||
|
||||
if metadata.authentication == AuthScheme::None {
|
||||
match metadata {
|
||||
| &get_public_rooms::v3::Request::METADATA => {
|
||||
match token {
|
||||
| Token::Appservice(_) | Token::User(_) => {
|
||||
// we should have validated the token above
|
||||
// already
|
||||
},
|
||||
| Token::None | Token::Invalid => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::MissingToken,
|
||||
"Missing or invalid access token.",
|
||||
));
|
||||
},
|
||||
}
|
||||
},
|
||||
| &get_profile::v3::Request::METADATA
|
||||
| &get_profile_key::unstable::Request::METADATA
|
||||
| &get_display_name::v3::Request::METADATA
|
||||
| &get_avatar_url::v3::Request::METADATA => {
|
||||
if services.server.config.require_auth_for_profile_requests {
|
||||
match token {
|
||||
| Token::Appservice(_) | Token::User(_) => {
|
||||
// we should have validated the token above
|
||||
// already
|
||||
},
|
||||
| Token::None | Token::Invalid => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::MissingToken,
|
||||
"Missing or invalid access token.",
|
||||
));
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
Self::verify(services, output, incoming_request, query, route).await
|
||||
}
|
||||
}
|
||||
|
||||
match (metadata.authentication, token) {
|
||||
| (AuthScheme::AccessToken, Token::Appservice(info)) =>
|
||||
Ok(auth_appservice(services, request, info).await?),
|
||||
| (
|
||||
AuthScheme::None | AuthScheme::AccessTokenOptional | AuthScheme::AppserviceToken,
|
||||
Token::Appservice(info),
|
||||
) => Ok(Auth {
|
||||
origin: None,
|
||||
sender_user: None,
|
||||
sender_device: None,
|
||||
appservice_info: Some(*info),
|
||||
}),
|
||||
| (AuthScheme::AccessToken, Token::None) => match metadata {
|
||||
| &get_turn_server_info::v3::Request::METADATA => {
|
||||
if services.server.config.turn_allow_guests {
|
||||
Ok(Auth {
|
||||
origin: None,
|
||||
sender_user: None,
|
||||
sender_device: None,
|
||||
appservice_info: None,
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))
|
||||
}
|
||||
},
|
||||
| _ => Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token.")),
|
||||
},
|
||||
| (
|
||||
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
|
||||
Token::User((user_id, device_id)),
|
||||
) => {
|
||||
let is_locked = services.users.is_locked(&user_id).await.map_err(|e| {
|
||||
err!(Request(Forbidden(warn!("Failed to check user lock status: {e}"))))
|
||||
fn verify<B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> impl Future<Output = Result<Auth>> + Send;
|
||||
}
|
||||
|
||||
impl CheckAuth for ServerSignatures {
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Auth> {
|
||||
let destination = services.globals.server_name();
|
||||
if output
|
||||
.destination
|
||||
.as_ref()
|
||||
.is_some_and(|supplied_destination| supplied_destination != destination)
|
||||
{
|
||||
return Err!(Request(Unauthorized("Destination mismatch.")));
|
||||
}
|
||||
|
||||
let key = services
|
||||
.server_keys
|
||||
.get_verify_key(&output.origin, &output.key)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(Unauthorized(warn!("Failed to fetch signing keys: {e}"))))
|
||||
})?;
|
||||
if is_locked {
|
||||
// Only /logout and /logout/all are allowed for locked users
|
||||
if !matches!(
|
||||
metadata,
|
||||
&ruma::api::client::session::logout::v3::Request::METADATA
|
||||
| &ruma::api::client::session::logout_all::v3::Request::METADATA
|
||||
) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UserLocked,
|
||||
"This account has been locked.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Auth {
|
||||
origin: None,
|
||||
sender_user: Some(user_id),
|
||||
sender_device: Some(device_id),
|
||||
appservice_info: None,
|
||||
})
|
||||
},
|
||||
| (AuthScheme::ServerSignatures, Token::None) =>
|
||||
Ok(auth_server(services, request, json_body).await?),
|
||||
| (
|
||||
AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional,
|
||||
Token::None,
|
||||
) => Ok(Auth {
|
||||
sender_user: None,
|
||||
sender_device: None,
|
||||
origin: None,
|
||||
appservice_info: None,
|
||||
}),
|
||||
| (AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) =>
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::Unauthorized,
|
||||
"Only server signatures should be used on this endpoint.",
|
||||
)),
|
||||
| (AuthScheme::AppserviceToken, Token::User(_)) => Err(Error::BadRequest(
|
||||
ErrorKind::Unauthorized,
|
||||
"Only appservice access tokens should be used on this endpoint.",
|
||||
)),
|
||||
| (AuthScheme::None, Token::Invalid) => {
|
||||
// OpenID federation endpoint uses a query param with the same name, drop this
|
||||
// once query params for user auth are removed from the spec. This is
|
||||
// required to make integration manager work.
|
||||
if request.query.access_token.is_some()
|
||||
&& metadata == &get_openid_userinfo::v1::Request::METADATA
|
||||
{
|
||||
Ok(Auth {
|
||||
origin: None,
|
||||
sender_user: None,
|
||||
sender_device: None,
|
||||
appservice_info: None,
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::UnknownToken { soft_logout: false },
|
||||
"Unknown access token.",
|
||||
))
|
||||
}
|
||||
},
|
||||
| (_, Token::Invalid) => Err(Error::BadRequest(
|
||||
ErrorKind::UnknownToken { soft_logout: false },
|
||||
"Unknown access token.",
|
||||
)),
|
||||
|
||||
let keys: PubKeys = [(output.key.to_string(), key.key)].into();
|
||||
let keys: PubKeyMap = [(output.origin.as_str().into(), keys)].into();
|
||||
|
||||
match output.verify_request(request, destination, &keys) {
|
||||
| Ok(_) => Ok(Auth {
|
||||
origin: Some(output.origin.to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
| Err(err) =>
|
||||
Err!(Request(Unauthorized(warn!("Failed to verify X-Matrix header: {err}")))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_appservice(
|
||||
services: &Services,
|
||||
request: &Request,
|
||||
info: Box<RegistrationInfo>,
|
||||
) -> Result<Auth> {
|
||||
let user_id_default = || {
|
||||
UserId::parse_with_server_name(
|
||||
info.registration.sender_localpart.as_str(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
};
|
||||
impl CheckAuth for AccessToken {
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Auth> {
|
||||
let Ok((sender_user, sender_device)) = services.users.find_from_token(&output).await
|
||||
else {
|
||||
return Err!(Request(Unauthorized("Invalid access token.")));
|
||||
};
|
||||
|
||||
let Ok(user_id) = request
|
||||
.query
|
||||
.user_id
|
||||
.clone()
|
||||
.map_or_else(user_id_default, OwnedUserId::parse)
|
||||
else {
|
||||
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||
};
|
||||
|
||||
if !info.is_user_match(&user_id) {
|
||||
return Err!(Request(Exclusive("User is not in namespace.")));
|
||||
}
|
||||
|
||||
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||
// The device_id can be provided via `device_id` or
|
||||
// `org.matrix.msc3202.device_id` query parameter.
|
||||
let sender_device = if let Some(ref device_id_str) = request.query.device_id {
|
||||
let device_id: &DeviceId = device_id_str.as_str().into();
|
||||
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&user_id, device_id)
|
||||
.is_locked(&sender_user)
|
||||
.await
|
||||
.is_err()
|
||||
.is_ok_and(std::convert::identity)
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Device does not exist for user or appservice cannot masquerade as this device."
|
||||
)));
|
||||
// Locked users can only use /logout and /logout/all
|
||||
|
||||
if !(route == TypeId::of::<ruma::api::client::session::logout::v3::Request>()
|
||||
|| route == TypeId::of::<ruma::api::client::session::logout_all::v3::Request>())
|
||||
{
|
||||
return Err!(Request(Unauthorized("Your account is locked.")));
|
||||
}
|
||||
}
|
||||
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Auth {
|
||||
origin: None,
|
||||
sender_user: Some(user_id),
|
||||
sender_device,
|
||||
appservice_info: Some(*info),
|
||||
})
|
||||
return Ok(Auth {
|
||||
sender_user: Some(sender_user),
|
||||
sender_device: Some(sender_device),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_server(
|
||||
services: &Services,
|
||||
request: &mut Request,
|
||||
body: Option<&CanonicalJsonValue>,
|
||||
) -> Result<Auth> {
|
||||
type Member = (String, CanonicalJsonValue);
|
||||
type Object = CanonicalJsonObject;
|
||||
type Value = CanonicalJsonValue;
|
||||
impl CheckAuth for AccessTokenOptional {
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Auth> {
|
||||
match output {
|
||||
| Some(token) =>
|
||||
<AccessToken as CheckAuth>::verify(services, token, request, query, route).await,
|
||||
| None => Ok(Auth::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let x_matrix = parse_x_matrix(request).await?;
|
||||
auth_server_checks(services, &x_matrix)?;
|
||||
impl CheckAuth for AppserviceToken {
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Auth> {
|
||||
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
|
||||
return Err!(Request(Unauthorized("Invalid appservice token.")));
|
||||
};
|
||||
|
||||
let destination = services.globals.server_name();
|
||||
let origin = &x_matrix.origin;
|
||||
let signature_uri = request
|
||||
.parts
|
||||
.uri
|
||||
.path_and_query()
|
||||
.expect("all requests have a path")
|
||||
.to_string();
|
||||
let Ok(sender_user) = query.user_id.clone().map_or_else(
|
||||
|| {
|
||||
UserId::parse_with_server_name(
|
||||
appservice_info.registration.sender_localpart.as_str(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
},
|
||||
UserId::parse,
|
||||
) else {
|
||||
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||
};
|
||||
|
||||
let signature: [Member; 1] =
|
||||
[(x_matrix.key.as_str().into(), Value::String(x_matrix.sig.to_string()))];
|
||||
|
||||
let signatures: [Member; 1] = [(origin.as_str().into(), Value::Object(signature.into()))];
|
||||
|
||||
let authorization: Object = if let Some(body) = body.cloned() {
|
||||
let authorization: [Member; 6] = [
|
||||
("content".into(), body),
|
||||
("destination".into(), Value::String(destination.into())),
|
||||
("method".into(), Value::String(request.parts.method.as_str().into())),
|
||||
("origin".into(), Value::String(origin.as_str().into())),
|
||||
("signatures".into(), Value::Object(signatures.into())),
|
||||
("uri".into(), Value::String(signature_uri)),
|
||||
];
|
||||
|
||||
authorization.into()
|
||||
} else {
|
||||
let authorization: [Member; 5] = [
|
||||
("destination".into(), Value::String(destination.into())),
|
||||
("method".into(), Value::String(request.parts.method.as_str().into())),
|
||||
("origin".into(), Value::String(origin.as_str().into())),
|
||||
("signatures".into(), Value::Object(signatures.into())),
|
||||
("uri".into(), Value::String(signature_uri)),
|
||||
];
|
||||
|
||||
authorization.into()
|
||||
};
|
||||
|
||||
let key = services
|
||||
.server_keys
|
||||
.get_verify_key(origin, &x_matrix.key)
|
||||
.await
|
||||
.map_err(|e| err!(Request(Forbidden(warn!("Failed to fetch signing keys: {e}")))))?;
|
||||
|
||||
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
|
||||
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
|
||||
if let Err(e) = ruma::signatures::verify_json(&keys, authorization) {
|
||||
debug_error!("Failed to verify federation request from {origin}: {e}");
|
||||
if request.parts.uri.to_string().contains('@') {
|
||||
warn!(
|
||||
"Request uri contained '@' character. Make sure your reverse proxy gives \
|
||||
conduwuit the raw uri (apache: use nocanon)"
|
||||
);
|
||||
if !appservice_info.is_user_match(&sender_user) {
|
||||
return Err!(Request(Exclusive("User is not in namespace.")));
|
||||
}
|
||||
|
||||
return Err!(Request(Forbidden("Failed to verify X-Matrix signatures.")));
|
||||
}
|
||||
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||
// The device_id can be provided via `device_id` or
|
||||
// `org.matrix.msc3202.device_id` query parameter.
|
||||
let sender_device = if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&sender_user, device_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Device does not exist for user or appservice cannot masquerade as this \
|
||||
device."
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Auth {
|
||||
origin: origin.to_owned().into(),
|
||||
sender_user: None,
|
||||
sender_device: None,
|
||||
appservice_info: None,
|
||||
})
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Auth {
|
||||
appservice_info: Some(appservice_info),
|
||||
sender_user: Some(sender_user),
|
||||
sender_device,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result<()> {
|
||||
if !services.config.allow_federation {
|
||||
return Err!(Config("allow_federation", "Federation is disabled."));
|
||||
impl CheckAuth for AppserviceTokenOptional {
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Auth> {
|
||||
match output {
|
||||
| Some(token) =>
|
||||
<AppserviceToken as CheckAuth>::verify(services, token, request, query, route)
|
||||
.await,
|
||||
| None => Ok(Auth::default()),
|
||||
}
|
||||
}
|
||||
|
||||
let destination = services.globals.server_name();
|
||||
if x_matrix.destination.as_deref() != Some(destination) {
|
||||
return Err!(Request(Forbidden("Invalid destination.")));
|
||||
}
|
||||
|
||||
let origin = &x_matrix.origin;
|
||||
if services.moderation.is_remote_server_forbidden(origin) {
|
||||
return Err!(Request(Forbidden(debug_warn!(
|
||||
"Federation requests from {origin} denied."
|
||||
))));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn parse_x_matrix(request: &mut Request) -> Result<XMatrix> {
|
||||
let TypedHeader(Authorization(x_matrix)) = request
|
||||
.parts
|
||||
.extract::<TypedHeader<Authorization<XMatrix>>>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = match e.reason() {
|
||||
| TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
|
||||
| TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
|
||||
| _ => "Unknown header-related error",
|
||||
};
|
||||
impl CheckAuth for NoAuthentication {
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
_services: &Services,
|
||||
_output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Auth> {
|
||||
Ok(Auth::default())
|
||||
}
|
||||
}
|
||||
|
||||
err!(Request(Forbidden(warn!("{msg}: {e}"))))
|
||||
impl CheckAuth for NoAccessToken {
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
_output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Auth> {
|
||||
// We handle these the same as AccessTokenOptional
|
||||
let token = AccessTokenOptional::extract_authentication(request).map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!("Failed to extract authorization: {}", err))))
|
||||
})?;
|
||||
|
||||
Ok(x_matrix)
|
||||
}
|
||||
|
||||
async fn find_token(services: &Services, token: Option<&str>) -> Result<Token> {
|
||||
let Some(token) = token else {
|
||||
return Ok(Token::None);
|
||||
};
|
||||
|
||||
let user_token = services.users.find_from_token(token).map_ok(Token::User);
|
||||
|
||||
let appservice_token = services
|
||||
.appservice
|
||||
.find_from_token(token)
|
||||
.map_ok(Box::new)
|
||||
.map_ok(Token::Appservice);
|
||||
|
||||
pin_mut!(user_token, appservice_token);
|
||||
// Returns Ok if either token type succeeds, Err only if both fail
|
||||
match select_ok([Left(user_token), Right(appservice_token)]).await {
|
||||
| Err(e) if !e.is_not_found() => Err(e),
|
||||
| Ok((token, _)) => Ok(token),
|
||||
| _ => Ok(Token::Invalid),
|
||||
<AccessTokenOptional as CheckAuth>::verify(services, token, request, query, route).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ impl<Err, Req, Fut, Fun, $($tx,)*> RumaHandler<($($tx,)* Ruma<Req>,)> for Fun
|
||||
where
|
||||
Fun: Fn($($tx,)* Ruma<Req>,) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Req::OutgoingResponse, Err>> + Send,
|
||||
Req: IncomingRequest + Send + Sync + 'static,
|
||||
Req: IncomingRequest<Authentication: $crate::router::auth::CheckAuth> + Send + Sync + 'static,
|
||||
Err: IntoResponse + Send,
|
||||
<Req as IncomingRequest>::OutgoingResponse: Send,
|
||||
$( $tx: FromRequestParts<State> + Send + Sync + 'static, )*
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
use std::str;
|
||||
|
||||
use axum::{RequestExt, RequestPartsExt, extract::Path};
|
||||
use bytes::Bytes;
|
||||
use conduwuit::{Result, err};
|
||||
use http::request::Parts;
|
||||
use serde::Deserialize;
|
||||
use service::Services;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct QueryParams {
|
||||
pub(super) access_token: Option<String>,
|
||||
pub(super) user_id: Option<String>,
|
||||
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
|
||||
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
|
||||
#[serde(alias = "org.matrix.msc3202.device_id")]
|
||||
pub(super) device_id: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) struct Request {
|
||||
pub(super) path: Path<Vec<String>>,
|
||||
pub(super) query: QueryParams,
|
||||
pub(super) body: Bytes,
|
||||
pub(super) parts: Parts,
|
||||
}
|
||||
|
||||
pub(super) async fn from(
|
||||
services: &Services,
|
||||
request: hyper::Request<axum::body::Body>,
|
||||
) -> Result<Request> {
|
||||
let limited = request.with_limited_body();
|
||||
let (mut parts, body) = limited.into_parts();
|
||||
|
||||
let path: Path<Vec<String>> = parts.extract().await?;
|
||||
let query = parts.uri.query().unwrap_or_default();
|
||||
let query = serde_html_form::from_str(query)
|
||||
.map_err(|e| err!(Request(Unknown("Failed to read query parameters: {e}"))))?;
|
||||
|
||||
let max_body_size = services.server.config.max_request_size;
|
||||
|
||||
// Check if the Content-Length header is present and valid, saves us streaming
|
||||
// the response into memory
|
||||
if let Some(content_length) = parts.headers.get(http::header::CONTENT_LENGTH) {
|
||||
if let Ok(content_length) = content_length
|
||||
.to_str()
|
||||
.map(|s| s.parse::<usize>().unwrap_or_default())
|
||||
{
|
||||
if content_length > max_body_size {
|
||||
return Err(err!(Request(TooLarge("Request body too large"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let body = axum::body::to_bytes(body, max_body_size)
|
||||
.await
|
||||
.map_err(|e| err!(Request(TooLarge("Request body too large: {e}"))))?;
|
||||
|
||||
Ok(Request { path, query, body, parts })
|
||||
}
|
||||
@@ -710,18 +710,6 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub allow_public_room_directory_over_federation: bool,
|
||||
|
||||
/// Allow guests/unauthenticated users to access TURN credentials.
|
||||
///
|
||||
/// This is the equivalent of Synapse's `turn_allow_guests` config option.
|
||||
/// This allows any unauthenticated user to call the endpoint
|
||||
/// `/_matrix/client/v3/voip/turnServer`.
|
||||
///
|
||||
/// It is unlikely you need to enable this as all major clients support
|
||||
/// authentication for this endpoint and prevents misuse of your TURN server
|
||||
/// from potential bots.
|
||||
#[serde(default)]
|
||||
pub turn_allow_guests: bool,
|
||||
|
||||
/// Set this to true to lock down your server's public room directory and
|
||||
/// only allow admins to publish rooms to the room directory. Unpublishing
|
||||
/// is still allowed by all users with this enabled.
|
||||
|
||||
@@ -60,6 +60,8 @@ pub enum Error {
|
||||
Path(#[from] axum::extract::rejection::PathRejection),
|
||||
#[error("Mutex poisoned: {0}")]
|
||||
Poison(Cow<'static, str>),
|
||||
#[error(transparent)]
|
||||
Query(#[from] axum::extract::rejection::QueryRejection),
|
||||
#[error("Regex error: {0}")]
|
||||
Regex(#[from] regex::Error),
|
||||
#[error("{0}")]
|
||||
|
||||
Reference in New Issue
Block a user