diff --git a/Cargo.lock b/Cargo.lock index 449d1cc25..83d031dfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4946,8 +4946,8 @@ dependencies = [ [[package]] name = "ruma" -version = "0.17.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.16.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "assign", "js_int", @@ -4965,8 +4965,8 @@ dependencies = [ [[package]] name = "ruma-appservice-api" -version = "0.17.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.16.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "http", "js_int", @@ -4978,8 +4978,8 @@ dependencies = [ [[package]] name = "ruma-client-api" -version = "0.25.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.24.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "as_variant", "assign", @@ -5000,11 +5000,11 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.20.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.19.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "as_variant", - "base64 0.23.1", + "base64 0.22.1", "bytes", "date_header", "form_urlencoded", @@ -5033,8 +5033,8 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.35.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.34.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "as_variant", "indexmap 2.14.2", @@ -5054,10 +5054,11 @@ dependencies = [ [[package]] name = "ruma-federation-api" -version = "0.16.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.15.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "bytes", + "headers", "http", "http-auth", "httparse", @@ -5077,7 +5078,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.12.1" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "js_int", "thiserror 2.0.21", @@ -5085,8 +5086,8 @@ dependencies = [ [[package]] name = "ruma-macros" -version = "0.20.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.19.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "as_variant", "cfg-if", @@ -5101,8 +5102,8 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" -version = "0.16.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.15.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "js_int", "ruma-common", @@ -5113,10 +5114,10 @@ dependencies = [ [[package]] name = "ruma-signatures" -version = "0.22.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.21.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ - "base64 0.23.1", + "base64 0.22.1", "ed25519-dalek 3.0.0", "memchr", "pkcs8 0.11.0", @@ -5130,8 +5131,8 @@ dependencies = [ [[package]] name = "ruma-state-res" -version = "0.18.0" -source = "git+https://github.com/ruma/ruma.git?rev=e7384f01eacd454c593683de50491201e2160fd1#e7384f01eacd454c593683de50491201e2160fd1" +version = "0.17.0" +source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index bd605174f..67ac2ad10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -348,7 +348,7 @@ version = "1.1.1" # Used for matrix spec type definitions and helpers [workspace.dependencies.ruma] git = "https://github.com/ruma/ruma.git" -rev = "e7384f01eacd454c593683de50491201e2160fd1" +rev = "e927699d374114843f9f41ee133a5ced08449229" features = [ "appservice-api-c", "client-api", @@ -385,6 +385,7 @@ features = [ "unstable-msc4480", "unstable-msc4466", "unstable-msc4494", + "unstable-msc4484", "unstable-extensible-events", ] diff --git a/src/api/router/args.rs b/src/api/router/args.rs index 6a868fe70..beee4a3d0 100644 --- a/src/api/router/args.rs +++ b/src/api/router/args.rs @@ -8,7 +8,7 @@ use conduwuit::{Error, Result, err}; use ruma::{ CanonicalJsonObject, - api::{IncomingRequest, IncomingRequestExt}, + api::{IncomingRequest, IncomingRequestExt, auth_scheme::AuthScheme}, }; use serde::Deserialize; @@ -92,13 +92,22 @@ async fn from_request( let request = hyper::Request::from_parts(parts, body.as_ref()); // Check authentication - let auth = - R::Authentication::authenticate::(services, &request, auth_query).await?; + let authentication = + R::Authentication::extract_authentication(&request).map_err(|err| { + err!(Request(Unauthorized(warn!( + path = request.uri().path(), + err = err.into(), + "Failed to extract authentication" + )))) + })?; + + let identity = + R::Authentication::check::(services, &request, authentication, auth_query).await?; // Deserialize the body let body = R::try_from_http_request(request, &borrowed_path) .map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))?; - Ok(Self { body, json_body, identity: auth }) + Ok(Self { body, json_body, identity }) } } diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index b29dc2e13..b943ce2b6 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -84,75 +84,50 @@ pub(crate) fn is_appservice(&self) -> bool { matches!(self, Self::Appservice { . pub(crate) trait CheckAuth: AuthScheme { type Identity: Send; - fn authenticate + Sync>( + fn check( services: &Services, - incoming_request: &hyper::Request, + incoming_request: &hyper::Request<&[u8]>, + authentication: Self::Output, query: AuthQueryParams, - ) -> impl Future> + Send { - async move { - let route = TypeId::of::(); - - let output = Self::extract_authentication(incoming_request).map_err(|err| { - err!(Request(Unauthorized(warn!( - "Failed to extract authorization: {}", - err.into() - )))) - })?; - - Self::verify(services, output, incoming_request, query, route).await - } - } - - fn verify + Sync>( - services: &Services, - output: Self::Output, - request: &hyper::Request, - query: AuthQueryParams, - route: TypeId, ) -> impl Future> + Send; } impl CheckAuth for ServerSignatures { type Identity = OwnedServerName; - async fn verify + Sync>( + async fn check( services: &Services, - output: Self::Output, - request: &hyper::Request, + incoming_request: &hyper::Request<&[u8]>, + authentication: Self::Output, _query: AuthQueryParams, - _route: TypeId, ) -> Result { - 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 min_valid_ts = MilliSecondsSinceUnixEpoch(UInt::new_saturating(millis_since_unix_epoch())); + let key = services .server_keys - .get_single_verify_key(&output.origin, &output.key, min_valid_ts) + .get_single_verify_key(&authentication.origin, &authentication.key, min_valid_ts) .await .ok_or_else(|| { err!(Request(Unauthorized(debug_warn!( - origin=%output.origin, + origin=%authentication.origin, "Unable to acquire your signing key (ID: {})", - output.key + authentication.key )))) })?; - let keys: PubKeys = [(output.key.to_string(), key.key)].into(); - let keys: PubKeyMap = [(output.origin.as_str().into(), keys)].into(); + let keys: PubKeys = [(authentication.key.to_string(), key.key)].into(); + let keys: PubKeyMap = [(authentication.origin.as_str().into(), keys)].into(); - match output.verify_http_request(request, destination, &keys) { + match authentication.verify_http_request( + incoming_request, + services.globals.server_name(), + &keys, + ) { | Ok(()) => { if services .moderation - .is_remote_server_forbidden(&output.origin) + .is_remote_server_forbidden(&authentication.origin) { return Err!(Request(Forbidden( "You are blocked from federating with this server." @@ -160,15 +135,15 @@ async fn verify + Sync>( } // Ping the server as healthy - if services.federation.mark_healthy(&output.origin) { + if services.federation.mark_healthy(&authentication.origin) { services .sending - .flush_servers([output.origin.clone()].stream()) + .flush_servers([authentication.origin.clone()].stream()) .await .ok(); } - Ok(output.origin) + Ok(authentication.origin) }, | Err(err) => Err!(Request(Unauthorized(debug_warn!( "Failed to verify X-Matrix header: {err}" @@ -180,120 +155,31 @@ async fn verify + Sync>( impl CheckAuth for AccessToken { type Identity = ClientIdentity; - async fn verify + Sync>( + async fn check( services: &Services, - output: Self::Output, - _request: &hyper::Request, + _incoming_request: &hyper::Request<&[u8]>, + authentication: Self::Output, query: AuthQueryParams, - route: TypeId, ) -> Result { - if output.is_empty() { - return Err!(Request(Unauthorized("Missing access token."))); - } - if let Some((sender_user, sender_device, status)) = - services.users.find_from_token(&output).await - { - // If the token is expired we return a soft logout - if matches!(status, AccessTokenStatus::Expired) { - return Err(Error::Request( - ErrorKind::UnknownToken( - assign!(UnknownTokenErrorData::new(), { soft_logout: true }), - ), - "This token has expired".into(), - StatusCode::UNAUTHORIZED, - )); - } - - // Locked users can only use /logout and /logout/all - if services - .users - .is_locked(&sender_user) - .await - .is_ok_and(std::convert::identity) - { - if !(route == TypeId::of::() - || route == TypeId::of::()) - { - return Err!(Request(UserLocked("Your account is locked."))); - } - } - - Ok(ClientIdentity::User { sender_user, sender_device }) - } else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await { - 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."))); - }; - - if !appservice_info.is_user_match(&sender_user) { - 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(device_id) = query - .device_id - .or(query.legacy_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." - ))); - } - - Some(device_id.to_owned()) - } else { - None - }; - - Ok(ClientIdentity::Appservice { - sender_user, - sender_device, - appservice_info: Box::new(appservice_info), - }) - } else { - Err(Error::Request( - ErrorKind::UnknownToken(UnknownTokenErrorData::new()), - "Invalid token".into(), - StatusCode::UNAUTHORIZED, - )) - } + check_access_token(services, &authentication, query, TypeId::of::()).await } } impl CheckAuth for AccessTokenOptional { type Identity = Option; - async fn verify + Sync>( + async fn check( services: &Services, - output: Self::Output, - request: &hyper::Request, + _incoming_request: &hyper::Request<&[u8]>, + authentication: Self::Output, query: AuthQueryParams, - route: TypeId, ) -> Result { - match output { - | Some(token) => - ::verify(services, token, request, query, route) - .await - .map(Some), - | None => Ok(None), + if let Some(authentication) = authentication { + check_access_token(services, &authentication, query, TypeId::of::()) + .await + .map(Some) + } else { + Ok(None) } } } @@ -301,40 +187,31 @@ async fn verify + Sync>( impl CheckAuth for AppserviceToken { type Identity = RegistrationInfo; - async fn verify + Sync>( + async fn check( services: &Services, - output: Self::Output, - _request: &hyper::Request, + _incoming_request: &hyper::Request<&[u8]>, + authentication: Self::Output, _query: AuthQueryParams, - _route: TypeId, ) -> Result { - if output.is_empty() { - return Err!(Request(Unauthorized("Missing access token."))); - } - let Ok(appservice_info) = services.appservice.find_from_token(&output).await else { - return Err!(Request(Unauthorized("Invalid appservice token."))); - }; - - Ok(appservice_info) + check_appservice_token(services, &authentication).await } } impl CheckAuth for AppserviceTokenOptional { type Identity = Option; - async fn verify + Sync>( + async fn check( services: &Services, - output: Self::Output, - request: &hyper::Request, - query: AuthQueryParams, - route: TypeId, + _incoming_request: &hyper::Request<&[u8]>, + authentication: Self::Output, + _query: AuthQueryParams, ) -> Result { - match output { - | Some(token) => - ::verify(services, token, request, query, route) - .await - .map(Some), - | None => Ok(None), + if let Some(authentication) = authentication { + check_appservice_token(services, &authentication) + .await + .map(Some) + } else { + Ok(None) } } } @@ -342,13 +219,12 @@ async fn verify + Sync>( impl CheckAuth for NoAuthentication { type Identity = (); - fn verify + Sync>( + fn check( _services: &Services, - _output: Self::Output, - _request: &hyper::Request, + _incoming_request: &hyper::Request<&[u8]>, + _authentication: Self::Output, _query: AuthQueryParams, - _route: TypeId, - ) -> impl Future> { + ) -> impl Future> + Send { std::future::ready(Ok(())) } } @@ -356,31 +232,129 @@ fn verify + Sync>( impl CheckAuth for NoAccessToken { type Identity = Option; - async fn verify + Sync>( + async fn check( services: &Services, - _output: Self::Output, - request: &hyper::Request, + incoming_request: &hyper::Request<&[u8]>, + _authentication: Self::Output, query: AuthQueryParams, - route: TypeId, ) -> Result { // 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)))) - })?; + let authentication = AccessTokenOptional::extract_authentication(incoming_request) + .map_err(|err| { + err!(Request(Unauthorized(warn!("Failed to extract authorization: {}", err)))) + })?; - // Check special access restrictions - if (route == TypeId::of::() - || route == TypeId::of::() - || route == TypeId::of::() - || route == TypeId::of::()) - && services.config.require_auth_for_profile_requests - && token.is_none() - { - return Err!(Request(Unauthorized( - "This server requires authentication to access user profiles." - ))); + if let Some(authentication) = authentication { + check_access_token(services, &authentication, query, TypeId::of::()) + .await + .map(Some) + } else { + Ok(None) } - - ::verify(services, token, request, query, route).await } } + +async fn check_access_token( + services: &Services, + token: &str, + query: AuthQueryParams, + route: TypeId, +) -> Result { + if token.is_empty() { + return Err!(Request(Unauthorized("Empty access token."))); + } + + if let Some((sender_user, sender_device, status)) = + services.users.find_from_token(token).await + { + // If the token is expired we return a soft logout + if matches!(status, AccessTokenStatus::Expired) { + return Err(Error::Request( + ErrorKind::UnknownToken( + assign!(UnknownTokenErrorData::new(), { soft_logout: true }), + ), + "This token has expired".into(), + StatusCode::UNAUTHORIZED, + )); + } + + // Locked users can only use /logout and /logout/all + if services + .users + .is_locked(&sender_user) + .await + .is_ok_and(std::convert::identity) + { + if !(route == TypeId::of::() + || route == TypeId::of::()) + { + return Err!(Request(UserLocked("Your account is locked."))); + } + } + + Ok(ClientIdentity::User { sender_user, sender_device }) + } else if let Ok(appservice_info) = services.appservice.find_from_token(token).await { + 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."))); + }; + + if !appservice_info.is_user_match(&sender_user) { + 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(device_id) = query + .device_id + .or(query.legacy_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." + ))); + } + + Some(device_id.to_owned()) + } else { + None + }; + + Ok(ClientIdentity::Appservice { + sender_user, + sender_device, + appservice_info: Box::new(appservice_info), + }) + } else { + Err(Error::Request( + ErrorKind::UnknownToken(UnknownTokenErrorData::new()), + "Invalid token".into(), + StatusCode::UNAUTHORIZED, + )) + } +} + +async fn check_appservice_token(services: &Services, token: &str) -> Result { + let Ok(appservice_info) = services.appservice.find_from_token(token).await else { + return Err!(Request(Unauthorized("Invalid appservice token."))); + }; + + Ok(appservice_info) +}