From 7115fb2796f4b3756432295220fcde7d26ebd5f5 Mon Sep 17 00:00:00 2001 From: dasha_uwu Date: Fri, 5 Dec 2025 14:00:28 +0500 Subject: [PATCH] Refactor join, alias services Split knock, user register from api into services Fix autojoin not working with v12 rooms Fix 'm.login.registration_token/validity' for reloaded registration tokens Change join servers order Move autojoin for ldap --- src/admin/user/commands.rs | 170 +------ src/admin/user/mod.rs | 6 +- src/api/client/alias.rs | 24 +- src/api/client/device.rs | 10 +- src/api/client/membership/invite.rs | 3 +- src/api/client/membership/join.rs | 34 +- src/api/client/membership/knock.rs | 581 +--------------------- src/api/client/membership/mod.rs | 191 ++----- src/api/client/mod.rs | 3 - src/api/client/register.rs | 292 ++--------- src/api/client/room/summary.rs | 2 +- src/api/client/session/ldap.rs | 65 +-- src/api/client/session/mod.rs | 52 +- src/api/client/voip.rs | 2 +- src/core/config/mod.rs | 2 +- src/service/globals/mod.rs | 66 +-- src/service/membership/join.rs | 104 +++- src/service/membership/knock.rs | 655 +++++++++++++++++++++++++ src/service/membership/mod.rs | 1 + src/service/rooms/alias/mod.rs | 4 +- src/service/uiaa/mod.rs | 34 +- src/service/users/dehydrated_device.rs | 4 +- src/service/users/device.rs | 19 +- src/service/users/mod.rs | 7 +- src/service/users/register.rs | 156 ++++++ 25 files changed, 1153 insertions(+), 1334 deletions(-) create mode 100644 src/service/membership/knock.rs create mode 100644 src/service/users/register.rs diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index b2feff10..9b4eea69 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, fmt::Write as _}; +use std::collections::BTreeMap; use futures::{FutureExt, StreamExt}; use ruma::{ @@ -13,10 +13,9 @@ use ruma::{ }, }; use tuwunel_core::{ - Err, Result, debug, debug_warn, error, info, is_equal_to, + Err, Result, debug_warn, info, matrix::{Event, pdu::PduBuilder}, utils::{self, ReadyExt}, - warn, }; use tuwunel_service::Services; @@ -62,148 +61,11 @@ pub(super) async fn create_user(&self, username: String, password: Option { - info!("Automatically joined room {room} for user {user_id}"); - }, - | 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}" - ); - self.services - .admin - .send_text(&format!( - "Failed to automatically join room {room} for user {user_id}: \ - {e}" - )) - .await; - }, - } - - drop(state_lock); - } - } - } - - // we dont add a device since we're not the user, just the creator - - // if this account creation is from the CLI / --execute, invite the first user - // to admin room - if let Ok(admin_room) = self.services.admin.get_admin_room().await { - if self - .services - .state_cache - .room_joined_count(&admin_room) - .await - .is_ok_and(is_equal_to!(1)) - { - self.services - .admin - .make_user_admin(&user_id) - .boxed() - .await?; - warn!("Granting {user_id} admin privileges as the first user"); - } - } else { - debug!("create_user admin command called without an admin room being available"); - } - self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`")) .await } @@ -403,7 +265,7 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result { #[admin_command] pub(super) async fn force_join_list_of_local_users( &self, - room_id: OwnedRoomOrAliasId, + room: OwnedRoomOrAliasId, yes_i_want_to_do_this: bool, ) -> Result { if self.body.len() < 2 @@ -415,7 +277,7 @@ pub(super) async fn force_join_list_of_local_users( if !yes_i_want_to_do_this { return Err!( - "You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force \ + "You must pass the --yes-i-want-to-do-this flag to ensure you really want to force \ bulk join all specified local users.", ); } @@ -427,7 +289,7 @@ pub(super) async fn force_join_list_of_local_users( let (room_id, servers) = self .services .alias - .maybe_resolve_with_servers(&room_id, None) + .maybe_resolve_with_servers(&room, None) .await?; if !self @@ -505,9 +367,10 @@ pub(super) async fn force_join_list_of_local_users( .join( &user_id, &room_id, + Some(&room), Some(String::from(BULK_JOIN_REASON)), &servers, - &None, + false, &state_lock, ) .await @@ -534,7 +397,7 @@ pub(super) async fn force_join_list_of_local_users( #[admin_command] pub(super) async fn force_join_all_local_users( &self, - room_id: OwnedRoomOrAliasId, + room: OwnedRoomOrAliasId, yes_i_want_to_do_this: bool, ) -> Result { if !yes_i_want_to_do_this { @@ -551,7 +414,7 @@ pub(super) async fn force_join_all_local_users( let (room_id, servers) = self .services .alias - .maybe_resolve_with_servers(&room_id, None) + .maybe_resolve_with_servers(&room, None) .await?; if !self @@ -600,9 +463,10 @@ pub(super) async fn force_join_all_local_users( .join( user_id, &room_id, + Some(&room), Some(String::from(BULK_JOIN_REASON)), &servers, - &None, + false, &state_lock, ) .await @@ -627,16 +491,12 @@ pub(super) async fn force_join_all_local_users( } #[admin_command] -pub(super) async fn force_join_room( - &self, - user_id: String, - room_id: OwnedRoomOrAliasId, -) -> Result { +pub(super) async fn force_join_room(&self, user_id: String, room: OwnedRoomOrAliasId) -> Result { let user_id = parse_local_user_id(self.services, &user_id)?; let (room_id, servers) = self .services .alias - .maybe_resolve_with_servers(&room_id, None) + .maybe_resolve_with_servers(&room, None) .await?; assert!( @@ -648,7 +508,7 @@ pub(super) async fn force_join_room( self.services .membership - .join(&user_id, &room_id, None, &servers, &None, &state_lock) + .join(&user_id, &room_id, Some(&room), None, &servers, false, &state_lock) .await?; drop(state_lock); diff --git a/src/admin/user/mod.rs b/src/admin/user/mod.rs index 9e571e14..fcbc8cd8 100644 --- a/src/admin/user/mod.rs +++ b/src/admin/user/mod.rs @@ -78,7 +78,7 @@ pub(super) enum UserCommand { /// - Manually join a local user to a room. ForceJoinRoom { user_id: String, - room_id: OwnedRoomOrAliasId, + room: OwnedRoomOrAliasId, }, /// - Manually leave a local user from a room. @@ -148,7 +148,7 @@ pub(super) enum UserCommand { /// /// Requires the `--yes-i-want-to-do-this` flag. ForceJoinListOfLocalUsers { - room_id: OwnedRoomOrAliasId, + room: OwnedRoomOrAliasId, #[arg(long)] yes_i_want_to_do_this: bool, @@ -160,7 +160,7 @@ pub(super) enum UserCommand { /// /// Requires the `--yes-i-want-to-do-this` flag. ForceJoinAllLocalUsers { - room_id: OwnedRoomOrAliasId, + room: OwnedRoomOrAliasId, #[arg(long)] yes_i_want_to_do_this: bool, diff --git a/src/api/client/alias.rs b/src/api/client/alias.rs index 7976d347..3db04bda 100644 --- a/src/api/client/alias.rs +++ b/src/api/client/alias.rs @@ -121,26 +121,16 @@ async fn room_available_servers( // insert our server as the very first choice if in list, else check if we can // prefer the room alias server first - match servers + if let Some(server_index) = servers .iter() .position(|server_name| services.globals.server_is_ours(server_name)) { - | Some(server_index) => { - servers.swap_remove(server_index); - servers.insert(0, services.globals.server_name().to_owned()); - }, - | _ => { - match servers - .iter() - .position(|server| server == room_alias.server_name()) - { - | Some(alias_server_index) => { - servers.swap_remove(alias_server_index); - servers.insert(0, room_alias.server_name().into()); - }, - | _ => {}, - } - }, + servers.swap(0, server_index); + } else if let Some(alias_server_index) = servers + .iter() + .position(|server| server == room_alias.server_name()) + { + servers.swap(0, alias_server_index); } servers diff --git a/src/api/client/device.rs b/src/api/client/device.rs index 67f4f46e..4dc58a97 100644 --- a/src/api/client/device.rs +++ b/src/api/client/device.rs @@ -2,14 +2,14 @@ use axum::extract::State; use axum_client_ip::InsecureClientIp; use futures::StreamExt; use ruma::{ - MilliSecondsSinceUnixEpoch, OwnedDeviceId, + MilliSecondsSinceUnixEpoch, api::client::device::{ self, delete_device, delete_devices, get_device, get_devices, update_device, }, }; -use tuwunel_core::{Err, Result, debug, err, utils, utils::string::to_small_string}; +use tuwunel_core::{Err, Result, debug, err, utils::string::to_small_string}; -use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa}; +use crate::{Ruma, router::auth_uiaa}; /// # `GET /_matrix/client/r0/devices` /// @@ -94,13 +94,11 @@ pub(crate) async fn update_device_route( appservice.registration.id ); - let device_id = OwnedDeviceId::from(utils::random_string(DEVICE_ID_LENGTH)); - services .users .create_device( sender_user, - &device_id, + None, (Some(&appservice.registration.as_token), None), None, None, diff --git a/src/api/client/membership/invite.rs b/src/api/client/membership/invite.rs index f39217b0..d01a78c8 100644 --- a/src/api/client/membership/invite.rs +++ b/src/api/client/membership/invite.rs @@ -22,8 +22,7 @@ pub(crate) async fn invite_user_route( invite_check(&services, sender_user, room_id).await?; - banned_room_check(&services, sender_user, Some(room_id), room_id.server_name(), client) - .await?; + banned_room_check(&services, sender_user, room_id, None, client).await?; let invite_user::v3::InvitationRecipient::UserId { user_id } = &body.recipient else { return Err!(Request(ThreepidDenied("Third party identifiers are not implemented"))); diff --git a/src/api/client/membership/join.rs b/src/api/client/membership/join.rs index 8fea6334..c0b464d1 100644 --- a/src/api/client/membership/join.rs +++ b/src/api/client/membership/join.rs @@ -2,13 +2,13 @@ use axum::extract::State; use axum_client_ip::InsecureClientIp; use futures::FutureExt; use ruma::{ - RoomId, RoomOrAliasId, + RoomId, api::client::membership::{join_room_by_id, join_room_by_id_or_alias}, }; use tuwunel_core::{Result, warn}; use super::banned_room_check; -use crate::{Ruma, client::membership::get_join_params}; +use crate::Ruma; /// # `POST /_matrix/client/r0/rooms/{roomId}/join` /// @@ -28,23 +28,20 @@ pub(crate) async fn join_room_by_id_route( let room_id: &RoomId = &body.room_id; - banned_room_check(&services, sender_user, Some(room_id), room_id.server_name(), client) - .await?; + banned_room_check(&services, sender_user, room_id, None, client).await?; - let (room_id, servers) = - get_join_params(&services, sender_user, <&RoomOrAliasId>::from(room_id), &[]).await?; - - let state_lock = services.state.mutex.lock(&room_id).await; + let state_lock = services.state.mutex.lock(room_id).await; let mut errors = 0_usize; while let Err(e) = services .membership .join( sender_user, - &room_id, + room_id, + None, body.reason.clone(), - &servers, - &body.appservice_info, + &[], + body.appservice_info.is_some(), &state_lock, ) .boxed() @@ -62,7 +59,7 @@ pub(crate) async fn join_room_by_id_route( drop(state_lock); - Ok(join_room_by_id::v3::Response { room_id }) + Ok(join_room_by_id::v3::Response { room_id: room_id.to_owned() }) } /// # `POST /_matrix/client/r0/join/{roomIdOrAlias}` @@ -83,10 +80,12 @@ pub(crate) async fn join_room_by_id_or_alias_route( let sender_user = body.sender_user(); let appservice_info = &body.appservice_info; - let (room_id, servers) = - get_join_params(&services, sender_user, &body.room_id_or_alias, &body.via).await?; + let (room_id, servers) = services + .alias + .maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via)) + .await?; - banned_room_check(&services, sender_user, Some(&room_id), room_id.server_name(), client) + banned_room_check(&services, sender_user, &room_id, Some(&body.room_id_or_alias), client) .await?; let state_lock = services.state.mutex.lock(&room_id).await; @@ -97,9 +96,10 @@ pub(crate) async fn join_room_by_id_or_alias_route( .join( sender_user, &room_id, + Some(&body.room_id_or_alias), body.reason.clone(), &servers, - appservice_info, + appservice_info.is_some(), &state_lock, ) .boxed() @@ -117,5 +117,5 @@ pub(crate) async fn join_room_by_id_or_alias_route( drop(state_lock); - Ok(join_room_by_id_or_alias::v3::Response { room_id }) + Ok(join_room_by_id_or_alias::v3::Response { room_id: room_id.clone() }) } diff --git a/src/api/client/membership/knock.rs b/src/api/client/membership/knock.rs index ff72dd9f..af20f7df 100644 --- a/src/api/client/membership/knock.rs +++ b/src/api/client/membership/knock.rs @@ -1,45 +1,10 @@ -use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc}; - use axum::extract::State; use axum_client_ip::InsecureClientIp; -use futures::{FutureExt, StreamExt}; -use ruma::{ - CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, RoomId, - RoomVersionId, UserId, - api::{ - client::knock::knock_room, - federation::{ - membership::RawStrippedState, - {self}, - }, - }, - canonical_json::to_canonical_value, - events::{ - StateEventType, - room::member::{MembershipState, RoomMemberEventContent}, - }, -}; -use tuwunel_core::{ - Err, Result, debug, debug_info, debug_warn, err, extract_variant, info, - matrix::{ - PduCount, - event::{Event, gen_event_id}, - pdu::{PduBuilder, PduEvent}, - }, - trace, - utils::{self}, - warn, -}; -use tuwunel_service::{ - Services, - rooms::{ - state::RoomMutexGuard, - state_compressor::{CompressedState, HashSetCompressStateEvent}, - }, -}; +use ruma::api::client::knock::knock_room; +use tuwunel_core::Result; use super::banned_room_check; -use crate::{Ruma, client::membership::get_join_params}; +use crate::Ruma; /// # `POST /_matrix/client/*/knock/{roomIdOrAlias}` /// @@ -51,542 +16,30 @@ pub(crate) async fn knock_room_route( body: Ruma, ) -> Result { let sender_user = body.sender_user(); - let body = &body.body; - let (room_id, servers) = - get_join_params(&services, sender_user, &body.room_id_or_alias, &body.via).await?; - - banned_room_check(&services, sender_user, Some(&room_id), room_id.server_name(), client) + let (room_id, servers) = services + .alias + .maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via)) .await?; - knock_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers).await -} - -async fn knock_room_by_id_helper( - services: &Services, - sender_user: &UserId, - room_id: &RoomId, - reason: Option, - servers: &[OwnedServerName], -) -> Result { - let state_lock = services.state.mutex.lock(room_id).await; - - if services - .state_cache - .is_invited(sender_user, room_id) - .await - { - debug_warn!("{sender_user} is already invited in {room_id} but attempted to knock"); - return Err!(Request(Forbidden( - "You cannot knock on a room you are already invited/accepted to." - ))); - } - - if services - .state_cache - .is_joined(sender_user, room_id) - .await - { - debug_warn!("{sender_user} is already joined in {room_id} but attempted to knock"); - return Err!(Request(Forbidden("You cannot knock on a room you are already joined in."))); - } - - if services - .state_cache - .is_knocked(sender_user, room_id) - .await - { - debug_warn!("{sender_user} is already knocked in {room_id}"); - return Ok(knock_room::v3::Response { room_id: room_id.into() }); - } - - if let Ok(membership) = services - .state_accessor - .get_member(room_id, sender_user) - .await - { - if membership.membership == MembershipState::Ban { - debug_warn!("{sender_user} is banned from {room_id} but attempted to knock"); - return Err!(Request(Forbidden("You cannot knock on a room you are banned from."))); - } - } - - let server_in_room = services - .state_cache - .server_in_room(services.globals.server_name(), room_id) - .await; - - let local_knock = server_in_room - || servers.is_empty() - || (servers.len() == 1 && services.globals.server_is_ours(&servers[0])); - - if local_knock { - knock_room_helper_local(services, sender_user, room_id, reason, servers, state_lock) - .boxed() - .await?; - } else { - knock_room_helper_remote(services, sender_user, room_id, reason, servers, state_lock) - .boxed() - .await?; - } - - Ok(knock_room::v3::Response::new(room_id.to_owned())) -} - -async fn knock_room_helper_local( - services: &Services, - sender_user: &UserId, - room_id: &RoomId, - reason: Option, - servers: &[OwnedServerName], - state_lock: RoomMutexGuard, -) -> Result { - debug_info!("We can knock locally"); - - let room_version_id = services.state.get_room_version(room_id).await?; - - if matches!( - room_version_id, - RoomVersionId::V1 - | RoomVersionId::V2 - | RoomVersionId::V3 - | RoomVersionId::V4 - | RoomVersionId::V5 - | RoomVersionId::V6 - ) { - return Err!(Request(Forbidden("This room does not support knocking."))); - } - - let content = RoomMemberEventContent { - displayname: services.users.displayname(sender_user).await.ok(), - avatar_url: services.users.avatar_url(sender_user).await.ok(), - blurhash: services.users.blurhash(sender_user).await.ok(), - reason: reason.clone(), - ..RoomMemberEventContent::new(MembershipState::Knock) - }; - - // Try normal knock first - let Err(error) = services - .timeline - .build_and_append_pdu( - PduBuilder::state(sender_user.to_string(), &content), - sender_user, - room_id, - &state_lock, - ) - .await - else { - return Ok(()); - }; - - if servers.is_empty() || (servers.len() == 1 && services.globals.server_is_ours(&servers[0])) - { - return Err(error); - } - - warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock"); - - let (make_knock_response, remote_server) = - make_knock_request(services, sender_user, room_id, servers).await?; - - info!("make_knock finished"); - - let room_version_id = make_knock_response.room_version; - - if !services - .server - .supported_room_version(&room_version_id) - { - return Err!(BadServerResponse( - "Remote room version {room_version_id} is not supported by tuwunel" - )); - } - - let mut knock_event_stub = serde_json::from_str::( - make_knock_response.event.get(), - ) - .map_err(|e| { - err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}")) - })?; - - knock_event_stub.insert( - "origin".into(), - CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()), - ); - knock_event_stub.insert( - "origin_server_ts".into(), - CanonicalJsonValue::Integer( - utils::millis_since_unix_epoch() - .try_into() - .expect("Timestamp is valid js_int value"), - ), - ); - knock_event_stub.insert( - "content".into(), - to_canonical_value(RoomMemberEventContent { - displayname: services.users.displayname(sender_user).await.ok(), - avatar_url: services.users.avatar_url(sender_user).await.ok(), - blurhash: services.users.blurhash(sender_user).await.ok(), - reason, - ..RoomMemberEventContent::new(MembershipState::Knock) - }) - .expect("event is valid, we just created it"), - ); - - // In order to create a compatible ref hash (EventID) the `hashes` field needs - // to be present - services - .server_keys - .hash_and_sign_event(&mut knock_event_stub, &room_version_id)?; - - // Generate event id - let event_id = gen_event_id(&knock_event_stub, &room_version_id)?; - - // Add event_id - knock_event_stub - .insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into())); - - // It has enough fields to be called a proper event now - let knock_event = knock_event_stub; - - info!("Asking {remote_server} for send_knock in room {room_id}"); - let send_knock_request = federation::membership::create_knock_event::v1::Request { - room_id: room_id.to_owned(), - event_id: event_id.clone(), - pdu: services - .federation - .format_pdu_into(knock_event.clone(), Some(&room_version_id)) - .await, - }; - - let send_knock_response = services - .federation - .execute(&remote_server, send_knock_request) + banned_room_check(&services, sender_user, &room_id, Some(&body.room_id_or_alias), client) .await?; - info!("send_knock finished"); + let state_lock = services.state.mutex.lock(&room_id).await; services - .short - .get_or_create_shortroomid(room_id) - .await; - - info!("Parsing knock event"); - - let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone()) - .map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?; - - info!("Updating membership locally to knock state with provided stripped state events"); - let count = services.globals.next_count(); - services - .state_cache - .update_membership( - room_id, + .membership + .knock( sender_user, - parsed_knock_pdu - .get_content::() - .expect("we just created this"), - sender_user, - Some( - send_knock_response - .knock_room_state - .into_iter() - .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped)) - .collect(), - ), - None, - false, - PduCount::Normal(*count), - ) - .await?; - - info!("Appending room knock event locally"); - services - .timeline - .append_pdu( - &parsed_knock_pdu, - knock_event, - once(parsed_knock_pdu.event_id.borrow()), + &room_id, + Some(&body.room_id_or_alias), + body.reason.clone(), + &servers, &state_lock, ) .await?; - Ok(()) -} - -async fn knock_room_helper_remote( - services: &Services, - sender_user: &UserId, - room_id: &RoomId, - reason: Option, - servers: &[OwnedServerName], - state_lock: RoomMutexGuard, -) -> Result { - info!("Knocking {room_id} over federation."); - - let (make_knock_response, remote_server) = - make_knock_request(services, sender_user, room_id, servers).await?; - - info!("make_knock finished"); - - let room_version_id = make_knock_response.room_version; - - if !services - .server - .supported_room_version(&room_version_id) - { - return Err!(BadServerResponse( - "Remote room version {room_version_id} is not supported by tuwunel" - )); - } - - let mut knock_event_stub: CanonicalJsonObject = - serde_json::from_str(make_knock_response.event.get()).map_err(|e| { - err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}")) - })?; - - knock_event_stub.insert( - "origin".into(), - CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()), - ); - knock_event_stub.insert( - "origin_server_ts".into(), - CanonicalJsonValue::Integer( - utils::millis_since_unix_epoch() - .try_into() - .expect("Timestamp is valid js_int value"), - ), - ); - knock_event_stub.insert( - "content".into(), - to_canonical_value(RoomMemberEventContent { - displayname: services.users.displayname(sender_user).await.ok(), - avatar_url: services.users.avatar_url(sender_user).await.ok(), - blurhash: services.users.blurhash(sender_user).await.ok(), - reason, - ..RoomMemberEventContent::new(MembershipState::Knock) - }) - .expect("event is valid, we just created it"), - ); - - // In order to create a compatible ref hash (EventID) the `hashes` field needs - // to be present - services - .server_keys - .hash_and_sign_event(&mut knock_event_stub, &room_version_id)?; - - // Generate event id - let event_id = gen_event_id(&knock_event_stub, &room_version_id)?; - - // Add event_id - knock_event_stub - .insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into())); - - // It has enough fields to be called a proper event now - let knock_event = knock_event_stub; - - info!("Asking {remote_server} for send_knock in room {room_id}"); - let send_knock_request = federation::membership::create_knock_event::v1::Request { - room_id: room_id.to_owned(), - event_id: event_id.clone(), - pdu: services - .federation - .format_pdu_into(knock_event.clone(), Some(&room_version_id)) - .await, - }; - - let send_knock_response = services - .federation - .execute(&remote_server, send_knock_request) - .await?; - - info!("send_knock finished"); - - services - .short - .get_or_create_shortroomid(room_id) - .await; - - info!("Parsing knock event"); - let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone()) - .map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?; - - info!("Going through send_knock response knock state events"); - let state = send_knock_response - .knock_room_state - .iter() - .map(|event| { - serde_json::from_str::( - extract_variant!(event.clone(), RawStrippedState::Stripped) - .expect("Raw") - .json() - .get(), - ) - }) - .filter_map(Result::ok); - - let mut state_map: HashMap = HashMap::new(); - - for event in state { - let Some(state_key) = event.get("state_key") else { - debug_warn!("send_knock stripped state event missing state_key: {event:?}"); - continue; - }; - let Some(event_type) = event.get("type") else { - debug_warn!("send_knock stripped state event missing event type: {event:?}"); - continue; - }; - - let Ok(state_key) = serde_json::from_value::(state_key.clone().into()) else { - debug_warn!("send_knock stripped state event has invalid state_key: {event:?}"); - continue; - }; - let Ok(event_type) = serde_json::from_value::(event_type.clone().into()) - else { - debug_warn!("send_knock stripped state event has invalid event type: {event:?}"); - continue; - }; - - let event_id = gen_event_id(&event, &room_version_id)?; - let shortstatekey = services - .short - .get_or_create_shortstatekey(&event_type, &state_key) - .await; - - services - .timeline - .add_pdu_outlier(&event_id, &event); - - state_map.insert(shortstatekey, event_id.clone()); - } - - info!("Compressing state from send_knock"); - let compressed: CompressedState = services - .state_compressor - .compress_state_events( - state_map - .iter() - .map(|(ssk, eid)| (ssk, eid.borrow())), - ) - .collect() - .await; - - debug!("Saving compressed state"); - let HashSetCompressStateEvent { - shortstatehash: statehash_before_knock, - added, - removed, - } = services - .state_compressor - .save_state(room_id, Arc::new(compressed)) - .await?; - - debug!("Forcing state for new room"); - services - .state - .force_state(room_id, statehash_before_knock, added, removed, &state_lock) - .await?; - - let statehash_after_knock = services - .state - .append_to_state(&parsed_knock_pdu) - .await?; - - info!("Updating membership locally to knock state with provided stripped state events"); - let count = services.globals.next_count(); - services - .state_cache - .update_membership( - room_id, - sender_user, - parsed_knock_pdu - .get_content::() - .expect("we just created this"), - sender_user, - Some( - send_knock_response - .knock_room_state - .into_iter() - .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped)) - .collect(), - ), - None, - false, - PduCount::Normal(*count), - ) - .await?; - - info!("Appending room knock event locally"); - services - .timeline - .append_pdu( - &parsed_knock_pdu, - knock_event, - once(parsed_knock_pdu.event_id.borrow()), - &state_lock, - ) - .await?; - - info!("Setting final room state for new room"); - // We set the room state after inserting the pdu, so that we never have a moment - // in time where events in the current room state do not exist - services - .state - .set_room_state(room_id, statehash_after_knock, &state_lock); - - Ok(()) -} - -async fn make_knock_request( - services: &Services, - sender_user: &UserId, - room_id: &RoomId, - servers: &[OwnedServerName], -) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> { - let mut make_knock_response_and_server = - Err!(BadServerResponse("No server available to assist in knocking.")); - - let mut make_knock_counter: usize = 0; - - for remote_server in servers { - if services.globals.server_is_ours(remote_server) { - continue; - } - - info!("Asking {remote_server} for make_knock ({make_knock_counter})"); - - let make_knock_response = services - .federation - .execute(remote_server, federation::membership::prepare_knock_event::v1::Request { - room_id: room_id.to_owned(), - user_id: sender_user.to_owned(), - ver: services - .server - .supported_room_versions() - .collect(), - }) - .await; - - trace!("make_knock response: {make_knock_response:?}"); - make_knock_counter = make_knock_counter.saturating_add(1); - - make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone())); - - if make_knock_response_and_server.is_ok() { - break; - } - - if make_knock_counter > 40 { - warn!( - "50 servers failed to provide valid make_knock response, assuming no server can \ - assist in knocking." - ); - make_knock_response_and_server = - Err!(BadServerResponse("No server available to assist in knocking.")); - - return make_knock_response_and_server; - } - } - - make_knock_response_and_server + drop(state_lock); + + Ok(knock_room::v3::Response::new(room_id.clone())) } diff --git a/src/api/client/membership/mod.rs b/src/api/client/membership/mod.rs index 22143f52..877e84d4 100644 --- a/src/api/client/membership/mod.rs +++ b/src/api/client/membership/mod.rs @@ -8,15 +8,12 @@ mod leave; mod members; mod unban; -use std::{cmp::Ordering, net::IpAddr}; +use std::net::IpAddr; use axum::extract::State; use futures::{FutureExt, StreamExt}; -use ruma::{ - OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName, UserId, - api::client::membership::joined_rooms, -}; -use tuwunel_core::{Err, Result, result::LogErr, utils::shuffle, warn}; +use ruma::{RoomId, RoomOrAliasId, UserId, api::client::membership::joined_rooms}; +use tuwunel_core::{Err, Result, result::LogErr, warn}; use tuwunel_service::Services; pub(crate) use self::{ @@ -58,57 +55,42 @@ pub(crate) async fn joined_rooms_route( pub(crate) async fn banned_room_check( services: &Services, user_id: &UserId, - room_id: Option<&RoomId>, - server_name: Option<&ServerName>, + room_id: &RoomId, + orig_room_id: Option<&RoomOrAliasId>, client_ip: IpAddr, ) -> Result { if services.users.is_admin(user_id).await { return Ok(()); } - // TODO: weird condition - if let Some(room_id) = room_id { - if services.metadata.is_banned(room_id).await - || (room_id.server_name().is_some() - && services - .config - .forbidden_remote_server_names - .is_match( - room_id - .server_name() - .expect("legacy room mxid") - .host(), - )) { - warn!( - "User {user_id} who is not an admin attempted to send an invite for or \ - attempted to join a banned room or banned room server name: {room_id}" - ); + // room id is banned ... + if services.metadata.is_banned(room_id).await + // ... or legacy room id server is banned ... + || room_id.server_name().is_some_and(|server_name| { + services + .config + .forbidden_remote_server_names + .is_match(server_name.host()) + }) + // ... or alias server is banned + || orig_room_id.is_some_and(|orig_room_id| { + orig_room_id.server_name().is_some_and(|orig_server_name| + services + .config + .forbidden_remote_server_names + .is_match(orig_server_name.host())) + }) { + warn!( + "User {user_id} who is not an admin attempted to send an invite for or attempted to \ + join a banned room or banned room server name: {room_id}" + ); - maybe_deactivate(services, user_id, client_ip) - .await - .log_err() - .ok(); + maybe_deactivate(services, user_id, client_ip) + .await + .log_err() + .ok(); - return Err!(Request(Forbidden("This room is banned on this homeserver."))); - } - } else if let Some(server_name) = server_name { - if services - .config - .forbidden_remote_server_names - .is_match(server_name.host()) - { - warn!( - "User {user_id} who is not an admin tried joining a room which has the server \ - name {server_name} that is globally forbidden. Rejecting.", - ); - - maybe_deactivate(services, user_id, client_ip) - .await - .log_err() - .ok(); - - return Err!(Request(Forbidden("This remote server is banned on this homeserver."))); - } + return Err!(Request(Forbidden("This room is banned on this homeserver."))); } Ok(()) @@ -120,16 +102,15 @@ async fn maybe_deactivate(services: &Services, user_id: &UserId, client_ip: IpAd .config .auto_deactivate_banned_room_attempts { - warn!("Automatically deactivating user {user_id} due to attempted banned room join"); + let notice = format!( + "Automatically deactivating user {user_id} due to attempted banned room join from \ + IP {client_ip}" + ); + + warn!("{notice}"); if services.server.config.admin_room_notices { - services - .admin - .send_text(&format!( - "Automatically deactivating user {user_id} due to attempted banned room \ - join from IP {client_ip}" - )) - .await; + services.admin.send_text(¬ice).await; } services @@ -141,99 +122,3 @@ async fn maybe_deactivate(services: &Services, user_id: &UserId, client_ip: IpAd Ok(()) } - -// TODO: should this be in services? banned check would have to resolve again if -// room_id is not available at callsite -async fn get_join_params( - services: &Services, - user_id: &UserId, - room_id_or_alias: &RoomOrAliasId, - via: &[OwnedServerName], -) -> Result<(OwnedRoomId, Vec)> { - // servers tried first, additional_servers shuffled then tried after - let (room_id, mut primary_servers, mut additional_servers) = - match OwnedRoomId::try_from(room_id_or_alias.to_owned()) { - // if room id, shuffle via + room_id server_name ... - | Ok(room_id) => { - let mut additional_servers = via.to_vec(); - - if let Some(server) = room_id.server_name() { - additional_servers.push(server.to_owned()); - } - - (room_id, Vec::new(), additional_servers) - }, - // ... if room alias, resolve and don't shuffle ... - | Err(room_alias) => { - let (room_id, servers) = services.alias.resolve_alias(&room_alias).await?; - - (room_id, servers, Vec::new()) - }, - }; - - // either way, add invited vias - additional_servers.extend( - services - .state_cache - .servers_invite_via(&room_id) - .map(ToOwned::to_owned) - .collect::>() - .await, - ); - - // either way, add invite senders' servers - additional_servers.extend( - services - .state_cache - .invite_state(user_id, &room_id) - .await - .unwrap_or_default() - .iter() - .filter_map(|event| event.get_field("sender").ok().flatten()) - .filter_map(|sender: &str| UserId::parse(sender).ok()) - .map(|user| user.server_name().to_owned()), - ); - - primary_servers.sort_unstable(); - primary_servers.dedup(); - shuffle(&mut primary_servers); - - // shuffle additionals, append to base servers - additional_servers.sort_unstable(); - additional_servers.dedup(); - shuffle(&mut additional_servers); - - let mut servers: Vec<_> = room_id_or_alias - .server_name() - .filter(|_| room_id_or_alias.is_room_alias_id()) - .map(ToOwned::to_owned) - .into_iter() - .chain(primary_servers.into_iter()) - .chain(additional_servers.into_iter()) - .collect(); - - // sort deprioritized servers last - servers.sort_by(|a, b| { - let a_matches = services - .server - .config - .deprioritize_joins_through_servers - .is_match(a.host()); - - let b_matches = services - .server - .config - .deprioritize_joins_through_servers - .is_match(b.host()); - - if a_matches && !b_matches { - Ordering::Greater - } else if !a_matches && b_matches { - Ordering::Less - } else { - Ordering::Equal - } - }); - - Ok((room_id, servers)) -} diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index 55233bdd..3c7b39e2 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -88,9 +88,6 @@ pub(super) use user_directory::*; pub(super) use voip::*; pub(super) use well_known::*; -/// generated device ID length -const DEVICE_ID_LENGTH: usize = 10; - /// generated user access token length const TOKEN_LENGTH: usize = tuwunel_service::users::device::TOKEN_LENGTH; diff --git a/src/api/client/register.rs b/src/api/client/register.rs index 57a08cc3..85701639 100644 --- a/src/api/client/register.rs +++ b/src/api/client/register.rs @@ -2,7 +2,6 @@ use std::fmt::Write; use axum::extract::State; use axum_client_ip::InsecureClientIp; -use futures::FutureExt; use register::RegistrationKind; use ruma::{ UserId, @@ -13,15 +12,11 @@ use ruma::{ }, uiaa::{AuthFlow, AuthType, UiaaInfo}, }, - events::GlobalAccountDataEventType, - push, -}; -use tuwunel_core::{ - Err, Error, Result, debug_info, debug_warn, error, info, is_equal_to, utils, warn, }; +use tuwunel_core::{Err, Error, Result, debug_info, debug_warn, info, utils}; use tuwunel_service::users::device::generate_refresh_token; -use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH}; +use super::SESSION_ID_LENGTH; use crate::Ruma; const RANDOM_USER_ID_LENGTH: usize = 10; @@ -48,15 +43,10 @@ pub(crate) async fn get_register_available_route( .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") + let id = &appservice.registration.id; + id == "irc" + || id.contains("matrix-appservice-irc") + || id.contains("matrix_appservice_irc") }); if services @@ -148,71 +138,32 @@ pub(crate) async fn register_route( 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() { - 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" - ); - }, - } + 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 { - let display_name = body - .initial_device_display_name - .as_deref() - .unwrap_or(""); - debug_warn!( - "Guest registration disabled / registration enabled with token configured, \ - rejecting guest registration attempt, initial device name: \"{display_name}\"" + %device_name, + "Guest registration disabled, rejecting guest registration attempt" ); 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 { - let display_name = body - .initial_device_display_name - .as_deref() - .unwrap_or(""); - - warn!( - "Guest account attempted to register before a real admin user has been registered, \ - rejecting registration. Guest's initial device name: \"{display_name}\"" - ); - - 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 @@ -313,7 +264,13 @@ pub(crate) async fn register_route( // UIAA let mut uiaainfo; - let skip_auth = if services.globals.registration_token.is_some() && !is_guest { + let skip_auth = if !services + .globals + .get_registration_tokens() + .await + .is_empty() + && !is_guest + { // Registration token required uiaainfo = UiaaInfo { flows: vec![AuthFlow { @@ -378,45 +335,9 @@ pub(crate) async fn register_route( 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 - .config - .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), - }, - })?, - ) + .full_register(&user_id, password, None, body.appservice_info.as_ref(), is_guest, true) .await?; if (!is_guest && body.inhibit_login) @@ -426,169 +347,56 @@ pub(crate) async fn register_route( .is_some_and(|appservice| appservice.registration.device_management) { return Ok(register::v3::Response { - access_token: None, user_id, device_id: None, + access_token: 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()); + 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.body.refresh_token); + .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 - services + let device_id = services .users .create_device( &user_id, - &device_id, + device_id, (Some(&access_token), expires_in), refresh_token.as_deref(), - body.initial_device_display_name.clone(), + body.initial_device_display_name.as_deref(), 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(""); + 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" }); - // 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}\"" - ); + write!(notice, " registered on this server from IP {client}")?; - 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; - } + if let Some(device_name) = body.initial_device_display_name.as_deref() { + write!(notice, " with device name {device_name}")?; } - } - // 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; - } + if !is_guest { + info!("{notice}"); } 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; - } + debug_info!("{notice}"); } - } - // 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 - && services.config.grant_admin_to_first_user - && let Ok(admin_room) = services.admin.get_admin_room().await - && services - .state_cache - .room_joined_count(&admin_room) - .await - .is_ok_and(is_equal_to!(1)) - { - services - .admin - .make_user_admin(&user_id) - .boxed() - .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.alias.maybe_resolve(room).await else { - error!( - "Failed to resolve room alias to room ID when attempting to auto join \ - {room}, skipping" - ); - continue; - }; - - if !services - .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() { - let state_lock = services.state.mutex.lock(&room_id).await; - - match services - .membership - .join( - &user_id, - &room_id, - Some("Automatically joining this room upon registration".to_owned()), - &[services.globals.server_name().to_owned(), room_server_name.to_owned()], - &body.appservice_info, - &state_lock, - ) - .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}"); - }, - } - - drop(state_lock); - } + if services.server.config.admin_room_notices { + services.admin.notice(¬ice).await; } } @@ -611,9 +419,13 @@ pub(crate) async fn check_registration_token_validity( State(services): State, body: Ruma, ) -> Result { - let Some(reg_token) = services.globals.registration_token.clone() else { - return Err!(Request(Forbidden("Server does not allow token registration"))); - }; + let tokens = services.globals.get_registration_tokens().await; - Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token }) + 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 }) } diff --git a/src/api/client/room/summary.rs b/src/api/client/room/summary.rs index 7182ec7f..2d21a692 100644 --- a/src/api/client/room/summary.rs +++ b/src/api/client/room/summary.rs @@ -55,7 +55,7 @@ pub(crate) async fn get_room_summary( ) -> Result { let (room_id, servers) = services .alias - .maybe_resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone())) + .maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via)) .await?; if services.metadata.is_banned(&room_id).await { diff --git a/src/api/client/session/ldap.rs b/src/api/client/session/ldap.rs index 4324fa22..5eabfaf3 100644 --- a/src/api/client/session/ldap.rs +++ b/src/api/client/session/ldap.rs @@ -1,6 +1,6 @@ use futures::FutureExt; use ruma::{OwnedUserId, UserId}; -use tuwunel_core::{Err, Result, debug, error, info, warn}; +use tuwunel_core::{Err, Result, debug}; use tuwunel_service::Services; use super::password_login; @@ -51,69 +51,8 @@ pub(super) async fn ldap_login( if !services.users.exists(lowercased_user_id).await { services .users - .create(lowercased_user_id, Some("*"), Some("ldap")) + .full_register(lowercased_user_id, Some("*"), Some("ldap"), None, false, false) .await?; - - // Auto-join rooms for newly created LDAP users - if !services.server.config.auto_join_rooms.is_empty() { - for room in &services.server.config.auto_join_rooms { - let Ok(room_id) = services.alias.maybe_resolve(room).await else { - error!( - "Failed to resolve room alias to room ID when attempting to auto join \ - {room}, skipping" - ); - continue; - }; - - if !services - .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() { - let state_lock = services.state.mutex.lock(&room_id).await; - - match services - .membership - .join( - lowercased_user_id, - &room_id, - Some("Automatically joining this room upon first login".to_owned()), - &[ - services.globals.server_name().to_owned(), - room_server_name.to_owned(), - ], - &None, - &state_lock, - ) - .boxed() - .await - { - | Err(e) => { - // don't return this error so we don't fail logins - error!( - "Failed to automatically join room {room} for user \ - {lowercased_user_id}: {e}" - ); - }, - | _ => { - info!( - "Automatically joined room {room} for user {lowercased_user_id}" - ); - }, - } - - drop(state_lock); - } - } - } } let is_tuwunel_admin = services diff --git a/src/api/client/session/mod.rs b/src/api/client/session/mod.rs index edb0fe66..9bf0aad2 100644 --- a/src/api/client/session/mod.rs +++ b/src/api/client/session/mod.rs @@ -21,7 +21,7 @@ use ruma::api::client::session::{ v3::{DiscoveryInfo, HomeserverInfo, LoginInfo}, }, }; -use tuwunel_core::{Err, Result, info, utils, utils::stream::ReadyExt}; +use tuwunel_core::{Err, Result, info, utils::stream::ReadyExt}; use tuwunel_service::users::device::generate_refresh_token; use self::{ldap::ldap_login, password::password_login}; @@ -30,7 +30,7 @@ pub(crate) use self::{ refresh::refresh_token_route, token::login_token_route, }; -use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH}; +use super::TOKEN_LENGTH; use crate::Ruma; /// # `GET /_matrix/client/v3/login` @@ -97,43 +97,39 @@ pub(crate) async fn login_route( // Generate a new refresh_token if requested by client let refresh_token = expires_in.is_some().then(generate_refresh_token); - // 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()); - // Determine if device_id was provided and exists in the db for this user - let device_exists = services - .users - .all_device_ids(&user_id) - .ready_any(|v| v == device_id) - .await; - - if !device_exists { - services + let device_id = if let Some(device_id) = &body.device_id + && services .users - .create_device( - &user_id, - &device_id, - (Some(&access_token), expires_in), - refresh_token.as_deref(), - body.initial_device_display_name.clone(), - Some(client.to_string()), - ) - .await?; - } else { + .all_device_ids(&user_id) + .ready_any(|v| v == device_id) + .await + { services .users .set_access_token( &user_id, - &device_id, + device_id, &access_token, expires_in, refresh_token.as_deref(), ) .await?; - } + + device_id.clone() + } else { + services + .users + .create_device( + &user_id, + body.device_id.as_deref(), + (Some(&access_token), expires_in), + refresh_token.as_deref(), + body.initial_device_display_name.as_deref(), + Some(client.to_string()), + ) + .await? + }; info!("{user_id} logged in"); diff --git a/src/api/client/voip.rs b/src/api/client/voip.rs index 69ae11e5..d0c441fe 100644 --- a/src/api/client/voip.rs +++ b/src/api/client/voip.rs @@ -27,7 +27,7 @@ pub(crate) async fn turn_server_route( let turn_secret = &services.globals.turn_secret; - let (username, password) = if !turn_secret.is_empty() { + let (username, password) = if let Some(turn_secret) = turn_secret { let expiry = SecondsSinceUnixEpoch::from_system_time( SystemTime::now() .checked_add(Duration::from_secs(services.config.turn_ttl)) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index db68d1e6..36a7ab6e 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1006,7 +1006,7 @@ pub struct Config { /// /// display: sensitive #[serde(default)] - pub turn_secret: String, + pub turn_secret: Option, /// TURN secret to use that's read from the file path specified. /// diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs index 4637770e..2e4439fc 100644 --- a/src/service/globals/mod.rs +++ b/src/service/globals/mod.rs @@ -1,6 +1,6 @@ mod data; -use std::{ops::Range, sync::Arc}; +use std::{collections::HashSet, ops::Range, sync::Arc}; use data::Data; use ruma::{OwnedUserId, RoomAliasId, ServerName, UserId}; @@ -13,8 +13,7 @@ pub struct Service { server: Arc, pub server_user: OwnedUserId, - pub turn_secret: String, - pub registration_token: Option, + pub turn_secret: Option, } impl crate::Service for Service { @@ -22,32 +21,17 @@ impl crate::Service for Service { let db = Data::new(args); let config = &args.server.config; - let turn_secret = config.turn_secret_file.as_ref().map_or_else( - || config.turn_secret.clone(), - |path| { - std::fs::read_to_string(path).unwrap_or_else(|e| { - error!("Failed to read the TURN secret file: {e}"); - - config.turn_secret.clone() - }) - }, - ); - - let registration_token = config - .registration_token_file + let turn_secret = config + .turn_secret_file .as_ref() - .map_or_else( - || config.registration_token.clone(), - |path| { - let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| { - error!("Failed to read the registration token file: {e}"); - }) else { - return config.registration_token.clone(); - }; - - Some(token) - }, - ); + .and_then(|path| { + std::fs::read_to_string(path) + .inspect_err(|e| { + error!("Failed to read the TURN secret file: {e}"); + }) + .ok() + }) + .or_else(|| config.turn_secret.clone()); Ok(Arc::new(Self { db, @@ -58,7 +42,6 @@ impl crate::Service for Service { ) .expect("@conduit:server_name is valid"), turn_secret, - registration_token, })) } @@ -122,4 +105,29 @@ impl Service { #[inline] #[must_use] pub fn is_read_only(&self) -> bool { self.db.db.is_read_only() } + + pub async fn get_registration_tokens(&self) -> HashSet { + let mut tokens = HashSet::new(); + if let Some(file) = &self + .server + .config + .registration_token_file + .as_ref() + { + match std::fs::read_to_string(file) { + | Err(e) => error!("Failed to read the registration token file: {e}"), + | Ok(text) => { + text.split_ascii_whitespace().for_each(|token| { + tokens.insert(token.to_owned()); + }); + }, + } + } + + if let Some(token) = &self.server.config.registration_token { + tokens.insert(token.to_owned()); + } + + tokens + } } diff --git a/src/service/membership/join.rs b/src/service/membership/join.rs index 7633e4b6..7c0738c4 100644 --- a/src/service/membership/join.rs +++ b/src/service/membership/join.rs @@ -1,9 +1,14 @@ -use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc}; +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet}, + iter::once, + sync::Arc, +}; use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, pin_mut}; use ruma::{ - CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, OwnedUserId, RoomId, RoomVersionId, - UserId, + CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, OwnedUserId, RoomId, RoomOrAliasId, + RoomVersionId, UserId, api::{client::error::ErrorKind, federation}, canonical_json::to_canonical_value, events::{ @@ -20,13 +25,13 @@ use tuwunel_core::{ matrix::{event::gen_event_id_canonical_json, room_version}, pdu::{PduBuilder, format::from_incoming_federation}, state_res, trace, - utils::{self, IterStream, ReadyExt}, + utils::{self, IterStream, ReadyExt, shuffle}, warn, }; use super::Service; use crate::{ - appservice::RegistrationInfo, + Services, rooms::{ state::RoomMutexGuard, state_compressor::{CompressedState, HashSetCompressStateEvent}, @@ -39,22 +44,27 @@ use crate::{ skip_all, fields(%sender_user, %room_id) )] +#[allow(clippy::too_many_arguments)] pub async fn join( &self, sender_user: &UserId, room_id: &RoomId, + orig_room_id: Option<&RoomOrAliasId>, reason: Option, servers: &[OwnedServerName], - appservice_info: &Option, + is_appservice: bool, state_lock: &RoomMutexGuard, ) -> Result { + let servers = + get_servers_for_room(&self.services, sender_user, room_id, orig_room_id, servers).await?; + let user_is_guest = self .services .users .is_deactivated(sender_user) .await .unwrap_or(false) - && appservice_info.is_none(); + && !is_appservice; if user_is_guest && !self @@ -99,12 +109,12 @@ pub async fn join( || (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0])); if local_join { - self.join_local(sender_user, room_id, reason, servers, state_lock) + self.join_local(sender_user, room_id, reason, &servers, state_lock) .boxed() .await?; } else { // Ask a remote server if we are not participating in this room - self.join_remote(sender_user, room_id, reason, servers, state_lock) + self.join_remote(sender_user, room_id, reason, &servers, state_lock) .boxed() .await?; } @@ -838,3 +848,79 @@ async fn make_join_request( make_join_response_and_server } + +pub(super) async fn get_servers_for_room( + services: &Services, + user_id: &UserId, + room_id: &RoomId, + orig_room_id: Option<&RoomOrAliasId>, + via: &[OwnedServerName], +) -> Result> { + // add invited vias + let mut additional_servers = services + .state_cache + .servers_invite_via(room_id) + .map(ToOwned::to_owned) + .collect::>() + .await; + + // add invite senders' servers + additional_servers.extend( + services + .state_cache + .invite_state(user_id, room_id) + .await + .unwrap_or_default() + .iter() + .filter_map(|event| event.get_field("sender").ok().flatten()) + .filter_map(|sender: &str| UserId::parse(sender).ok()) + .map(|user| user.server_name().to_owned()), + ); + + let mut servers = Vec::from(via); + shuffle(&mut servers); + + if let Some(server_name) = room_id.server_name() { + servers.insert(0, server_name.to_owned()); + } + + if let Some(orig_room_id) = orig_room_id + && let Some(orig_server_name) = orig_room_id.server_name() + { + servers.insert(0, orig_server_name.to_owned()); + } + + shuffle(&mut additional_servers); + + servers.extend_from_slice(&additional_servers); + + // 1. (room alias server)? + // 2. (room id server)? + // 3. shuffle [via query + resolve servers]? + // 4. shuffle [invited via, inviters servers]? + + info!("{servers:?}"); + + // dedup preserving order + let mut set = HashSet::new(); + servers.retain(|x| set.insert(x.clone())); + + info!("{servers:?}"); + + // sort deprioritized servers last + if !servers.is_empty() { + for i in 0..servers.len() { + if services + .server + .config + .deprioritize_joins_through_servers + .is_match(servers[i].host()) + { + let server = servers.remove(i); + servers.push(server); + } + } + } + + Ok(servers) +} diff --git a/src/service/membership/knock.rs b/src/service/membership/knock.rs new file mode 100644 index 00000000..f7b90e6b --- /dev/null +++ b/src/service/membership/knock.rs @@ -0,0 +1,655 @@ +use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc}; + +use futures::{FutureExt, StreamExt}; +use ruma::{ + CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, RoomId, + RoomOrAliasId, RoomVersionId, UserId, + api::federation::{self, membership::RawStrippedState}, + canonical_json::to_canonical_value, + events::{ + StateEventType, + room::member::{MembershipState, RoomMemberEventContent}, + }, +}; +use tuwunel_core::{ + Err, Event, PduCount, Result, debug, debug_info, debug_warn, err, extract_variant, implement, + info, + matrix::event::gen_event_id, + pdu::{PduBuilder, PduEvent}, + trace, utils, warn, +}; + +use super::Service; +use crate::{ + membership::join::get_servers_for_room, + rooms::{ + state::RoomMutexGuard, + state_compressor::{CompressedState, HashSetCompressStateEvent}, + }, +}; + +#[implement(Service)] +#[tracing::instrument( + level = "debug", + skip_all, + fields(%sender_user, %room_id) +)] +pub async fn knock( + &self, + sender_user: &UserId, + room_id: &RoomId, + orig_server_name: Option<&RoomOrAliasId>, + reason: Option, + servers: &[OwnedServerName], + state_lock: &RoomMutexGuard, +) -> Result { + let servers = + get_servers_for_room(&self.services, sender_user, room_id, orig_server_name, servers) + .await?; + + if self + .services + .state_cache + .is_invited(sender_user, room_id) + .await + { + debug_warn!("{sender_user} is already invited in {room_id} but attempted to knock"); + return Err!(Request(Forbidden( + "You cannot knock on a room you are already invited/accepted to." + ))); + } + + if self + .services + .state_cache + .is_joined(sender_user, room_id) + .await + { + debug_warn!("{sender_user} is already joined in {room_id} but attempted to knock"); + return Err!(Request(Forbidden("You cannot knock on a room you are already joined in."))); + } + + if self + .services + .state_cache + .is_knocked(sender_user, room_id) + .await + { + debug_warn!("{sender_user} is already knocked in {room_id}"); + return Ok(()); + } + + if let Ok(membership) = self + .services + .state_accessor + .get_member(room_id, sender_user) + .await + { + if membership.membership == MembershipState::Ban { + debug_warn!("{sender_user} is banned from {room_id} but attempted to knock"); + return Err!(Request(Forbidden("You cannot knock on a room you are banned from."))); + } + } + + let server_in_room = self + .services + .state_cache + .server_in_room(self.services.globals.server_name(), room_id) + .await; + + let local_knock = server_in_room + || servers.is_empty() + || (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0])); + + if local_knock { + self.knock_room_helper_local(sender_user, room_id, reason, &servers, state_lock) + .boxed() + .await + } else { + self.knock_room_helper_remote(sender_user, room_id, reason, &servers, state_lock) + .boxed() + .await + } +} + +#[implement(Service)] +async fn knock_room_helper_local( + &self, + sender_user: &UserId, + room_id: &RoomId, + reason: Option, + servers: &[OwnedServerName], + state_lock: &RoomMutexGuard, +) -> Result { + debug_info!("We can knock locally"); + + let room_version_id = self + .services + .state + .get_room_version(room_id) + .await?; + + if matches!( + room_version_id, + RoomVersionId::V1 + | RoomVersionId::V2 + | RoomVersionId::V3 + | RoomVersionId::V4 + | RoomVersionId::V5 + | RoomVersionId::V6 + ) { + return Err!(Request(Forbidden("This room does not support knocking."))); + } + + let content = RoomMemberEventContent { + displayname: self + .services + .users + .displayname(sender_user) + .await + .ok(), + avatar_url: self + .services + .users + .avatar_url(sender_user) + .await + .ok(), + blurhash: self + .services + .users + .blurhash(sender_user) + .await + .ok(), + reason: reason.clone(), + ..RoomMemberEventContent::new(MembershipState::Knock) + }; + + // Try normal knock first + let Err(error) = self + .services + .timeline + .build_and_append_pdu( + PduBuilder::state(sender_user.to_string(), &content), + sender_user, + room_id, + state_lock, + ) + .await + else { + return Ok(()); + }; + + if servers.is_empty() + || (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0])) + { + return Err(error); + } + + warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock"); + + let (make_knock_response, remote_server) = self + .make_knock_request(sender_user, room_id, servers) + .await?; + + info!("make_knock finished"); + + let room_version_id = make_knock_response.room_version; + + if !self + .services + .server + .supported_room_version(&room_version_id) + { + return Err!(BadServerResponse( + "Remote room version {room_version_id} is not supported by tuwunel" + )); + } + + let mut knock_event_stub = serde_json::from_str::( + make_knock_response.event.get(), + ) + .map_err(|e| { + err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}")) + })?; + + knock_event_stub.insert( + "origin".into(), + CanonicalJsonValue::String( + self.services + .globals + .server_name() + .as_str() + .to_owned(), + ), + ); + knock_event_stub.insert( + "origin_server_ts".into(), + CanonicalJsonValue::Integer( + utils::millis_since_unix_epoch() + .try_into() + .expect("Timestamp is valid js_int value"), + ), + ); + knock_event_stub.insert( + "content".into(), + to_canonical_value(RoomMemberEventContent { + displayname: self + .services + .users + .displayname(sender_user) + .await + .ok(), + avatar_url: self + .services + .users + .avatar_url(sender_user) + .await + .ok(), + blurhash: self + .services + .users + .blurhash(sender_user) + .await + .ok(), + reason, + ..RoomMemberEventContent::new(MembershipState::Knock) + }) + .expect("event is valid, we just created it"), + ); + + // In order to create a compatible ref hash (EventID) the `hashes` field needs + // to be present + self.services + .server_keys + .hash_and_sign_event(&mut knock_event_stub, &room_version_id)?; + + // Generate event id + let event_id = gen_event_id(&knock_event_stub, &room_version_id)?; + + // Add event_id + knock_event_stub + .insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into())); + + // It has enough fields to be called a proper event now + let knock_event = knock_event_stub; + + info!("Asking {remote_server} for send_knock in room {room_id}"); + let send_knock_request = federation::membership::create_knock_event::v1::Request { + room_id: room_id.to_owned(), + event_id: event_id.clone(), + pdu: self + .services + .federation + .format_pdu_into(knock_event.clone(), Some(&room_version_id)) + .await, + }; + + let send_knock_response = self + .services + .federation + .execute(&remote_server, send_knock_request) + .await?; + + info!("send_knock finished"); + + self.services + .short + .get_or_create_shortroomid(room_id) + .await; + + info!("Parsing knock event"); + + let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone()) + .map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?; + + info!("Updating membership locally to knock state with provided stripped state events"); + let count = self.services.globals.next_count(); + self.services + .state_cache + .update_membership( + room_id, + sender_user, + parsed_knock_pdu + .get_content::() + .expect("we just created this"), + sender_user, + Some( + send_knock_response + .knock_room_state + .into_iter() + .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped)) + .collect(), + ), + None, + false, + PduCount::Normal(*count), + ) + .await?; + + info!("Appending room knock event locally"); + self.services + .timeline + .append_pdu( + &parsed_knock_pdu, + knock_event, + once(parsed_knock_pdu.event_id.borrow()), + state_lock, + ) + .await?; + + Ok(()) +} + +#[implement(Service)] +async fn knock_room_helper_remote( + &self, + sender_user: &UserId, + room_id: &RoomId, + reason: Option, + servers: &[OwnedServerName], + state_lock: &RoomMutexGuard, +) -> Result { + info!("Knocking {room_id} over federation."); + + let (make_knock_response, remote_server) = self + .make_knock_request(sender_user, room_id, servers) + .await?; + + info!("make_knock finished"); + + let room_version_id = make_knock_response.room_version; + + if !self + .services + .server + .supported_room_version(&room_version_id) + { + return Err!(BadServerResponse( + "Remote room version {room_version_id} is not supported by tuwunel" + )); + } + + let mut knock_event_stub: CanonicalJsonObject = + serde_json::from_str(make_knock_response.event.get()).map_err(|e| { + err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}")) + })?; + + knock_event_stub.insert( + "origin".into(), + CanonicalJsonValue::String( + self.services + .globals + .server_name() + .as_str() + .to_owned(), + ), + ); + knock_event_stub.insert( + "origin_server_ts".into(), + CanonicalJsonValue::Integer( + utils::millis_since_unix_epoch() + .try_into() + .expect("Timestamp is valid js_int value"), + ), + ); + knock_event_stub.insert( + "content".into(), + to_canonical_value(RoomMemberEventContent { + displayname: self + .services + .users + .displayname(sender_user) + .await + .ok(), + avatar_url: self + .services + .users + .avatar_url(sender_user) + .await + .ok(), + blurhash: self + .services + .users + .blurhash(sender_user) + .await + .ok(), + reason, + ..RoomMemberEventContent::new(MembershipState::Knock) + }) + .expect("event is valid, we just created it"), + ); + + // In order to create a compatible ref hash (EventID) the `hashes` field needs + // to be present + self.services + .server_keys + .hash_and_sign_event(&mut knock_event_stub, &room_version_id)?; + + // Generate event id + let event_id = gen_event_id(&knock_event_stub, &room_version_id)?; + + // Add event_id + knock_event_stub + .insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into())); + + // It has enough fields to be called a proper event now + let knock_event = knock_event_stub; + + info!("Asking {remote_server} for send_knock in room {room_id}"); + let send_knock_request = federation::membership::create_knock_event::v1::Request { + room_id: room_id.to_owned(), + event_id: event_id.clone(), + pdu: self + .services + .federation + .format_pdu_into(knock_event.clone(), Some(&room_version_id)) + .await, + }; + + let send_knock_response = self + .services + .federation + .execute(&remote_server, send_knock_request) + .await?; + + info!("send_knock finished"); + + self.services + .short + .get_or_create_shortroomid(room_id) + .await; + + info!("Parsing knock event"); + let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone()) + .map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?; + + info!("Going through send_knock response knock state events"); + let state = send_knock_response + .knock_room_state + .iter() + .map(|event| { + serde_json::from_str::( + extract_variant!(event.clone(), RawStrippedState::Stripped) + .expect("Raw") + .json() + .get(), + ) + }) + .filter_map(Result::ok); + + let mut state_map: HashMap = HashMap::new(); + + for event in state { + let Some(state_key) = event.get("state_key") else { + debug_warn!("send_knock stripped state event missing state_key: {event:?}"); + continue; + }; + let Some(event_type) = event.get("type") else { + debug_warn!("send_knock stripped state event missing event type: {event:?}"); + continue; + }; + + let Ok(state_key) = serde_json::from_value::(state_key.clone().into()) else { + debug_warn!("send_knock stripped state event has invalid state_key: {event:?}"); + continue; + }; + let Ok(event_type) = serde_json::from_value::(event_type.clone().into()) + else { + debug_warn!("send_knock stripped state event has invalid event type: {event:?}"); + continue; + }; + + let event_id = gen_event_id(&event, &room_version_id)?; + let shortstatekey = self + .services + .short + .get_or_create_shortstatekey(&event_type, &state_key) + .await; + + self.services + .timeline + .add_pdu_outlier(&event_id, &event); + + state_map.insert(shortstatekey, event_id.clone()); + } + + info!("Compressing state from send_knock"); + let compressed: CompressedState = self + .services + .state_compressor + .compress_state_events( + state_map + .iter() + .map(|(ssk, eid)| (ssk, eid.borrow())), + ) + .collect() + .await; + + debug!("Saving compressed state"); + let HashSetCompressStateEvent { + shortstatehash: statehash_before_knock, + added, + removed, + } = self + .services + .state_compressor + .save_state(room_id, Arc::new(compressed)) + .await?; + + debug!("Forcing state for new room"); + self.services + .state + .force_state(room_id, statehash_before_knock, added, removed, state_lock) + .await?; + + let statehash_after_knock = self + .services + .state + .append_to_state(&parsed_knock_pdu) + .await?; + + info!("Updating membership locally to knock state with provided stripped state events"); + let count = self.services.globals.next_count(); + self.services + .state_cache + .update_membership( + room_id, + sender_user, + parsed_knock_pdu + .get_content::() + .expect("we just created this"), + sender_user, + Some( + send_knock_response + .knock_room_state + .into_iter() + .filter_map(|s| extract_variant!(s, RawStrippedState::Stripped)) + .collect(), + ), + None, + false, + PduCount::Normal(*count), + ) + .await?; + + info!("Appending room knock event locally"); + self.services + .timeline + .append_pdu( + &parsed_knock_pdu, + knock_event, + once(parsed_knock_pdu.event_id.borrow()), + state_lock, + ) + .await?; + + info!("Setting final room state for new room"); + // We set the room state after inserting the pdu, so that we never have a moment + // in time where events in the current room state do not exist + self.services + .state + .set_room_state(room_id, statehash_after_knock, state_lock); + + Ok(()) +} + +#[implement(Service)] +async fn make_knock_request( + &self, + sender_user: &UserId, + room_id: &RoomId, + servers: &[OwnedServerName], +) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> { + let mut make_knock_response_and_server = + Err!(BadServerResponse("No server available to assist in knocking.")); + + let mut make_knock_counter: usize = 0; + + for remote_server in servers { + if self + .services + .globals + .server_is_ours(remote_server) + { + continue; + } + + info!("Asking {remote_server} for make_knock ({make_knock_counter})"); + + let make_knock_response = self + .services + .federation + .execute(remote_server, federation::membership::prepare_knock_event::v1::Request { + room_id: room_id.to_owned(), + user_id: sender_user.to_owned(), + ver: self + .services + .server + .supported_room_versions() + .collect(), + }) + .await; + + trace!("make_knock response: {make_knock_response:?}"); + make_knock_counter = make_knock_counter.saturating_add(1); + + make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone())); + + if make_knock_response_and_server.is_ok() { + break; + } + + if make_knock_counter > 40 { + warn!( + "50 servers failed to provide valid make_knock response, assuming no server can \ + assist in knocking." + ); + make_knock_response_and_server = + Err!(BadServerResponse("No server available to assist in knocking.")); + + return make_knock_response_and_server; + } + } + + make_knock_response_and_server +} diff --git a/src/service/membership/mod.rs b/src/service/membership/mod.rs index 1b8e66ee..0fd2328d 100644 --- a/src/service/membership/mod.rs +++ b/src/service/membership/mod.rs @@ -2,6 +2,7 @@ mod ban; mod invite; mod join; mod kick; +mod knock; mod leave; mod unban; diff --git a/src/service/rooms/alias/mod.rs b/src/service/rooms/alias/mod.rs index 3b9179ee..377351f8 100644 --- a/src/service/rooms/alias/mod.rs +++ b/src/service/rooms/alias/mod.rs @@ -109,10 +109,10 @@ impl Service { pub async fn maybe_resolve_with_servers( &self, room: &RoomOrAliasId, - servers: Option>, + servers: Option<&[OwnedServerName]>, ) -> Result<(OwnedRoomId, Vec)> { match <&RoomId>::try_from(room) { - | Ok(room_id) => Ok((room_id.to_owned(), servers.unwrap_or_default())), + | Ok(room_id) => Ok((room_id.to_owned(), Vec::from(servers.unwrap_or_default()))), | Err(alias) => self.resolve_alias(alias).await, } } diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index b292ec0c..f85e0919 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, HashSet}, + collections::BTreeMap, sync::{Arc, RwLock}, }; @@ -45,32 +45,6 @@ impl crate::Service for Service { fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } } -#[implement(Service)] -pub async fn read_tokens(&self) -> Result> { - let mut tokens = HashSet::new(); - if let Some(file) = &self - .services - .config - .registration_token_file - .as_ref() - { - match std::fs::read_to_string(file) { - | Err(e) => error!("Failed to read the registration token file: {e}"), - | Ok(text) => { - text.split_ascii_whitespace().for_each(|token| { - tokens.insert(token.to_owned()); - }); - }, - } - } - - if let Some(token) = &self.services.config.registration_token { - tokens.insert(token.to_owned()); - } - - Ok(tokens) -} - /// Creates a new Uiaa session. Make sure the session token is unique. #[implement(Service)] pub fn create( @@ -175,7 +149,11 @@ pub async fn try_auth( uiaainfo.completed.push(AuthType::Password); }, | AuthData::RegistrationToken(t) => { - let tokens = self.read_tokens().await?; + let tokens = self + .services + .globals + .get_registration_tokens() + .await; if tokens.contains(t.token.trim()) { uiaainfo .completed diff --git a/src/service/users/dehydrated_device.rs b/src/service/users/dehydrated_device.rs index e2112b34..8295c9f0 100644 --- a/src/service/users/dehydrated_device.rs +++ b/src/service/users/dehydrated_device.rs @@ -51,10 +51,10 @@ pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) -> self.create_device( user_id, - &request.device_id, + Some(&request.device_id), (None, None), None, - request.initial_device_display_name.clone(), + request.initial_device_display_name.as_deref(), None, ) .await?; diff --git a/src/service/users/device.rs b/src/service/users/device.rs index 3ab34d87..713d5db7 100644 --- a/src/service/users/device.rs +++ b/src/service/users/device.rs @@ -19,6 +19,9 @@ use tuwunel_core::{ }; use tuwunel_database::{Deserialized, Ignore, Interfix, Json, Map}; +/// generated device ID length +const DEVICE_ID_LENGTH: usize = 10; + /// generated user access token length pub const TOKEN_LENGTH: usize = 32; @@ -28,12 +31,16 @@ pub const TOKEN_LENGTH: usize = 32; pub async fn create_device( &self, user_id: &UserId, - device_id: &DeviceId, + device_id: Option<&DeviceId>, (access_token, expires_in): (Option<&str>, Option), refresh_token: Option<&str>, - initial_device_display_name: Option, + initial_device_display_name: Option<&str>, client_ip: Option, -) -> Result { +) -> Result { + let device_id = device_id + .map(ToOwned::to_owned) + .unwrap_or_else(|| OwnedDeviceId::from(utils::random_string(DEVICE_ID_LENGTH))); + if !self.exists(user_id).await { return Err!(Request(InvalidParam(error!( "Called create_device for non-existent user {user_id}" @@ -42,18 +49,18 @@ pub async fn create_device( let notify = true; self.put_device_metadata(user_id, notify, &Device { - device_id: device_id.into(), + device_id: device_id.clone(), display_name: initial_device_display_name.map(Into::into), last_seen_ip: client_ip.map(Into::into), last_seen_ts: Some(MilliSecondsSinceUnixEpoch::now()), }); if let Some(access_token) = access_token { - self.set_access_token(user_id, device_id, access_token, expires_in, refresh_token) + self.set_access_token(user_id, &device_id, access_token, expires_in, refresh_token) .await?; } - Ok(()) + Ok(device_id) } /// Removes a device from a user. diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 8e7e8bcc..53ce4dc0 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -3,6 +3,7 @@ pub mod device; mod keys; mod ldap; mod profile; +mod register; use std::sync::Arc; @@ -124,10 +125,8 @@ impl Service { password: Option<&str>, origin: Option<&str>, ) -> Result { - origin.map_or_else( - || self.db.userid_origin.insert(user_id, "password"), - |origin| self.db.userid_origin.insert(user_id, origin), - ); + let origin = origin.unwrap_or("password"); + self.db.userid_origin.insert(user_id, origin); self.set_password(user_id, password).await } diff --git a/src/service/users/register.rs b/src/service/users/register.rs new file mode 100644 index 00000000..c8ce6e5f --- /dev/null +++ b/src/service/users/register.rs @@ -0,0 +1,156 @@ +use std::fmt::Write; + +use futures::FutureExt; +use ruma::{UserId, events::GlobalAccountDataEventType, push}; +use tuwunel_core::{Err, Result, error, implement, info, is_equal_to, warn}; + +use crate::appservice::RegistrationInfo; + +/// Fully register a local user +/// +/// Returns a device id and access token for the registered user +#[implement(super::Service)] +pub async fn full_register( + &self, + user_id: &UserId, + password: Option<&str>, + origin: Option<&str>, + appservice_info: Option<&RegistrationInfo>, + is_guest: bool, + grant_admin: bool, +) -> Result { + if !self.services.globals.user_is_local(user_id) { + return Err!("Cannot register remote user"); + } + + if self.services.users.exists(user_id).await { + return Err!(Request(UserInUse("User ID is not available."))); + } + + // Create user + self.services + .users + .create(user_id, password, origin) + .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 !self + .services + .config + .new_user_displayname_suffix + .is_empty() + && appservice_info.is_none() + { + write!( + displayname, + " {}", + self.services + .server + .config + .new_user_displayname_suffix + )?; + } + + self.services + .users + .set_displayname(user_id, Some(displayname.clone())); + + // Initial account data + self.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 this is the first real user, grant them admin privileges except for guest + // users + // Note: the server user is generated first + if !is_guest + && grant_admin + && self.services.config.grant_admin_to_first_user + && let Ok(admin_room) = self.services.admin.get_admin_room().await + && self + .services + .state_cache + .room_joined_count(&admin_room) + .await + .is_ok_and(is_equal_to!(1)) + { + self.services + .admin + .make_user_admin(user_id) + .boxed() + .await?; + warn!("Granting {user_id} admin privileges as the first user"); + } + + if appservice_info.is_none() + && (self.services.config.allow_guests_auto_join_rooms || !is_guest) + { + for room in &self.services.server.config.auto_join_rooms { + let Ok(room_id) = self.services.alias.maybe_resolve(room).await else { + error!( + "Failed to resolve room alias to room ID when attempting to auto join \ + {room}, skipping" + ); + continue; + }; + + if !self + .services + .state_cache + .server_in_room(self.services.globals.server_name(), &room_id) + .await + { + warn!( + "Skipping room {room} to automatically join as we have never joined before." + ); + continue; + } + + let state_lock = self.services.state.mutex.lock(&room_id).await; + + match self + .services + .membership + .join( + user_id, + &room_id, + Some(room), + Some("Automatically joining this room upon registration".to_owned()), + &[], + false, + &state_lock, + ) + .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}"); + }, + } + + drop(state_lock); + } + } + + Ok(()) +}