Files
tuwunel/src/api/client/register.rs
2026-01-26 19:28:56 +00:00

437 lines
12 KiB
Rust

use std::fmt::Write;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use ruma::{
UserId,
api::client::{
account::{
check_registration_token_validity, get_username_availability,
register::{self, LoginType, RegistrationKind},
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
};
use tuwunel_core::{Err, Error, Result, debug_info, debug_warn, info, utils};
use tuwunel_service::users::{Register, device::generate_refresh_token};
use super::SESSION_ID_LENGTH;
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
/// # `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")]
pub(crate) async fn get_register_available_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc = body
.appservice_info
.as_ref()
.is_some_and(|appservice| {
let id = &appservice.registration.id;
id == "irc"
|| id.contains("matrix-appservice-irc")
|| id.contains("matrix_appservice_irc")
});
if services
.config
.forbidden_usernames
.is_match(&body.username)
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
body.username.clone()
} else {
body.username.to_lowercase()
};
// 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() {
// unless the username is from the broken matrix appservice IRC bridge, we
// should follow synapse's behaviour on not allowing things like spaces
// and UTF-8 characters in usernames
if !is_matrix_appservice_irc {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or spaces: \
{e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
// 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
&& !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 { available: true })
}
/// # `POST /_matrix/client/v3/register`
///
/// Register an account on this homeserver.
///
/// You can use [`GET
/// /_matrix/client/v3/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
#[expect(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register")]
pub(crate) async fn register_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some();
let user = body.username.as_deref().unwrap_or("");
let device_name = body
.initial_device_display_name
.as_deref()
.unwrap_or("");
if !services.config.allow_registration && body.appservice_info.is_none() {
info!(
%is_guest,
%user,
%device_name,
"Rejecting registration attempt as registration is disabled"
);
return Err!(Request(Forbidden("Registration has been disabled.")));
}
if is_guest && !services.config.allow_guest_registration {
debug_warn!(
%device_name,
"Guest registration disabled, rejecting guest registration attempt"
);
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
let user_id = match (body.username.as_ref(), is_guest) {
| (Some(username), false) => {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc =
body.appservice_info
.as_ref()
.is_some_and(|appservice| {
appservice.registration.id == "irc"
|| appservice
.registration
.id
.contains("matrix-appservice-irc")
|| appservice
.registration
.id
.contains("matrix_appservice_irc")
});
if services
.config
.forbidden_usernames
.is_match(username)
&& !emergency_mode_enabled
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
username.clone()
} else {
username.to_lowercase()
};
let proposed_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() {
// unless the username is from the broken matrix appservice IRC bridge, or
// we are in emergency mode, we should follow synapse's behaviour on
// not allowing things like spaces and UTF-8 characters in usernames
if !is_matrix_appservice_irc && !emergency_mode_enabled {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or \
spaces: {e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
if services.users.exists(&proposed_user_id).await {
return Err!(Request(UserInUse("User ID is not available.")));
}
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).await {
break proposed_user_id;
}
},
};
if body.body.login_type == Some(LoginType::ApplicationService) {
match body.appservice_info {
| Some(ref info) =>
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive(
"Username is not in an appservice namespace."
)));
},
| _ => {
return Err!(Request(MissingToken("Missing appservice token.")));
},
}
} else if services
.appservice
.is_exclusive_user_id(&user_id)
.await && !emergency_mode_enabled
{
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
// UIAA
let mut uiaainfo;
let skip_auth = if !services
.globals
.get_registration_tokens()
.is_empty()
&& !is_guest
{
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
stages: vec![AuthType::RegistrationToken],
}],
completed: Vec::new(),
params: Default::default(),
session: None,
auth_error: None,
};
body.appservice_info.is_some()
} else {
// No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Default::default(),
session: None,
auth_error: None,
};
body.appservice_info.is_some() || is_guest
};
if !skip_auth {
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
auth,
&uiaainfo,
)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services.uiaa.create(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
&uiaainfo,
json,
);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
}
let password = if is_guest { None } else { body.password.as_deref() };
services
.users
.full_register(Register {
user_id: Some(&user_id),
password,
appservice_info: body.appservice_info.as_ref(),
is_guest,
grant_first_user_admin: true,
..Default::default()
})
.await?;
if (!is_guest && body.inhibit_login)
|| body
.appservice_info
.as_ref()
.is_some_and(|appservice| appservice.registration.device_management)
{
return Ok(register::v3::Response {
user_id,
device_id: None,
access_token: None,
refresh_token: None,
expires_in: None,
});
}
let device_id = if is_guest { None } else { body.device_id.as_deref() };
// Generate new token for the device
let (access_token, expires_in) = services
.users
.generate_access_token(body.refresh_token);
// Generate a new refresh_token if requested by client
let refresh_token = expires_in.is_some().then(generate_refresh_token);
// Create device for this account
let device_id = services
.users
.create_device(
&user_id,
device_id,
(Some(&access_token), expires_in),
refresh_token.as_deref(),
body.initial_device_display_name.as_deref(),
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
if body.appservice_info.is_none() && (!is_guest || services.config.log_guest_registrations) {
let mut notice = String::from(if is_guest { "New guest user" } else { "New user" });
write!(notice, " registered on this server from IP {client}")?;
if let Some(device_name) = body.initial_device_display_name.as_deref() {
write!(notice, " with device name {device_name}")?;
}
if !is_guest {
info!("{notice}");
} else {
debug_info!("{notice}");
}
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
}
}
Ok(register::v3::Response {
user_id,
device_id: Some(device_id),
access_token: Some(access_token),
refresh_token,
expires_in,
})
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking
///
/// Currently does not have any ratelimiting, and this isn't very practical as
/// there is only one registration token allowed.
pub(crate) async fn check_registration_token_validity(
State(services): State<crate::State>,
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let tokens = services.globals.get_registration_tokens();
if tokens.is_empty() {
return Err!(Request(Forbidden("Server does not allow token registration")));
}
let valid = tokens.contains(&body.token);
Ok(check_registration_token_validity::v1::Response { valid })
}