mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +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
|
// This is a restricted room, check if we can complete the join requirements
|
||||||
// locally.
|
// locally.
|
||||||
let needs_auth_user =
|
let needs_auth_user =
|
||||||
user_can_perform_restricted_join(services, sender_user, room_id, &room_version)
|
user_can_perform_restricted_join(services, sender_user, room_id).await;
|
||||||
.await;
|
|
||||||
if needs_auth_user.is_ok_and(is_true!()) {
|
if needs_auth_user.is_ok_and(is_true!()) {
|
||||||
// If there was an error or the value is false, we'll try joining over
|
// 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.
|
// federation. Since it's Ok(true), we can authorise this locally.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
mod args;
|
mod args;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod handler;
|
mod handler;
|
||||||
mod request;
|
|
||||||
mod response;
|
mod response;
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|||||||
+70
-68
@@ -1,15 +1,29 @@
|
|||||||
use std::{mem, ops::Deref};
|
use std::ops::Deref;
|
||||||
|
|
||||||
use axum::{body::Body, extract::FromRequest};
|
use axum::{
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
RequestExt, RequestPartsExt,
|
||||||
use conduwuit::{Error, Result, debug, debug_warn, err, trace};
|
body::Body,
|
||||||
use ruma::{
|
extract::{FromRequest, Path, Query},
|
||||||
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
|
||||||
OwnedUserId, ServerName, UserId, api::IncomingRequest,
|
|
||||||
};
|
};
|
||||||
|
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, router::auth::CheckAuth, service::appservice::RegistrationInfo};
|
||||||
use crate::{State, 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
|
/// Extractor for Ruma request structs
|
||||||
pub(crate) struct Args<T> {
|
pub(crate) struct Args<T> {
|
||||||
@@ -77,9 +91,9 @@ where
|
|||||||
fn deref(&self) -> &Self::Target { &self.body }
|
fn deref(&self) -> &Self::Target { &self.body }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> FromRequest<State, Body> for Args<T>
|
impl<R> FromRequest<State, Body> for Args<R>
|
||||||
where
|
where
|
||||||
T: IncomingRequest + Send + Sync + 'static,
|
R: IncomingRequest<Authentication: CheckAuth> + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
type Rejection = Error;
|
type Rejection = Error;
|
||||||
|
|
||||||
@@ -87,27 +101,53 @@ where
|
|||||||
request: hyper::Request<Body>,
|
request: hyper::Request<Body>,
|
||||||
services: &State,
|
services: &State,
|
||||||
) -> Result<Self, Self::Rejection> {
|
) -> Result<Self, Self::Rejection> {
|
||||||
let mut request = request::from(services, request).await?;
|
let limited = request.with_limited_body();
|
||||||
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&request.body).ok();
|
|
||||||
|
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 {
|
Ok(Self {
|
||||||
body: make_body::<T>(&mut request, json_body.as_mut())?,
|
body,
|
||||||
origin: auth.origin,
|
origin: auth.origin,
|
||||||
sender_user: auth.sender_user,
|
sender_user: auth.sender_user,
|
||||||
sender_device: auth.sender_device,
|
sender_device: auth.sender_device,
|
||||||
@@ -116,41 +156,3 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|||||||
+201
-339
@@ -1,30 +1,15 @@
|
|||||||
use axum::RequestPartsExt;
|
use std::any::{Any, TypeId};
|
||||||
use axum_extra::{
|
|
||||||
TypedHeader,
|
use conduwuit::{Err, Result, err};
|
||||||
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 ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
||||||
OwnedUserId, UserId,
|
|
||||||
api::{
|
api::{
|
||||||
AuthScheme, IncomingRequest, Metadata,
|
IncomingRequest,
|
||||||
client::{
|
auth_scheme::{
|
||||||
directory::get_public_rooms,
|
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
|
||||||
error::ErrorKind,
|
AuthScheme, NoAccessToken, NoAuthentication,
|
||||||
profile::{get_avatar_url, get_display_name, get_profile, get_profile_key},
|
|
||||||
voip::get_turn_server_info,
|
|
||||||
},
|
},
|
||||||
federation::{authentication::XMatrix, openid::get_openid_userinfo},
|
federation::authentication::ServerSignatures,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use service::{
|
use service::{
|
||||||
@@ -32,8 +17,7 @@ use service::{
|
|||||||
server_keys::{PubKeyMap, PubKeys},
|
server_keys::{PubKeyMap, PubKeys},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::request::Request;
|
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
|
||||||
use crate::service::appservice::RegistrationInfo;
|
|
||||||
|
|
||||||
enum Token {
|
enum Token {
|
||||||
Appservice(Box<RegistrationInfo>),
|
Appservice(Box<RegistrationInfo>),
|
||||||
@@ -42,6 +26,7 @@ enum Token {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub(super) struct Auth {
|
pub(super) struct Auth {
|
||||||
pub(super) origin: Option<OwnedServerName>,
|
pub(super) origin: Option<OwnedServerName>,
|
||||||
pub(super) sender_user: Option<OwnedUserId>,
|
pub(super) sender_user: Option<OwnedUserId>,
|
||||||
@@ -49,350 +34,227 @@ pub(super) struct Auth {
|
|||||||
pub(super) appservice_info: Option<RegistrationInfo>,
|
pub(super) appservice_info: Option<RegistrationInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn auth(
|
pub(crate) trait CheckAuth: AuthScheme {
|
||||||
services: &Services,
|
fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>(
|
||||||
request: &mut Request,
|
services: &Services,
|
||||||
json_body: Option<&CanonicalJsonValue>,
|
incoming_request: &hyper::Request<B>,
|
||||||
metadata: &Metadata,
|
query: AuthQueryParams,
|
||||||
) -> Result<Auth> {
|
) -> impl Future<Output = Result<Auth>> + Send {
|
||||||
let bearer: Option<TypedHeader<Authorization<Bearer>>> =
|
async move {
|
||||||
request.parts.extract().await.unwrap_or(None);
|
let route = TypeId::of::<R>();
|
||||||
let token = match &bearer {
|
|
||||||
| Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
|
|
||||||
| None => request.query.access_token.as_deref(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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 {
|
Self::verify(services, output, incoming_request, query, route).await
|
||||||
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.",
|
|
||||||
));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| _ => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match (metadata.authentication, token) {
|
fn verify<B: AsRef<[u8]> + Sync>(
|
||||||
| (AuthScheme::AccessToken, Token::Appservice(info)) =>
|
services: &Services,
|
||||||
Ok(auth_appservice(services, request, info).await?),
|
output: Self::Output,
|
||||||
| (
|
request: &hyper::Request<B>,
|
||||||
AuthScheme::None | AuthScheme::AccessTokenOptional | AuthScheme::AppserviceToken,
|
query: AuthQueryParams,
|
||||||
Token::Appservice(info),
|
route: TypeId,
|
||||||
) => Ok(Auth {
|
) -> impl Future<Output = Result<Auth>> + Send;
|
||||||
origin: None,
|
}
|
||||||
sender_user: None,
|
|
||||||
sender_device: None,
|
impl CheckAuth for ServerSignatures {
|
||||||
appservice_info: Some(*info),
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||||
}),
|
services: &Services,
|
||||||
| (AuthScheme::AccessToken, Token::None) => match metadata {
|
output: Self::Output,
|
||||||
| &get_turn_server_info::v3::Request::METADATA => {
|
request: &hyper::Request<B>,
|
||||||
if services.server.config.turn_allow_guests {
|
_query: AuthQueryParams,
|
||||||
Ok(Auth {
|
_route: TypeId,
|
||||||
origin: None,
|
) -> Result<Auth> {
|
||||||
sender_user: None,
|
let destination = services.globals.server_name();
|
||||||
sender_device: None,
|
if output
|
||||||
appservice_info: None,
|
.destination
|
||||||
})
|
.as_ref()
|
||||||
} else {
|
.is_some_and(|supplied_destination| supplied_destination != destination)
|
||||||
Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))
|
{
|
||||||
}
|
return Err!(Request(Unauthorized("Destination mismatch.")));
|
||||||
},
|
}
|
||||||
| _ => Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token.")),
|
|
||||||
},
|
let key = services
|
||||||
| (
|
.server_keys
|
||||||
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
|
.get_verify_key(&output.origin, &output.key)
|
||||||
Token::User((user_id, device_id)),
|
.await
|
||||||
) => {
|
.map_err(|e| {
|
||||||
let is_locked = services.users.is_locked(&user_id).await.map_err(|e| {
|
err!(Request(Unauthorized(warn!("Failed to fetch signing keys: {e}"))))
|
||||||
err!(Request(Forbidden(warn!("Failed to check user lock status: {e}"))))
|
|
||||||
})?;
|
})?;
|
||||||
if is_locked {
|
|
||||||
// Only /logout and /logout/all are allowed for locked users
|
let keys: PubKeys = [(output.key.to_string(), key.key)].into();
|
||||||
if !matches!(
|
let keys: PubKeyMap = [(output.origin.as_str().into(), keys)].into();
|
||||||
metadata,
|
|
||||||
&ruma::api::client::session::logout::v3::Request::METADATA
|
match output.verify_request(request, destination, &keys) {
|
||||||
| &ruma::api::client::session::logout_all::v3::Request::METADATA
|
| Ok(_) => Ok(Auth {
|
||||||
) {
|
origin: Some(output.origin.to_owned()),
|
||||||
return Err(Error::BadRequest(
|
..Default::default()
|
||||||
ErrorKind::UserLocked,
|
}),
|
||||||
"This account has been locked.",
|
| Err(err) =>
|
||||||
));
|
Err!(Request(Unauthorized(warn!("Failed to verify X-Matrix header: {err}")))),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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.",
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_appservice(
|
impl CheckAuth for AccessToken {
|
||||||
services: &Services,
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||||
request: &Request,
|
services: &Services,
|
||||||
info: Box<RegistrationInfo>,
|
output: Self::Output,
|
||||||
) -> Result<Auth> {
|
_request: &hyper::Request<B>,
|
||||||
let user_id_default = || {
|
_query: AuthQueryParams,
|
||||||
UserId::parse_with_server_name(
|
route: TypeId,
|
||||||
info.registration.sender_localpart.as_str(),
|
) -> Result<Auth> {
|
||||||
services.globals.server_name(),
|
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
|
if services
|
||||||
.users
|
.users
|
||||||
.get_device_metadata(&user_id, device_id)
|
.is_locked(&sender_user)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_ok_and(std::convert::identity)
|
||||||
{
|
{
|
||||||
return Err!(Request(Forbidden(
|
// Locked users can only use /logout and /logout/all
|
||||||
"Device does not exist for user or appservice cannot masquerade as this device."
|
|
||||||
)));
|
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())
|
return Ok(Auth {
|
||||||
} else {
|
sender_user: Some(sender_user),
|
||||||
None
|
sender_device: Some(sender_device),
|
||||||
};
|
..Default::default()
|
||||||
|
});
|
||||||
Ok(Auth {
|
}
|
||||||
origin: None,
|
|
||||||
sender_user: Some(user_id),
|
|
||||||
sender_device,
|
|
||||||
appservice_info: Some(*info),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_server(
|
impl CheckAuth for AccessTokenOptional {
|
||||||
services: &Services,
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||||
request: &mut Request,
|
services: &Services,
|
||||||
body: Option<&CanonicalJsonValue>,
|
output: Self::Output,
|
||||||
) -> Result<Auth> {
|
request: &hyper::Request<B>,
|
||||||
type Member = (String, CanonicalJsonValue);
|
query: AuthQueryParams,
|
||||||
type Object = CanonicalJsonObject;
|
route: TypeId,
|
||||||
type Value = CanonicalJsonValue;
|
) -> 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?;
|
impl CheckAuth for AppserviceToken {
|
||||||
auth_server_checks(services, &x_matrix)?;
|
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 Ok(sender_user) = query.user_id.clone().map_or_else(
|
||||||
let origin = &x_matrix.origin;
|
|| {
|
||||||
let signature_uri = request
|
UserId::parse_with_server_name(
|
||||||
.parts
|
appservice_info.registration.sender_localpart.as_str(),
|
||||||
.uri
|
services.globals.server_name(),
|
||||||
.path_and_query()
|
)
|
||||||
.expect("all requests have a path")
|
},
|
||||||
.to_string();
|
UserId::parse,
|
||||||
|
) else {
|
||||||
|
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||||
|
};
|
||||||
|
|
||||||
let signature: [Member; 1] =
|
if !appservice_info.is_user_match(&sender_user) {
|
||||||
[(x_matrix.key.as_str().into(), Value::String(x_matrix.sig.to_string()))];
|
return Err!(Request(Exclusive("User is not in namespace.")));
|
||||||
|
|
||||||
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)"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
Some(device_id.to_owned())
|
||||||
origin: origin.to_owned().into(),
|
} else {
|
||||||
sender_user: None,
|
None
|
||||||
sender_device: None,
|
};
|
||||||
appservice_info: 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<()> {
|
impl CheckAuth for AppserviceTokenOptional {
|
||||||
if !services.config.allow_federation {
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||||
return Err!(Config("allow_federation", "Federation is disabled."));
|
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> {
|
impl CheckAuth for NoAuthentication {
|
||||||
let TypedHeader(Authorization(x_matrix)) = request
|
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||||
.parts
|
_services: &Services,
|
||||||
.extract::<TypedHeader<Authorization<XMatrix>>>()
|
_output: Self::Output,
|
||||||
.await
|
_request: &hyper::Request<B>,
|
||||||
.map_err(|e| {
|
_query: AuthQueryParams,
|
||||||
let msg = match e.reason() {
|
_route: TypeId,
|
||||||
| TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
|
) -> Result<Auth> {
|
||||||
| TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
|
Ok(Auth::default())
|
||||||
| _ => "Unknown header-related error",
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
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)
|
<AccessTokenOptional as CheckAuth>::verify(services, token, request, query, route).await
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ macro_rules! ruma_handler {
|
|||||||
where
|
where
|
||||||
Fun: Fn($($tx,)* Ruma<Req>,) -> Fut + Send + Sync + 'static,
|
Fun: Fn($($tx,)* Ruma<Req>,) -> Fut + Send + Sync + 'static,
|
||||||
Fut: Future<Output = Result<Req::OutgoingResponse, Err>> + Send,
|
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,
|
Err: IntoResponse + Send,
|
||||||
<Req as IncomingRequest>::OutgoingResponse: Send,
|
<Req as IncomingRequest>::OutgoingResponse: Send,
|
||||||
$( $tx: FromRequestParts<State> + Send + Sync + 'static, )*
|
$( $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)]
|
#[serde(default)]
|
||||||
pub allow_public_room_directory_over_federation: bool,
|
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
|
/// 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
|
/// only allow admins to publish rooms to the room directory. Unpublishing
|
||||||
/// is still allowed by all users with this enabled.
|
/// is still allowed by all users with this enabled.
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ pub enum Error {
|
|||||||
Path(#[from] axum::extract::rejection::PathRejection),
|
Path(#[from] axum::extract::rejection::PathRejection),
|
||||||
#[error("Mutex poisoned: {0}")]
|
#[error("Mutex poisoned: {0}")]
|
||||||
Poison(Cow<'static, str>),
|
Poison(Cow<'static, str>),
|
||||||
|
#[error(transparent)]
|
||||||
|
Query(#[from] axum::extract::rejection::QueryRejection),
|
||||||
#[error("Regex error: {0}")]
|
#[error("Regex error: {0}")]
|
||||||
Regex(#[from] regex::Error),
|
Regex(#[from] regex::Error),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
|
|||||||
Reference in New Issue
Block a user