From 2a2486182a2c1cace72d3753ed131e7b5af85582 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Sun, 8 Jun 2025 21:15:17 +0000 Subject: [PATCH] Split login case bodies into handlers. Signed-off-by: Jason Volk --- src/api/client/session/appservice.rs | 48 ++++++++ src/api/client/session/mod.rs | 171 ++++++--------------------- src/api/client/session/password.rs | 56 ++++++++- src/api/client/session/token.rs | 47 ++++++-- 4 files changed, 175 insertions(+), 147 deletions(-) create mode 100644 src/api/client/session/appservice.rs diff --git a/src/api/client/session/appservice.rs b/src/api/client/session/appservice.rs new file mode 100644 index 00000000..63cc1bbb --- /dev/null +++ b/src/api/client/session/appservice.rs @@ -0,0 +1,48 @@ +use ruma::{ + OwnedUserId, UserId, + api::client::{ + session::login::v3::{ApplicationService, Request}, + uiaa, + }, +}; +use tuwunel_core::{Err, Result, err}; +use tuwunel_service::Services; + +use crate::Ruma; + +pub(super) async fn handle_login( + services: &Services, + body: &Ruma, + info: &ApplicationService, +) -> Result { + #[allow(deprecated)] + let ApplicationService { identifier, user } = info; + + let Some(ref info) = body.appservice_info else { + return Err!(Request(MissingToken("Missing appservice token."))); + }; + + let user_id = if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + UserId::parse_with_server_name(user_id, &services.config.server_name) + } else if let Some(user) = user { + UserId::parse_with_server_name(user, &services.config.server_name) + } else { + return Err!(Request(Unknown(debug_warn!( + ?body.login_info, + "Valid identifier or username was not provided (invalid or unsupported login type?)" + )))); + } + .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; + + if !services.globals.user_is_local(&user_id) { + return Err!(Request(Unknown("User ID does not belong to this homeserver"))); + } + + let emergency_mode_enabled = services.config.emergency_password.is_some(); + + if !info.is_user_match(&user_id) && !emergency_mode_enabled { + return Err!(Request(Exclusive("Username is not in an appservice namespace."))); + } + + Ok(user_id) +} diff --git a/src/api/client/session/mod.rs b/src/api/client/session/mod.rs index e4a418a5..450ec43f 100644 --- a/src/api/client/session/mod.rs +++ b/src/api/client/session/mod.rs @@ -1,3 +1,4 @@ +mod appservice; mod ldap; mod logout; mod password; @@ -5,24 +6,17 @@ mod token; use axum::extract::State; use axum_client_ip::InsecureClientIp; -use futures::FutureExt; -use ruma::{ - UserId, - api::client::{ - session::{ - get_login_types::{ - self, - v3::{ApplicationServiceLoginType, PasswordLoginType, TokenLoginType}, - }, - login::{ - self, - v3::{DiscoveryInfo, HomeserverInfo}, - }, - }, - uiaa, +use ruma::api::client::session::{ + get_login_types::{ + self, + v3::{ApplicationServiceLoginType, LoginType, PasswordLoginType, TokenLoginType}, + }, + login::{ + self, + v3::{DiscoveryInfo, HomeserverInfo, LoginInfo}, }, }; -use tuwunel_core::{Err, Result, debug, err, info, utils, utils::stream::ReadyExt}; +use tuwunel_core::{Err, Result, info, utils, utils::stream::ReadyExt}; use self::{ldap::ldap_login, password::password_login}; pub(crate) use self::{ @@ -43,10 +37,10 @@ pub(crate) async fn get_login_types_route( _body: Ruma, ) -> Result { Ok(get_login_types::v3::Response::new(vec![ - get_login_types::v3::LoginType::Password(PasswordLoginType::default()), - get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()), - get_login_types::v3::LoginType::Token(TokenLoginType { - get_login_token: services.server.config.login_via_existing_session, + LoginType::Password(PasswordLoginType::default()), + LoginType::ApplicationService(ApplicationServiceLoginType::default()), + LoginType::Token(TokenLoginType { + get_login_token: services.config.login_via_existing_session, }), ])) } @@ -65,137 +59,44 @@ pub(crate) async fn get_login_types_route( /// Note: You can use [`GET /// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see /// supported login types. -#[tracing::instrument(skip_all, fields(%client), name = "login")] +#[tracing::instrument(name = "login", skip_all, fields(%client, ?body.login_info))] pub(crate) async fn login_route( State(services): State, InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { - let emergency_mode_enabled = services.config.emergency_password.is_some(); - // Validate login method - // TODO: Other login methods let user_id = match &body.login_info { - #[allow(deprecated)] - | login::v3::LoginInfo::Password(login::v3::Password { - identifier, - password, - user, - .. - }) => { - debug!("Got password login type"); - let user_id = - if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { - UserId::parse_with_server_name(user_id, &services.config.server_name) - } else if let Some(user) = user { - UserId::parse_with_server_name(user, &services.config.server_name) - } else { - return Err!(Request(Unknown( - debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") - ))); - } - .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; - - let lowercased_user_id = UserId::parse_with_server_name( - user_id.localpart().to_lowercase(), - &services.config.server_name, - )?; - - if !services.globals.user_is_local(&user_id) - || !services - .globals - .user_is_local(&lowercased_user_id) - { - return Err!(Request(Unknown("User ID does not belong to this homeserver"))); - } - - if cfg!(feature = "ldap") && services.config.ldap.enable { - ldap_login(&services, &user_id, &lowercased_user_id, password) - .boxed() - .await? - } else { - password_login(&services, &user_id, &lowercased_user_id, password) - .boxed() - .await? - } - }, - | login::v3::LoginInfo::Token(login::v3::Token { token }) => { - debug!("Got token login type"); - if !services.server.config.login_via_existing_session { - return Err!(Request(Unknown("Token login is not enabled."))); - } - services - .users - .find_from_login_token(token) - .await? - }, - #[allow(deprecated)] - | login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService { - identifier, - user, - }) => { - debug!("Got appservice login type"); - - let Some(ref info) = body.appservice_info else { - return Err!(Request(MissingToken("Missing appservice token."))); - }; - - let user_id = - if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { - UserId::parse_with_server_name(user_id, &services.config.server_name) - } else if let Some(user) = user { - UserId::parse_with_server_name(user, &services.config.server_name) - } else { - return Err!(Request(Unknown( - debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") - ))); - } - .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; - - if !services.globals.user_is_local(&user_id) { - return Err!(Request(Unknown("User ID does not belong to this homeserver"))); - } - - if !info.is_user_match(&user_id) && !emergency_mode_enabled { - return Err!(Request(Exclusive("Username is not in an appservice namespace."))); - } - - user_id - }, + | LoginInfo::Password(info) => password::handle_login(&services, &body, info).await?, + | LoginInfo::Token(info) => token::handle_login(&services, &body, info).await?, + | LoginInfo::ApplicationService(info) => + appservice::handle_login(&services, &body, info).await?, | _ => { - debug!("/login json_body: {:?}", &body.json_body); - return Err!(Request(Unknown( - debug_warn!(?body.login_info, "Invalid or unsupported login type") - ))); + return Err!(Request(Unknown(debug_warn!( + ?body.login_info, + ?body.json_body, + "Invalid or unsupported login type", + )))); }, }; + // Generate a new token for the device + let token = utils::random_string(TOKEN_LENGTH); + // Generate new device id if the user didn't specify one let device_id = body .device_id .clone() .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); - // Generate a new token for the device - let token = utils::random_string(TOKEN_LENGTH); - // Determine if device_id was provided and exists in the db for this user - let device_exists = if body.device_id.is_some() { - services - .users - .all_device_ids(&user_id) - .ready_any(|v| v == device_id) - .await - } else { - false - }; + let device_exists = services + .users + .all_device_ids(&user_id) + .ready_any(|v| v == device_id) + .await; - if device_exists { - services - .users - .set_token(&user_id, &device_id, &token) - .await?; - } else { + if !device_exists { services .users .create_device( @@ -206,11 +107,15 @@ pub(crate) async fn login_route( Some(client.to_string()), ) .await?; + } else { + services + .users + .set_token(&user_id, &device_id, &token) + .await?; } // send client well-known if specified so the client knows to reconfigure itself let client_discovery_info: Option = services - .server .config .well_known .client diff --git a/src/api/client/session/password.rs b/src/api/client/session/password.rs index 8ff293c4..2ecb009f 100644 --- a/src/api/client/session/password.rs +++ b/src/api/client/session/password.rs @@ -1,8 +1,60 @@ -use futures::TryFutureExt; -use ruma::{OwnedUserId, UserId}; +use futures::{FutureExt, TryFutureExt}; +use ruma::{ + OwnedUserId, UserId, + api::client::{ + session::login::v3::{Password, Request}, + uiaa, + }, +}; use tuwunel_core::{Err, Result, debug_error, err, utils::hash}; use tuwunel_service::Services; +use super::ldap_login; +use crate::Ruma; + +pub(super) async fn handle_login( + services: &Services, + body: &Ruma, + info: &Password, +) -> Result { + #[allow(deprecated)] + let Password { identifier, password, user, .. } = info; + + let user_id = if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + UserId::parse_with_server_name(user_id, &services.config.server_name) + } else if let Some(user) = user { + UserId::parse_with_server_name(user, &services.config.server_name) + } else { + return Err!(Request(Unknown(debug_warn!( + ?body.login_info, + "Valid identifier or username was not provided (invalid or unsupported login type?)" + )))); + } + .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; + + let lowercased_user_id = UserId::parse_with_server_name( + user_id.localpart().to_lowercase(), + &services.config.server_name, + )?; + + let user_is_remote = !services.globals.user_is_local(&user_id) + || !services + .globals + .user_is_local(&lowercased_user_id); + + if user_is_remote { + return Err!(Request(Unknown("User ID does not belong to this homeserver"))); + } + + if cfg!(feature = "ldap") && services.config.ldap.enable { + ldap_login(services, &user_id, &lowercased_user_id, password) + .boxed() + .await + } else { + password_login(services, &user_id, &lowercased_user_id, password).await + } +} + /// Authenticates the given user by its ID and its password. /// /// Returns the user ID if successful, and an error otherwise. diff --git a/src/api/client/session/token.rs b/src/api/client/session/token.rs index de81dee5..2dae0fd3 100644 --- a/src/api/client/session/token.rs +++ b/src/api/client/session/token.rs @@ -2,13 +2,36 @@ use std::time::Duration; use axum::extract::State; use axum_client_ip::InsecureClientIp; -use ruma::api::client::{session::get_login_token, uiaa}; -use tuwunel_core::{Err, Error, Result, utils}; -use tuwunel_service::uiaa::SESSION_ID_LENGTH; +use ruma::{ + OwnedUserId, + api::client::{ + session::{ + get_login_token, + login::v3::{Request, Token}, + }, + uiaa, + }, +}; +use tuwunel_core::{Err, Result, utils::random_string}; +use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH}; use super::TOKEN_LENGTH; use crate::Ruma; +pub(super) async fn handle_login( + services: &Services, + _body: &Ruma, + info: &Token, +) -> Result { + let Token { token } = info; + + if !services.config.login_via_existing_session { + return Err!(Request(Unknown("Token login is not enabled."))); + } + + services.users.find_from_login_token(token).await +} + /// # `POST /_matrix/client/v1/login/get_token` /// /// Allows a logged-in user to get a short-lived token which can be used @@ -21,7 +44,7 @@ pub(crate) async fn login_token_route( InsecureClientIp(client): InsecureClientIp, body: Ruma, ) -> Result { - if !services.server.config.login_via_existing_session { + if !services.config.login_via_existing_session { return Err!(Request(Forbidden("Login via an existing session is not enabled"))); } @@ -29,8 +52,10 @@ pub(crate) async fn login_token_route( // TODO: How do we make only UIA sessions that have not been used before valid? let (sender_user, sender_device) = body.sender(); + let password_flow = uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }; + let mut uiaainfo = uiaa::UiaaInfo { - flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }], + flows: vec![password_flow], completed: Vec::new(), params: Box::default(), session: None, @@ -45,27 +70,25 @@ pub(crate) async fn login_token_route( .await?; if !worked { - return Err(Error::Uiaa(uiaainfo)); + return Err!(Uiaa(uiaainfo)); } // Success! }, | _ => match body.json_body.as_ref() { | Some(json) => { - uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); + uiaainfo.session = Some(random_string(SESSION_ID_LENGTH)); services .uiaa .create(sender_user, sender_device, &uiaainfo, json); - return Err(Error::Uiaa(uiaainfo)); - }, - | _ => { - return Err!(Request(NotJson("No JSON body was sent when required."))); + return Err!(Uiaa(uiaainfo)); }, + | _ => return Err!(Request(NotJson("No JSON body was sent when required."))), }, } - let login_token = utils::random_string(TOKEN_LENGTH); + let login_token = random_string(TOKEN_LENGTH); let expires_in = services .users .create_login_token(sender_user, &login_token);