use axum::extract::State; use axum_client_ip::ClientIp; use conduwuit::{ Err, Result, err, info, pdu::PartialPdu, utils::{ReadyExt, stream::BroadbandExt}, }; use conduwuit_service::Services; use futures::{FutureExt, StreamExt}; use lettre::{Address, message::Mailbox}; use ruma::{ OwnedRoomId, UserId, api::client::{ account::{ ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity, deactivate, get_username_availability, request_password_change_token_via_email, whoami, }, uiaa::{AuthFlow, AuthType}, }, assign, events::room::{ member::{MembershipState, RoomMemberEventContent}, power_levels::RoomPowerLevelsEventContent, }, }; use service::{mailer::messages, uiaa::Identity}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper}; use crate::Ruma; pub(crate) mod register; pub(crate) mod threepid; /// # `GET /_matrix/client/v3/register/available` /// /// Checks if a username is valid and available on this server. /// /// Conditions for returning true: /// - The user id is not historical /// - The server name of the user id matches this server /// - No user or appservice on this server already claimed this username /// /// Note: This will not reserve the username, so the username might become /// invalid when trying to register #[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")] pub(crate) async fn get_register_available_route( State(services): State, ClientIp(client): ClientIp, body: Ruma, ) -> Result { // Validate user id let user_id = match UserId::parse_with_server_name(&body.username, services.globals.server_name()) { | Ok(user_id) => { if let Err(e) = user_id.validate_strict() { return Err!(Request(InvalidUsername(debug_warn!( "Username {} contains disallowed characters or spaces: {e}", body.username )))); } user_id }, | Err(e) => { return Err!(Request(InvalidUsername(debug_warn!( "Username {} is not valid: {e}", body.username )))); }, }; // Check if username is creative enough if services.users.exists(&user_id).await { return Err!(Request(UserInUse("User ID is not available."))); } if let Some(ref info) = body.appservice_info { if !info.is_user_match(&user_id) { return Err!(Request(Exclusive("Username is not in an appservice namespace."))); } } if services.appservice.is_exclusive_user_id(&user_id).await { return Err!(Request(Exclusive("Username is reserved by an appservice."))); } Ok(get_username_availability::v3::Response::new(true)) } /// # `POST /_matrix/client/r0/account/password` /// /// Changes the password of this account. /// /// - Requires UIAA to verify user password /// - Changes the password of the sender user /// - The password hash is calculated using argon2 with 32 character salt, the /// plain password is /// not saved /// /// If logout_devices is true it does the following for each device except the /// sender device: /// - Invalidates access token /// - Deletes device metadata (device id, device display name, last seen ip, /// last seen ts) /// - Forgets to-device events /// - Triggers device list updates #[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")] pub(crate) async fn change_password_route( State(services): State, ClientIp(client): ClientIp, body: Ruma, ) -> Result { let identity = if let Some(ref user_id) = body.sender_user { // A signed-in user is trying to change their password, prompt them for their // existing one services .uiaa .authenticate( &body.auth, vec![AuthFlow::new(vec![AuthType::Password])], Box::default(), Some(Identity::from_user_id(user_id)), ) .await? } else { // A signed-out user is trying to reset their password, prompt them for email // confirmation. Note that we do not _send_ an email here, their client should // have already hit `/account/password/requestToken` to send the email. We // just validate it. services .uiaa .authenticate( &body.auth, vec![AuthFlow::new(vec![AuthType::EmailIdentity])], Box::default(), None, ) .await? }; let sender_user = UserId::parse(format!( "@{}:{}", identity.localpart.expect("localpart should be known"), services.globals.server_name() )) .expect("user ID should be valid"); services .users .set_password(&sender_user, Some(&body.new_password)) .await?; if body.logout_devices { // Logout all devices except the current one services .users .all_device_ids(&sender_user) .ready_filter(|id| *id != body.sender_device()) .for_each(async |id| services.users.remove_device(&sender_user, &id).await) .await; // Remove all pushers except the ones associated with this session services .pusher .get_pushkeys(&sender_user) .map(ToOwned::to_owned) .broad_filter_map(async |pushkey| { services .pusher .get_pusher_device(&pushkey) .await .ok() .as_ref() .is_some_and(|pusher_device| pusher_device != body.sender_device()) .then_some(pushkey) }) .for_each(async |pushkey| { services.pusher.delete_pusher(&sender_user, &pushkey).await; }) .await; } info!("User {} changed their password.", &sender_user); if services.server.config.admin_room_notices { services .admin .notice(&format!("User {} changed their password.", &sender_user)) .await; } Ok(change_password::v3::Response::new()) } /// # `POST /_matrix/client/v3/account/password/email/requestToken` /// /// Requests a validation email for the purpose of resetting a user's password. pub(crate) async fn request_password_change_token_via_email_route( State(services): State, body: Ruma, ) -> Result { let Ok(email) = Address::try_from(body.email.clone()) else { return Err!(Request(InvalidParam("Invalid email address."))); }; let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else { return Err!(Request(ThreepidNotFound( "No account is associated with this email address" ))); }; let user_id = UserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap(); let display_name = services.users.displayname(&user_id).await.ok(); let session = services .threepid .send_validation_email( Mailbox::new(display_name.clone(), email), |verification_link| messages::PasswordReset { display_name: display_name.as_deref(), user_id: &user_id, verification_link, }, &body.client_secret, body.send_attempt.try_into().unwrap(), ) .await?; Ok(request_password_change_token_via_email::v3::Response::new(session)) } /// # `GET /_matrix/client/v3/account/whoami` /// /// Get `user_id` of the sender user. /// /// Note: Also works for Application Services pub(crate) async fn whoami_route( State(services): State, body: Ruma, ) -> Result { let is_guest = services .users .is_deactivated(body.sender_user()) .await .map_err(|_| { err!(Request(Forbidden("Application service has not registered this user."))) })? && body.appservice_info.is_none(); Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), is_guest), { device_id: body.sender_device.clone(), })) } /// # `POST /_matrix/client/r0/account/deactivate` /// /// Deactivate sender user account. /// /// - Leaves all rooms and rejects all invitations /// - Invalidates all access tokens /// - Deletes all device metadata (device id, device display name, last seen ip, /// last seen ts) /// - Forgets all to-device events /// - Triggers device list updates /// - Removes ability to log in again #[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")] pub(crate) async fn deactivate_route( State(services): State, ClientIp(client): ClientIp, body: Ruma, ) -> Result { // Authentication for this endpoint is technically optional, // but we require the user to be logged in let sender_user = body .sender_user .as_ref() .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; // Prompt the user to confirm with their password using UIAA let _ = services .uiaa .authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user))) .await?; // Remove profile pictures and display name let all_joined_rooms: Vec = services .rooms .state_cache .rooms_joined(sender_user) .map(Into::into) .collect() .await; full_user_deactivate(&services, sender_user, &all_joined_rooms) .boxed() .await?; info!("User {sender_user} deactivated their account."); if services.server.config.admin_room_notices { services .admin .notice(&format!("User {sender_user} deactivated their account.")) .await; } Ok(deactivate::v3::Response::new(ThirdPartyIdRemovalStatus::Success)) } /// # `GET /_matrix/client/v1/register/m.login.registration_token/validity` /// /// Checks if the provided registration token is valid at the time of checking. pub(crate) async fn check_registration_token_validity( State(services): State, body: Ruma, ) -> Result { // TODO: ratelimit this pretty heavily let valid = services .registration_tokens .validate_token(body.token.clone()) .await .is_some(); Ok(check_registration_token_validity::v1::Response::new(valid)) } /// Runs through all the deactivation steps: /// /// - Mark as deactivated /// - Removing display name /// - Removing avatar URL and blurhash /// - Removing all profile data /// - Leaving all rooms (and forgets all of them) pub async fn full_user_deactivate( services: &Services, user_id: &UserId, all_joined_rooms: &[OwnedRoomId], ) -> Result<()> { services.users.deactivate_account(user_id).await.ok(); if services.globals.user_is_local(user_id) { let _ = services .threepid .disassociate_localpart_email(user_id.localpart()) .await; } services.users.clear_profile(user_id).await; services .pusher .get_pushkeys(user_id) .for_each(async |pushkey| { services.pusher.delete_pusher(user_id, pushkey).await; }) .await; // TODO: Rescind all user invites let mut pdu_queue: Vec<(PartialPdu, &OwnedRoomId)> = Vec::new(); for room_id in all_joined_rooms { let room_power_levels = services .rooms .state_accessor .get_room_power_levels(room_id) .await; let user_can_demote_self = room_power_levels.user_can_change_user_power_level(user_id, user_id); if user_can_demote_self && let Ok(mut power_levels_content) = RoomPowerLevelsEventContent::try_from(room_power_levels) { power_levels_content.users.remove(user_id); let pl_evt = PartialPdu::state(String::new(), &power_levels_content); pdu_queue.push((pl_evt, room_id)); } // Leave the room pdu_queue.push(( PartialPdu::state( user_id.to_string(), &RoomMemberEventContent::new(MembershipState::Leave), ), room_id, )); // TODO: Redact all messages sent by the user in the room } for (pdu, room_id) in pdu_queue { let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await; let _ = services .rooms .timeline .build_and_append_pdu(pdu, user_id, Some(room_id.as_ref()), &state_lock) .await; } for room_id in all_joined_rooms { services.rooms.state_cache.forget(room_id, user_id); } Ok(()) }