Split api/client/register from api/client/account suite.
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
@@ -1,29 +1,23 @@
|
|||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use register::RegistrationKind;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedRoomId, UserId,
|
OwnedRoomId, UserId,
|
||||||
api::client::{
|
api::client::{
|
||||||
account::{
|
account::{
|
||||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
|
||||||
deactivate, get_3pids, get_username_availability,
|
|
||||||
register::{self, LoginType},
|
|
||||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||||
whoami,
|
whoami,
|
||||||
},
|
},
|
||||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||||
},
|
},
|
||||||
events::{
|
events::{
|
||||||
GlobalAccountDataEventType, StateEventType,
|
StateEventType,
|
||||||
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||||
},
|
},
|
||||||
push,
|
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Error, Result, debug_info, err, error, info, is_equal_to,
|
Err, Error, Result, err, info,
|
||||||
matrix::{Event, pdu::PduBuilder},
|
matrix::{Event, pdu::PduBuilder},
|
||||||
utils,
|
utils,
|
||||||
utils::{ReadyExt, stream::BroadbandExt},
|
utils::{ReadyExt, stream::BroadbandExt},
|
||||||
@@ -31,570 +25,9 @@ use tuwunel_core::{
|
|||||||
};
|
};
|
||||||
use tuwunel_service::Services;
|
use tuwunel_service::Services;
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
use super::SESSION_ID_LENGTH;
|
||||||
use crate::Ruma;
|
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| {
|
|
||||||
appservice.registration.id == "irc"
|
|
||||||
|| appservice
|
|
||||||
.registration
|
|
||||||
.id
|
|
||||||
.contains("matrix-appservice-irc")
|
|
||||||
|| appservice
|
|
||||||
.registration
|
|
||||||
.id
|
|
||||||
.contains("matrix_appservice_irc")
|
|
||||||
});
|
|
||||||
|
|
||||||
if services
|
|
||||||
.globals
|
|
||||||
.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 {
|
|
||||||
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 { 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
|
|
||||||
#[allow(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();
|
|
||||||
|
|
||||||
if !services.config.allow_registration && body.appservice_info.is_none() {
|
|
||||||
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
|
||||||
| (Some(username), Some(device_display_name)) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
user = %username,
|
|
||||||
device_name = %device_display_name,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (Some(username), _) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
user = %username,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (_, Some(device_display_name)) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
device_name = %device_display_name,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (None, _) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err!(Request(Forbidden("Registration has been disabled.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_guest
|
|
||||||
&& (!services.config.allow_guest_registration
|
|
||||||
|| (services.config.allow_registration
|
|
||||||
&& services.globals.registration_token.is_some()))
|
|
||||||
{
|
|
||||||
info!(
|
|
||||||
"Guest registration disabled / registration enabled with token configured, \
|
|
||||||
rejecting guest registration attempt, initial device name: \"{}\"",
|
|
||||||
body.initial_device_display_name
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("")
|
|
||||||
);
|
|
||||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// forbid guests from registering if there is not a real admin user yet. give
|
|
||||||
// generic user error.
|
|
||||||
if is_guest && services.users.count().await < 2 {
|
|
||||||
warn!(
|
|
||||||
"Guest account attempted to register before a real admin user has been registered, \
|
|
||||||
rejecting registration. Guest's initial device name: \"{}\"",
|
|
||||||
body.initial_device_display_name
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("")
|
|
||||||
);
|
|
||||||
return Err!(Request(Forbidden("Registration is temporarily 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
|
|
||||||
.globals
|
|
||||||
.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.registration_token.is_some() {
|
|
||||||
// Registration token required
|
|
||||||
uiaainfo = UiaaInfo {
|
|
||||||
flows: vec![AuthFlow {
|
|
||||||
stages: vec![AuthType::RegistrationToken],
|
|
||||||
}],
|
|
||||||
completed: Vec::new(),
|
|
||||||
params: Box::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: Box::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() };
|
|
||||||
|
|
||||||
// Create user
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.create(&user_id, password, None)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Default to pretty displayname
|
|
||||||
let mut displayname = user_id.localpart().to_owned();
|
|
||||||
|
|
||||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
|
||||||
// content is set to the user's display name with a space before it
|
|
||||||
if !services
|
|
||||||
.globals
|
|
||||||
.new_user_displayname_suffix()
|
|
||||||
.is_empty()
|
|
||||||
&& body.appservice_info.is_none()
|
|
||||||
{
|
|
||||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
.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 {
|
|
||||||
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(),
|
|
||||||
Some(client.to_string()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug_info!(%user_id, %device_id, "User account was created");
|
|
||||||
|
|
||||||
let device_display_name = body
|
|
||||||
.initial_device_display_name
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// log in conduit admin channel if a non-guest user registered
|
|
||||||
if body.appservice_info.is_none() && !is_guest {
|
|
||||||
if !device_display_name.is_empty() {
|
|
||||||
let notice = format!(
|
|
||||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
|
||||||
display name \"{device_display_name}\""
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("{notice}");
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services.admin.notice(¬ice).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
|
||||||
|
|
||||||
info!("{notice}");
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services.admin.notice(¬ice).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// log in conduit admin channel if a guest registered
|
|
||||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
|
||||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
|
||||||
|
|
||||||
if !device_display_name.is_empty() {
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!(
|
|
||||||
"Guest user \"{user_id}\" with device display name \
|
|
||||||
\"{device_display_name}\" registered on this server from IP {client}"
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#[allow(clippy::collapsible_else_if)]
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!(
|
|
||||||
"Guest user \"{user_id}\" with no device display name registered on \
|
|
||||||
this server from IP {client}",
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the first real user, grant them admin privileges except for guest
|
|
||||||
// users
|
|
||||||
// Note: the server user is generated first
|
|
||||||
if !is_guest {
|
|
||||||
if let Ok(admin_room) = services.admin.get_admin_room().await {
|
|
||||||
if services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.room_joined_count(&admin_room)
|
|
||||||
.await
|
|
||||||
.is_ok_and(is_equal_to!(1))
|
|
||||||
{
|
|
||||||
services.admin.make_user_admin(&user_id).await?;
|
|
||||||
warn!("Granting {user_id} admin privileges as the first user");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.appservice_info.is_none()
|
|
||||||
&& !services.server.config.auto_join_rooms.is_empty()
|
|
||||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
|
||||||
{
|
|
||||||
for room in &services.server.config.auto_join_rooms {
|
|
||||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
|
||||||
error!(
|
|
||||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
|
||||||
{room}, skipping"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.server_in_room(services.globals.server_name(), &room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Skipping room {room} to automatically join as we have never joined before."
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room_server_name) = room.server_name() {
|
|
||||||
match join_room_by_id_helper(
|
|
||||||
&services,
|
|
||||||
&user_id,
|
|
||||||
&room_id,
|
|
||||||
Some("Automatically joining this room upon registration".to_owned()),
|
|
||||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
|
||||||
None,
|
|
||||||
&body.appservice_info,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Err(e) => {
|
|
||||||
// don't return this error so we don't fail registrations
|
|
||||||
error!(
|
|
||||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
info!("Automatically joined room {room} for user {user_id}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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`
|
/// # `POST /_matrix/client/r0/account/password`
|
||||||
///
|
///
|
||||||
/// Changes the password of this account.
|
/// Changes the password of this account.
|
||||||
@@ -861,23 +294,6 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
|||||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # `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 Some(reg_token) = services.globals.registration_token.clone() else {
|
|
||||||
return Err!(Request(Forbidden("Server does not allow token registration")));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs through all the deactivation steps:
|
/// Runs through all the deactivation steps:
|
||||||
///
|
///
|
||||||
/// - Mark as deactivated
|
/// - Mark as deactivated
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub(super) mod profile;
|
|||||||
pub(super) mod push;
|
pub(super) mod push;
|
||||||
pub(super) mod read_marker;
|
pub(super) mod read_marker;
|
||||||
pub(super) mod redact;
|
pub(super) mod redact;
|
||||||
|
pub(super) mod register;
|
||||||
pub(super) mod relations;
|
pub(super) mod relations;
|
||||||
pub(super) mod report;
|
pub(super) mod report;
|
||||||
pub(super) mod room;
|
pub(super) mod room;
|
||||||
@@ -63,6 +64,7 @@ pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
|
|||||||
pub(super) use push::*;
|
pub(super) use push::*;
|
||||||
pub(super) use read_marker::*;
|
pub(super) use read_marker::*;
|
||||||
pub(super) use redact::*;
|
pub(super) use redact::*;
|
||||||
|
pub(super) use register::*;
|
||||||
pub(super) use relations::*;
|
pub(super) use relations::*;
|
||||||
pub(super) use report::*;
|
pub(super) use report::*;
|
||||||
pub(super) use room::*;
|
pub(super) use room::*;
|
||||||
|
|||||||
600
src/api/client/register.rs
Normal file
600
src/api/client/register.rs
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum_client_ip::InsecureClientIp;
|
||||||
|
use futures::FutureExt;
|
||||||
|
use register::RegistrationKind;
|
||||||
|
use ruma::{
|
||||||
|
UserId,
|
||||||
|
api::client::{
|
||||||
|
account::{
|
||||||
|
check_registration_token_validity, get_username_availability,
|
||||||
|
register::{self, LoginType},
|
||||||
|
},
|
||||||
|
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||||
|
},
|
||||||
|
events::GlobalAccountDataEventType,
|
||||||
|
push,
|
||||||
|
};
|
||||||
|
use tuwunel_core::{Err, Error, Result, debug_info, error, info, is_equal_to, utils, warn};
|
||||||
|
|
||||||
|
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||||
|
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| {
|
||||||
|
appservice.registration.id == "irc"
|
||||||
|
|| appservice
|
||||||
|
.registration
|
||||||
|
.id
|
||||||
|
.contains("matrix-appservice-irc")
|
||||||
|
|| appservice
|
||||||
|
.registration
|
||||||
|
.id
|
||||||
|
.contains("matrix_appservice_irc")
|
||||||
|
});
|
||||||
|
|
||||||
|
if services
|
||||||
|
.globals
|
||||||
|
.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 {
|
||||||
|
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 { 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
|
||||||
|
#[allow(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();
|
||||||
|
|
||||||
|
if !services.config.allow_registration && body.appservice_info.is_none() {
|
||||||
|
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
||||||
|
| (Some(username), Some(device_display_name)) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
user = %username,
|
||||||
|
device_name = %device_display_name,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| (Some(username), _) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
user = %username,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| (_, Some(device_display_name)) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
device_name = %device_display_name,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| (None, _) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err!(Request(Forbidden("Registration has been disabled.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_guest
|
||||||
|
&& (!services.config.allow_guest_registration
|
||||||
|
|| (services.config.allow_registration
|
||||||
|
&& services.globals.registration_token.is_some()))
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"Guest registration disabled / registration enabled with token configured, \
|
||||||
|
rejecting guest registration attempt, initial device name: \"{}\"",
|
||||||
|
body.initial_device_display_name
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
);
|
||||||
|
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// forbid guests from registering if there is not a real admin user yet. give
|
||||||
|
// generic user error.
|
||||||
|
if is_guest && services.users.count().await < 2 {
|
||||||
|
warn!(
|
||||||
|
"Guest account attempted to register before a real admin user has been registered, \
|
||||||
|
rejecting registration. Guest's initial device name: \"{}\"",
|
||||||
|
body.initial_device_display_name
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
);
|
||||||
|
return Err!(Request(Forbidden("Registration is temporarily 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
|
||||||
|
.globals
|
||||||
|
.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.registration_token.is_some() {
|
||||||
|
// Registration token required
|
||||||
|
uiaainfo = UiaaInfo {
|
||||||
|
flows: vec![AuthFlow {
|
||||||
|
stages: vec![AuthType::RegistrationToken],
|
||||||
|
}],
|
||||||
|
completed: Vec::new(),
|
||||||
|
params: Box::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: Box::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() };
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.create(&user_id, password, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Default to pretty displayname
|
||||||
|
let mut displayname = user_id.localpart().to_owned();
|
||||||
|
|
||||||
|
// If `new_user_displayname_suffix` is set, registration will push whatever
|
||||||
|
// content is set to the user's display name with a space before it
|
||||||
|
if !services
|
||||||
|
.globals
|
||||||
|
.new_user_displayname_suffix()
|
||||||
|
.is_empty()
|
||||||
|
&& body.appservice_info.is_none()
|
||||||
|
{
|
||||||
|
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.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 {
|
||||||
|
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(),
|
||||||
|
Some(client.to_string()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug_info!(%user_id, %device_id, "User account was created");
|
||||||
|
|
||||||
|
let device_display_name = body
|
||||||
|
.initial_device_display_name
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
// log in conduit admin channel if a non-guest user registered
|
||||||
|
if body.appservice_info.is_none() && !is_guest {
|
||||||
|
if !device_display_name.is_empty() {
|
||||||
|
let notice = format!(
|
||||||
|
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||||
|
display name \"{device_display_name}\""
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("{notice}");
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services.admin.notice(¬ice).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||||
|
|
||||||
|
info!("{notice}");
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services.admin.notice(¬ice).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// log in conduit admin channel if a guest registered
|
||||||
|
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||||
|
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||||
|
|
||||||
|
if !device_display_name.is_empty() {
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.notice(&format!(
|
||||||
|
"Guest user \"{user_id}\" with device display name \
|
||||||
|
\"{device_display_name}\" registered on this server from IP {client}"
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#[allow(clippy::collapsible_else_if)]
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.notice(&format!(
|
||||||
|
"Guest user \"{user_id}\" with no device display name registered on \
|
||||||
|
this server from IP {client}",
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the first real user, grant them admin privileges except for guest
|
||||||
|
// users
|
||||||
|
// Note: the server user is generated first
|
||||||
|
if !is_guest {
|
||||||
|
if let Ok(admin_room) = services.admin.get_admin_room().await {
|
||||||
|
if services
|
||||||
|
.rooms
|
||||||
|
.state_cache
|
||||||
|
.room_joined_count(&admin_room)
|
||||||
|
.await
|
||||||
|
.is_ok_and(is_equal_to!(1))
|
||||||
|
{
|
||||||
|
services.admin.make_user_admin(&user_id).await?;
|
||||||
|
warn!("Granting {user_id} admin privileges as the first user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.appservice_info.is_none()
|
||||||
|
&& !services.server.config.auto_join_rooms.is_empty()
|
||||||
|
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||||
|
{
|
||||||
|
for room in &services.server.config.auto_join_rooms {
|
||||||
|
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||||
|
error!(
|
||||||
|
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||||
|
{room}, skipping"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !services
|
||||||
|
.rooms
|
||||||
|
.state_cache
|
||||||
|
.server_in_room(services.globals.server_name(), &room_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"Skipping room {room} to automatically join as we have never joined before."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(room_server_name) = room.server_name() {
|
||||||
|
match join_room_by_id_helper(
|
||||||
|
&services,
|
||||||
|
&user_id,
|
||||||
|
&room_id,
|
||||||
|
Some("Automatically joining this room upon registration".to_owned()),
|
||||||
|
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||||
|
None,
|
||||||
|
&body.appservice_info,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Err(e) => {
|
||||||
|
// don't return this error so we don't fail registrations
|
||||||
|
error!(
|
||||||
|
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| _ => {
|
||||||
|
info!("Automatically joined room {room} for user {user_id}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(register::v3::Response {
|
||||||
|
access_token: Some(token),
|
||||||
|
user_id,
|
||||||
|
device_id: Some(device_id),
|
||||||
|
refresh_token: None,
|
||||||
|
expires_in: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `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 Some(reg_token) = services.globals.registration_token.clone() else {
|
||||||
|
return Err!(Request(Forbidden("Server does not allow token registration")));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user