use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH}; use crate::{api::client_server, services, utils, Error, Result, Ruma}; use ruma::{ api::client::{ account::{ change_password, deactivate, get_3pids, get_username_availability, register, request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami, ThirdPartyIdRemovalStatus, }, error::ErrorKind, uiaa::{AuthFlow, AuthType, UiaaInfo}, }, events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType}, push, UserId, }; use tracing::{error, info, warn}; use register::RegistrationKind; const RANDOM_USER_ID_LENGTH: usize = 10; /// # `GET /_matrix/client/r0/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 pub async fn get_register_available_route( body: Ruma<get_username_availability::v3::Request>, ) -> Result<get_username_availability::v3::Response> { // Validate user id let user_id = UserId::parse_with_server_name( body.username.to_lowercase(), services().globals.server_name(), ) .ok() .filter(|user_id| { !user_id.is_historical() && user_id.server_name() == services().globals.server_name() }) .ok_or(Error::BadRequest( ErrorKind::InvalidUsername, "Username is invalid.", ))?; // Check if username is creative enough if services().users.exists(&user_id)? { return Err(Error::BadRequest( ErrorKind::UserInUse, "Desired user ID is already taken.", )); } // TODO add check for appservice namespaces // If no if check is true we have an username that's available to be used. Ok(get_username_availability::v3::Response { available: true }) } /// # `POST /_matrix/client/r0/register` /// /// Register an account on this homeserver. /// /// You can use [`GET /_matrix/client/r0/register/available`](fn.get_register_available_route.html) /// to check if the user id is valid and available. /// /// - Only works if registration is enabled /// - If type is guest: ignores all parameters except initial_device_display_name /// - If sender is not appservice: Requires UIAA (but we only use a dummy stage) /// - If type is not guest and no username is given: Always fails after UIAA check /// - Creates a new account and populates it with default account data /// - If `inhibit_login` is false: Creates a device and returns device id and access_token pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> { if !services().globals.allow_registration() && !body.from_appservice && services().globals.config.registration_token.is_none() { info!("Registration disabled, no reg token configured, rejecting registration attempt for username {:?}", body.username); return Err(Error::BadRequest( ErrorKind::Forbidden, "Registration has been disabled.", )); } let is_guest = body.kind == RegistrationKind::Guest; if is_guest && (!services().globals.allow_guest_registration() || !services().globals.allow_registration()) { info!("Guest registration disabled / registration fully disabled, rejecting guest registration, initial device name: {:?}", body.initial_device_display_name); return Err(Error::BadRequest( ErrorKind::GuestAccessForbidden, "Guest registration is disabled.", )); } let user_id = match (&body.username, is_guest) { (Some(username), false) => { let proposed_user_id = UserId::parse_with_server_name( username.to_lowercase(), services().globals.server_name(), ) .ok() .filter(|user_id| { !user_id.is_historical() && user_id.server_name() == services().globals.server_name() }) .ok_or(Error::BadRequest( ErrorKind::InvalidUsername, "Username is invalid.", ))?; if services().users.exists(&proposed_user_id)? { return Err(Error::BadRequest( ErrorKind::UserInUse, "Desired user ID is already taken.", )); } proposed_user_id } _ => loop { let proposed_user_id = UserId::parse_with_server_name( utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(), services().globals.server_name(), ) .unwrap(); if !services().users.exists(&proposed_user_id)? { break proposed_user_id; } }, }; // UIAA let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: if services().globals.config.registration_token.is_some() { vec![AuthType::RegistrationToken] } else { vec![AuthType::Dummy] }, }], completed: Vec::new(), params: Default::default(), session: None, auth_error: None, }; if !body.from_appservice && !is_guest { if let Some(auth) = &body.auth { let (worked, uiaainfo) = services().uiaa.try_auth( &UserId::parse_with_server_name("", services().globals.server_name()) .expect("we know this is valid"), "".into(), auth, &uiaainfo, )?; if !worked { return Err(Error::Uiaa(uiaainfo)); } // Success! } else if let Some(json) = body.json_body { uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); services().uiaa.create( &UserId::parse_with_server_name("", services().globals.server_name()) .expect("we know this is valid"), "".into(), &uiaainfo, &json, )?; return Err(Error::Uiaa(uiaainfo)); } else { return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); } } let password = if is_guest { None } else { body.password.as_deref() }; // Create user services().users.create(&user_id, password)?; // Default to pretty displayname let mut displayname = user_id.localpart().to_owned(); // If enabled append lightning bolt to display name (default true) if services().globals.enable_lightning_bolt() { displayname.push_str(" ⚡️"); } services() .users .set_displayname(&user_id, Some(displayname.clone()))?; // Initial account data services().account_data.update( None, &user_id, GlobalAccountDataEventType::PushRules.to_string().into(), &serde_json::to_value(ruma::events::push_rules::PushRulesEvent { content: ruma::events::push_rules::PushRulesEventContent { global: push::Ruleset::server_default(&user_id), }, }) .expect("to json always works"), )?; // Inhibit login does not work for guests if !is_guest && body.inhibit_login { return Ok(register::v3::Response { access_token: None, user_id, device_id: None, refresh_token: None, expires_in: None, }); } // Generate new device id if the user didn't specify one let device_id = if is_guest { None } else { body.device_id.clone() } .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); // Generate new token for the device let token = utils::random_string(TOKEN_LENGTH); // Create device for this account services().users.create_device( &user_id, &device_id, &token, body.initial_device_display_name.clone(), )?; info!("New user \"{}\" registered on this server.", user_id); // log in conduit admin channel if a non-guest user registered if !body.from_appservice && !is_guest { services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( "New user \"{user_id}\" registered on this server." ))); } // log in conduit admin channel if a guest registered if !body.from_appservice && is_guest { services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( "Guest user \"{user_id}\" with device display name `{:?}` registered on this server.", body.initial_device_display_name ))); } // If this is the first real user, grant them admin privileges except for guest users // Note: the server user, @conduit:servername, is generated first if services().users.count()? == 2 && !is_guest { services() .admin .make_user_admin(&user_id, displayname) .await?; warn!("Granting {} admin privileges as the first user", user_id); } else { error!("First registered user \"{user_id}\" is a guest account, not granting admin privileges.\n Recommend disabling public and guest registrations, and using emergency password to get access back, or reset your database with disabled registration."); } Ok(register::v3::Response { access_token: Some(token), user_id, device_id: Some(device_id), refresh_token: None, expires_in: None, }) } /// # `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 pub async fn change_password_route( body: Ruma<change_password::v3::Request>, ) -> Result<change_password::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_device = body.sender_device.as_ref().expect("user is authenticated"); let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Password], }], completed: Vec::new(), params: Default::default(), session: None, auth_error: None, }; if let Some(auth) = &body.auth { let (worked, uiaainfo) = services() .uiaa .try_auth(sender_user, sender_device, auth, &uiaainfo)?; if !worked { return Err(Error::Uiaa(uiaainfo)); } // Success! } else if let Some(json) = body.json_body { uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); services() .uiaa .create(sender_user, sender_device, &uiaainfo, &json)?; return Err(Error::Uiaa(uiaainfo)); } else { return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); } services() .users .set_password(sender_user, Some(&body.new_password))?; if body.logout_devices { // Logout all devices except the current one for id in services() .users .all_device_ids(sender_user) .filter_map(|id| id.ok()) .filter(|id| id != sender_device) { services().users.remove_device(sender_user, &id)?; } } info!("User {} changed their password.", sender_user); services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( "User {sender_user} changed their password." ))); Ok(change_password::v3::Response {}) } /// # `GET _matrix/client/r0/account/whoami` /// /// Get user_id of the sender user. /// /// Note: Also works for Application Services pub async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoami::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let device_id = body.sender_device.as_ref().cloned(); Ok(whoami::v3::Response { user_id: sender_user.clone(), device_id, is_guest: services().users.is_deactivated(sender_user)? && !body.from_appservice, }) } /// # `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 pub async fn deactivate_route( body: Ruma<deactivate::v3::Request>, ) -> Result<deactivate::v3::Response> { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_device = body.sender_device.as_ref().expect("user is authenticated"); let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec![AuthType::Password], }], completed: Vec::new(), params: Default::default(), session: None, auth_error: None, }; if let Some(auth) = &body.auth { let (worked, uiaainfo) = services() .uiaa .try_auth(sender_user, sender_device, auth, &uiaainfo)?; if !worked { return Err(Error::Uiaa(uiaainfo)); } // Success! } else if let Some(json) = body.json_body { uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); services() .uiaa .create(sender_user, sender_device, &uiaainfo, &json)?; return Err(Error::Uiaa(uiaainfo)); } else { return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); } // Make the user leave all rooms before deactivation client_server::leave_all_rooms(sender_user).await?; // Remove devices and mark account as deactivated services().users.deactivate_account(sender_user)?; info!("User {} deactivated their account.", sender_user); services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( "User {sender_user} deactivated their account." ))); Ok(deactivate::v3::Response { id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport, }) } /// # `GET _matrix/client/v3/account/3pid` /// /// Get a list of third party identifiers associated with this account. /// /// - Currently always returns empty list pub async fn third_party_route( body: Ruma<get_3pids::v3::Request>, ) -> Result<get_3pids::v3::Response> { let _sender_user = body.sender_user.as_ref().expect("user is authenticated"); Ok(get_3pids::v3::Response::new(Vec::new())) } /// # `POST /_matrix/client/v3/account/3pid/email/requestToken` /// /// "This API should be used to request validation tokens when adding an email address to an account" /// /// - 403 signals that The homeserver does not allow the third party identifier as a contact option. pub async fn request_3pid_management_token_via_email_route( _body: Ruma<request_3pid_management_token_via_email::v3::Request>, ) -> Result<request_3pid_management_token_via_email::v3::Response> { Err(Error::BadRequest( ErrorKind::ThreepidDenied, "Third party identifier is not allowed", )) } /// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken` /// /// "This API should be used to request validation tokens when adding an phone number to an account" /// /// - 403 signals that The homeserver does not allow the third party identifier as a contact option. pub async fn request_3pid_management_token_via_msisdn_route( _body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>, ) -> Result<request_3pid_management_token_via_msisdn::v3::Response> { Err(Error::BadRequest( ErrorKind::ThreepidDenied, "Third party identifier is not allowed", )) }