mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7f8eec282 | |||
| 0d4bbe612d | |||
| c4d297ae3b | |||
| d15064871e | |||
| b925936195 | |||
| 56feba0ea0 | |||
| 8d89ba94d5 | |||
| 0b135c7717 |
Generated
-67
@@ -1088,7 +1088,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde-saphyr",
|
"serde-saphyr",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"termimad",
|
"termimad",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1108,29 +1107,18 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"conduwuit_api",
|
|
||||||
"conduwuit_build_metadata",
|
"conduwuit_build_metadata",
|
||||||
"conduwuit_core",
|
"conduwuit_core",
|
||||||
"conduwuit_database",
|
|
||||||
"conduwuit_service",
|
"conduwuit_service",
|
||||||
"form_urlencoded",
|
|
||||||
"futures",
|
"futures",
|
||||||
"lettre",
|
|
||||||
"memory-serve",
|
"memory-serve",
|
||||||
"rand 0.10.1",
|
"rand 0.10.1",
|
||||||
"recaptcha-verify",
|
|
||||||
"reqwest 0.12.28",
|
|
||||||
"ruma",
|
"ruma",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"serde_urlencoded",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-sec-fetch",
|
"tower-sec-fetch",
|
||||||
"tower-sessions",
|
|
||||||
"tower-sessions-core",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
|
||||||
"validator",
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1538,7 +1526,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5556,22 +5543,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-cookies"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
|
|
||||||
dependencies = [
|
|
||||||
"axum-core",
|
|
||||||
"cookie",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.11"
|
version = "0.6.11"
|
||||||
@@ -5620,44 +5591,6 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions"
|
|
||||||
version = "0.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "518dca34b74a17cadfcee06e616a09d2bd0c3984eff1769e1e76d58df978fc78"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"http",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tower-cookies",
|
|
||||||
"tower-layer",
|
|
||||||
"tower-service",
|
|
||||||
"tower-sessions-core",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tower-sessions-core"
|
|
||||||
version = "0.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "568531ec3dfcf3ffe493de1958ae5662a0284ac5d767476ecdb6a34ff8c6b06c"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"axum-core",
|
|
||||||
"base64 0.22.1",
|
|
||||||
"futures",
|
|
||||||
"http",
|
|
||||||
"parking_lot",
|
|
||||||
"rand 0.9.4",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
|
|||||||
@@ -559,9 +559,6 @@ features = ["std"]
|
|||||||
[workspace.dependencies.nonzero_ext]
|
[workspace.dependencies.nonzero_ext]
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
||||||
[workspace.dependencies.serde_urlencoded]
|
|
||||||
version = "0.7.1"
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Patches
|
# Patches
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Users may now be forbidden from deactivating their own accounts with the new `allow_deactivation` config option. Contributed by @ginger.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Added support for authenticating clients using the new OAuth 2.0 login API. Contributed by @ginger.
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Improved the performance and reliability of fetching missing events, improving network partition recovery. Contributed
|
||||||
|
by @nex.
|
||||||
+10
-38
@@ -297,7 +297,7 @@
|
|||||||
|
|
||||||
# This item is undocumented. Please contribute documentation for it.
|
# This item is undocumented. Please contribute documentation for it.
|
||||||
#
|
#
|
||||||
#max_fetch_prev_events = 192
|
#max_fetch_prev_events = 256
|
||||||
|
|
||||||
# How many incoming federation transactions the server is willing to be
|
# How many incoming federation transactions the server is willing to be
|
||||||
# processing at any given time before it becomes overloaded and starts
|
# processing at any given time before it becomes overloaded and starts
|
||||||
@@ -521,15 +521,17 @@
|
|||||||
#
|
#
|
||||||
#recaptcha_private_site_key =
|
#recaptcha_private_site_key =
|
||||||
|
|
||||||
# Controls whether users are allowed to deactivate their own accounts
|
# Policy documents, such as terms and conditions or a privacy policy,
|
||||||
# through the account management panel or their Matrix clients. Server
|
# which users must agree to when registering an account.
|
||||||
# admins can always deactivate users using the relevant admin commands.
|
|
||||||
#
|
#
|
||||||
# Note that, in some jurisdictions, you may be legally required to honor
|
# Example:
|
||||||
# users who request to deactivate their accounts if you set this option
|
# ```ignore
|
||||||
# to `false`.
|
# [global.registration_terms.privacy_policy]
|
||||||
|
# en = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||||
|
# es = { name = "Política de Privacidad", url = "https://homeserver.example/es/privacy_policy.html" }
|
||||||
|
# ```
|
||||||
#
|
#
|
||||||
#allow_deactivation = true
|
#registration_terms = {}
|
||||||
|
|
||||||
# Controls whether encrypted rooms and events are allowed.
|
# Controls whether encrypted rooms and events are allowed.
|
||||||
#
|
#
|
||||||
@@ -1985,33 +1987,3 @@
|
|||||||
# `require_email_for_registration`.
|
# `require_email_for_registration`.
|
||||||
#
|
#
|
||||||
#require_email_for_token_registration = false
|
#require_email_for_token_registration = false
|
||||||
|
|
||||||
#[global.registration_terms]
|
|
||||||
|
|
||||||
# The language code to provide to clients along with the policy documents.
|
|
||||||
#
|
|
||||||
#language = "en"
|
|
||||||
|
|
||||||
# Policy documents, such as terms and conditions or a privacy policy,
|
|
||||||
# which users must agree to when registering an account.
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# ```ignore
|
|
||||||
# [global.registration_terms.documents]
|
|
||||||
# privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
#documents = {}
|
|
||||||
|
|
||||||
#[global.oauth]
|
|
||||||
|
|
||||||
# The compatibility mode to use for OAuth.
|
|
||||||
#
|
|
||||||
# - "disabled": OAuth will be unavailable. Users will only be able to log
|
|
||||||
# in using legacy authentication.
|
|
||||||
# - "hybrid": OAuth and legacy authentication will both be available. Some
|
|
||||||
# clients may only use one or the other.
|
|
||||||
# - "exclusive": Only OAuth will be available. Clients which require
|
|
||||||
# legacy authentication will be unable to log in.
|
|
||||||
#
|
|
||||||
#compatibility_mode = "hybrid"
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(name = conduwuit_core::BRANDING, version = conduwuit_core::version())]
|
#[command(name = conduwuit_core::name(), version = conduwuit_core::version())]
|
||||||
pub enum AdminCommand {
|
pub enum AdminCommand {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
/// Commands for managing appservices
|
/// Commands for managing appservices
|
||||||
|
|||||||
@@ -30,37 +30,14 @@ pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
|
|||||||
.issue_token(self.sender_or_service_user().into(), expires);
|
.issue_token(self.sender_or_service_user().into(), expires);
|
||||||
|
|
||||||
self.write_str(&format!(
|
self.write_str(&format!(
|
||||||
"New registration token issued: `{token}` . {}.",
|
"New registration token issued: `{token}`. {}.",
|
||||||
if let Some(expires) = info.expires {
|
if let Some(expires) = info.expires {
|
||||||
format!("{expires}")
|
format!("{expires}")
|
||||||
} else {
|
} else {
|
||||||
"Never expires".to_owned()
|
"Never expires".to_owned()
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
.await?;
|
.await
|
||||||
|
|
||||||
if self
|
|
||||||
.services
|
|
||||||
.config
|
|
||||||
.oauth
|
|
||||||
.compatibility_mode
|
|
||||||
.oauth_available()
|
|
||||||
{
|
|
||||||
self.write_str(&format!(
|
|
||||||
"\nInvite link using this token: {}",
|
|
||||||
self.services
|
|
||||||
.config
|
|
||||||
.get_client_domain()
|
|
||||||
.join(&format!(
|
|
||||||
"{}/account/register/?flow=trusted&token={token}",
|
|
||||||
conduwuit::ROUTE_PREFIX
|
|
||||||
))
|
|
||||||
.unwrap()
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
|
|||||||
+147
-11
@@ -1,10 +1,13 @@
|
|||||||
use std::collections::{BTreeMap, HashSet};
|
use std::{
|
||||||
|
collections::{BTreeMap, HashSet},
|
||||||
|
fmt::Write as _,
|
||||||
|
};
|
||||||
|
|
||||||
use api::client::{
|
use api::client::{
|
||||||
full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room,
|
full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room,
|
||||||
};
|
};
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Result, debug_warn, info,
|
Err, Result, debug_warn, error, info,
|
||||||
matrix::{Event, pdu::PartialPdu},
|
matrix::{Event, pdu::PartialPdu},
|
||||||
utils::{self, ReadyExt},
|
utils::{self, ReadyExt},
|
||||||
warn,
|
warn,
|
||||||
@@ -50,22 +53,130 @@ pub(super) async fn list_users(&self) -> Result {
|
|||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn create_user(&self, username: String, password: Option<String>) -> Result {
|
pub(super) async fn create_user(&self, username: String, password: Option<String>) -> Result {
|
||||||
// Validate user id
|
// Validate user id
|
||||||
let user_id = self
|
let user_id = parse_local_user_id(self.services, &username)?;
|
||||||
.services
|
|
||||||
|
if let Err(e) = user_id.validate_strict() {
|
||||||
|
if self.services.config.emergency_password.is_none() {
|
||||||
|
return Err!("Username {user_id} contains disallowed characters or spaces: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.services.users.exists(&user_id).await {
|
||||||
|
return Err!("User {user_id} already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
self.services
|
||||||
.users
|
.users
|
||||||
.determine_registration_user_id(Some(username), None, None)
|
.create(&user_id, Some(HashedPassword::new(&password)?))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let password = HashedPassword::new(
|
// Default to pretty displayname
|
||||||
&password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)),
|
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
|
||||||
|
.server
|
||||||
|
.config
|
||||||
|
.new_user_displayname_suffix
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
write!(displayname, " {}", self.services.server.config.new_user_displayname_suffix)?;
|
||||||
|
}
|
||||||
|
|
||||||
self.services
|
self.services
|
||||||
.users
|
.users
|
||||||
.create_local_account(&user_id, password, None)
|
.set_displayname(&user_id, Some(displayname));
|
||||||
.await;
|
|
||||||
|
|
||||||
self.write_str(&format!("Created user {user_id}")).await
|
// Initial account data
|
||||||
|
self.services
|
||||||
|
.account_data
|
||||||
|
.update(
|
||||||
|
None,
|
||||||
|
&user_id,
|
||||||
|
ruma::events::GlobalAccountDataEventType::PushRules
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent::new(
|
||||||
|
ruma::events::push_rules::PushRulesEventContent::new(
|
||||||
|
ruma::push::Ruleset::server_default(&user_id),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !self.services.server.config.auto_join_rooms.is_empty() {
|
||||||
|
for room in &self.services.server.config.auto_join_rooms {
|
||||||
|
let Ok(room_id) = self.services.rooms.alias.resolve(room).await else {
|
||||||
|
error!(
|
||||||
|
%user_id,
|
||||||
|
"Failed to resolve room alias to room ID when attempting to auto join {room}, skipping"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(room_server_name) = room.server_name() {
|
||||||
|
match self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.membership
|
||||||
|
.join_room(
|
||||||
|
&user_id,
|
||||||
|
&room_id,
|
||||||
|
Some("Automatically joining this room upon registration".to_owned()),
|
||||||
|
&[
|
||||||
|
self.services.globals.server_name().to_owned(),
|
||||||
|
room_server_name.to_owned(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok(_response) => {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we dont add a device since we're not the user, just the creator
|
||||||
|
|
||||||
|
// Make the first user to register an administrator and disable first-run mode.
|
||||||
|
self.services.firstrun.empower_first_user(&user_id).await?;
|
||||||
|
|
||||||
|
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
@@ -191,6 +302,31 @@ pub(super) async fn reset_password(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn issue_password_reset_link(&self, username: String) -> Result {
|
||||||
|
use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM};
|
||||||
|
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
|
let mut reset_url = self
|
||||||
|
.services
|
||||||
|
.config
|
||||||
|
.get_client_domain()
|
||||||
|
.join(PASSWORD_RESET_PATH)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let user_id = parse_local_user_id(self.services, &username)?;
|
||||||
|
let token = self.services.password_reset.issue_token(user_id).await?;
|
||||||
|
reset_url
|
||||||
|
.query_pairs_mut()
|
||||||
|
.append_pair(RESET_TOKEN_QUERY_PARAM, &token.token);
|
||||||
|
|
||||||
|
self.write_str(&format!("Password reset link issued for {username}: {reset_url}"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
|
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
|
||||||
if self.body.len() < 2
|
if self.body.len() < 2
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ pub enum UserCommand {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Issue a self-service password reset link for a user.
|
||||||
|
IssuePasswordResetLink {
|
||||||
|
/// Username of the user who may use the link
|
||||||
|
username: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// Get a user's associated email address.
|
/// Get a user's associated email address.
|
||||||
GetEmail {
|
GetEmail {
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ zstd_compression = [
|
|||||||
"reqwest/zstd",
|
"reqwest/zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
admin_api = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
axum-client-ip.workspace = true
|
axum-client-ip.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod rooms;
|
||||||
@@ -6,7 +6,7 @@ use ruminuwuity::admin::continuwuity::rooms;
|
|||||||
|
|
||||||
use crate::{Ruma, client::leave_room};
|
use crate::{Ruma, client::leave_room};
|
||||||
|
|
||||||
/// # `PUT /_continuwuity/admin/v1/rooms/{roomID}/ban`
|
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
|
||||||
///
|
///
|
||||||
/// Bans or unbans a room.
|
/// Bans or unbans a room.
|
||||||
pub(crate) async fn ban_room(
|
pub(crate) async fn ban_room(
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use axum::extract::State;
|
||||||
|
use conduwuit::{Err, Result};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use ruma::OwnedRoomId;
|
||||||
|
use ruminuwuity::admin::continuwuity::rooms;
|
||||||
|
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
/// # `GET /_continuwuity/admin/rooms/list`
|
||||||
|
///
|
||||||
|
/// Lists all rooms known to this server, excluding banned ones.
|
||||||
|
pub(crate) async fn list_rooms(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<rooms::list::v1::Request>,
|
||||||
|
) -> Result<rooms::list::v1::Response> {
|
||||||
|
let sender_user = body.identity.sender_user();
|
||||||
|
if !services.users.is_admin(sender_user).await {
|
||||||
|
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rooms: Vec<OwnedRoomId> = services
|
||||||
|
.rooms
|
||||||
|
.metadata
|
||||||
|
.iter_ids()
|
||||||
|
.filter_map(|room_id| async move {
|
||||||
|
if !services.rooms.metadata.is_banned(&room_id).await {
|
||||||
|
Some(room_id.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
rooms.sort();
|
||||||
|
Ok(rooms::list::v1::Response::new(rooms))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod ban;
|
||||||
|
pub mod list;
|
||||||
@@ -24,7 +24,7 @@ use ruma::{
|
|||||||
power_levels::RoomPowerLevelsEventContent,
|
power_levels::RoomPowerLevelsEventContent,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
|
use service::{mailer::messages, uiaa::Identity, users::HashedPassword};
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||||
use crate::{Ruma, router::ClientIdentity};
|
use crate::{Ruma, router::ClientIdentity};
|
||||||
@@ -49,16 +49,39 @@ pub(crate) async fn get_register_available_route(
|
|||||||
ClientIp(client): ClientIp,
|
ClientIp(client): ClientIp,
|
||||||
body: Ruma<get_username_availability::v3::Request>,
|
body: Ruma<get_username_availability::v3::Request>,
|
||||||
) -> Result<get_username_availability::v3::Response> {
|
) -> Result<get_username_availability::v3::Response> {
|
||||||
let _ = services
|
// Validate user id
|
||||||
.users
|
let user_id =
|
||||||
.determine_registration_user_id(
|
match UserId::parse_with_server_name(&body.username, services.globals.server_name()) {
|
||||||
Some(body.username.clone()),
|
| Ok(user_id) => {
|
||||||
None,
|
if let Err(e) = user_id.validate_strict() {
|
||||||
body.identity
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
.as_ref()
|
"Username {} contains disallowed characters or spaces: {e}",
|
||||||
.and_then(ClientIdentity::appservice_info),
|
body.username
|
||||||
)
|
))));
|
||||||
.await?;
|
}
|
||||||
|
|
||||||
|
user_id
|
||||||
|
},
|
||||||
|
| Err(e) => {
|
||||||
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
|
"Username {} is not valid: {e}",
|
||||||
|
body.username
|
||||||
|
))));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if username is creative enough
|
||||||
|
if services.users.exists(&user_id).await {
|
||||||
|
return Err!(Request(UserInUse("User ID is not available.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ClientIdentity::Appservice { appservice_info, .. }) = &body.identity
|
||||||
|
&& !appservice_info.is_user_match(&user_id)
|
||||||
|
{
|
||||||
|
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
||||||
|
} else if services.appservice.is_exclusive_user_id(&user_id).await {
|
||||||
|
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(get_username_availability::v3::Response::new(true))
|
Ok(get_username_availability::v3::Response::new(true))
|
||||||
}
|
}
|
||||||
@@ -86,7 +109,8 @@ pub(crate) async fn change_password_route(
|
|||||||
ClientIp(client): ClientIp,
|
ClientIp(client): ClientIp,
|
||||||
body: Ruma<change_password::v3::Request>,
|
body: Ruma<change_password::v3::Request>,
|
||||||
) -> Result<change_password::v3::Response> {
|
) -> Result<change_password::v3::Response> {
|
||||||
let identity = if let Some(identity) = body.identity.as_ref() {
|
let identity = if let Some(user_id) = body.identity.as_ref().map(ClientIdentity::sender_user)
|
||||||
|
{
|
||||||
// A signed-in user is trying to change their password, prompt them for their
|
// A signed-in user is trying to change their password, prompt them for their
|
||||||
// existing one
|
// existing one
|
||||||
|
|
||||||
@@ -96,7 +120,7 @@ pub(crate) async fn change_password_route(
|
|||||||
&body.auth,
|
&body.auth,
|
||||||
vec![AuthFlow::new(vec![AuthType::Password])],
|
vec![AuthFlow::new(vec![AuthType::Password])],
|
||||||
Box::default(),
|
Box::default(),
|
||||||
Some(UiaaInitiator::new(identity.sender_user(), identity.sender_device())),
|
Some(Identity::from_user_id(user_id)),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
@@ -252,24 +276,16 @@ pub(crate) async fn deactivate_route(
|
|||||||
) -> Result<deactivate::v3::Response> {
|
) -> Result<deactivate::v3::Response> {
|
||||||
// Authentication for this endpoint is technically optional,
|
// Authentication for this endpoint is technically optional,
|
||||||
// but we require the user to be logged in
|
// but we require the user to be logged in
|
||||||
let identity = body
|
let sender_user = body
|
||||||
.identity
|
.identity
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
.map(ClientIdentity::sender_user)
|
||||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||||
|
|
||||||
let sender_user = identity.sender_user();
|
|
||||||
|
|
||||||
if !services.config.allow_deactivation {
|
|
||||||
return Err!(Request(Unauthorized(
|
|
||||||
"You may not deactivate your own account. Contact your server's administrator for \
|
|
||||||
assistance."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt the user to confirm with their password using UIAA
|
// Prompt the user to confirm with their password using UIAA
|
||||||
let _ = services
|
let _ = services
|
||||||
.uiaa
|
.uiaa
|
||||||
.authenticate_password(&body.auth, sender_user, identity.sender_device(), None)
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Remove profile pictures and display name
|
// Remove profile pictures and display name
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, fmt::Write};
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::ClientIp;
|
use axum_client_ip::ClientIp;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Result, debug_info, info,
|
Err, Result, debug_info, error, info,
|
||||||
utils::{self},
|
utils::{self},
|
||||||
|
warn,
|
||||||
};
|
};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::StreamExt;
|
use futures::{FutureExt, StreamExt};
|
||||||
use lettre::{Address, message::Mailbox};
|
use lettre::{Address, message::Mailbox};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
|
OwnedUserId, UserId,
|
||||||
api::client::{
|
api::client::{
|
||||||
account::{
|
account::{
|
||||||
register::{self, LoginType, RegistrationKind},
|
register::{self, LoginType, RegistrationKind},
|
||||||
@@ -18,6 +20,11 @@ use ruma::{
|
|||||||
uiaa::{AuthFlow, AuthType},
|
uiaa::{AuthFlow, AuthType},
|
||||||
},
|
},
|
||||||
assign,
|
assign,
|
||||||
|
events::{
|
||||||
|
GlobalAccountDataEventType, push_rules::PushRulesEvent,
|
||||||
|
room::message::RoomMessageEventContent,
|
||||||
|
},
|
||||||
|
push,
|
||||||
};
|
};
|
||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
use service::{mailer::messages, users::HashedPassword};
|
use service::{mailer::messages, users::HashedPassword};
|
||||||
@@ -25,6 +32,8 @@ use service::{mailer::messages, users::HashedPassword};
|
|||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
|
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/register`
|
/// # `POST /_matrix/client/v3/register`
|
||||||
///
|
///
|
||||||
/// Register an account on this homeserver.
|
/// Register an account on this homeserver.
|
||||||
@@ -43,6 +52,8 @@ pub(crate) async fn register_route(
|
|||||||
return Err!(Request(GuestAccessForbidden("Guests may not register on this server.")));
|
return Err!(Request(GuestAccessForbidden("Guests may not register on this server.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||||
|
|
||||||
// Allow registration if it's enabled in the config file or if this is the first
|
// Allow registration if it's enabled in the config file or if this is the first
|
||||||
// run (so the first user account can be created)
|
// run (so the first user account can be created)
|
||||||
let allow_registration =
|
let allow_registration =
|
||||||
@@ -60,59 +71,99 @@ pub(crate) async fn register_route(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = if body.body.login_type == Some(LoginType::ApplicationService) {
|
let identity = if body.identity.is_some() {
|
||||||
let Some(appservice_info) = &body.identity else {
|
// Appservices can skip auth
|
||||||
return Err!(Request(Forbidden(
|
None
|
||||||
"Only appservices can use the appservice login type."
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_id = services
|
|
||||||
.users
|
|
||||||
.determine_registration_user_id(body.username.clone(), None, Some(appservice_info))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
services.users.create(&user_id, None).await?;
|
|
||||||
|
|
||||||
user_id
|
|
||||||
} else {
|
} else {
|
||||||
// Perform UIAA to determine the user's identity
|
// Perform UIAA to determine the user's identity
|
||||||
let (flows, params) = create_registration_uiaa_session(&services).await?;
|
let (flows, params) = create_registration_uiaa_session(&services).await?;
|
||||||
|
|
||||||
let identity = services
|
Some(
|
||||||
.uiaa
|
services
|
||||||
.authenticate(&body.auth, flows, params, None)
|
.uiaa
|
||||||
.await?;
|
.authenticate(&body.auth, flows, params, None)
|
||||||
|
.await?,
|
||||||
let password = if let Some(password) = &body.password {
|
)
|
||||||
HashedPassword::new(password)?
|
|
||||||
} else {
|
|
||||||
return Err!(Request(InvalidParam("A password must be provided.")));
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_id = services
|
|
||||||
.users
|
|
||||||
.determine_registration_user_id(body.username.clone(), identity.email.as_ref(), None)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.create_local_account(&user_id, password, identity.email)
|
|
||||||
.await;
|
|
||||||
services.users.join_auto_join_rooms(&user_id).await;
|
|
||||||
user_id
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (token, device) = if !body.inhibit_login {
|
// If the user didn't supply a username but did supply an email, use
|
||||||
// If UIAA is disabled, we can't create a device. In that case only appservices
|
// the email's user as their initial localpart to avoid falling back to
|
||||||
// can reach this point in the first place, so we return an error for them.
|
// a randomly generated localpart
|
||||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
let supplied_username = body.username.clone().or_else(|| {
|
||||||
return Err!(Request(AppserviceLoginUnsupported(
|
if let Some(identity) = &identity
|
||||||
"User-interactive appservice registration is not available on this server."
|
&& let Some(email) = &identity.email
|
||||||
)));
|
{
|
||||||
|
Some(email.user().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Generate new device id if the user didn't specify one
|
let user_id =
|
||||||
|
determine_registration_user_id(&services, supplied_username, emergency_mode_enabled)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||||
|
// For appservice logins, make sure that the user ID is in the appservice's
|
||||||
|
// namespace
|
||||||
|
|
||||||
|
match body.identity {
|
||||||
|
| 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
|
||||||
|
{
|
||||||
|
// For non-appservice logins, ban user IDs which are in an appservice's
|
||||||
|
// namespace (unless emergency mode is enabled)
|
||||||
|
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = if body.identity.is_some() {
|
||||||
|
None
|
||||||
|
} else if let Some(password) = body.password.as_deref() {
|
||||||
|
Some(HashedPassword::new(password)?)
|
||||||
|
} else {
|
||||||
|
return Err!(Request(InvalidParam("A password must be provided")));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
services.users.create(&user_id, password).await?;
|
||||||
|
|
||||||
|
// Set an initial display name
|
||||||
|
let mut displayname = user_id.localpart().to_owned();
|
||||||
|
|
||||||
|
// Apply the new user displayname suffix, if it's set
|
||||||
|
if !services.globals.new_user_displayname_suffix().is_empty() && body.identity.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(PushRulesEvent::new(
|
||||||
|
push::Ruleset::server_default(&user_id).into(),
|
||||||
|
))
|
||||||
|
.expect("should be able to serialize push rules"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Generate new device id if the user didn't specify one
|
||||||
|
let (token, device) = if !body.inhibit_login {
|
||||||
let device_id = body
|
let device_id = body
|
||||||
.device_id
|
.device_id
|
||||||
.clone()
|
.clone()
|
||||||
@@ -128,7 +179,6 @@ pub(crate) async fn register_route(
|
|||||||
&user_id,
|
&user_id,
|
||||||
&device_id,
|
&device_id,
|
||||||
&new_token,
|
&new_token,
|
||||||
None,
|
|
||||||
body.initial_device_display_name.clone(),
|
body.initial_device_display_name.clone(),
|
||||||
Some(client.to_string()),
|
Some(client.to_string()),
|
||||||
)
|
)
|
||||||
@@ -139,7 +189,118 @@ pub(crate) async fn register_route(
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
debug_info!(%user_id, ?device, "New account created via legacy registration");
|
debug_info!(%user_id, ?device, "User account was created");
|
||||||
|
|
||||||
|
// If the user registered with an email, associate it with their account.
|
||||||
|
if let Some(identity) = identity
|
||||||
|
&& let Some(email) = identity.email
|
||||||
|
{
|
||||||
|
// This may fail if the email is already in use, but we already check for that
|
||||||
|
// in `/requestToken`, so ignoring the error is acceptable here in the rare case
|
||||||
|
// that an email is sniped by another user between the `/requestToken` request
|
||||||
|
// and the `/register` request.
|
||||||
|
let _ = services
|
||||||
|
.threepid
|
||||||
|
.associate_localpart_email(user_id.localpart(), &email)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
if body.identity.is_none() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the first user to register an administrator and disable first-run mode.
|
||||||
|
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||||
|
|
||||||
|
// If the registering user was not the first and we're suspending users on
|
||||||
|
// register, suspend them.
|
||||||
|
if !was_first_user && services.config.suspend_on_register {
|
||||||
|
// Note that we can still do auto joins for suspended users
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.suspend_account(&user_id, &services.globals.server_user)
|
||||||
|
.await;
|
||||||
|
// And send an @room notice to the admin room, to prompt admins to review the
|
||||||
|
// new user and ideally unsuspend them if deemed appropriate.
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||||
|
"User {user_id} has been suspended as they are not the first user on this \
|
||||||
|
server. Please review and unsuspend them if appropriate."
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.identity.is_none() && !services.server.config.auto_join_rooms.is_empty() {
|
||||||
|
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 services
|
||||||
|
.rooms
|
||||||
|
.membership
|
||||||
|
.join_room(
|
||||||
|
&user_id,
|
||||||
|
&room_id,
|
||||||
|
Some("Automatically joining this room upon registration".to_owned()),
|
||||||
|
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||||
|
)
|
||||||
|
.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(assign!(register::v3::Response::new(user_id), {
|
Ok(assign!(register::v3::Response::new(user_id), {
|
||||||
access_token: token,
|
access_token: token,
|
||||||
@@ -211,21 +372,21 @@ async fn create_registration_uiaa_session(
|
|||||||
|
|
||||||
// Require all users to agree to the terms and conditions, if configured
|
// Require all users to agree to the terms and conditions, if configured
|
||||||
let terms = &services.config.registration_terms;
|
let terms = &services.config.registration_terms;
|
||||||
if !terms.documents.is_empty() {
|
if !terms.is_empty() {
|
||||||
let mut terms_map = HashMap::new();
|
let mut terms =
|
||||||
|
serde_json::to_value(terms.clone()).expect("failed to serialize terms");
|
||||||
|
|
||||||
for (id, document) in &terms.documents {
|
// Insert a dummy `version` field
|
||||||
terms_map.insert(id.to_owned(), serde_json::json!({
|
for (_, documents) in terms.as_object_mut().unwrap() {
|
||||||
terms.language.clone(): serde_json::to_value(document).expect("should be able to serialize document")
|
let documents = documents.as_object_mut().unwrap();
|
||||||
}));
|
|
||||||
|
documents.insert("version".to_owned(), "latest".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
terms_map.insert("version".to_owned(), "latest".into());
|
|
||||||
|
|
||||||
params.insert(
|
params.insert(
|
||||||
AuthType::Terms.as_str().to_owned(),
|
AuthType::Terms.as_str().to_owned(),
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"policies": terms_map,
|
"policies": terms,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -258,6 +419,81 @@ async fn create_registration_uiaa_session(
|
|||||||
Ok((flows, params))
|
Ok((flows, params))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn determine_registration_user_id(
|
||||||
|
services: &Services,
|
||||||
|
supplied_username: Option<String>,
|
||||||
|
emergency_mode_enabled: bool,
|
||||||
|
) -> Result<OwnedUserId> {
|
||||||
|
if let Some(supplied_username) = supplied_username {
|
||||||
|
// The user gets to pick their username. Do some validation to make sure it's
|
||||||
|
// acceptable.
|
||||||
|
|
||||||
|
// Don't allow registration with forbidden usernames.
|
||||||
|
if services
|
||||||
|
.globals
|
||||||
|
.forbidden_usernames()
|
||||||
|
.is_match(&supplied_username)
|
||||||
|
&& !emergency_mode_enabled
|
||||||
|
{
|
||||||
|
return Err!(Request(Forbidden("Username is forbidden")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and validate the user ID
|
||||||
|
let user_id = match UserId::parse_with_server_name(
|
||||||
|
&supplied_username,
|
||||||
|
services.globals.server_name(),
|
||||||
|
) {
|
||||||
|
| Ok(user_id) => {
|
||||||
|
if let Err(e) = user_id.validate_strict() {
|
||||||
|
// Unless we are in emergency mode, we should follow synapse's behaviour on
|
||||||
|
// not allowing things like spaces and UTF-8 characters in usernames
|
||||||
|
if !emergency_mode_enabled {
|
||||||
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
|
"Username {supplied_username} contains disallowed characters or \
|
||||||
|
spaces: {e}"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow registration with user IDs that aren't local
|
||||||
|
if !services.globals.user_is_local(&user_id) {
|
||||||
|
return Err!(Request(InvalidUsername(
|
||||||
|
"Username {supplied_username} is not local to this server"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id
|
||||||
|
},
|
||||||
|
| Err(e) => {
|
||||||
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
|
"Username {supplied_username} is not valid: {e}"
|
||||||
|
))));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if services.users.exists(&user_id).await {
|
||||||
|
return Err!(Request(UserInUse("User ID is not available.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(user_id)
|
||||||
|
} else {
|
||||||
|
// The user didn't specify a username. Generate a username for
|
||||||
|
// them.
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let 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(&user_id).await {
|
||||||
|
break Ok(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
||||||
///
|
///
|
||||||
/// Requests a validation email for the purpose of registering a new account.
|
/// Requests a validation email for the purpose of registering a new account.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
thirdparty::{Medium, ThirdPartyIdentifierInit},
|
thirdparty::{Medium, ThirdPartyIdentifierInit},
|
||||||
};
|
};
|
||||||
use service::mailer::messages;
|
use service::{mailer::messages, uiaa::Identity};
|
||||||
|
|
||||||
use crate::{Ruma, router::ClientIdentity};
|
use crate::{Ruma, router::ClientIdentity};
|
||||||
|
|
||||||
@@ -124,18 +124,15 @@ pub(crate) async fn add_3pid_route(
|
|||||||
.uiaa
|
.uiaa
|
||||||
.authenticate_password(
|
.authenticate_password(
|
||||||
&body.auth,
|
&body.auth,
|
||||||
body.identity.sender_user(),
|
Some(Identity::from_user_id(body.identity.sender_user())),
|
||||||
body.identity.sender_device(),
|
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let email = services
|
let email = services
|
||||||
.threepid
|
.threepid
|
||||||
.get_valid_session(&body.sid, &body.client_secret)
|
.consume_valid_session(&body.sid, &body.client_secret)
|
||||||
.await
|
.await
|
||||||
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?
|
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
|
||||||
.consume();
|
|
||||||
|
|
||||||
services
|
services
|
||||||
.threepid
|
.threepid
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
use axum::extract::State;
|
|
||||||
use conduwuit::Err;
|
|
||||||
use futures::future::{join, join3};
|
|
||||||
use ruma::api::client::admin::{is_user_locked, lock_user};
|
|
||||||
|
|
||||||
use crate::router::Ruma;
|
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v1/admin/lock/{userId}`
|
|
||||||
///
|
|
||||||
/// Check the account lock status of a target user
|
|
||||||
pub(crate) async fn get_locked_status(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<is_user_locked::v1::Request>,
|
|
||||||
) -> conduwuit::Result<is_user_locked::v1::Response> {
|
|
||||||
let sender_user = body.sender_user();
|
|
||||||
|
|
||||||
let (admin, active) =
|
|
||||||
join(services.users.is_admin(sender_user), services.users.is_active(&body.user_id)).await;
|
|
||||||
if !admin {
|
|
||||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
|
||||||
}
|
|
||||||
if !services.globals.user_is_local(&body.user_id) {
|
|
||||||
return Err!(Request(InvalidParam("Can only check the lock status of local users")));
|
|
||||||
}
|
|
||||||
if !active {
|
|
||||||
return Err!(Request(NotFound("Unknown user")));
|
|
||||||
}
|
|
||||||
Ok(is_user_locked::v1::Response::new(
|
|
||||||
services.users.is_locked(&body.user_id).await?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `PUT /_matrix/client/v1/admin/lock/{userId}`
|
|
||||||
///
|
|
||||||
/// Set the account lock status of a target user
|
|
||||||
pub(crate) async fn put_locked_status(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<lock_user::v1::Request>,
|
|
||||||
) -> conduwuit::Result<lock_user::v1::Response> {
|
|
||||||
let sender_user = body.sender_user();
|
|
||||||
|
|
||||||
let (sender_admin, active, target_admin) = join3(
|
|
||||||
services.users.is_admin(sender_user),
|
|
||||||
services.users.is_active(&body.user_id),
|
|
||||||
services.users.is_admin(&body.user_id),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !sender_admin {
|
|
||||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
|
||||||
}
|
|
||||||
if !services.globals.user_is_local(&body.user_id) {
|
|
||||||
return Err!(Request(InvalidParam("Can only set the locked status of local users")));
|
|
||||||
}
|
|
||||||
if !active {
|
|
||||||
return Err!(Request(NotFound("Unknown user")));
|
|
||||||
}
|
|
||||||
if body.user_id == *sender_user {
|
|
||||||
return Err!(Request(Forbidden("You cannot lock yourself")));
|
|
||||||
}
|
|
||||||
if target_admin {
|
|
||||||
return Err!(Request(Forbidden("You cannot lock another server administrator")));
|
|
||||||
}
|
|
||||||
if services.users.is_locked(&body.user_id).await? == body.locked {
|
|
||||||
// No change
|
|
||||||
return Ok(lock_user::v1::Response::new(body.locked));
|
|
||||||
}
|
|
||||||
|
|
||||||
let action = if body.locked {
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.lock_account(&body.user_id, sender_user)
|
|
||||||
.await;
|
|
||||||
"suspended"
|
|
||||||
} else {
|
|
||||||
services.users.unlock_account(&body.user_id).await;
|
|
||||||
"unsuspended"
|
|
||||||
};
|
|
||||||
|
|
||||||
if services.config.admin_room_notices {
|
|
||||||
// Notify the admin room that an account has been un/suspended
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.send_text(&format!("{} has been {} by {}.", body.user_id, action, sender_user))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(lock_user::v1::Response::new(body.locked))
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
mod lock;
|
|
||||||
pub(crate) mod site;
|
|
||||||
mod suspend;
|
mod suspend;
|
||||||
|
|
||||||
pub(crate) use self::{lock::*, suspend::*};
|
pub(crate) use self::suspend::*;
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
pub(crate) mod rooms;
|
|
||||||
pub(crate) mod users;
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
use axum::extract::State;
|
|
||||||
use conduwuit::{
|
|
||||||
Err, Event, Result,
|
|
||||||
utils::stream::{BroadbandExt, WidebandExt},
|
|
||||||
};
|
|
||||||
use futures::StreamExt;
|
|
||||||
use ruma::{
|
|
||||||
OwnedRoomId,
|
|
||||||
events::{
|
|
||||||
StateEventType,
|
|
||||||
room::{
|
|
||||||
create::RoomCreateEventContent,
|
|
||||||
encryption::PossiblyRedactedRoomEncryptionEventContent,
|
|
||||||
tombstone::PossiblyRedactedRoomTombstoneEventContent,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use ruminuwuity::admin::continuwuity::rooms;
|
|
||||||
use tokio::join;
|
|
||||||
|
|
||||||
use crate::Ruma;
|
|
||||||
|
|
||||||
/// # `GET /_continuwuity/admin/rooms`
|
|
||||||
///
|
|
||||||
/// Lists all room IDs known to this server, excluding banned ones.
|
|
||||||
///
|
|
||||||
/// This is the legacy version of the endpoint, which does not support
|
|
||||||
/// pagination or including banned rooms. It is recommended to use the
|
|
||||||
/// `/v1/rooms` endpoint instead. This endpoint may be removed in a future
|
|
||||||
/// release.
|
|
||||||
pub(crate) async fn legacy_list_rooms_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<rooms::list::unstable::Request>,
|
|
||||||
) -> Result<rooms::list::unstable::Response> {
|
|
||||||
let sender_user = body.identity.sender_user();
|
|
||||||
if !services.users.is_admin(sender_user).await {
|
|
||||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rooms: Vec<OwnedRoomId> = services
|
|
||||||
.rooms
|
|
||||||
.metadata
|
|
||||||
.iter_ids()
|
|
||||||
.filter_map(|room_id| async move {
|
|
||||||
if !services.rooms.metadata.is_banned(&room_id).await {
|
|
||||||
Some(room_id.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
rooms.sort();
|
|
||||||
Ok(rooms::list::unstable::Response::new(rooms))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `GET /_continuwuity/admin/v1/rooms`
|
|
||||||
///
|
|
||||||
/// Lists rooms known to this server.
|
|
||||||
pub(crate) async fn list_rooms_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<rooms::list::v1::Request>,
|
|
||||||
) -> Result<rooms::list::v1::Response> {
|
|
||||||
let sender_user = body.sender_user();
|
|
||||||
if !services.users.is_admin(sender_user).await {
|
|
||||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let include_banned_rooms = body.include_banned_rooms;
|
|
||||||
let rooms = services
|
|
||||||
.rooms
|
|
||||||
.metadata
|
|
||||||
.iter_ids()
|
|
||||||
.wide_filter_map(|room_id| async move {
|
|
||||||
if include_banned_rooms || !services.rooms.metadata.is_banned(&room_id).await {
|
|
||||||
Some(room_id.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.skip(body.offset.unwrap_or_default())
|
|
||||||
.take(body.limit.unwrap_or(100).min(100))
|
|
||||||
.broad_filter_map(|room_id| async move {
|
|
||||||
let (
|
|
||||||
banned,
|
|
||||||
disabled,
|
|
||||||
member_count,
|
|
||||||
local_member_count,
|
|
||||||
resident_server_count,
|
|
||||||
published,
|
|
||||||
create_event,
|
|
||||||
encryption_event,
|
|
||||||
name_event,
|
|
||||||
topic_event,
|
|
||||||
canonical_alias_event,
|
|
||||||
join_rules_event,
|
|
||||||
history_visibility_event,
|
|
||||||
tombstone_event,
|
|
||||||
) = join!(
|
|
||||||
services.rooms.metadata.is_banned(&room_id),
|
|
||||||
services.rooms.metadata.is_disabled(&room_id),
|
|
||||||
services.rooms.state_cache.room_joined_count(&room_id),
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.active_local_users_in_room(&room_id)
|
|
||||||
.count(),
|
|
||||||
services.rooms.state_cache.room_servers(&room_id).count(),
|
|
||||||
services.rooms.directory.is_public_room(&room_id),
|
|
||||||
services.rooms.state_accessor.room_state_get(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomCreate,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get_content::<PossiblyRedactedRoomEncryptionEventContent>(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomEncryption,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
services.rooms.state_accessor.room_state_get_content(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomName,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
services.rooms.state_accessor.room_state_get_content(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomTopic,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
services.rooms.state_accessor.room_state_get_content(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomCanonicalAlias,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
services.rooms.state_accessor.room_state_get_content(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomJoinRules,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
services.rooms.state_accessor.room_state_get_content(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomHistoryVisibility,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get_content::<PossiblyRedactedRoomTombstoneEventContent>(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomTombstone,
|
|
||||||
""
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let Ok(create_event) = create_event else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let create_content = create_event
|
|
||||||
.get_content::<RoomCreateEventContent>()
|
|
||||||
.expect("m.room.create content must be valid");
|
|
||||||
Some(rooms::list::v1::MinimalRoomInfo {
|
|
||||||
room_id,
|
|
||||||
banned,
|
|
||||||
disabled,
|
|
||||||
member_count: usize::try_from(member_count.unwrap_or_default())
|
|
||||||
.expect("u64 should fit in usize"),
|
|
||||||
local_member_count,
|
|
||||||
resident_server_count,
|
|
||||||
creators: vec![create_event.sender],
|
|
||||||
encrypted: encryption_event.is_ok_and(|c| c.algorithm.is_some()),
|
|
||||||
federated: create_content.federate,
|
|
||||||
published,
|
|
||||||
version: create_content.room_version,
|
|
||||||
name: name_event.unwrap_or(None),
|
|
||||||
topic: topic_event.unwrap_or(None),
|
|
||||||
canonical_alias: canonical_alias_event.unwrap_or(None),
|
|
||||||
join_rules: join_rules_event.unwrap_or(None),
|
|
||||||
history_visibility: history_visibility_event.unwrap_or(None),
|
|
||||||
predecessor: create_content.predecessor.map(|c| c.room_id),
|
|
||||||
successor: tombstone_event.map_or(None, |c| c.replacement_room),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
Ok(rooms::list::v1::Response::new(rooms))
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod ban;
|
|
||||||
mod list;
|
|
||||||
|
|
||||||
pub(crate) use ban::ban_room;
|
|
||||||
pub(crate) use list::*;
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
use axum::extract::State;
|
|
||||||
use conduwuit::{
|
|
||||||
Err, err, error, info,
|
|
||||||
utils::{IterStream, stream::BroadbandExt},
|
|
||||||
warn,
|
|
||||||
};
|
|
||||||
use futures::{FutureExt, StreamExt};
|
|
||||||
use ruma::UserId;
|
|
||||||
use ruminuwuity::admin::continuwuity::users;
|
|
||||||
use service::users::HashedPassword;
|
|
||||||
|
|
||||||
use crate::router::Ruma;
|
|
||||||
|
|
||||||
/// # `POST /_continuwuity/admin/v1/users/create`
|
|
||||||
///
|
|
||||||
/// Creates a new user.
|
|
||||||
pub(crate) async fn create_user_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<users::create::v1::Request>,
|
|
||||||
) -> conduwuit::Result<users::create::v1::Response> {
|
|
||||||
let sender_user = body.sender_user();
|
|
||||||
|
|
||||||
if !services.users.is_admin(sender_user).await {
|
|
||||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
|
||||||
}
|
|
||||||
let user_id =
|
|
||||||
&UserId::parse_with_server_name(&body.localpart, services.globals.server_name())?;
|
|
||||||
if services.users.is_active_local(user_id).await {
|
|
||||||
return Err!(Conflict("A user with this username already exists"));
|
|
||||||
}
|
|
||||||
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.create_local_account(
|
|
||||||
user_id,
|
|
||||||
HashedPassword::new(&body.password)?,
|
|
||||||
body.email
|
|
||||||
.clone()
|
|
||||||
.map(lettre::Address::try_from)
|
|
||||||
.transpose()
|
|
||||||
.map_err(|e| err!(Request(BadJson("Invalid email address: {e}"))))?,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if body.suspended {
|
|
||||||
services.users.suspend_account(user_id, sender_user).await;
|
|
||||||
}
|
|
||||||
if body.locked {
|
|
||||||
services.users.lock_account(user_id, sender_user).await;
|
|
||||||
}
|
|
||||||
if body.login_disabled {
|
|
||||||
services.users.disable_login(user_id);
|
|
||||||
}
|
|
||||||
if let Some(ref value) = body.display_name {
|
|
||||||
services.users.set_profile_key(
|
|
||||||
user_id,
|
|
||||||
"displayname",
|
|
||||||
Some(serde_json::to_value(value)?),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(ref value) = body.avatar_url {
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.set_profile_key(user_id, "avatar_url", Some(serde_json::to_value(value)?));
|
|
||||||
}
|
|
||||||
if body.admin {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.make_user_admin(user_id)
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| error!("failed to make new user {user_id} an admin: {e}"))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
if !body.skip_auto_join {
|
|
||||||
services.users.join_auto_join_rooms(user_id).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.auto_join_rooms
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.stream()
|
|
||||||
.broad_filter_map(|room| async move {
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.alias
|
|
||||||
.resolve_with_servers(&room, None)
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| {
|
|
||||||
warn!(
|
|
||||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
|
||||||
{room}: {e}"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.for_each_concurrent(None, |(room_id, servers)| async move {
|
|
||||||
match services
|
|
||||||
.rooms
|
|
||||||
.membership
|
|
||||||
.join_room(
|
|
||||||
user_id,
|
|
||||||
&room_id,
|
|
||||||
Some("Automatically joining this room upon registration".to_owned()),
|
|
||||||
servers.as_ref(),
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Err(e) => {
|
|
||||||
warn!("Failed to automatically join {user_id} to {room_id}: {e}");
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
info!("Automatically joined room {user_id} to {room_id}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(users::create::v1::Response::new(user_id.to_owned()))
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
use axum::extract::State;
|
|
||||||
use conduwuit::{Err, utils::stream::WidebandExt};
|
|
||||||
use futures::StreamExt;
|
|
||||||
use ruminuwuity::admin::continuwuity::users;
|
|
||||||
use tokio::join;
|
|
||||||
|
|
||||||
use crate::router::Ruma;
|
|
||||||
|
|
||||||
/// # `GET /_continuwuity/admin/v1/users`
|
|
||||||
///
|
|
||||||
/// Lists all users on this homeserver.
|
|
||||||
pub(crate) async fn list_users_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<users::list::v1::Request>,
|
|
||||||
) -> conduwuit::Result<users::list::v1::Response> {
|
|
||||||
let sender_user = body.sender_user();
|
|
||||||
|
|
||||||
if !services.users.is_admin(sender_user).await {
|
|
||||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let users = services
|
|
||||||
.users
|
|
||||||
.list_local_users()
|
|
||||||
.skip(body.offset.unwrap_or_default())
|
|
||||||
.take(body.limit.unwrap_or(100).min(100))
|
|
||||||
.wide_filter_map(|user_id| async move {
|
|
||||||
let (deactivated, suspended, locked, admin, login_disabled) = join!(
|
|
||||||
services.users.is_deactivated(&user_id),
|
|
||||||
services.users.is_suspended(&user_id),
|
|
||||||
services.users.is_locked(&user_id),
|
|
||||||
services.users.is_admin(&user_id),
|
|
||||||
services.users.is_login_disabled(&user_id),
|
|
||||||
);
|
|
||||||
Some(users::list::v1::User {
|
|
||||||
user_id: user_id.clone(),
|
|
||||||
deactivated: deactivated.unwrap_or_default(),
|
|
||||||
suspended: suspended.unwrap_or_default(),
|
|
||||||
locked: locked.unwrap_or_default(),
|
|
||||||
admin,
|
|
||||||
login_disabled,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(users::list::v1::Response::new(users))
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod create;
|
|
||||||
mod list;
|
|
||||||
|
|
||||||
pub(crate) use create::*;
|
|
||||||
pub(crate) use list::*;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::{Err, Result};
|
use conduwuit::{Err, Result};
|
||||||
use futures::future::{join, join3};
|
use futures::future::{join, join3};
|
||||||
use ruma::api::client::admin::{is_user_suspended, suspend_user};
|
use ruminuwuity::admin::{get_suspended, set_suspended};
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ use crate::Ruma;
|
|||||||
/// Check the suspension status of a target user
|
/// Check the suspension status of a target user
|
||||||
pub(crate) async fn get_suspended_status(
|
pub(crate) async fn get_suspended_status(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<is_user_suspended::v1::Request>,
|
body: Ruma<get_suspended::v1::Request>,
|
||||||
) -> Result<is_user_suspended::v1::Response> {
|
) -> Result<get_suspended::v1::Response> {
|
||||||
let (admin, active) = join(
|
let (admin, active) = join(
|
||||||
services.users.is_admin(body.identity.sender_user()),
|
services.users.is_admin(body.identity.sender_user()),
|
||||||
services.users.is_active(&body.user_id),
|
services.users.is_active(&body.user_id),
|
||||||
@@ -26,7 +26,7 @@ pub(crate) async fn get_suspended_status(
|
|||||||
if !active {
|
if !active {
|
||||||
return Err!(Request(NotFound("Unknown user")));
|
return Err!(Request(NotFound("Unknown user")));
|
||||||
}
|
}
|
||||||
Ok(is_user_suspended::v1::Response::new(
|
Ok(get_suspended::v1::Response::new(
|
||||||
services.users.is_suspended(&body.user_id).await?,
|
services.users.is_suspended(&body.user_id).await?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,8 @@ pub(crate) async fn get_suspended_status(
|
|||||||
/// Set the suspension status of a target user
|
/// Set the suspension status of a target user
|
||||||
pub(crate) async fn put_suspended_status(
|
pub(crate) async fn put_suspended_status(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<suspend_user::v1::Request>,
|
body: Ruma<set_suspended::v1::Request>,
|
||||||
) -> Result<suspend_user::v1::Response> {
|
) -> Result<set_suspended::v1::Response> {
|
||||||
let sender_user = body.identity.sender_user();
|
let sender_user = body.identity.sender_user();
|
||||||
|
|
||||||
let (sender_admin, active, target_admin) = join3(
|
let (sender_admin, active, target_admin) = join3(
|
||||||
@@ -64,7 +64,7 @@ pub(crate) async fn put_suspended_status(
|
|||||||
}
|
}
|
||||||
if services.users.is_suspended(&body.user_id).await? == body.suspended {
|
if services.users.is_suspended(&body.user_id).await? == body.suspended {
|
||||||
// No change
|
// No change
|
||||||
return Ok(suspend_user::v1::Response::new(body.suspended));
|
return Ok(set_suspended::v1::Response::new(body.suspended));
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = if body.suspended {
|
let action = if body.suspended {
|
||||||
@@ -86,5 +86,5 @@ pub(crate) async fn put_suspended_status(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(suspend_user::v1::Response::new(body.suspended))
|
Ok(set_suspended::v1::Response::new(body.suspended))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use ruma::{
|
|||||||
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use service::uiaa::Identity;
|
||||||
|
|
||||||
use crate::{Ruma, client::DEVICE_ID_LENGTH};
|
use crate::{Ruma, client::DEVICE_ID_LENGTH};
|
||||||
|
|
||||||
@@ -94,7 +95,6 @@ pub(crate) async fn update_device_route(
|
|||||||
&device_id,
|
&device_id,
|
||||||
&appservice.registration.as_token,
|
&appservice.registration.as_token,
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
Some(client.to_string()),
|
Some(client.to_string()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -119,13 +119,14 @@ pub(crate) async fn delete_device_route(
|
|||||||
body: Ruma<delete_device::v3::Request>,
|
body: Ruma<delete_device::v3::Request>,
|
||||||
) -> Result<delete_device::v3::Response> {
|
) -> Result<delete_device::v3::Response> {
|
||||||
let sender_user = body.identity.sender_user();
|
let sender_user = body.identity.sender_user();
|
||||||
|
let appservice = body.identity.appservice_info();
|
||||||
|
|
||||||
// Appservices get to skip UIAA for this endpoint
|
// Appservices get to skip UIAA for this endpoint
|
||||||
if let Some(sender_device) = body.identity.sender_device() {
|
if appservice.is_none() {
|
||||||
// Prompt the user to confirm with their password using UIAA
|
// Prompt the user to confirm with their password using UIAA
|
||||||
let _ = services
|
let _ = services
|
||||||
.uiaa
|
.uiaa
|
||||||
.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,13 +155,14 @@ pub(crate) async fn delete_devices_route(
|
|||||||
body: Ruma<delete_devices::v3::Request>,
|
body: Ruma<delete_devices::v3::Request>,
|
||||||
) -> Result<delete_devices::v3::Response> {
|
) -> Result<delete_devices::v3::Response> {
|
||||||
let sender_user = body.identity.sender_user();
|
let sender_user = body.identity.sender_user();
|
||||||
|
let appservice = body.identity.appservice_info();
|
||||||
|
|
||||||
// Appservices get to skip UIAA for this endpoint
|
// Appservices get to skip UIAA for this endpoint
|
||||||
if let Some(sender_device) = body.identity.sender_device() {
|
if appservice.is_none() {
|
||||||
// Prompt the user to confirm with their password using UIAA
|
// Prompt the user to confirm with their password using UIAA
|
||||||
let _ = services
|
let _ = services
|
||||||
.uiaa
|
.uiaa
|
||||||
.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use ruma::{
|
|||||||
serde::Raw,
|
serde::Raw,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use service::oauth::OAuthTicket;
|
use service::uiaa::Identity;
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
@@ -205,12 +205,7 @@ pub(crate) async fn upload_signing_keys_route(
|
|||||||
{
|
{
|
||||||
let _ = services
|
let _ = services
|
||||||
.uiaa
|
.uiaa
|
||||||
.authenticate_password(
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
&body.auth,
|
|
||||||
sender_user,
|
|
||||||
body.identity.sender_device(),
|
|
||||||
Some(OAuthTicket::CrossSigningReset),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ pub(super) mod media_legacy;
|
|||||||
pub(super) mod membership;
|
pub(super) mod membership;
|
||||||
pub(super) mod message;
|
pub(super) mod message;
|
||||||
pub(super) mod mutual_rooms;
|
pub(super) mod mutual_rooms;
|
||||||
pub(super) mod oauth;
|
|
||||||
pub(super) mod openid;
|
pub(super) mod openid;
|
||||||
pub(super) mod presence;
|
pub(super) mod presence;
|
||||||
pub(super) mod profile;
|
pub(super) mod profile;
|
||||||
@@ -62,7 +61,6 @@ pub(super) use membership::*;
|
|||||||
pub use membership::{leave_all_rooms, leave_room, remote_leave_room};
|
pub use membership::{leave_all_rooms, leave_room, remote_leave_room};
|
||||||
pub(super) use message::*;
|
pub(super) use message::*;
|
||||||
pub(super) use mutual_rooms::*;
|
pub(super) use mutual_rooms::*;
|
||||||
pub(super) use oauth::*;
|
|
||||||
pub(super) use openid::*;
|
pub(super) use openid::*;
|
||||||
pub(super) use presence::*;
|
pub(super) use presence::*;
|
||||||
pub(super) use profile::*;
|
pub(super) use profile::*;
|
||||||
@@ -75,7 +73,6 @@ pub(super) use report::*;
|
|||||||
pub(super) use room::*;
|
pub(super) use room::*;
|
||||||
pub(super) use search::*;
|
pub(super) use search::*;
|
||||||
pub(super) use send::*;
|
pub(super) use send::*;
|
||||||
pub use session::handle_login;
|
|
||||||
pub(super) use session::*;
|
pub(super) use session::*;
|
||||||
pub(super) use space::*;
|
pub(super) use space::*;
|
||||||
pub(super) use state::*;
|
pub(super) use state::*;
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
use axum::{
|
|
||||||
Json, Router,
|
|
||||||
extract::{Request, State},
|
|
||||||
middleware::{self, Next},
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
routing::method_routing::{get, post},
|
|
||||||
};
|
|
||||||
use const_str::concat;
|
|
||||||
use http::StatusCode;
|
|
||||||
use serde_json::json;
|
|
||||||
pub(crate) use server_metadata::*;
|
|
||||||
|
|
||||||
mod register_client;
|
|
||||||
mod server_metadata;
|
|
||||||
mod token;
|
|
||||||
|
|
||||||
const BASE_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/oauth2/");
|
|
||||||
const AUTH_CODE_PATH: &str = "grant/authorization_code";
|
|
||||||
const JWKS_URI_PATH: &str = "client/keys.json";
|
|
||||||
const CLIENT_REGISTER_PATH: &str = "client/register";
|
|
||||||
const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
|
||||||
const TOKEN_PATH: &str = "grant/token";
|
|
||||||
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
|
||||||
|
|
||||||
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
|
||||||
Router::new()
|
|
||||||
.nest(BASE_PATH, oauth_router())
|
|
||||||
.route(
|
|
||||||
"/.well-known/openid-configuration",
|
|
||||||
get(
|
|
||||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
|
||||||
async |State(services): State<crate::State>| {
|
|
||||||
Json(authorization_server_metadata(&services).await)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.layer(middleware::from_fn_with_state(
|
|
||||||
state,
|
|
||||||
async |State(state): State<crate::State>, request: Request, next: Next| -> Response {
|
|
||||||
if state.config.oauth.compatibility_mode.oauth_available() {
|
|
||||||
next.run(request).await
|
|
||||||
} else {
|
|
||||||
(StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn oauth_router() -> Router<crate::State> {
|
|
||||||
Router::new()
|
|
||||||
.route(concat!("/", CLIENT_REGISTER_PATH), post(register_client::register_client_route))
|
|
||||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
|
||||||
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
|
|
||||||
.route(concat!("/", TOKEN_PATH), post(token::token_route))
|
|
||||||
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
use axum::{
|
|
||||||
Json,
|
|
||||||
extract::State,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use http::StatusCode;
|
|
||||||
use serde::Serialize;
|
|
||||||
use service::oauth::client_metadata::ClientMetadata;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct RegisteredClient {
|
|
||||||
client_id: String,
|
|
||||||
#[serde(flatten)]
|
|
||||||
metadata: ClientMetadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn register_client_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
Json(metadata): Json<ClientMetadata>,
|
|
||||||
) -> Result<Response, Response> {
|
|
||||||
let client_id = services
|
|
||||||
.oauth
|
|
||||||
.register_client(&metadata)
|
|
||||||
.await
|
|
||||||
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_owned()).into_response())?;
|
|
||||||
|
|
||||||
Ok(Json(RegisteredClient { client_id, metadata }).into_response())
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
use axum::extract::State;
|
|
||||||
use conduwuit::{Err, Result};
|
|
||||||
use ruma::{
|
|
||||||
api::client::discovery::get_authorization_server_metadata::{
|
|
||||||
self, v1::AccountManagementAction,
|
|
||||||
},
|
|
||||||
serde::Raw,
|
|
||||||
};
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
use service::Services;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Ruma,
|
|
||||||
client::oauth::{
|
|
||||||
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH,
|
|
||||||
TOKEN_REVOKE_PATH,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) async fn get_authorization_server_metadata_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
_body: Ruma<get_authorization_server_metadata::v1::Request>,
|
|
||||||
) -> Result<get_authorization_server_metadata::v1::Response> {
|
|
||||||
if !services.config.oauth.compatibility_mode.oauth_available() {
|
|
||||||
return Err!(Request(Unrecognized("OAuth is unavailable on this server")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = Raw::new(&authorization_server_metadata(&services).await).unwrap();
|
|
||||||
|
|
||||||
Ok(get_authorization_server_metadata::v1::Response::new(metadata.cast_unchecked()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn authorization_server_metadata(services: &Services) -> Value {
|
|
||||||
let endpoint_base = services
|
|
||||||
.config
|
|
||||||
.get_client_domain()
|
|
||||||
.join(super::BASE_PATH)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
json!({
|
|
||||||
"account_management_uri": endpoint_base.join(ACCOUNT_MANAGEMENT_PATH).unwrap(),
|
|
||||||
"account_management_actions_supported": [
|
|
||||||
AccountManagementAction::AccountDeactivate,
|
|
||||||
AccountManagementAction::CrossSigningReset,
|
|
||||||
AccountManagementAction::DeviceDelete,
|
|
||||||
AccountManagementAction::DeviceView,
|
|
||||||
AccountManagementAction::DevicesList,
|
|
||||||
AccountManagementAction::Profile,
|
|
||||||
],
|
|
||||||
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
|
|
||||||
"code_challenge_methods_supported": ["S256"],
|
|
||||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
|
||||||
"issuer": services.config.get_client_domain(),
|
|
||||||
"jwks_uri": endpoint_base.join(JWKS_URI_PATH).unwrap(),
|
|
||||||
"prompt_values_supported": ["create"],
|
|
||||||
"registration_endpoint": endpoint_base.join(CLIENT_REGISTER_PATH).unwrap(),
|
|
||||||
"response_modes_supported": ["query", "fragment"],
|
|
||||||
"response_types_supported": ["code"],
|
|
||||||
"revocation_endpoint": endpoint_base.join(TOKEN_REVOKE_PATH).unwrap(),
|
|
||||||
"token_endpoint": endpoint_base.join(TOKEN_PATH).unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
use axum::{Form, Json, extract::State, response::IntoResponse};
|
|
||||||
use http::StatusCode;
|
|
||||||
use service::oauth::grant::{RevokeTokenRequest, TokenRequest};
|
|
||||||
|
|
||||||
pub(crate) async fn token_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
Form(request): Form<TokenRequest>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
match services.oauth.issue_token(request).await {
|
|
||||||
| Ok(response) => Ok(Json(response)),
|
|
||||||
| Err(err) => Err((StatusCode::BAD_REQUEST, err.message())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn revoke_token_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
Form(request): Form<RevokeTokenRequest>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
match services.oauth.revoke_token(request.token).await {
|
|
||||||
| Ok(()) => Ok(StatusCode::OK),
|
|
||||||
| Err(err) => Err((StatusCode::BAD_REQUEST, err.message())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+20
-31
@@ -21,7 +21,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
login::{
|
login::{
|
||||||
self,
|
self,
|
||||||
v3::{DiscoveryInfo, HomeserverInfo, LoginInfo},
|
v3::{DiscoveryInfo, HomeserverInfo},
|
||||||
},
|
},
|
||||||
logout, logout_all,
|
logout, logout_all,
|
||||||
},
|
},
|
||||||
@@ -29,6 +29,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
assign,
|
assign,
|
||||||
};
|
};
|
||||||
|
use service::uiaa::Identity;
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
@@ -43,12 +44,6 @@ pub(crate) async fn get_login_types_route(
|
|||||||
ClientIp(client): ClientIp,
|
ClientIp(client): ClientIp,
|
||||||
_body: Ruma<get_login_types::v3::Request>,
|
_body: Ruma<get_login_types::v3::Request>,
|
||||||
) -> Result<get_login_types::v3::Response> {
|
) -> Result<get_login_types::v3::Response> {
|
||||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
|
||||||
return Err!(Request(Unrecognized(
|
|
||||||
"User-interactive authentication is not available on this server."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(get_login_types::v3::Response::new(vec![
|
Ok(get_login_types::v3::Response::new(vec![
|
||||||
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
||||||
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
||||||
@@ -58,7 +53,7 @@ pub(crate) async fn get_login_types_route(
|
|||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_login(
|
pub(crate) async fn handle_login(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
identifier: Option<&UserIdentifier>,
|
identifier: Option<&UserIdentifier>,
|
||||||
password: &str,
|
password: &str,
|
||||||
@@ -92,6 +87,10 @@ pub async fn handle_login(
|
|||||||
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
|
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if services.users.is_locked(&user_id).await? {
|
||||||
|
return Err!(Request(UserLocked("This account has been locked.")));
|
||||||
|
}
|
||||||
|
|
||||||
if services.users.is_login_disabled(&user_id).await {
|
if services.users.is_login_disabled(&user_id).await {
|
||||||
warn!(%user_id, "user attempted to log in with a login-disabled account");
|
warn!(%user_id, "user attempted to log in with a login-disabled account");
|
||||||
return Err!(Request(Forbidden("This account is not permitted to log in.")));
|
return Err!(Request(Forbidden("This account is not permitted to log in.")));
|
||||||
@@ -120,29 +119,19 @@ pub(crate) async fn login_route(
|
|||||||
ClientIp(client): ClientIp,
|
ClientIp(client): ClientIp,
|
||||||
body: Ruma<login::v3::Request>,
|
body: Ruma<login::v3::Request>,
|
||||||
) -> Result<login::v3::Response> {
|
) -> Result<login::v3::Response> {
|
||||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
|
||||||
return match body.login_info {
|
|
||||||
| LoginInfo::ApplicationService(_) => {
|
|
||||||
Err!(Request(AppserviceLoginUnsupported(
|
|
||||||
"User-interactive appservice login is not available on this server."
|
|
||||||
)))
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
Err!(Request(Unrecognized(
|
|
||||||
"User-interactive authentication is not available on this server."
|
|
||||||
)))
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||||
|
|
||||||
// Validate login method
|
// Validate login method
|
||||||
|
// TODO: Other login methods
|
||||||
let user_id = match &body.login_info {
|
let user_id = match &body.login_info {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
| LoginInfo::Password(login::v3::Password { identifier, password, user, .. }) =>
|
| login::v3::LoginInfo::Password(login::v3::Password {
|
||||||
handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
|
identifier,
|
||||||
| LoginInfo::Token(login::v3::Token { token, .. }) => {
|
password,
|
||||||
|
user,
|
||||||
|
..
|
||||||
|
}) => handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
|
||||||
|
| login::v3::LoginInfo::Token(login::v3::Token { token, .. }) => {
|
||||||
debug!("Got token login type");
|
debug!("Got token login type");
|
||||||
if !services.server.config.login_via_existing_session {
|
if !services.server.config.login_via_existing_session {
|
||||||
return Err!(Request(Unknown("Token login is not enabled.")));
|
return Err!(Request(Unknown("Token login is not enabled.")));
|
||||||
@@ -150,7 +139,7 @@ pub(crate) async fn login_route(
|
|||||||
services.users.find_from_login_token(token).await?
|
services.users.find_from_login_token(token).await?
|
||||||
},
|
},
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
| LoginInfo::ApplicationService(login::v3::ApplicationService {
|
| login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
|
||||||
identifier,
|
identifier,
|
||||||
user,
|
user,
|
||||||
..
|
..
|
||||||
@@ -184,6 +173,7 @@ pub(crate) async fn login_route(
|
|||||||
user_id
|
user_id
|
||||||
},
|
},
|
||||||
| _ => {
|
| _ => {
|
||||||
|
debug!("/login json_body: {:?}", &body.json_body);
|
||||||
return Err!(Request(Unknown(
|
return Err!(Request(Unknown(
|
||||||
debug_warn!(?body.login_info, "Invalid or unsupported login type")
|
debug_warn!(?body.login_info, "Invalid or unsupported login type")
|
||||||
)));
|
)));
|
||||||
@@ -213,7 +203,7 @@ pub(crate) async fn login_route(
|
|||||||
if device_exists {
|
if device_exists {
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
.set_token(&user_id, &device_id, &token, None)
|
.set_token(&user_id, &device_id, &token)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
services
|
services
|
||||||
@@ -222,7 +212,6 @@ pub(crate) async fn login_route(
|
|||||||
&user_id,
|
&user_id,
|
||||||
&device_id,
|
&device_id,
|
||||||
&token,
|
&token,
|
||||||
None,
|
|
||||||
body.initial_device_display_name.clone(),
|
body.initial_device_display_name.clone(),
|
||||||
Some(client.to_string()),
|
Some(client.to_string()),
|
||||||
)
|
)
|
||||||
@@ -261,7 +250,7 @@ pub(crate) async fn login_token_route(
|
|||||||
ClientIp(client): ClientIp,
|
ClientIp(client): ClientIp,
|
||||||
body: Ruma<get_login_token::v1::Request>,
|
body: Ruma<get_login_token::v1::Request>,
|
||||||
) -> Result<get_login_token::v1::Response> {
|
) -> Result<get_login_token::v1::Response> {
|
||||||
if !services.config.login_via_existing_session {
|
if !services.server.config.login_via_existing_session {
|
||||||
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
|
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +259,7 @@ pub(crate) async fn login_token_route(
|
|||||||
// Prompt the user to confirm with their password using UIAA
|
// Prompt the user to confirm with their password using UIAA
|
||||||
let _ = services
|
let _ = services
|
||||||
.uiaa
|
.uiaa
|
||||||
.authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let login_token = utils::random_string(TOKEN_LENGTH);
|
let login_token = utils::random_string(TOKEN_LENGTH);
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ pub(crate) async fn sync_events_v5_route(
|
|||||||
ClientIp(client_ip): ClientIp,
|
ClientIp(client_ip): ClientIp,
|
||||||
body: Ruma<sync_events::v5::Request>,
|
body: Ruma<sync_events::v5::Request>,
|
||||||
) -> Result<sync_events::v5::Response> {
|
) -> Result<sync_events::v5::Response> {
|
||||||
|
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted");
|
||||||
let sender_user = body.identity.sender_user();
|
let sender_user = body.identity.sender_user();
|
||||||
let sender_device = body.identity.expect_sender_device()?;
|
let sender_device = body.identity.expect_sender_device()?;
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ pub(crate) async fn get_supported_versions_route(
|
|||||||
/// `/_matrix/federation/v1/version`
|
/// `/_matrix/federation/v1/version`
|
||||||
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
|
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"name": conduwuit::BRANDING,
|
"name": conduwuit::version::name(),
|
||||||
"version": conduwuit::version(),
|
"version": conduwuit::version::version(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use conduwuit::{Err, Result};
|
|||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::discovery::{
|
api::client::discovery::{
|
||||||
discover_homeserver::{self, HomeserverInfo},
|
discover_homeserver::{self, HomeserverInfo},
|
||||||
discover_policy_server, discover_support,
|
discover_policy_server,
|
||||||
|
discover_support::{self, Contact, ContactRole},
|
||||||
},
|
},
|
||||||
assign,
|
assign,
|
||||||
};
|
};
|
||||||
@@ -66,7 +67,46 @@ pub(crate) async fn well_known_support(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(ToString::to_string);
|
.map(ToString::to_string);
|
||||||
|
|
||||||
let contacts = services.admin.get_support_contacts().await;
|
let email_address = services.config.well_known.support_email.clone();
|
||||||
|
let matrix_id = services.config.well_known.support_mxid.clone();
|
||||||
|
let pgp_key = services.config.well_known.support_pgp_key.clone();
|
||||||
|
|
||||||
|
// TODO: support defining multiple contacts in the config
|
||||||
|
let mut contacts: Vec<Contact> = vec![];
|
||||||
|
|
||||||
|
let role = services
|
||||||
|
.config
|
||||||
|
.well_known
|
||||||
|
.support_role
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(ContactRole::Admin);
|
||||||
|
|
||||||
|
// Add configured contact if at least one contact method is specified
|
||||||
|
let configured_contact = match (matrix_id, email_address) {
|
||||||
|
| (Some(matrix_id), email_address) =>
|
||||||
|
Some(assign!(Contact::with_matrix_id(role, matrix_id), { email_address })),
|
||||||
|
| (None, Some(email_address)) => Some(Contact::with_email_address(role, email_address)),
|
||||||
|
| (None, None) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut configured_contact) = configured_contact {
|
||||||
|
configured_contact.pgp_key = pgp_key;
|
||||||
|
|
||||||
|
contacts.push(configured_contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to add admin users as contacts if no contacts are configured
|
||||||
|
if contacts.is_empty() {
|
||||||
|
let admin_users = services.admin.get_admins().await;
|
||||||
|
|
||||||
|
for user_id in &admin_users {
|
||||||
|
if *user_id == services.globals.server_user {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts.push(Contact::with_matrix_id(ContactRole::Admin, user_id.to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if contacts.is_empty() && support_page.is_none() {
|
if contacts.is_empty() && support_page.is_none() {
|
||||||
// No admin room, no configured contacts, and no support page
|
// No admin room, no configured contacts, and no support page
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,4 @@
|
|||||||
#![type_length_limit = "16384"] //TODO: reduce me
|
#![type_length_limit = "16384"] //TODO: reduce me
|
||||||
#![recursion_limit = "256"] // My Giant Async Function
|
|
||||||
#![allow(clippy::toplevel_ref_arg)]
|
#![allow(clippy::toplevel_ref_arg)]
|
||||||
|
|
||||||
extern crate conduwuit_core as conduwuit;
|
extern crate conduwuit_core as conduwuit;
|
||||||
@@ -11,6 +10,8 @@ pub mod client;
|
|||||||
pub mod router;
|
pub mod router;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
|
pub mod admin;
|
||||||
|
|
||||||
pub(crate) use self::router::{Ruma, RumaResponse, State};
|
pub(crate) use self::router::{Ruma, RumaResponse, State};
|
||||||
|
|
||||||
conduwuit::mod_ctor! {}
|
conduwuit::mod_ctor! {}
|
||||||
|
|||||||
+7
-21
@@ -10,18 +10,16 @@ use axum::{
|
|||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
routing::{any, get, post},
|
routing::{any, get, post},
|
||||||
};
|
};
|
||||||
use conduwuit::err;
|
use conduwuit::{Server, err};
|
||||||
pub(super) use conduwuit_service::state::State;
|
pub(super) use conduwuit_service::state::State;
|
||||||
use http::{Uri, uri};
|
use http::{Uri, uri};
|
||||||
|
|
||||||
use self::handler::RouterExt;
|
use self::handler::RouterExt;
|
||||||
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
||||||
#[cfg(feature = "admin_api")]
|
use crate::{admin, client, server};
|
||||||
use crate::client::admin::site as admin_api;
|
|
||||||
use crate::{client, server};
|
|
||||||
|
|
||||||
pub fn build(router: Router<State>, state: State) -> Router<State> {
|
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||||
let config = &state.server.config;
|
let config = &server.config;
|
||||||
let mut router = router
|
let mut router = router
|
||||||
.ruma_route(&client::appservice_ping)
|
.ruma_route(&client::appservice_ping)
|
||||||
.ruma_route(&client::get_supported_versions_route)
|
.ruma_route(&client::get_supported_versions_route)
|
||||||
@@ -183,17 +181,15 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
|||||||
.ruma_route(&client::get_room_summary)
|
.ruma_route(&client::get_room_summary)
|
||||||
.ruma_route(&client::get_suspended_status)
|
.ruma_route(&client::get_suspended_status)
|
||||||
.ruma_route(&client::put_suspended_status)
|
.ruma_route(&client::put_suspended_status)
|
||||||
.ruma_route(&client::get_locked_status)
|
|
||||||
.ruma_route(&client::put_locked_status)
|
|
||||||
.ruma_route(&client::well_known_support)
|
.ruma_route(&client::well_known_support)
|
||||||
.ruma_route(&client::well_known_client)
|
.ruma_route(&client::well_known_client)
|
||||||
.ruma_route(&client::well_known_policy_server)
|
.ruma_route(&client::well_known_policy_server)
|
||||||
.ruma_route(&client::get_rtc_transports)
|
.ruma_route(&client::get_rtc_transports)
|
||||||
.ruma_route(&client::room_initial_sync_route)
|
.ruma_route(&client::room_initial_sync_route)
|
||||||
.ruma_route(&client::get_authorization_server_metadata_route)
|
|
||||||
.merge(client::oauth::router(state))
|
|
||||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version));
|
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||||
|
.ruma_route(&admin::rooms::ban::ban_room)
|
||||||
|
.ruma_route(&admin::rooms::list::list_rooms);
|
||||||
|
|
||||||
if config.allow_federation {
|
if config.allow_federation {
|
||||||
router = router
|
router = router
|
||||||
@@ -279,16 +275,6 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
|||||||
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
|
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "admin_api")]
|
|
||||||
{
|
|
||||||
router = router
|
|
||||||
.ruma_route(&admin_api::users::list_users_route)
|
|
||||||
.ruma_route(&admin_api::users::create_user_route)
|
|
||||||
.ruma_route(&admin_api::rooms::ban_room)
|
|
||||||
.ruma_route(&admin_api::rooms::legacy_list_rooms_route)
|
|
||||||
.ruma_route(&admin_api::rooms::list_rooms_route);
|
|
||||||
};
|
|
||||||
|
|
||||||
router
|
router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-25
@@ -1,7 +1,6 @@
|
|||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
|
|
||||||
use conduwuit::{Err, Error, Result, err};
|
use conduwuit::{Err, Result, err};
|
||||||
use http::StatusCode;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
||||||
api::{
|
api::{
|
||||||
@@ -11,15 +10,12 @@ use ruma::{
|
|||||||
AuthScheme, NoAccessToken, NoAuthentication,
|
AuthScheme, NoAccessToken, NoAuthentication,
|
||||||
},
|
},
|
||||||
client,
|
client,
|
||||||
error::{ErrorKind, UnknownTokenErrorData},
|
|
||||||
federation::authentication::ServerSignatures,
|
federation::authentication::ServerSignatures,
|
||||||
},
|
},
|
||||||
assign,
|
|
||||||
};
|
};
|
||||||
use service::{
|
use service::{
|
||||||
Services,
|
Services,
|
||||||
server_keys::{PubKeyMap, PubKeys},
|
server_keys::{PubKeyMap, PubKeys},
|
||||||
users::AccessTokenStatus,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
|
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
|
||||||
@@ -157,20 +153,7 @@ impl CheckAuth for AccessToken {
|
|||||||
query: AuthQueryParams,
|
query: AuthQueryParams,
|
||||||
route: TypeId,
|
route: TypeId,
|
||||||
) -> Result<Self::Identity> {
|
) -> Result<Self::Identity> {
|
||||||
if let Some((sender_user, sender_device, status)) =
|
if let Ok((sender_user, sender_device)) = services.users.find_from_token(&output).await {
|
||||||
services.users.find_from_token(&output).await
|
|
||||||
{
|
|
||||||
// If the token is expired we return a soft logout
|
|
||||||
if matches!(status, AccessTokenStatus::Expired) {
|
|
||||||
return Err(Error::Request(
|
|
||||||
ErrorKind::UnknownToken(
|
|
||||||
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
|
|
||||||
),
|
|
||||||
"This token has expired".into(),
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Locked users can only use /logout and /logout/all
|
// Locked users can only use /logout and /logout/all
|
||||||
if services
|
if services
|
||||||
.users
|
.users
|
||||||
@@ -181,7 +164,7 @@ impl CheckAuth for AccessToken {
|
|||||||
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|
||||||
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
|
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
|
||||||
{
|
{
|
||||||
return Err!(Request(UserLocked("Your account is locked.")));
|
return Err!(Request(Unauthorized("Your account is locked.")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,11 +215,7 @@ impl CheckAuth for AccessToken {
|
|||||||
appservice_info: Box::new(appservice_info),
|
appservice_info: Box::new(appservice_info),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Request(
|
Err!(Request(Unauthorized("Invalid access token.")))
|
||||||
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
|
|
||||||
"Invalid token".into(),
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ async fn handle_room(
|
|||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
|
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
|
||||||
|
.boxed()
|
||||||
.await
|
.await
|
||||||
.map(|_| ());
|
.map(|_| ());
|
||||||
results.push((event_id, result));
|
results.push((event_id, result));
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ pub(crate) async fn get_server_version_route(
|
|||||||
) -> Result<get_server_version::v1::Response> {
|
) -> Result<get_server_version::v1::Response> {
|
||||||
Ok(assign!(get_server_version::v1::Response::new(), {
|
Ok(assign!(get_server_version::v1::Response::new(), {
|
||||||
server: Some(assign!(get_server_version::v1::Server::new(), {
|
server: Some(assign!(get_server_version::v1::Server::new(), {
|
||||||
name: Some(conduwuit::BRANDING.into()),
|
name: Some(conduwuit::version::name().into()),
|
||||||
version: Some(conduwuit::version().into()),
|
version: Some(conduwuit::version::version().into()),
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-81
@@ -4,7 +4,7 @@ pub mod manager;
|
|||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet, HashMap},
|
||||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
@@ -375,7 +375,7 @@ pub struct Config {
|
|||||||
#[serde(default = "default_max_request_size")]
|
#[serde(default = "default_max_request_size")]
|
||||||
pub max_request_size: usize,
|
pub max_request_size: usize,
|
||||||
|
|
||||||
/// default: 192
|
/// default: 256
|
||||||
#[serde(default = "default_max_fetch_prev_events")]
|
#[serde(default = "default_max_fetch_prev_events")]
|
||||||
pub max_fetch_prev_events: u16,
|
pub max_fetch_prev_events: u16,
|
||||||
|
|
||||||
@@ -655,25 +655,19 @@ pub struct Config {
|
|||||||
/// even if `recaptcha_site_key` is set.
|
/// even if `recaptcha_site_key` is set.
|
||||||
pub recaptcha_private_site_key: Option<String>,
|
pub recaptcha_private_site_key: Option<String>,
|
||||||
|
|
||||||
/// display: nested
|
/// Policy documents, such as terms and conditions or a privacy policy,
|
||||||
#[serde(default)]
|
/// which users must agree to when registering an account.
|
||||||
pub registration_terms: RegistrationTerms,
|
|
||||||
|
|
||||||
/// display: nested
|
|
||||||
#[serde(default)]
|
|
||||||
pub oauth: OauthConfig,
|
|
||||||
|
|
||||||
/// Controls whether users are allowed to deactivate their own accounts
|
|
||||||
/// through the account management panel or their Matrix clients. Server
|
|
||||||
/// admins can always deactivate users using the relevant admin commands.
|
|
||||||
///
|
///
|
||||||
/// Note that, in some jurisdictions, you may be legally required to honor
|
/// Example:
|
||||||
/// users who request to deactivate their accounts if you set this option
|
/// ```ignore
|
||||||
/// to `false`.
|
/// [global.registration_terms.privacy_policy]
|
||||||
|
/// en = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||||
|
/// es = { name = "Política de Privacidad", url = "https://homeserver.example/es/privacy_policy.html" }
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// default: true
|
/// default: {}
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default)]
|
||||||
pub allow_deactivation: bool,
|
pub registration_terms: HashMap<String, HashMap<String, TermsDocument>>,
|
||||||
|
|
||||||
/// Controls whether encrypted rooms and events are allowed.
|
/// Controls whether encrypted rooms and events are allowed.
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
@@ -2357,30 +2351,6 @@ pub struct SmtpConfig {
|
|||||||
pub require_email_for_token_registration: bool,
|
pub require_email_for_token_registration: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[config_example_generator(
|
|
||||||
filename = "conduwuit-example.toml",
|
|
||||||
section = "global.registration_terms",
|
|
||||||
optional = "true"
|
|
||||||
)]
|
|
||||||
pub struct RegistrationTerms {
|
|
||||||
/// The language code to provide to clients along with the policy documents.
|
|
||||||
///
|
|
||||||
/// default: "en"
|
|
||||||
pub language: String,
|
|
||||||
/// Policy documents, such as terms and conditions or a privacy policy,
|
|
||||||
/// which users must agree to when registering an account.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```ignore
|
|
||||||
/// [global.registration_terms.documents]
|
|
||||||
/// privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// default: {}
|
|
||||||
pub documents: BTreeMap<String, TermsDocument>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A policy document for use with a m.login.terms stage.
|
/// A policy document for use with a m.login.terms stage.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct TermsDocument {
|
pub struct TermsDocument {
|
||||||
@@ -2388,43 +2358,6 @@ pub struct TermsDocument {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
|
||||||
#[config_example_generator(
|
|
||||||
filename = "conduwuit-example.toml",
|
|
||||||
section = "global.oauth",
|
|
||||||
optional = "true"
|
|
||||||
)]
|
|
||||||
pub struct OauthConfig {
|
|
||||||
/// The compatibility mode to use for OAuth.
|
|
||||||
///
|
|
||||||
/// - "disabled": OAuth will be unavailable. Users will only be able to log
|
|
||||||
/// in using legacy authentication.
|
|
||||||
/// - "hybrid": OAuth and legacy authentication will both be available. Some
|
|
||||||
/// clients may only use one or the other.
|
|
||||||
/// - "exclusive": Only OAuth will be available. Clients which require
|
|
||||||
/// legacy authentication will be unable to log in.
|
|
||||||
///
|
|
||||||
/// default: "hybrid"
|
|
||||||
pub compatibility_mode: OAuthMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum OAuthMode {
|
|
||||||
Disabled,
|
|
||||||
#[default]
|
|
||||||
Hybrid,
|
|
||||||
Exclusive,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OAuthMode {
|
|
||||||
#[must_use]
|
|
||||||
pub fn uiaa_available(&self) -> bool { matches!(self, Self::Disabled | Self::Hybrid) }
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn oauth_available(&self) -> bool { matches!(self, Self::Hybrid | Self::Exclusive) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEPRECATED_KEYS: &[&str] = &[
|
const DEPRECATED_KEYS: &[&str] = &[
|
||||||
"cache_capacity",
|
"cache_capacity",
|
||||||
"conduit_cache_capacity_modifier",
|
"conduit_cache_capacity_modifier",
|
||||||
@@ -2616,7 +2549,7 @@ fn default_pusher_timeout() -> u64 { 60 }
|
|||||||
|
|
||||||
fn default_pusher_idle_timeout() -> u64 { 15 }
|
fn default_pusher_idle_timeout() -> u64 { 15 }
|
||||||
|
|
||||||
fn default_max_fetch_prev_events() -> u16 { 192_u16 }
|
fn default_max_fetch_prev_events() -> u16 { 256_u16 }
|
||||||
|
|
||||||
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
|
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ impl Error {
|
|||||||
match self {
|
match self {
|
||||||
| Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
|
| Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
|
||||||
| Self::Ruma(error) => response::ruma_error_message(error),
|
| Self::Ruma(error) => response::ruma_error_message(error),
|
||||||
| Self::Request(_, message, _) => message.clone().into_owned(),
|
|
||||||
| _ => format!("{self}"),
|
| _ => format!("{self}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,11 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
|||||||
// 413
|
// 413
|
||||||
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
|
|
||||||
|
// 405
|
||||||
|
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
|
||||||
|
|
||||||
// 404
|
// 404
|
||||||
| Unrecognized | NotFound => StatusCode::NOT_FOUND,
|
| NotFound => StatusCode::NOT_FOUND,
|
||||||
|
|
||||||
// 403
|
// 403
|
||||||
| GuestAccessForbidden
|
| GuestAccessForbidden
|
||||||
|
|||||||
@@ -7,16 +7,19 @@
|
|||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
pub const BRANDING: &str = "continuwuity";
|
static BRANDING: &str = "continuwuity";
|
||||||
pub const ROUTE_PREFIX: &str = "/_continuwuity";
|
static WEBSITE: &str = "https://continuwuity.org";
|
||||||
pub const WEBSITE: &str = "https://continuwuity.org";
|
static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
|
||||||
pub const SEMANTIC: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
static VERSION: OnceLock<String> = OnceLock::new();
|
static VERSION: OnceLock<String> = OnceLock::new();
|
||||||
static VERSION_UA: OnceLock<String> = OnceLock::new();
|
static VERSION_UA: OnceLock<String> = OnceLock::new();
|
||||||
static USER_AGENT: OnceLock<String> = OnceLock::new();
|
static USER_AGENT: OnceLock<String> = OnceLock::new();
|
||||||
static USER_AGENT_MEDIA: OnceLock<String> = OnceLock::new();
|
static USER_AGENT_MEDIA: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn name() -> &'static str { BRANDING }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
|
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
|
||||||
|
|
||||||
@@ -29,10 +32,10 @@ pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn user_agent_media() -> &'static str { USER_AGENT_MEDIA.get_or_init(init_user_agent_media) }
|
pub fn user_agent_media() -> &'static str { USER_AGENT_MEDIA.get_or_init(init_user_agent_media) }
|
||||||
|
|
||||||
fn init_user_agent() -> String { format!("{BRANDING}/{} (bot; +{WEBSITE})", version_ua()) }
|
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
|
||||||
|
|
||||||
fn init_user_agent_media() -> String {
|
fn init_user_agent_media() -> String {
|
||||||
format!("{BRANDING}/{} (embedbot; facebookexternalhit/1.1; +{WEBSITE})", version_ua())
|
format!("{}/{} (embedbot; facebookexternalhit/1.1; +{WEBSITE})", name(), version_ua())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_version_ua() -> String {
|
fn init_version_ua() -> String {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ pub fn versions() -> Vec<String> {
|
|||||||
"v1.12".to_owned(),
|
"v1.12".to_owned(),
|
||||||
"v1.13".to_owned(),
|
"v1.13".to_owned(),
|
||||||
"v1.14".to_owned(),
|
"v1.14".to_owned(),
|
||||||
"v1.15".to_owned(),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -34,7 +34,10 @@ pub use ::tracing;
|
|||||||
pub use conduwuit_build_metadata as build_metadata;
|
pub use conduwuit_build_metadata as build_metadata;
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use info::version::*;
|
pub use info::{
|
||||||
|
version,
|
||||||
|
version::{name, version},
|
||||||
|
};
|
||||||
pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu, state_res};
|
pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu, state_res};
|
||||||
pub use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
|
pub use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
|
||||||
pub use server::Server;
|
pub use server::Server;
|
||||||
|
|||||||
+10
-16
@@ -61,23 +61,17 @@ pub fn format(ts: SystemTime, str: &str) -> String {
|
|||||||
pub fn pretty(d: Duration) -> String {
|
pub fn pretty(d: Duration) -> String {
|
||||||
use Unit::*;
|
use Unit::*;
|
||||||
|
|
||||||
let fmt = |w, u| {
|
let fmt = |w, f, u| format!("{w}.{f} {u}");
|
||||||
if w == 1 {
|
let gen64 = |w, f, u| fmt(w, (f * 100.0) as u32, u);
|
||||||
format!("{w} {u}")
|
let gen128 = |w, f, u| gen64(u64::try_from(w).expect("u128 to u64"), f, u);
|
||||||
} else {
|
|
||||||
format!("{w} {u}s")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let gen64 = |w, u| fmt(w, u);
|
|
||||||
let gen128 = |w, u| gen64(u64::try_from(w).expect("u128 to u64"), u);
|
|
||||||
match whole_and_frac(d) {
|
match whole_and_frac(d) {
|
||||||
| (Days(whole), _) => gen64(whole, "day"),
|
| (Days(whole), frac) => gen64(whole, frac, "days"),
|
||||||
| (Hours(whole), _) => gen64(whole, "hour"),
|
| (Hours(whole), frac) => gen64(whole, frac, "hours"),
|
||||||
| (Mins(whole), _) => gen64(whole, "minute"),
|
| (Mins(whole), frac) => gen64(whole, frac, "minutes"),
|
||||||
| (Secs(whole), _) => gen64(whole, "second"),
|
| (Secs(whole), frac) => gen64(whole, frac, "seconds"),
|
||||||
| (Millis(whole), _) => gen128(whole, "millisecond"),
|
| (Millis(whole), frac) => gen128(whole, frac, "milliseconds"),
|
||||||
| (Micros(whole), _) => gen128(whole, "microsecond"),
|
| (Micros(whole), frac) => gen128(whole, frac, "microseconds"),
|
||||||
| (Nanos(whole), _) => gen128(whole, "nanosecond"),
|
| (Nanos(whole), frac) => gen128(whole, frac, "nanoseconds"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-20
@@ -49,10 +49,6 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "bannedroomids",
|
name: "bannedroomids",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
},
|
},
|
||||||
Descriptor {
|
|
||||||
name: "clientid_clientmetadata",
|
|
||||||
..descriptor::RANDOM_SMALL
|
|
||||||
},
|
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "disabledroomids",
|
name: "disabledroomids",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
@@ -161,10 +157,6 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "referencedevents",
|
name: "referencedevents",
|
||||||
..descriptor::RANDOM
|
..descriptor::RANDOM
|
||||||
},
|
},
|
||||||
Descriptor {
|
|
||||||
name: "refreshtoken_refreshtokeninfo",
|
|
||||||
..descriptor::RANDOM_SMALL
|
|
||||||
},
|
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "registrationtoken_info",
|
name: "registrationtoken_info",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
@@ -195,6 +187,10 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
val_size_hint: Some(8),
|
val_size_hint: Some(8),
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
},
|
},
|
||||||
|
Descriptor {
|
||||||
|
name: "roomid_mindepth",
|
||||||
|
..descriptor::RANDOM_SMALL
|
||||||
|
},
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "roomserverids",
|
name: "roomserverids",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
@@ -379,14 +375,6 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "userdevicetxnid_response",
|
name: "userdevicetxnid_response",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
},
|
},
|
||||||
Descriptor {
|
|
||||||
name: "userdeviceid_oauthsessioninfo",
|
|
||||||
..descriptor::RANDOM_SMALL
|
|
||||||
},
|
|
||||||
Descriptor {
|
|
||||||
name: "userdeviceid_tokenexpires",
|
|
||||||
..descriptor::RANDOM_SMALL
|
|
||||||
},
|
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "userfilterid_filter",
|
name: "userfilterid_filter",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
@@ -491,8 +479,4 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "userroomid_invitesender",
|
name: "userroomid_invitesender",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
},
|
},
|
||||||
Descriptor {
|
|
||||||
name: "websessionid_session",
|
|
||||||
..descriptor::RANDOM_SMALL
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ full = [
|
|||||||
"jemalloc_prof",
|
"jemalloc_prof",
|
||||||
"perf_measurements",
|
"perf_measurements",
|
||||||
"tokio_console",
|
"tokio_console",
|
||||||
"conduwuit-api/admin_api",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
brotli_compression = [
|
brotli_compression = [
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ use conduwuit_core::{
|
|||||||
#[clap(
|
#[clap(
|
||||||
about,
|
about,
|
||||||
long_about = None,
|
long_about = None,
|
||||||
name = conduwuit_core::BRANDING,
|
name = conduwuit_core::name(),
|
||||||
version = conduwuit_core::version(),
|
version = conduwuit_core::version(),
|
||||||
)]
|
)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@ pub(crate) fn init(
|
|||||||
.with_batch_exporter(exporter)
|
.with_batch_exporter(exporter)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let tracer = provider.tracer(conduwuit_core::BRANDING);
|
let tracer = provider.tracer(conduwuit_core::name());
|
||||||
|
|
||||||
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
|
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ fn options(config: &Config) -> ClientOptions {
|
|||||||
traces_sample_rate: config.sentry_traces_sample_rate,
|
traces_sample_rate: config.sentry_traces_sample_rate,
|
||||||
debug: cfg!(debug_assertions),
|
debug: cfg!(debug_assertions),
|
||||||
release: release_name(),
|
release: release_name(),
|
||||||
user_agent: conduwuit_core::user_agent().into(),
|
user_agent: conduwuit_core::version::user_agent().into(),
|
||||||
attach_stacktrace: config.sentry_attach_stacktrace,
|
attach_stacktrace: config.sentry_attach_stacktrace,
|
||||||
before_send: Some(Arc::new(before_send)),
|
before_send: Some(Arc::new(before_send)),
|
||||||
before_breadcrumb: Some(Arc::new(before_breadcrumb)),
|
before_breadcrumb: Some(Arc::new(before_breadcrumb)),
|
||||||
|
|||||||
@@ -112,9 +112,7 @@ fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result<Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status == StatusCode::METHOD_NOT_ALLOWED {
|
if status == StatusCode::METHOD_NOT_ALLOWED {
|
||||||
return Ok(
|
return Ok(err!(Request(Unrecognized("Method Not Allowed"))).into_response());
|
||||||
err!(Request(Unrecognized("Method not allowed"), METHOD_NOT_ALLOWED)).into_response()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use ruma::api::error::ErrorKind;
|
|||||||
pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
|
pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
|
||||||
let router = Router::<state::State>::new();
|
let router = Router::<state::State>::new();
|
||||||
let (state, guard) = state::create(services.clone());
|
let (state, guard) = state::create(services.clone());
|
||||||
let router = conduwuit_api::router::build(router, state)
|
let router = conduwuit_api::router::build(router, &services.server)
|
||||||
.merge(conduwuit_web::build(services))
|
.merge(conduwuit_web::build())
|
||||||
.fallback(not_found)
|
.fallback(not_found)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
pub mod rooms;
|
pub mod rooms;
|
||||||
pub mod users;
|
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ pub mod v1 {
|
|||||||
rate_limited: false,
|
rate_limited: false,
|
||||||
authentication: AccessToken,
|
authentication: AccessToken,
|
||||||
history: {
|
history: {
|
||||||
unstable("org.continuwuity.admin") => "/_continuwuity/admin/rooms/{room_id}/ban",
|
1.0 => "/_continuwuity/admin/rooms/{room_id}/ban",
|
||||||
1.0 => "/_continuwuity/admin/v1/rooms/{room_id}/ban",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,11 +29,8 @@ pub mod v1 {
|
|||||||
|
|
||||||
#[response]
|
#[response]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
/// Users who were successfully kicked from this room.
|
|
||||||
pub kicked_users: Vec<OwnedUserId>,
|
pub kicked_users: Vec<OwnedUserId>,
|
||||||
/// Users who could not be kicked from the room.
|
|
||||||
pub failed_kicked_users: Vec<OwnedUserId>,
|
pub failed_kicked_users: Vec<OwnedUserId>,
|
||||||
/// Any local aliases that were removed from the room.
|
|
||||||
pub local_aliases: Vec<OwnedRoomAliasId>,
|
pub local_aliases: Vec<OwnedRoomAliasId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub mod unstable {
|
pub mod v1 {
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedRoomId,
|
OwnedRoomId,
|
||||||
api::{auth_scheme::AccessToken, request, response},
|
api::{auth_scheme::AccessToken, request, response},
|
||||||
@@ -10,7 +10,7 @@ pub mod unstable {
|
|||||||
rate_limited: false,
|
rate_limited: false,
|
||||||
authentication: AccessToken,
|
authentication: AccessToken,
|
||||||
history: {
|
history: {
|
||||||
unstable => "/_continuwuity/admin/rooms/list",
|
1.0 => "/_continuwuity/admin/rooms/list",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ pub mod unstable {
|
|||||||
|
|
||||||
#[response]
|
#[response]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
/// A list of room IDs known to this server.
|
|
||||||
pub rooms: Vec<OwnedRoomId>,
|
pub rooms: Vec<OwnedRoomId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,133 +33,3 @@ pub mod unstable {
|
|||||||
pub fn new(rooms: Vec<OwnedRoomId>) -> Self { Self { rooms } }
|
pub fn new(rooms: Vec<OwnedRoomId>) -> Self { Self { rooms } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod v1 {
|
|
||||||
use ruma::{
|
|
||||||
OwnedRoomId, OwnedUserId, RoomVersionId,
|
|
||||||
api::{auth_scheme::AccessToken, request, response},
|
|
||||||
events::room::{
|
|
||||||
canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
|
|
||||||
history_visibility::PossiblyRedactedRoomHistoryVisibilityEventContent,
|
|
||||||
join_rules::PossiblyRedactedRoomJoinRulesEventContent,
|
|
||||||
name::PossiblyRedactedRoomNameEventContent,
|
|
||||||
topic::PossiblyRedactedRoomTopicEventContent,
|
|
||||||
},
|
|
||||||
metadata,
|
|
||||||
serde::{default_true, is_default},
|
|
||||||
};
|
|
||||||
|
|
||||||
metadata! {
|
|
||||||
method: GET,
|
|
||||||
rate_limited: false,
|
|
||||||
authentication: AccessToken,
|
|
||||||
history: {
|
|
||||||
1.0 => "/_continuwuity/admin/v1/rooms",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[request]
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Request {
|
|
||||||
/// The maximum number of results to return in this page. Maximum (and
|
|
||||||
/// default) is 100.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
|
|
||||||
/// The number of results to skip over before returning results. Default
|
|
||||||
/// is 0.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub offset: Option<usize>,
|
|
||||||
|
|
||||||
/// If true, includes banned rooms in the response.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub include_banned_rooms: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct MinimalRoomInfo {
|
|
||||||
/// The room's unique ID.
|
|
||||||
pub room_id: OwnedRoomId,
|
|
||||||
/// If true, this room is banned, and cannot be joined by non-admins.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub banned: bool,
|
|
||||||
/// If true, this room has federation disabled, but can still be locally
|
|
||||||
/// used.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub disabled: bool,
|
|
||||||
/// The total number of joined members in this room.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub member_count: usize,
|
|
||||||
/// The total number of joined members in this room that are local to
|
|
||||||
/// this server.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub local_member_count: usize,
|
|
||||||
/// The number of unique homeservers currently joined to this room.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub resident_server_count: usize,
|
|
||||||
/// The users who created this room.
|
|
||||||
///
|
|
||||||
/// The first entry is always the sender of the `m.room.create` event.
|
|
||||||
/// Any entries thereafter are additional creators in v12+ rooms. An
|
|
||||||
/// empty vec indicates the room is not known.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub creators: Vec<OwnedUserId>,
|
|
||||||
/// If true, this room has encryption enabled.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub encrypted: bool,
|
|
||||||
/// If true, this room is allowed to be federated (`m.federate` is not
|
|
||||||
/// `false` in `m.room.create`).
|
|
||||||
#[serde(default = "default_true", skip_serializing_if = "is_default")]
|
|
||||||
pub federated: bool,
|
|
||||||
/// If true, this room is published to this server's room directory.
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub published: bool,
|
|
||||||
/// The version of the room.
|
|
||||||
pub version: RoomVersionId,
|
|
||||||
/// The event content for the `m.room.name` event, if any is present.
|
|
||||||
/// May be redacted.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<PossiblyRedactedRoomNameEventContent>,
|
|
||||||
/// The event content for the `m.room.topic` event, if any is present.
|
|
||||||
/// May be redacted.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub topic: Option<PossiblyRedactedRoomTopicEventContent>,
|
|
||||||
/// The event content for the `m.room.canonical_alias` event, if any is
|
|
||||||
/// present. May be redacted.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub canonical_alias: Option<PossiblyRedactedRoomCanonicalAliasEventContent>,
|
|
||||||
/// The event content for the `m.room.join_rules` event, if any is
|
|
||||||
/// present. May be redacted.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub join_rules: Option<PossiblyRedactedRoomJoinRulesEventContent>,
|
|
||||||
/// The event content for the `m.room.history_visibility` event, if any
|
|
||||||
/// is present. May be redacted.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub history_visibility: Option<PossiblyRedactedRoomHistoryVisibilityEventContent>,
|
|
||||||
/// The ID of the room which replaces this one, if any.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub successor: Option<OwnedRoomId>,
|
|
||||||
/// The ID of the room which preceded this one, if any.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub predecessor: Option<OwnedRoomId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[response]
|
|
||||||
pub struct Response {
|
|
||||||
/// A list of rooms known to this server.
|
|
||||||
pub rooms: Vec<MinimalRoomInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self { Self::default() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(rooms: Vec<MinimalRoomInfo>) -> Self { Self { rooms } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
pub mod v1 {
|
|
||||||
use ruma::{
|
|
||||||
OwnedMxcUri, OwnedRoomOrAliasId, OwnedUserId,
|
|
||||||
api::{auth_scheme::AccessToken, request, response},
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
metadata! {
|
|
||||||
method: POST,
|
|
||||||
rate_limited: false,
|
|
||||||
authentication: AccessToken,
|
|
||||||
history: {
|
|
||||||
1.0 => "/_continuwuity/admin/v1/users/create",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[request]
|
|
||||||
pub struct Request {
|
|
||||||
/// The user's localpart (the identifier between `@` and `:`). Cannot be
|
|
||||||
/// blank.
|
|
||||||
pub localpart: String,
|
|
||||||
|
|
||||||
/// The user's desired password. Cannot be blank.
|
|
||||||
pub password: String,
|
|
||||||
|
|
||||||
/// The user's email address, if any.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub email: Option<String>,
|
|
||||||
|
|
||||||
/// The display name to set upon creation.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
|
|
||||||
/// The avatar URI to set upon creation.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub avatar_url: Option<OwnedMxcUri>,
|
|
||||||
|
|
||||||
/// Suspends the user immediately upon creation. They can still log in.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub suspended: bool,
|
|
||||||
|
|
||||||
/// Locks the user immediately upon creation. They will receive
|
|
||||||
/// M_USER_LOCKED upon login.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub locked: bool,
|
|
||||||
|
|
||||||
/// Disables the user's login immediately upon creation.
|
|
||||||
///
|
|
||||||
/// The user can still be used if an admin generates an access token for
|
|
||||||
/// the account, but the user will not be able to use `POST
|
|
||||||
/// /_matrix/client/v3/login`.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub login_disabled: bool,
|
|
||||||
|
|
||||||
/// Promotes the user to a server administrator immediately upon
|
|
||||||
/// creation.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub admin: bool,
|
|
||||||
|
|
||||||
/// Skips joining rooms in the server's configured auto_join_rooms.
|
|
||||||
///
|
|
||||||
/// If this is false, all rooms in the config.toml's `auto_join_rooms`
|
|
||||||
/// will be automatically joined upon creation. If `auto_join_rooms`
|
|
||||||
/// is supplied in this request too, those rooms will be joined
|
|
||||||
/// afterwards.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub skip_auto_join: bool,
|
|
||||||
|
|
||||||
/// Additional rooms to auto-join the new user to. If `skip_auto_join`
|
|
||||||
/// is `true`, these rooms will still be joined.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub auto_join_rooms: Vec<OwnedRoomOrAliasId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[response]
|
|
||||||
pub struct Response {
|
|
||||||
/// The fully qualified user ID of the newly created user.
|
|
||||||
pub user_id: OwnedUserId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(localpart: String, password: String) -> Self {
|
|
||||||
Self {
|
|
||||||
localpart,
|
|
||||||
password,
|
|
||||||
email: None,
|
|
||||||
display_name: None,
|
|
||||||
avatar_url: None,
|
|
||||||
suspended: false,
|
|
||||||
locked: false,
|
|
||||||
login_disabled: false,
|
|
||||||
admin: false,
|
|
||||||
skip_auto_join: false,
|
|
||||||
auto_join_rooms: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(user_id: OwnedUserId) -> Self { Self { user_id } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
pub mod v1 {
|
|
||||||
use ruma::{
|
|
||||||
OwnedUserId,
|
|
||||||
api::{auth_scheme::AccessToken, request, response},
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
metadata! {
|
|
||||||
method: GET,
|
|
||||||
rate_limited: false,
|
|
||||||
authentication: AccessToken,
|
|
||||||
history: {
|
|
||||||
1.0 => "/_continuwuity/admin/v1/users",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[request]
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Request {
|
|
||||||
/// If true, includes deactivated users in the response.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub include_deactivated: bool,
|
|
||||||
/// If true, includes locked users in the response.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub include_locked: bool,
|
|
||||||
/// If true, includes suspended users in the response.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub include_suspended: bool,
|
|
||||||
|
|
||||||
/// The maximum number of results to return in this page. Maximum (and
|
|
||||||
/// default) is 100.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
|
|
||||||
/// The number of results to skip over before returning results. Default
|
|
||||||
/// is 0.
|
|
||||||
#[ruma_api(query)]
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub offset: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, serde::Serialize)]
|
|
||||||
pub struct User {
|
|
||||||
/// The full user ID of the user.
|
|
||||||
pub user_id: OwnedUserId,
|
|
||||||
|
|
||||||
/// Whether this user is deactivated.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub deactivated: bool,
|
|
||||||
|
|
||||||
/// Whether this user is suspended.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub suspended: bool,
|
|
||||||
|
|
||||||
/// Whether this user is locked.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub locked: bool,
|
|
||||||
|
|
||||||
/// Whether this user is an admin.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub admin: bool,
|
|
||||||
|
|
||||||
/// Whether this user has their login disabled.
|
|
||||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
|
||||||
pub login_disabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(user_id: OwnedUserId) -> Self {
|
|
||||||
Self {
|
|
||||||
user_id,
|
|
||||||
deactivated: false,
|
|
||||||
suspended: false,
|
|
||||||
locked: false,
|
|
||||||
admin: false,
|
|
||||||
login_disabled: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[response]
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Response {
|
|
||||||
pub users: Vec<User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self { Self::default() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(users: Vec<User>) -> Self { Self { users } }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use assign::assign;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn request_defaults() {
|
|
||||||
let req = Request::new();
|
|
||||||
assert!(!req.include_deactivated && !req.include_locked && !req.include_suspended);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_serialize_omits_default_values() {
|
|
||||||
let user_id = OwnedUserId::try_from("@alice:example.org".to_owned()).unwrap();
|
|
||||||
let user = User::new(user_id.clone());
|
|
||||||
|
|
||||||
let expected = json!({ "user_id": user_id.to_string() });
|
|
||||||
assert_eq!(serde_json::to_value(&user).expect("failed to serialize user"), expected);
|
|
||||||
|
|
||||||
let suspended_user = assign!(user, {suspended: true});
|
|
||||||
let expected2 = json!({ "user_id": "@alice:example.org", "suspended": true});
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_value(&suspended_user).expect("failed to serialize user"),
|
|
||||||
expected2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_defaults() {
|
|
||||||
let response = Response::default();
|
|
||||||
assert!(response.users.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod create;
|
|
||||||
pub mod list;
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
//! `GET /_matrix/client/v1/admin/suspend/{userId}`
|
||||||
|
//!
|
||||||
|
//! Check the suspension status of a target user
|
||||||
|
|
||||||
|
pub mod v1 {
|
||||||
|
//! `/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{userID}`
|
||||||
|
//! ([msc])
|
||||||
|
//!
|
||||||
|
//! [msc]: https://github.com/matrix-org/matrix-spec-proposals/pull/4323
|
||||||
|
|
||||||
|
use ruma::{
|
||||||
|
OwnedUserId,
|
||||||
|
api::{auth_scheme::AccessToken, request, response},
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata! {
|
||||||
|
method: GET,
|
||||||
|
rate_limited: false,
|
||||||
|
authentication: AccessToken,
|
||||||
|
history: {
|
||||||
|
unstable => "/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{user_id}",
|
||||||
|
1.18 => "/_matrix/client/v1/admin/suspend/{user_id}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request type for the get & set user suspension status endpoint.
|
||||||
|
#[request(error = ruma::api::error::Error)]
|
||||||
|
pub struct Request {
|
||||||
|
/// The user to look up.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub user_id: OwnedUserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response type for the suspension endpoints
|
||||||
|
#[response(error = ruma::api::error::Error)]
|
||||||
|
pub struct Response {
|
||||||
|
/// Whether the user is currently suspended.
|
||||||
|
pub suspended: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Creates a new `Request` with the given user id.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(user_id: OwnedUserId) -> Self { Self { user_id } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Creates a new `Response` with the given suspension status.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(suspended: bool) -> Self { Self { suspended } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
pub mod continuwuity;
|
pub mod continuwuity;
|
||||||
|
pub mod get_suspended;
|
||||||
|
pub mod set_suspended;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//! `PUT /_matrix/client/v1/admin/suspend/{userId}`
|
||||||
|
//!
|
||||||
|
//! Set the suspension status of a target user
|
||||||
|
|
||||||
|
pub mod v1 {
|
||||||
|
//! `/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{userID}`
|
||||||
|
//! ([msc])
|
||||||
|
//!
|
||||||
|
//! [msc]: https://github.com/matrix-org/matrix-spec-proposals/pull/4323
|
||||||
|
|
||||||
|
use ruma::{
|
||||||
|
OwnedUserId,
|
||||||
|
api::{auth_scheme::AccessToken, request, response},
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata! {
|
||||||
|
method: PUT,
|
||||||
|
rate_limited: false,
|
||||||
|
authentication: AccessToken,
|
||||||
|
history: {
|
||||||
|
unstable => "/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{user_id}",
|
||||||
|
1.18 => "/_matrix/client/v1/admin/suspend/{user_id}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request type for the set user suspension status endpoint.
|
||||||
|
#[request(error = ruma::api::error::Error)]
|
||||||
|
pub struct Request {
|
||||||
|
/// The user to look up.
|
||||||
|
#[ruma_api(path)]
|
||||||
|
pub user_id: OwnedUserId,
|
||||||
|
|
||||||
|
pub suspended: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response type for the suspension endpoints
|
||||||
|
#[response(error = ruma::api::error::Error)]
|
||||||
|
pub struct Response {
|
||||||
|
/// Whether the user is currently suspended.
|
||||||
|
pub suspended: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// Creates a new `Request` with the given user id.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(user_id: OwnedUserId, suspended: bool) -> Self { Self { user_id, suspended } }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Creates a new `Response` with the given suspension status.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(suspended: bool) -> Self { Self { suspended } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,7 +119,6 @@ recaptcha-verify = { version = "0.2.0", default-features = false }
|
|||||||
reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated
|
reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated
|
||||||
yansi.workspace = true
|
yansi.workspace = true
|
||||||
lettre.workspace = true
|
lettre.workspace = true
|
||||||
serde_urlencoded.workspace = true
|
|
||||||
|
|
||||||
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
|
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
|
||||||
sd-notify.workspace = true
|
sd-notify.workspace = true
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ use futures::{Future, FutureExt, StreamExt, TryFutureExt};
|
|||||||
use loole::{Receiver, Sender};
|
use loole::{Receiver, Sender};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
|
OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
|
||||||
api::client::discovery::discover_support::{Contact, ContactRole},
|
|
||||||
assign,
|
|
||||||
events::{
|
events::{
|
||||||
Mentions,
|
Mentions,
|
||||||
room::message::{
|
room::message::{
|
||||||
@@ -30,7 +28,7 @@ use ruma::{
|
|||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Dep, account_data, config, globals,
|
Dep, account_data, globals,
|
||||||
media::{MXC_LENGTH, mxc::Mxc},
|
media::{MXC_LENGTH, mxc::Mxc},
|
||||||
rooms::{self, state::RoomMutexGuard},
|
rooms::{self, state::RoomMutexGuard},
|
||||||
};
|
};
|
||||||
@@ -46,7 +44,6 @@ pub struct Service {
|
|||||||
|
|
||||||
struct Services {
|
struct Services {
|
||||||
server: Arc<Server>,
|
server: Arc<Server>,
|
||||||
config: Dep<config::Service>,
|
|
||||||
globals: Dep<globals::Service>,
|
globals: Dep<globals::Service>,
|
||||||
alias: Dep<rooms::alias::Service>,
|
alias: Dep<rooms::alias::Service>,
|
||||||
timeline: Dep<rooms::timeline::Service>,
|
timeline: Dep<rooms::timeline::Service>,
|
||||||
@@ -118,7 +115,6 @@ impl crate::Service for Service {
|
|||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
services: Services {
|
services: Services {
|
||||||
server: args.server.clone(),
|
server: args.server.clone(),
|
||||||
config: args.depend::<config::Service>("config"),
|
|
||||||
globals: args.depend::<globals::Service>("globals"),
|
globals: args.depend::<globals::Service>("globals"),
|
||||||
alias: args.depend::<rooms::alias::Service>("rooms::alias"),
|
alias: args.depend::<rooms::alias::Service>("rooms::alias"),
|
||||||
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
||||||
@@ -623,52 +619,4 @@ impl Service {
|
|||||||
let weak = services.map(Arc::downgrade);
|
let weak = services.map(Arc::downgrade);
|
||||||
*receiver = weak;
|
*receiver = weak;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the server's configured support contacts.
|
|
||||||
pub async fn get_support_contacts(&self) -> Vec<Contact> {
|
|
||||||
let email_address = self.services.config.well_known.support_email.clone();
|
|
||||||
let matrix_id = self.services.config.well_known.support_mxid.clone();
|
|
||||||
let pgp_key = self.services.config.well_known.support_pgp_key.clone();
|
|
||||||
|
|
||||||
// TODO: support defining multiple contacts in the config
|
|
||||||
let mut contacts: Vec<Contact> = vec![];
|
|
||||||
|
|
||||||
let role = self
|
|
||||||
.services
|
|
||||||
.config
|
|
||||||
.well_known
|
|
||||||
.support_role
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(ContactRole::Admin);
|
|
||||||
|
|
||||||
// Add configured contact if at least one contact method is specified
|
|
||||||
let configured_contact = match (matrix_id, email_address) {
|
|
||||||
| (Some(matrix_id), email_address) =>
|
|
||||||
Some(assign!(Contact::with_matrix_id(role, matrix_id), { email_address })),
|
|
||||||
| (None, Some(email_address)) =>
|
|
||||||
Some(Contact::with_email_address(role, email_address)),
|
|
||||||
| (None, None) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(mut configured_contact) = configured_contact {
|
|
||||||
configured_contact.pgp_key = pgp_key;
|
|
||||||
|
|
||||||
contacts.push(configured_contact);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to add admin users as contacts if no contacts are configured
|
|
||||||
if contacts.is_empty() {
|
|
||||||
let admin_users = self.get_admins().await;
|
|
||||||
|
|
||||||
for user_id in &admin_users {
|
|
||||||
if *user_id == self.services.globals.server_user {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
contacts.push(Contact::with_matrix_id(ContactRole::Admin, user_id.to_owned()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contacts
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ impl crate::Service for Service {
|
|||||||
for (id, registration) in appservices {
|
for (id, registration) in appservices {
|
||||||
// During startup, resolve any token collisions in favour of appservices
|
// During startup, resolve any token collisions in favour of appservices
|
||||||
// by logging out conflicting user devices
|
// by logging out conflicting user devices
|
||||||
if let Some((user_id, device_id, _)) = self
|
if let Ok((user_id, device_id)) = self
|
||||||
.services
|
.services
|
||||||
.users
|
.users
|
||||||
.find_from_token(®istration.as_token)
|
.find_from_token(®istration.as_token)
|
||||||
@@ -158,7 +158,7 @@ impl Service {
|
|||||||
.users
|
.users
|
||||||
.find_from_token(®istration.as_token)
|
.find_from_token(®istration.as_token)
|
||||||
.await
|
.await
|
||||||
.is_some()
|
.is_ok()
|
||||||
{
|
{
|
||||||
return Err(err!(Request(InvalidParam(
|
return Err(err!(Request(InvalidParam(
|
||||||
"Cannot register appservice: The provided token is already in use by a user \
|
"Cannot register appservice: The provided token is already in use by a user \
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl crate::Service for Service {
|
|||||||
let url_preview_user_agent = config
|
let url_preview_user_agent = config
|
||||||
.url_preview_user_agent
|
.url_preview_user_agent
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| conduwuit::user_agent_media().to_owned());
|
.unwrap_or_else(|| conduwuit::version::user_agent_media().to_owned());
|
||||||
|
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
default: base(config)?
|
default: base(config)?
|
||||||
@@ -149,7 +149,7 @@ fn base(config: &Config) -> Result<reqwest::ClientBuilder> {
|
|||||||
.timeout(Duration::from_secs(config.request_total_timeout))
|
.timeout(Duration::from_secs(config.request_total_timeout))
|
||||||
.pool_idle_timeout(Duration::from_secs(config.request_idle_timeout))
|
.pool_idle_timeout(Duration::from_secs(config.request_idle_timeout))
|
||||||
.pool_max_idle_per_host(config.request_idle_per_host.into())
|
.pool_max_idle_per_host(config.request_idle_per_host.into())
|
||||||
.user_agent(conduwuit::user_agent())
|
.user_agent(conduwuit::version::user_agent())
|
||||||
.redirect(redirect::Policy::limited(6))
|
.redirect(redirect::Policy::limited(6))
|
||||||
.danger_accept_invalid_certs(config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
|
.danger_accept_invalid_certs(config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
|
||||||
.connection_verbose(cfg!(debug_assertions));
|
.connection_verbose(cfg!(debug_assertions));
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::{
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use conduwuit::{Result, info, utils::ReadyExt};
|
use conduwuit::{Result, info, utils::ReadyExt};
|
||||||
use futures::StreamExt;
|
use futures::{FutureExt, StreamExt};
|
||||||
use ruma::{UserId, events::room::message::RoomMessageEventContent};
|
use ruma::{UserId, events::room::message::RoomMessageEventContent};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -120,7 +120,7 @@ impl Service {
|
|||||||
///
|
///
|
||||||
/// Returns Ok(true) if the specified user was the first user, and Ok(false)
|
/// Returns Ok(true) if the specified user was the first user, and Ok(false)
|
||||||
/// if they were not.
|
/// if they were not.
|
||||||
pub async fn empower_first_user(&self, user: &UserId) -> bool {
|
pub async fn empower_first_user(&self, user: &UserId) -> Result<bool> {
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "welcome.md")]
|
#[template(path = "welcome.md")]
|
||||||
struct WelcomeMessage<'a> {
|
struct WelcomeMessage<'a> {
|
||||||
@@ -130,14 +130,10 @@ impl Service {
|
|||||||
|
|
||||||
// If first run mode isn't active, do nothing.
|
// If first run mode isn't active, do nothing.
|
||||||
if !self.disable_first_run() {
|
if !self.disable_first_run() {
|
||||||
return false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.services
|
self.services.admin.make_user_admin(user).boxed().await?;
|
||||||
.admin
|
|
||||||
.make_user_admin(user)
|
|
||||||
.await
|
|
||||||
.expect("should have been able to empower the first user");
|
|
||||||
|
|
||||||
// Send the welcome message
|
// Send the welcome message
|
||||||
let welcome_message = WelcomeMessage {
|
let welcome_message = WelcomeMessage {
|
||||||
@@ -150,12 +146,11 @@ impl Service {
|
|||||||
self.services
|
self.services
|
||||||
.admin
|
.admin
|
||||||
.send_loud_message(RoomMessageEventContent::text_markdown(welcome_message))
|
.send_loud_message(RoomMessageEventContent::text_markdown(welcome_message))
|
||||||
.await
|
.await?;
|
||||||
.expect("should have been able to send welcome message");
|
|
||||||
|
|
||||||
info!("{user} has been invited to the admin room as the first user.");
|
info!("{user} has been invited to the admin room as the first user.");
|
||||||
|
|
||||||
true
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the single-use registration token which may be used to create the
|
/// Get the single-use registration token which may be used to create the
|
||||||
@@ -186,7 +181,7 @@ impl Service {
|
|||||||
eprintln!(
|
eprintln!(
|
||||||
"Welcome to {} {}!",
|
"Welcome to {} {}!",
|
||||||
"Continuwuity".bold().bright_magenta(),
|
"Continuwuity".bold().bright_magenta(),
|
||||||
conduwuit::version().bold()
|
conduwuit::version::version().bold()
|
||||||
);
|
);
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ impl Mailer<'_> {
|
|||||||
|
|
||||||
let message = MessageBuilder::new()
|
let message = MessageBuilder::new()
|
||||||
.from(self.sender.clone())
|
.from(self.sender.clone())
|
||||||
.to(recipient.clone())
|
.to(recipient)
|
||||||
.subject(subject.clone())
|
.subject(subject)
|
||||||
.date_now()
|
.date_now()
|
||||||
.header(ContentType::TEXT_PLAIN)
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(body)
|
.body(body)
|
||||||
@@ -104,8 +104,6 @@ impl Mailer<'_> {
|
|||||||
.await
|
.await
|
||||||
.map_err(|err: TransportError| err!("Failed to send message: {err}"))?;
|
.map_err(|err: TransportError| err!("Failed to send message: {err}"))?;
|
||||||
|
|
||||||
info!(recipient = recipient.to_string(), ?subject, "Email sent");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ pub mod key_backups;
|
|||||||
pub mod mailer;
|
pub mod mailer;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
pub mod moderation;
|
pub mod moderation;
|
||||||
pub mod oauth;
|
pub mod password_reset;
|
||||||
pub mod presence;
|
pub mod presence;
|
||||||
pub mod pusher;
|
pub mod pusher;
|
||||||
pub mod registration_tokens;
|
pub mod registration_tokens;
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
use std::{collections::BTreeSet, hash::Hash};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub struct ClientMetadata {
|
|
||||||
#[serde(default)]
|
|
||||||
pub application_type: ApplicationType,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub client_name: Option<String>,
|
|
||||||
|
|
||||||
pub client_uri: Url,
|
|
||||||
|
|
||||||
#[serde(default, deserialize_with = "btreeset_skip_err")]
|
|
||||||
pub grant_types: BTreeSet<GrantType>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub logo_uri: Option<Url>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub policy_uri: Option<Url>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub redirect_uris: Vec<Url>,
|
|
||||||
|
|
||||||
#[serde(default, deserialize_with = "btreeset_skip_err")]
|
|
||||||
pub response_types: BTreeSet<ResponseType>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub token_endpoint_auth_method: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub tos_uri: Option<Url>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientMetadata {
|
|
||||||
pub(super) const ACCEPTABLE_LOCALHOSTS: [&str; 3] = ["localhost", "127.0.0.1", "[::1]"];
|
|
||||||
|
|
||||||
pub(super) fn validate(&self) -> Result<(), &'static str> {
|
|
||||||
let Some(client_domain) = self.client_uri.domain() else {
|
|
||||||
return Err("Client URI must have a domain.");
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.client_uri.scheme() != "https" {
|
|
||||||
return Err("Client URI must be HTTPS.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.client_uri.username().is_empty() || self.client_uri.password().is_some() {
|
|
||||||
return Err("Client URI must not include credentials.");
|
|
||||||
}
|
|
||||||
|
|
||||||
for uri in [&self.logo_uri, &self.policy_uri, &self.tos_uri]
|
|
||||||
.iter()
|
|
||||||
.filter_map(|uri| uri.as_ref())
|
|
||||||
{
|
|
||||||
if uri.scheme() != "https" {
|
|
||||||
return Err("All metadata URIs must be HTTPS.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !uri.username().is_empty() || uri.password().is_some() {
|
|
||||||
return Err("All metadata URIs must not include credentials.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !uri
|
|
||||||
.domain()
|
|
||||||
.is_some_and(|domain| is_subdomain(domain, client_domain))
|
|
||||||
{
|
|
||||||
return Err("All metadata URIs must be subdomains of the client URI.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for uri in &self.redirect_uris {
|
|
||||||
match uri.scheme() {
|
|
||||||
| "https" => {
|
|
||||||
// HTTPS URIs are okay for native and web clients
|
|
||||||
|
|
||||||
if !uri.username().is_empty() || uri.password().is_some() {
|
|
||||||
return Err("HTTPS redirect URIs must not contain credentials.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| "http" if self.application_type == ApplicationType::Native => {
|
|
||||||
if uri
|
|
||||||
.host_str()
|
|
||||||
.is_none_or(|host| !Self::ACCEPTABLE_LOCALHOSTS.contains(&host))
|
|
||||||
{
|
|
||||||
return Err("HTTP redirect URIs for native applications must only \
|
|
||||||
refer to localhost.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if uri.port().is_some() {
|
|
||||||
return Err("HTTP redirect URIs for native applications do not need to \
|
|
||||||
specify a port. All ports will be accepted during \
|
|
||||||
authorization.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| private_scheme if self.application_type == ApplicationType::Native => {
|
|
||||||
let rdns_client_uri = client_domain.split('.').rev().join(".");
|
|
||||||
|
|
||||||
if !private_scheme.starts_with(&rdns_client_uri) {
|
|
||||||
return Err("Private-use scheme URIs for native applications must \
|
|
||||||
begin with the application's client URI domain in \
|
|
||||||
reverse-DNS notation.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if uri.has_authority() {
|
|
||||||
return Err("Private-use scheme URIs for native applications must not \
|
|
||||||
have an authority.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| _ =>
|
|
||||||
return Err("A redirect URI's scheme is not valid for this application type."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ApplicationType {
|
|
||||||
#[default]
|
|
||||||
Web,
|
|
||||||
Native,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum GrantType {
|
|
||||||
AuthorizationCode,
|
|
||||||
RefreshToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum ResponseType {
|
|
||||||
Code,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize a BTreeSet from a sequence, skipping items which fail to
|
|
||||||
/// deserialize. This is used as a deserialize helper for ClientMetadata to
|
|
||||||
/// ignore unknown enum variants in a few fields.
|
|
||||||
fn btreeset_skip_err<'de, D, V>(de: D) -> Result<BTreeSet<V>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
V: Deserialize<'de> + Hash + Eq + Ord,
|
|
||||||
{
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use serde::de::{SeqAccess, Visitor};
|
|
||||||
|
|
||||||
struct BTreeSetVisitor<V> {
|
|
||||||
item: PhantomData<V>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, V> Visitor<'de> for BTreeSetVisitor<V>
|
|
||||||
where
|
|
||||||
V: Deserialize<'de> + Hash + Eq + Ord,
|
|
||||||
{
|
|
||||||
type Value = BTreeSet<V>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(formatter, "a sequence")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: SeqAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut set = BTreeSet::new();
|
|
||||||
|
|
||||||
while let Some(element) = seq.next_element().transpose() {
|
|
||||||
if let Ok(element) = element {
|
|
||||||
set.insert(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(set)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
de.deserialize_seq(BTreeSetVisitor { item: PhantomData })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_subdomain(subdomain: &str, domain: &str) -> bool {
|
|
||||||
if subdomain == domain {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
subdomain.ends_with(&format!(".{domain}"))
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
use std::{collections::BTreeSet, fmt::Debug, hash::Hash, mem::discriminant};
|
|
||||||
|
|
||||||
use regex::Regex;
|
|
||||||
use ruma::OwnedDeviceId;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use super::client_metadata::ResponseType;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct AuthorizationCodeQuery {
|
|
||||||
pub response_type: ResponseType,
|
|
||||||
pub client_id: String,
|
|
||||||
pub redirect_uri: Url,
|
|
||||||
pub scope: RawScopes,
|
|
||||||
pub state: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub response_mode: ResponseMode,
|
|
||||||
pub code_challenge: String,
|
|
||||||
pub code_challenge_method: CodeChallengeMethod,
|
|
||||||
#[serde(default)]
|
|
||||||
pub prompt: Option<Prompt>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum ResponseMode {
|
|
||||||
#[default]
|
|
||||||
// default for `code` response type, see https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#:~:text=Client%2E-,For,encoding%2E,-See
|
|
||||||
Query,
|
|
||||||
Fragment,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum CodeChallengeMethod {
|
|
||||||
S256,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Prompt {
|
|
||||||
Create,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialOrd, Ord)]
|
|
||||||
pub enum Scope {
|
|
||||||
Device(OwnedDeviceId),
|
|
||||||
ClientApi,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Scope {
|
|
||||||
fn eq(&self, other: &Self) -> bool { discriminant(self) == discriminant(other) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for Scope {}
|
|
||||||
|
|
||||||
impl Hash for Scope {
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { discriminant(self).hash(state); }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Scope {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let urn = match self {
|
|
||||||
| Self::ClientApi => "urn:matrix:client:api:*".to_owned(),
|
|
||||||
| Self::Device(device_id) => format!("urn:matrix:client:device:{device_id}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
f.write_str(&urn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct RawScopes(String);
|
|
||||||
|
|
||||||
impl RawScopes {
|
|
||||||
pub fn to_scopes(&self) -> Result<BTreeSet<Scope>, String> {
|
|
||||||
let client_api_token_regex =
|
|
||||||
Regex::new(r"urn:matrix:(client|org.matrix.msc2967.client):api:\*").unwrap();
|
|
||||||
let device_token_regex = Regex::new(
|
|
||||||
r"urn:matrix:(client|org.matrix.msc2967.client):device:([a-zA-Z0-9-._~]{5,})",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut scopes = BTreeSet::new();
|
|
||||||
|
|
||||||
for token in self.0.split(' ') {
|
|
||||||
let scope_was_new = {
|
|
||||||
if client_api_token_regex.is_match(token) {
|
|
||||||
scopes.insert(Scope::ClientApi)
|
|
||||||
} else if let Some(captures) = device_token_regex.captures(token) {
|
|
||||||
scopes.insert(Scope::Device(captures.get(2).unwrap().as_str().into()))
|
|
||||||
} else if token == "openid" {
|
|
||||||
// TODO(unspecced): Element sets this scope but doesn't use it for anything
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
return Err(format!("Invalid scope: {token}"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !scope_was_new {
|
|
||||||
return Err("Scope was specified more than once".to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(scopes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct AuthorizationCodeResponse {
|
|
||||||
pub state: String,
|
|
||||||
pub code: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(tag = "grant_type", rename_all = "snake_case")]
|
|
||||||
pub enum TokenRequest {
|
|
||||||
AuthorizationCode {
|
|
||||||
code: String,
|
|
||||||
redirect_uri: Url,
|
|
||||||
client_id: String,
|
|
||||||
code_verifier: String,
|
|
||||||
},
|
|
||||||
RefreshToken {
|
|
||||||
client_id: String,
|
|
||||||
refresh_token: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TokenRequest {
|
|
||||||
#[must_use]
|
|
||||||
pub fn client_id(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
| Self::AuthorizationCode { client_id, .. }
|
|
||||||
| Self::RefreshToken { client_id, .. } => client_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct TokenResponse {
|
|
||||||
pub access_token: String,
|
|
||||||
pub token_type: TokenType,
|
|
||||||
pub expires_in: u64,
|
|
||||||
pub refresh_token: String,
|
|
||||||
pub scope: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub enum TokenType {
|
|
||||||
Bearer,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct RevokeTokenRequest {
|
|
||||||
pub token: String,
|
|
||||||
}
|
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::{BTreeSet, HashMap},
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use conduwuit::{
|
|
||||||
Err, Result, err, info,
|
|
||||||
utils::{self, hash::sha256},
|
|
||||||
};
|
|
||||||
use database::{Deserialized, Json, Map};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Dep,
|
|
||||||
oauth::{
|
|
||||||
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
|
||||||
grant::{
|
|
||||||
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod, ResponseMode,
|
|
||||||
Scope, TokenRequest, TokenResponse, TokenType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod client_metadata;
|
|
||||||
pub mod grant;
|
|
||||||
|
|
||||||
pub struct Service {
|
|
||||||
services: Services,
|
|
||||||
db: Data,
|
|
||||||
tickets: Mutex<HashMap<String, HashMap<OAuthTicket, SystemTime>>>,
|
|
||||||
pending_code_grants: tokio::sync::Mutex<HashMap<String, PendingCodeGrant>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Data {
|
|
||||||
clientid_clientmetadata: Arc<Map>,
|
|
||||||
userdeviceid_oauthsessioninfo: Arc<Map>,
|
|
||||||
refreshtoken_refreshtokeninfo: Arc<Map>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Services {
|
|
||||||
users: Dep<users::Service>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct SessionInfo {
|
|
||||||
pub client_id: String,
|
|
||||||
pub scopes: BTreeSet<Scope>,
|
|
||||||
current_refresh_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
struct RefreshTokenInfo {
|
|
||||||
client_id: String,
|
|
||||||
user_id: OwnedUserId,
|
|
||||||
device_id: OwnedDeviceId,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PendingCodeGrant {
|
|
||||||
authorizing_user: OwnedUserId,
|
|
||||||
requested_scopes: BTreeSet<Scope>,
|
|
||||||
client_name: Option<String>,
|
|
||||||
expected_client_id: String,
|
|
||||||
expected_redirect_uri: Url,
|
|
||||||
code_challenge: String,
|
|
||||||
requested_at: SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PendingCodeGrant {
|
|
||||||
const MAX_AGE: Duration = Duration::from_mins(1);
|
|
||||||
const RANDOM_CODE_LENGTH: usize = 32;
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn generate_code() -> String { utils::random_string(Self::RANDOM_CODE_LENGTH) }
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
|
||||||
let now = SystemTime::now();
|
|
||||||
|
|
||||||
self.expected_client_id == client_id
|
|
||||||
&& now
|
|
||||||
.duration_since(self.requested_at)
|
|
||||||
.is_ok_and(|age| age < Self::MAX_AGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A time-limited grant for a client to perform some sensitive action.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum OAuthTicket {
|
|
||||||
CrossSigningReset,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OAuthTicket {
|
|
||||||
const MAX_AGE: Duration = Duration::from_mins(10);
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn ticket_issue_path(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
| Self::CrossSigningReset => "/account/cross_signing_reset",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl crate::Service for Service {
|
|
||||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|
||||||
Ok(Arc::new(Self {
|
|
||||||
services: Services {
|
|
||||||
users: args.depend::<users::Service>("users"),
|
|
||||||
},
|
|
||||||
db: Data {
|
|
||||||
clientid_clientmetadata: args.db["clientid_clientmetadata"].clone(),
|
|
||||||
userdeviceid_oauthsessioninfo: args.db["userdeviceid_oauthsessioninfo"].clone(),
|
|
||||||
refreshtoken_refreshtokeninfo: args.db["refreshtoken_refreshtokeninfo"].clone(),
|
|
||||||
},
|
|
||||||
tickets: Mutex::default(),
|
|
||||||
pending_code_grants: tokio::sync::Mutex::default(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Service {
|
|
||||||
const ACCESS_TOKEN_MAX_AGE: Duration = Duration::from_hours(1);
|
|
||||||
const RANDOM_TOKEN_LENGTH: usize = 32;
|
|
||||||
|
|
||||||
fn generate_token() -> String { utils::random_string(Self::RANDOM_TOKEN_LENGTH) }
|
|
||||||
|
|
||||||
pub async fn register_client(
|
|
||||||
&self,
|
|
||||||
metadata: &ClientMetadata,
|
|
||||||
) -> Result<String, &'static str> {
|
|
||||||
metadata.validate()?;
|
|
||||||
|
|
||||||
let client_id = base64::prelude::BASE64_STANDARD
|
|
||||||
.encode(sha256::hash(serde_json::to_string(metadata).unwrap().as_bytes()));
|
|
||||||
|
|
||||||
if self
|
|
||||||
.db
|
|
||||||
.clientid_clientmetadata
|
|
||||||
.exists(&client_id)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
self.db
|
|
||||||
.clientid_clientmetadata
|
|
||||||
.raw_put(&client_id, Json(metadata.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(client_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_client_metadata(&self, client_id: &str) -> Option<ClientMetadata> {
|
|
||||||
self.db
|
|
||||||
.clientid_clientmetadata
|
|
||||||
.get(client_id)
|
|
||||||
.await
|
|
||||||
.deserialized()
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_session_info_for_device(
|
|
||||||
&self,
|
|
||||||
user_id: &UserId,
|
|
||||||
device_id: &DeviceId,
|
|
||||||
) -> Option<SessionInfo> {
|
|
||||||
self.db
|
|
||||||
.userdeviceid_oauthsessioninfo
|
|
||||||
.qry(&(user_id, device_id))
|
|
||||||
.await
|
|
||||||
.deserialized::<SessionInfo>()
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn request_authorization_code(
|
|
||||||
&self,
|
|
||||||
authorizing_user: OwnedUserId,
|
|
||||||
query: AuthorizationCodeQuery,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let Some(client_metadata) = self.get_client_metadata(&query.client_id).await else {
|
|
||||||
return Err("Invalid client ID".to_owned());
|
|
||||||
};
|
|
||||||
|
|
||||||
if !(client_metadata
|
|
||||||
.response_types
|
|
||||||
.contains(&query.response_type)
|
|
||||||
&& matches!(query.response_type, ResponseType::Code))
|
|
||||||
{
|
|
||||||
return Err("Invalid response type".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matches!(query.code_challenge_method, CodeChallengeMethod::S256) {
|
|
||||||
return Err("Invalid code challenge type".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut stripped_uri = query.redirect_uri.clone();
|
|
||||||
|
|
||||||
if client_metadata.application_type == ApplicationType::Native
|
|
||||||
&& query
|
|
||||||
.redirect_uri
|
|
||||||
.host_str()
|
|
||||||
.is_some_and(|host| ClientMetadata::ACCEPTABLE_LOCALHOSTS.contains(&host))
|
|
||||||
{
|
|
||||||
// Remove the port from localhost redirect URIs for native applications when
|
|
||||||
// checking if it's valid
|
|
||||||
stripped_uri.set_port(None).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !client_metadata.redirect_uris.contains(&stripped_uri) {
|
|
||||||
return Err("Invalid redirect URI".to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let requested_scopes = query.scope.to_scopes()?;
|
|
||||||
|
|
||||||
let redirect_uri_query_separator = match query.response_mode {
|
|
||||||
| ResponseMode::Fragment => '#',
|
|
||||||
| ResponseMode::Query => '?',
|
|
||||||
};
|
|
||||||
|
|
||||||
let code = PendingCodeGrant::generate_code();
|
|
||||||
|
|
||||||
info!(
|
|
||||||
client_id = &query.client_id,
|
|
||||||
client_name = &client_metadata.client_name,
|
|
||||||
?requested_scopes,
|
|
||||||
?authorizing_user,
|
|
||||||
"Issuing oauth authorization code"
|
|
||||||
);
|
|
||||||
|
|
||||||
let redirect_uri = format!(
|
|
||||||
"{}{}{}",
|
|
||||||
query.redirect_uri,
|
|
||||||
redirect_uri_query_separator,
|
|
||||||
serde_urlencoded::to_string(AuthorizationCodeResponse {
|
|
||||||
state: query.state,
|
|
||||||
code: code.clone(),
|
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let pending_grant = PendingCodeGrant {
|
|
||||||
authorizing_user,
|
|
||||||
requested_scopes,
|
|
||||||
client_name: client_metadata.client_name,
|
|
||||||
expected_client_id: query.client_id,
|
|
||||||
expected_redirect_uri: query.redirect_uri,
|
|
||||||
code_challenge: query.code_challenge,
|
|
||||||
requested_at: SystemTime::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.pending_code_grants
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.insert(code, pending_grant);
|
|
||||||
|
|
||||||
Ok(redirect_uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse> {
|
|
||||||
match request {
|
|
||||||
| TokenRequest::AuthorizationCode {
|
|
||||||
code,
|
|
||||||
redirect_uri,
|
|
||||||
client_id,
|
|
||||||
code_verifier,
|
|
||||||
} => {
|
|
||||||
let mut pending_grants = self.pending_code_grants.lock().await;
|
|
||||||
|
|
||||||
let Some(pending_grant) = pending_grants
|
|
||||||
.remove(&code)
|
|
||||||
.filter(|grant| grant.is_valid_for(&client_id))
|
|
||||||
else {
|
|
||||||
return Err!("Invalid code");
|
|
||||||
};
|
|
||||||
|
|
||||||
if redirect_uri != pending_grant.expected_redirect_uri {
|
|
||||||
return Err!("Unexpected redirect uri");
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected_code_challenge =
|
|
||||||
base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(sha256::hash(&code_verifier));
|
|
||||||
if expected_code_challenge != pending_grant.code_challenge {
|
|
||||||
return Err!("Invalid code challenge");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.create_session(
|
|
||||||
pending_grant.authorizing_user,
|
|
||||||
pending_grant.requested_scopes,
|
|
||||||
pending_grant.client_name,
|
|
||||||
client_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
},
|
|
||||||
| TokenRequest::RefreshToken { client_id, refresh_token } =>
|
|
||||||
self.refresh_session(client_id, refresh_token).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn revoke_token(&self, token: String) -> Result<()> {
|
|
||||||
let (user_id, device_id) = if let Ok(refresh_token_info) = self
|
|
||||||
.db
|
|
||||||
.refreshtoken_refreshtokeninfo
|
|
||||||
.get(&token)
|
|
||||||
.await
|
|
||||||
.deserialized::<RefreshTokenInfo>()
|
|
||||||
{
|
|
||||||
(refresh_token_info.user_id, refresh_token_info.device_id)
|
|
||||||
} else if let Some((user_id, device_id, _)) =
|
|
||||||
self.services.users.find_from_token(&token).await
|
|
||||||
{
|
|
||||||
(user_id, device_id)
|
|
||||||
} else {
|
|
||||||
return Err!("Invalid token");
|
|
||||||
};
|
|
||||||
|
|
||||||
// This will also call [`Self::remove_session`]
|
|
||||||
self.services
|
|
||||||
.users
|
|
||||||
.remove_device(&user_id, &device_id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_session(
|
|
||||||
&self,
|
|
||||||
authorizing_user: OwnedUserId,
|
|
||||||
requested_scopes: BTreeSet<Scope>,
|
|
||||||
client_name: Option<String>,
|
|
||||||
client_id: String,
|
|
||||||
) -> Result<TokenResponse> {
|
|
||||||
let access_token = Self::generate_token();
|
|
||||||
let refresh_token = Self::generate_token();
|
|
||||||
|
|
||||||
let device_id = requested_scopes
|
|
||||||
.iter()
|
|
||||||
.find_map(|scope| {
|
|
||||||
if let Scope::Device(device_id) = scope {
|
|
||||||
Some(device_id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok_or_else(|| err!("No device ID scope supplied"))?;
|
|
||||||
|
|
||||||
self.services
|
|
||||||
.users
|
|
||||||
.create_device(
|
|
||||||
&authorizing_user,
|
|
||||||
device_id,
|
|
||||||
&access_token,
|
|
||||||
Some(Self::ACCESS_TOKEN_MAX_AGE),
|
|
||||||
client_name,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.db.userdeviceid_oauthsessioninfo.put(
|
|
||||||
(&authorizing_user, device_id),
|
|
||||||
Json(SessionInfo {
|
|
||||||
client_id: client_id.clone(),
|
|
||||||
current_refresh_token: refresh_token.clone(),
|
|
||||||
scopes: requested_scopes.clone(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.db.refreshtoken_refreshtokeninfo.raw_put(
|
|
||||||
&refresh_token,
|
|
||||||
Json(RefreshTokenInfo {
|
|
||||||
client_id: client_id.clone(),
|
|
||||||
user_id: authorizing_user.clone(),
|
|
||||||
device_id: device_id.to_owned(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
info!(
|
|
||||||
?client_id,
|
|
||||||
?authorizing_user,
|
|
||||||
?device_id,
|
|
||||||
?requested_scopes,
|
|
||||||
"Created new oauth session"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(TokenResponse {
|
|
||||||
access_token,
|
|
||||||
token_type: TokenType::Bearer,
|
|
||||||
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
|
||||||
scope: requested_scopes.iter().join(" "),
|
|
||||||
refresh_token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn refresh_session(
|
|
||||||
&self,
|
|
||||||
client_id: String,
|
|
||||||
refresh_token: String,
|
|
||||||
) -> Result<TokenResponse> {
|
|
||||||
let Some(refresh_token_info) = self
|
|
||||||
.db
|
|
||||||
.refreshtoken_refreshtokeninfo
|
|
||||||
.get(&refresh_token)
|
|
||||||
.await
|
|
||||||
.deserialized::<RefreshTokenInfo>()
|
|
||||||
.ok()
|
|
||||||
else {
|
|
||||||
return Err!("Invalid refresh token");
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(&client_id, &refresh_token_info.client_id, "refresh token client id mismatch");
|
|
||||||
|
|
||||||
let mut session_info = self
|
|
||||||
.get_session_info_for_device(
|
|
||||||
&refresh_token_info.user_id,
|
|
||||||
&refresh_token_info.device_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("session info should exist");
|
|
||||||
|
|
||||||
assert_eq!(&client_id, &session_info.client_id, "session info client id mismatch");
|
|
||||||
|
|
||||||
let new_access_token = Self::generate_token();
|
|
||||||
let new_refresh_token = Self::generate_token();
|
|
||||||
let scope = session_info.scopes.iter().join(" ");
|
|
||||||
session_info
|
|
||||||
.current_refresh_token
|
|
||||||
.clone_from(&new_refresh_token);
|
|
||||||
|
|
||||||
self.services
|
|
||||||
.users
|
|
||||||
.set_token(
|
|
||||||
&refresh_token_info.user_id,
|
|
||||||
&refresh_token_info.device_id,
|
|
||||||
&new_access_token,
|
|
||||||
Some(Self::ACCESS_TOKEN_MAX_AGE),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.db.userdeviceid_oauthsessioninfo.put(
|
|
||||||
(&refresh_token_info.user_id, &refresh_token_info.device_id),
|
|
||||||
Json(session_info),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.db.refreshtoken_refreshtokeninfo.remove(&refresh_token);
|
|
||||||
drop(refresh_token);
|
|
||||||
self.db
|
|
||||||
.refreshtoken_refreshtokeninfo
|
|
||||||
.raw_put(&new_refresh_token, Json(refresh_token_info));
|
|
||||||
|
|
||||||
Ok(TokenResponse {
|
|
||||||
access_token: new_access_token,
|
|
||||||
token_type: TokenType::Bearer,
|
|
||||||
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
|
||||||
scope,
|
|
||||||
refresh_token: new_refresh_token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_session(&self, user_id: &UserId, device_id: &DeviceId) {
|
|
||||||
let session_info = self.get_session_info_for_device(user_id, device_id).await;
|
|
||||||
|
|
||||||
if let Some(session_info) = session_info {
|
|
||||||
self.db
|
|
||||||
.refreshtoken_refreshtokeninfo
|
|
||||||
.remove(&session_info.current_refresh_token);
|
|
||||||
self.db
|
|
||||||
.userdeviceid_oauthsessioninfo
|
|
||||||
.del((user_id, device_id));
|
|
||||||
info!(?user_id, ?device_id, "Removed OAuth session");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Issue a ticket for `localpart` to perform some action.
|
|
||||||
pub fn issue_ticket(&self, localpart: String, ticket: OAuthTicket) {
|
|
||||||
self.tickets
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.entry(localpart)
|
|
||||||
.or_default()
|
|
||||||
.insert(ticket, SystemTime::now());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to consume an unexpired ticket for `localpart`.
|
|
||||||
pub fn try_consume_ticket(&self, localpart: &str, ticket: OAuthTicket) -> bool {
|
|
||||||
let now = SystemTime::now();
|
|
||||||
|
|
||||||
self.tickets
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.get_mut(localpart)
|
|
||||||
.and_then(|tickets| tickets.remove(&ticket))
|
|
||||||
.is_some_and(|issued| {
|
|
||||||
now.duration_since(issued)
|
|
||||||
.is_ok_and(|duration| duration < OAuthTicket::MAX_AGE)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use conduwuit::utils::{ReadyExt, stream::TryExpect};
|
||||||
|
use database::{Database, Deserialized, Json, Map};
|
||||||
|
use ruma::{OwnedUserId, UserId};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub(super) struct Data {
|
||||||
|
passwordresettoken_info: Arc<Map>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ResetTokenInfo {
|
||||||
|
pub user: OwnedUserId,
|
||||||
|
pub issued_at: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResetTokenInfo {
|
||||||
|
// one hour
|
||||||
|
const MAX_TOKEN_AGE: Duration = Duration::from_hours(1);
|
||||||
|
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
let now = SystemTime::now();
|
||||||
|
|
||||||
|
now.duration_since(self.issued_at)
|
||||||
|
.is_ok_and(|duration| duration < Self::MAX_TOKEN_AGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Data {
|
||||||
|
pub(super) fn new(db: &Arc<Database>) -> Self {
|
||||||
|
Self {
|
||||||
|
passwordresettoken_info: db["passwordresettoken_info"].clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Associate a reset token with its info in the database.
|
||||||
|
pub(super) fn save_token(&self, token: &str, info: &ResetTokenInfo) {
|
||||||
|
self.passwordresettoken_info.raw_put(token, Json(info));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup the info for a reset token.
|
||||||
|
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<ResetTokenInfo> {
|
||||||
|
self.passwordresettoken_info
|
||||||
|
.get(token)
|
||||||
|
.await
|
||||||
|
.deserialized()
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a user's existing reset token, if any.
|
||||||
|
pub(super) async fn find_token_for_user(
|
||||||
|
&self,
|
||||||
|
user: &UserId,
|
||||||
|
) -> Option<(String, ResetTokenInfo)> {
|
||||||
|
self.passwordresettoken_info
|
||||||
|
.stream::<'_, String, ResetTokenInfo>()
|
||||||
|
.expect_ok()
|
||||||
|
.ready_find(|(_, info)| info.user == user)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a reset token.
|
||||||
|
pub(super) fn remove_token(&self, token: &str) { self.passwordresettoken_info.remove(token); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
mod data;
|
||||||
|
|
||||||
|
use std::{sync::Arc, time::SystemTime};
|
||||||
|
|
||||||
|
use conduwuit::{Err, Result, utils};
|
||||||
|
use data::{Data, ResetTokenInfo};
|
||||||
|
use ruma::OwnedUserId;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Dep, globals,
|
||||||
|
users::{self, HashedPassword},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PASSWORD_RESET_PATH: &str = "/_continuwuity/account/reset_password";
|
||||||
|
pub const RESET_TOKEN_QUERY_PARAM: &str = "token";
|
||||||
|
const RESET_TOKEN_LENGTH: usize = 32;
|
||||||
|
|
||||||
|
pub struct Service {
|
||||||
|
db: Data,
|
||||||
|
services: Services,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Services {
|
||||||
|
users: Dep<users::Service>,
|
||||||
|
globals: Dep<globals::Service>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ValidResetToken {
|
||||||
|
pub token: String,
|
||||||
|
pub info: ResetTokenInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::Service for Service {
|
||||||
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
db: Data::new(args.db),
|
||||||
|
services: Services {
|
||||||
|
users: args.depend::<users::Service>("users"),
|
||||||
|
globals: args.depend::<globals::Service>("globals"),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
/// Generate a random string suitable to be used as a password reset token.
|
||||||
|
#[must_use]
|
||||||
|
pub fn generate_token_string() -> String { utils::random_string(RESET_TOKEN_LENGTH) }
|
||||||
|
|
||||||
|
/// Issue a password reset token for `user`, who must be a local user with
|
||||||
|
/// the `password` origin.
|
||||||
|
pub async fn issue_token(&self, user_id: OwnedUserId) -> Result<ValidResetToken> {
|
||||||
|
if !self.services.globals.user_is_local(&user_id) {
|
||||||
|
return Err!("Cannot issue a password reset token for remote user {user_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_id == self.services.globals.server_user {
|
||||||
|
return Err!("Cannot issue a password reset token for the server user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.services.users.is_deactivated(&user_id).await? {
|
||||||
|
return Err!("Cannot issue a password reset token for deactivated user {user_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((existing_token, _)) = self.db.find_token_for_user(&user_id).await {
|
||||||
|
self.db.remove_token(&existing_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = Self::generate_token_string();
|
||||||
|
let info = ResetTokenInfo {
|
||||||
|
user: user_id,
|
||||||
|
issued_at: SystemTime::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.db.save_token(&token, &info);
|
||||||
|
|
||||||
|
Ok(ValidResetToken { token, info })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `token` represents a valid, non-expired password reset token.
|
||||||
|
pub async fn check_token(&self, token: &str) -> Option<ValidResetToken> {
|
||||||
|
self.db.lookup_token_info(token).await.and_then(|info| {
|
||||||
|
if info.is_valid() {
|
||||||
|
Some(ValidResetToken { token: token.to_owned(), info })
|
||||||
|
} else {
|
||||||
|
self.db.remove_token(token);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume the supplied valid token, using it to change its user's password
|
||||||
|
/// to `new_password`.
|
||||||
|
pub async fn consume_token(
|
||||||
|
&self,
|
||||||
|
ValidResetToken { token, info }: ValidResetToken,
|
||||||
|
new_password: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
if info.is_valid() {
|
||||||
|
self.db.remove_token(&token);
|
||||||
|
self.services
|
||||||
|
.users
|
||||||
|
.set_password(&info.user, Some(HashedPassword::new(new_password)?));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ use futures::{
|
|||||||
stream::{iter, once},
|
stream::{iter, once},
|
||||||
};
|
};
|
||||||
use ruma::OwnedUserId;
|
use ruma::OwnedUserId;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{Dep, config, firstrun};
|
use crate::{Dep, config, firstrun};
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ struct Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A validated registration token which may be used to create an account.
|
/// A validated registration token which may be used to create an account.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug)]
|
||||||
pub struct ValidToken {
|
pub struct ValidToken {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub source: ValidTokenSource,
|
pub source: ValidTokenSource,
|
||||||
@@ -45,7 +44,7 @@ impl PartialEq<str> for ValidToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The source of a valid database token.
|
/// The source of a valid database token.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug)]
|
||||||
pub enum ValidTokenSource {
|
pub enum ValidTokenSource {
|
||||||
/// The static token set in the homeserver's config file.
|
/// The static token set in the homeserver's config file.
|
||||||
Config,
|
Config,
|
||||||
|
|||||||
@@ -1,233 +1,456 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashSet, VecDeque, hash_map},
|
collections::{BTreeMap, HashMap, HashSet, VecDeque, hash_map},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use assign::assign;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Event, PduEvent, debug, debug_warn, implement, matrix::event::gen_event_id_canonical_json,
|
Event, PduEvent, debug, debug_info, debug_warn, err, error,
|
||||||
trace, utils::continue_exponential_backoff_secs, warn,
|
matrix::event::gen_event_id_canonical_json,
|
||||||
|
state_res::lexicographical_topological_sort,
|
||||||
|
trace,
|
||||||
|
utils::{IterStream, continue_exponential_backoff_secs, stream::BroadbandExt},
|
||||||
|
warn,
|
||||||
};
|
};
|
||||||
|
use futures::StreamExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
|
CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||||
api::federation::event::get_event,
|
RoomId, ServerName, UInt,
|
||||||
|
api::federation::event::{get_event, get_missing_events},
|
||||||
|
int,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::get_room_version_rules;
|
use super::get_room_version_rules;
|
||||||
|
|
||||||
/// Find the event and auth it. Once the event is validated (steps 1 - 8)
|
/// Attempts to build a localised directed acyclic graph out of the given PDUs,
|
||||||
/// it is appended to the outliers Tree.
|
/// returning them in a topologically sorted order.
|
||||||
///
|
///
|
||||||
/// Returns pdu and if we fetched it over federation the raw json.
|
/// This is used to attempt to process PDUs in an order that respects their
|
||||||
///
|
/// dependencies, however it is ultimately the sender's responsibility to send
|
||||||
/// a. Look in the main timeline (pduid_pdu tree)
|
/// them in a processable order, so this is just a best effort attempt. It does
|
||||||
/// b. Look at outlier pdu tree
|
/// not account for power levels or other tie breaks.
|
||||||
/// c. Ask origin server over federation
|
pub async fn build_local_dag<S: std::hash::BuildHasher>(
|
||||||
/// d. TODO: Ask other servers over federation?
|
pdu_map: &HashMap<OwnedEventId, CanonicalJsonObject, S>,
|
||||||
#[implement(super::Service)]
|
) -> conduwuit::Result<Vec<OwnedEventId>> {
|
||||||
pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
|
debug_assert!(pdu_map.len() >= 2, "needless call to build_local_dag with less than 2 PDUs");
|
||||||
&self,
|
let mut dag: HashMap<OwnedEventId, HashSet<OwnedEventId>> =
|
||||||
origin: &'a ServerName,
|
HashMap::with_capacity(pdu_map.len());
|
||||||
events: Events,
|
let mut id_origin_ts: HashMap<OwnedEventId, _> = HashMap::with_capacity(pdu_map.len());
|
||||||
create_event: &'a Pdu,
|
|
||||||
room_id: &'a RoomId,
|
|
||||||
) -> Vec<(PduEvent, Option<BTreeMap<String, CanonicalJsonValue>>)>
|
|
||||||
where
|
|
||||||
Pdu: Event + Send + Sync,
|
|
||||||
Events: Iterator<Item = &'a EventId> + Clone + Send,
|
|
||||||
{
|
|
||||||
let back_off = |id| match self
|
|
||||||
.services
|
|
||||||
.globals
|
|
||||||
.bad_event_ratelimiter
|
|
||||||
.write()
|
|
||||||
.entry(id)
|
|
||||||
{
|
|
||||||
| hash_map::Entry::Vacant(e) => {
|
|
||||||
e.insert((Instant::now(), 1));
|
|
||||||
},
|
|
||||||
| hash_map::Entry::Occupied(mut e) => {
|
|
||||||
*e.get_mut() = (Instant::now(), e.get().1.saturating_add(1));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut events_with_auth_events = Vec::with_capacity(events.clone().count());
|
for (event_id, value) in pdu_map {
|
||||||
trace!("Fetching {} outlier pdus", events.clone().count());
|
// We already checked that these properties are correct in parse_incoming_pdu,
|
||||||
|
// so it's safe to unwrap here.
|
||||||
|
// We also filter to remove any prev_events that are not in this pdu_map, as we
|
||||||
|
// need to have at least one event with zero out degrees for the lexico-topo
|
||||||
|
// sort below. If there are multiple events with omitted prevs, they will be
|
||||||
|
// ordered by timestamp, then event ID. At that point though, it's unlikely to
|
||||||
|
// matter.
|
||||||
|
let prev_events = value
|
||||||
|
.get("prev_events")
|
||||||
|
.unwrap()
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|v| EventId::parse(v.as_str().unwrap()).unwrap())
|
||||||
|
.filter(|id| pdu_map.contains_key(id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
for id in events {
|
dag.insert(event_id.clone(), prev_events);
|
||||||
// a. Look in the main timeline (pduid_pdu tree)
|
let origin_server_ts = value
|
||||||
// b. Look at outlier pdu tree
|
.get("origin_server_ts")
|
||||||
// (get_pdu_json checks both)
|
.and_then(CanonicalJsonValue::as_integer)
|
||||||
if let Ok(local_pdu) = self.services.timeline.get_pdu(id).await {
|
.unwrap_or_default();
|
||||||
trace!("Found {id} in main timeline or outlier tree");
|
id_origin_ts.insert(event_id.clone(), origin_server_ts);
|
||||||
events_with_auth_events.push((id.to_owned(), Some(local_pdu), vec![]));
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// c. Ask origin server over federation
|
debug!(count = dag.len(), "Sorting incoming events with partial graph");
|
||||||
// We also handle its auth chain here so we don't get a stack overflow in
|
lexicographical_topological_sort(&dag, &async |node_id| {
|
||||||
// handle_outlier_pdu.
|
// Note: we don't bother fetching power levels because that would massively slow
|
||||||
let mut todo_auth_events: VecDeque<_> = [id.to_owned()].into();
|
// this function down. This is a best-effort attempt to order events correctly
|
||||||
let mut events_in_reverse_order = Vec::with_capacity(todo_auth_events.len());
|
// for processing, however ultimately that should be the sender's job.
|
||||||
|
let ts = id_origin_ts
|
||||||
|
.get(&node_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| int!(0))
|
||||||
|
.to_string()
|
||||||
|
.parse::<u64>()
|
||||||
|
.ok()
|
||||||
|
.and_then(UInt::new)
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok((int!(0), MilliSecondsSinceUnixEpoch(ts)))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.inspect(|sorted| {
|
||||||
|
debug_assert_eq!(
|
||||||
|
sorted.len(),
|
||||||
|
pdu_map.len(),
|
||||||
|
"Sorted graph was not the same size as the input graph"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map_err(|e| err!("failed to resolve local graph: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
let mut events_all = HashSet::with_capacity(todo_auth_events.len());
|
impl super::Service {
|
||||||
while let Some(next_id) = todo_auth_events.pop_front() {
|
/// Uses `/_matrix/federation/v1/get_missing_events` to fill gaps in the
|
||||||
if let Some((time, tries)) = self
|
/// DAG.
|
||||||
.services
|
///
|
||||||
.globals
|
/// When this function is called, the "earliest events" (current forward
|
||||||
.bad_event_ratelimiter
|
/// extremities) will be collected, and the function will loop with an
|
||||||
.read()
|
/// exponentially incrementing limit (up to 100 per request) until it has
|
||||||
.get(&*next_id)
|
/// filled the gap, i.e. when the remote says there's no more events.
|
||||||
{
|
///
|
||||||
// Exponential backoff
|
/// This function will iterate until the remote returns no more events,
|
||||||
const MIN_DURATION: u64 = 60 * 2;
|
/// increasing the limit by a factor of 10. If 100 iterations are reached or
|
||||||
const MAX_DURATION: u64 = 60 * 60 * 8;
|
/// max_fetch_prev_events events are backfilled, the function will give up
|
||||||
if continue_exponential_backoff_secs(
|
/// and return what it has, to avoid pulling in too much data (for example,
|
||||||
MIN_DURATION,
|
/// absurdly large gaps).
|
||||||
MAX_DURATION,
|
///
|
||||||
time.elapsed(),
|
/// This function does not persist the events. The caller is responsible for
|
||||||
*tries,
|
/// passing them through handle_incoming_pdu.
|
||||||
) {
|
///
|
||||||
debug_warn!(
|
/// ## Parameters
|
||||||
tried = ?*tries,
|
///
|
||||||
elapsed = ?time.elapsed(),
|
/// - `room_id`: The room's ID.
|
||||||
"Backing off from {next_id}",
|
/// - `head`: The event we are potentially missing prev_events for.
|
||||||
);
|
/// - `tail`: The most recently known events in the graph (typically forward
|
||||||
continue;
|
/// extremities).
|
||||||
}
|
/// - `via`: The server to ask for missing events.
|
||||||
}
|
/// - `min_depth`: Don't process events with a `depth` lower than this
|
||||||
|
/// value. Not massively useful, but can help short-circuit infinite loops
|
||||||
|
/// and weird edge paths.
|
||||||
|
pub async fn get_missing_events(
|
||||||
|
&self,
|
||||||
|
room_id: &RoomId,
|
||||||
|
head: &PduEvent,
|
||||||
|
tail: Vec<OwnedEventId>,
|
||||||
|
via: &ServerName,
|
||||||
|
min_depth: UInt,
|
||||||
|
) -> conduwuit::Result<HashMap<OwnedEventId, PduEvent>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
let missing_count = head
|
||||||
|
.prev_events()
|
||||||
|
.stream()
|
||||||
|
.broad_filter_map(|event_id| async move {
|
||||||
|
match self
|
||||||
|
.services
|
||||||
|
.timeline
|
||||||
|
.get_non_outlier_pdu_json(event_id)
|
||||||
|
.await
|
||||||
|
.inspect(|_| debug!("Found prev_event {event_id} locally."))
|
||||||
|
.inspect_err(
|
||||||
|
|e| debug!(%e, "Could not find prev_event {event_id} locally."),
|
||||||
|
) {
|
||||||
|
| Ok(_) => None,
|
||||||
|
| Err(_) => Some(event_id),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
.await;
|
||||||
|
debug_assert_ne!(
|
||||||
|
missing_count, 0,
|
||||||
|
"event passed to get_missing_events is not missing any events (wasteful call)"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if events_all.contains(&next_id) {
|
let mut discovered = HashMap::with_capacity(20);
|
||||||
continue;
|
let mut latest_events = vec![head.event_id().to_owned()];
|
||||||
}
|
let mut iterations = 0_u8;
|
||||||
|
loop {
|
||||||
if self.services.timeline.pdu_exists(&next_id).await {
|
iterations = iterations.saturating_add(1);
|
||||||
trace!("Found {next_id} in db");
|
let limit = iterations.saturating_mul(10).min(100);
|
||||||
continue;
|
debug_info!(%limit, %via, %iterations, discovered=discovered.len(), %min_depth, "Attempting to gap fill missing events");
|
||||||
}
|
let response: get_missing_events::v1::Response = self
|
||||||
|
|
||||||
debug!("Fetching {next_id} over federation from {origin}.");
|
|
||||||
match self
|
|
||||||
.services
|
.services
|
||||||
.sending
|
.sending
|
||||||
.send_federation_request(
|
.send_federation_request(
|
||||||
origin,
|
via,
|
||||||
get_event::v1::Request::new((*next_id).to_owned()),
|
assign!(
|
||||||
|
get_missing_events::v1::Request::new(
|
||||||
|
room_id.to_owned(),
|
||||||
|
tail.clone(),
|
||||||
|
latest_events.clone()
|
||||||
|
),
|
||||||
|
{limit: limit.into(), min_depth}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
{
|
|
||||||
| Ok(res) => {
|
|
||||||
debug!("Got {next_id} over federation from {origin}");
|
|
||||||
let Ok(room_version_rules) = get_room_version_rules(create_event) else {
|
|
||||||
back_off((*next_id).to_owned());
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok((calculated_event_id, value)) =
|
if response.events.is_empty() {
|
||||||
gen_event_id_canonical_json(&res.pdu, &room_version_rules)
|
debug_info!(%via, "Finished gap filling missing events (remote returned no more events).");
|
||||||
else {
|
break;
|
||||||
back_off((*next_id).to_owned());
|
}
|
||||||
continue;
|
debug_info!("Got {} events back from remote", response.events.len());
|
||||||
};
|
|
||||||
|
|
||||||
if calculated_event_id != *next_id {
|
latest_events.clear();
|
||||||
warn!(
|
for raw_event in response.events {
|
||||||
"Server didn't return event id we requested: requested: {next_id}, \
|
let (_, event_id, pdu_json) = self.parse_incoming_pdu(&raw_event).await?;
|
||||||
we got {calculated_event_id}. Event: {:?}",
|
let pdu = PduEvent::from_id_val(&event_id, pdu_json).map_err(|e| {
|
||||||
&res.pdu
|
err!(Request(BadJson("Failed to parse backfilled event {event_id}: {e}")))
|
||||||
);
|
})?;
|
||||||
|
|
||||||
|
if pdu.depth < min_depth {
|
||||||
|
debug_warn!(
|
||||||
|
"Received PDU with depth {} below min_depth {}, ignoring",
|
||||||
|
pdu.depth,
|
||||||
|
min_depth
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for prev_event_id in pdu.prev_events() {
|
||||||
|
if discovered.contains_key(prev_event_id) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if self
|
||||||
if let Some(auth_events) = value
|
.services
|
||||||
.get("auth_events")
|
.timeline
|
||||||
.and_then(CanonicalJsonValue::as_array)
|
.non_outlier_pdu_exists(prev_event_id)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
for auth_event in auth_events {
|
continue;
|
||||||
match serde_json::from_value::<OwnedEventId>(
|
|
||||||
auth_event.clone().into(),
|
|
||||||
) {
|
|
||||||
| Ok(auth_event) => {
|
|
||||||
trace!(
|
|
||||||
"Found auth event id {auth_event} for event {next_id}"
|
|
||||||
);
|
|
||||||
todo_auth_events.push_back(auth_event);
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
warn!("Auth event id is not valid");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!("Auth event list invalid");
|
|
||||||
}
|
}
|
||||||
|
latest_events.push(prev_event_id.to_owned());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
events_in_reverse_order.push((next_id.clone(), value));
|
discovered.insert(event_id.clone(), pdu);
|
||||||
events_all.insert(next_id);
|
}
|
||||||
},
|
|
||||||
| Err(e) => {
|
if latest_events.is_empty() {
|
||||||
warn!("Failed to fetch auth event {next_id} from {origin}: {e}");
|
break;
|
||||||
back_off((*next_id).to_owned());
|
} else if discovered.len() > self.services.server.config.max_fetch_prev_events.into()
|
||||||
},
|
|| iterations >= 20
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
filled=discovered.len(),
|
||||||
|
max_fetch_prev_events=self.services.server.config.max_fetch_prev_events,
|
||||||
|
%iterations,
|
||||||
|
"Gap too large, giving up"
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events_with_auth_events.push((id.to_owned(), None, events_in_reverse_order));
|
Ok(discovered)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pdus = Vec::with_capacity(events_with_auth_events.len());
|
/// Find the event and auth it. Once the event is validated (steps 1 - 8)
|
||||||
for (id, local_pdu, events_in_reverse_order) in events_with_auth_events {
|
/// it is appended to the outliers Tree.
|
||||||
// a. Look in the main timeline (pduid_pdu tree)
|
///
|
||||||
// b. Look at outlier pdu tree
|
/// Returns pdu and if we fetched it over federation the raw json.
|
||||||
// (get_pdu_json checks both)
|
///
|
||||||
if let Some(local_pdu) = local_pdu {
|
/// a. Look in the main timeline (pduid_pdu tree)
|
||||||
trace!("Found {id} in main timeline or outlier tree");
|
/// b. Look at outlier pdu tree
|
||||||
pdus.push((local_pdu.clone(), None));
|
/// c. Ask origin server over federation
|
||||||
}
|
/// d. TODO: Ask other servers over federation?
|
||||||
|
#[deprecated]
|
||||||
|
pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
|
||||||
|
&self,
|
||||||
|
origin: &'a ServerName,
|
||||||
|
events: Events,
|
||||||
|
create_event: &'a Pdu,
|
||||||
|
room_id: &'a RoomId,
|
||||||
|
) -> Vec<(PduEvent, Option<BTreeMap<String, CanonicalJsonValue>>)>
|
||||||
|
where
|
||||||
|
Pdu: Event + Send + Sync,
|
||||||
|
Events: Iterator<Item = &'a EventId> + Clone + Send,
|
||||||
|
{
|
||||||
|
let back_off = |id| match self
|
||||||
|
.services
|
||||||
|
.globals
|
||||||
|
.bad_event_ratelimiter
|
||||||
|
.write()
|
||||||
|
.entry(id)
|
||||||
|
{
|
||||||
|
| hash_map::Entry::Vacant(e) => {
|
||||||
|
e.insert((Instant::now(), 1));
|
||||||
|
},
|
||||||
|
| hash_map::Entry::Occupied(mut e) => {
|
||||||
|
*e.get_mut() = (Instant::now(), e.get().1.saturating_add(1));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
for (next_id, value) in events_in_reverse_order.into_iter().rev() {
|
let mut events_with_auth_events = Vec::with_capacity(events.clone().count());
|
||||||
if let Some((time, tries)) = self
|
trace!("Fetching {} outlier pdus", events.clone().count());
|
||||||
.services
|
|
||||||
.globals
|
for id in events {
|
||||||
.bad_event_ratelimiter
|
// a. Look in the main timeline (pduid_pdu tree)
|
||||||
.read()
|
// b. Look at outlier pdu tree
|
||||||
.get(&*next_id)
|
// (get_pdu_json checks both)
|
||||||
{
|
if let Ok(local_pdu) = self.services.timeline.get_pdu(id).await {
|
||||||
// Exponential backoff
|
trace!("Found {id} in main timeline or outlier tree");
|
||||||
const MIN_DURATION: u64 = 5 * 60;
|
events_with_auth_events.push((id.to_owned(), Some(local_pdu), vec![]));
|
||||||
const MAX_DURATION: u64 = 60 * 60 * 24;
|
continue;
|
||||||
if continue_exponential_backoff_secs(
|
}
|
||||||
MIN_DURATION,
|
|
||||||
MAX_DURATION,
|
// c. Ask origin server over federation
|
||||||
time.elapsed(),
|
// We also handle its auth chain here so we don't get a stack overflow in
|
||||||
*tries,
|
// handle_outlier_pdu.
|
||||||
) {
|
let mut todo_auth_events: VecDeque<_> = [id.to_owned()].into();
|
||||||
debug!("Backing off from {next_id}");
|
let mut events_in_reverse_order = Vec::with_capacity(todo_auth_events.len());
|
||||||
|
|
||||||
|
let mut events_all = HashSet::with_capacity(todo_auth_events.len());
|
||||||
|
while let Some(next_id) = todo_auth_events.pop_front() {
|
||||||
|
if let Some((time, tries)) = self
|
||||||
|
.services
|
||||||
|
.globals
|
||||||
|
.bad_event_ratelimiter
|
||||||
|
.read()
|
||||||
|
.get(&*next_id)
|
||||||
|
{
|
||||||
|
// Exponential backoff
|
||||||
|
const MIN_DURATION: u64 = 60 * 2;
|
||||||
|
const MAX_DURATION: u64 = 60 * 60 * 8;
|
||||||
|
if continue_exponential_backoff_secs(
|
||||||
|
MIN_DURATION,
|
||||||
|
MAX_DURATION,
|
||||||
|
time.elapsed(),
|
||||||
|
*tries,
|
||||||
|
) {
|
||||||
|
debug_warn!(
|
||||||
|
tried = ?*tries,
|
||||||
|
elapsed = ?time.elapsed(),
|
||||||
|
"Backing off from {next_id}",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if events_all.contains(&next_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.services.timeline.pdu_exists(&next_id).await {
|
||||||
|
trace!("Found {next_id} in db");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Fetching {next_id} over federation from {origin}.");
|
||||||
|
match self
|
||||||
|
.services
|
||||||
|
.sending
|
||||||
|
.send_federation_request(
|
||||||
|
origin,
|
||||||
|
get_event::v1::Request::new((*next_id).to_owned()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok(res) => {
|
||||||
|
debug!("Got {next_id} over federation from {origin}");
|
||||||
|
let Ok(room_version_rules) = get_room_version_rules(create_event) else {
|
||||||
|
back_off((*next_id).to_owned());
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok((calculated_event_id, value)) =
|
||||||
|
gen_event_id_canonical_json(&res.pdu, &room_version_rules)
|
||||||
|
else {
|
||||||
|
back_off((*next_id).to_owned());
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if calculated_event_id != *next_id {
|
||||||
|
warn!(
|
||||||
|
"Server didn't return event id we requested: requested: \
|
||||||
|
{next_id}, we got {calculated_event_id}. Event: {:?}",
|
||||||
|
&res.pdu
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(auth_events) = value
|
||||||
|
.get("auth_events")
|
||||||
|
.and_then(CanonicalJsonValue::as_array)
|
||||||
|
{
|
||||||
|
for auth_event in auth_events {
|
||||||
|
match serde_json::from_value::<OwnedEventId>(
|
||||||
|
auth_event.clone().into(),
|
||||||
|
) {
|
||||||
|
| Ok(auth_event) => {
|
||||||
|
trace!(
|
||||||
|
"Found auth event id {auth_event} for event \
|
||||||
|
{next_id}"
|
||||||
|
);
|
||||||
|
todo_auth_events.push_back(auth_event);
|
||||||
|
},
|
||||||
|
| _ => {
|
||||||
|
warn!("Auth event id is not valid");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Auth event list invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
events_in_reverse_order.push((next_id.clone(), value));
|
||||||
|
events_all.insert(next_id);
|
||||||
|
},
|
||||||
|
| Err(e) => {
|
||||||
|
warn!("Failed to fetch auth event {next_id} from {origin}: {e}");
|
||||||
|
back_off((*next_id).to_owned());
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("Handling outlier {next_id}");
|
events_with_auth_events.push((id.to_owned(), None, events_in_reverse_order));
|
||||||
match Box::pin(self.handle_outlier_pdu(
|
}
|
||||||
origin,
|
|
||||||
create_event,
|
let mut pdus = Vec::with_capacity(events_with_auth_events.len());
|
||||||
&next_id,
|
for (id, local_pdu, events_in_reverse_order) in events_with_auth_events {
|
||||||
room_id,
|
// a. Look in the main timeline (pduid_pdu tree)
|
||||||
value.clone(),
|
// b. Look at outlier pdu tree
|
||||||
true,
|
// (get_pdu_json checks both)
|
||||||
))
|
if let Some(local_pdu) = local_pdu {
|
||||||
.await
|
trace!("Found {id} in main timeline or outlier tree");
|
||||||
{
|
pdus.push((local_pdu.clone(), None));
|
||||||
| Ok((pdu, json)) =>
|
}
|
||||||
if next_id == *id {
|
|
||||||
trace!("Handled outlier {next_id} (original request)");
|
for (next_id, value) in events_in_reverse_order.into_iter().rev() {
|
||||||
pdus.push((pdu, Some(json)));
|
if let Some((time, tries)) = self
|
||||||
|
.services
|
||||||
|
.globals
|
||||||
|
.bad_event_ratelimiter
|
||||||
|
.read()
|
||||||
|
.get(&*next_id)
|
||||||
|
{
|
||||||
|
// Exponential backoff
|
||||||
|
const MIN_DURATION: u64 = 5 * 60;
|
||||||
|
const MAX_DURATION: u64 = 60 * 60 * 24;
|
||||||
|
if continue_exponential_backoff_secs(
|
||||||
|
MIN_DURATION,
|
||||||
|
MAX_DURATION,
|
||||||
|
time.elapsed(),
|
||||||
|
*tries,
|
||||||
|
) {
|
||||||
|
debug!("Backing off from {next_id}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("Handling outlier {next_id}");
|
||||||
|
match Box::pin(self.handle_outlier_pdu(
|
||||||
|
origin,
|
||||||
|
create_event,
|
||||||
|
&next_id,
|
||||||
|
room_id,
|
||||||
|
value.clone(),
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok((pdu, json)) =>
|
||||||
|
if next_id == *id {
|
||||||
|
trace!("Handled outlier {next_id} (original request)");
|
||||||
|
pdus.push((pdu, Some(json)));
|
||||||
|
},
|
||||||
|
| Err(e) => {
|
||||||
|
warn!("Authentication of event {next_id} failed: {e:?}");
|
||||||
|
back_off(next_id);
|
||||||
},
|
},
|
||||||
| Err(e) => {
|
}
|
||||||
warn!("Authentication of event {next_id} failed: {e:?}");
|
|
||||||
back_off(next_id);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
trace!("Fetched and handled {} outlier pdus", pdus.len());
|
||||||
|
pdus
|
||||||
}
|
}
|
||||||
trace!("Fetched and handled {} outlier pdus", pdus.len());
|
|
||||||
pdus
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,128 +1,96 @@
|
|||||||
use std::{
|
use std::collections::HashMap;
|
||||||
collections::{BTreeMap, HashMap, HashSet, VecDeque},
|
|
||||||
iter::once,
|
|
||||||
};
|
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Event, PduEvent, Result, debug_warn, err, implement,
|
Event, PduEvent, debug, debug_info,
|
||||||
state_res::{self},
|
utils::{BoolExt, IterStream, stream::BroadbandExt},
|
||||||
};
|
warn,
|
||||||
use futures::{FutureExt, future};
|
|
||||||
use ruma::{
|
|
||||||
CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, ServerName,
|
|
||||||
int, uint,
|
|
||||||
};
|
};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use ruma::{RoomId, ServerName};
|
||||||
|
|
||||||
use super::check_room_id;
|
use crate::rooms::event_handler::build_local_dag;
|
||||||
|
|
||||||
#[implement(super::Service)]
|
impl super::Service {
|
||||||
#[tracing::instrument(
|
/// Fetches any missing prev_events for this event and persists them before
|
||||||
level = "debug",
|
/// returning.
|
||||||
skip_all,
|
pub(super) async fn fetch_prevs(
|
||||||
fields(%origin),
|
&self,
|
||||||
)]
|
room_id: &RoomId,
|
||||||
#[allow(clippy::type_complexity)]
|
create_event: &PduEvent,
|
||||||
pub(super) async fn fetch_prev<'a, Pdu, Events>(
|
incoming_pdu: &PduEvent,
|
||||||
&self,
|
origin: &ServerName,
|
||||||
origin: &ServerName,
|
) -> conduwuit::Result<()> {
|
||||||
create_event: &Pdu,
|
let missing = incoming_pdu
|
||||||
room_id: &RoomId,
|
.prev_events()
|
||||||
first_ts_in_room: MilliSecondsSinceUnixEpoch,
|
.stream()
|
||||||
initial_set: Events,
|
.broad_filter_map(|event_id| async move {
|
||||||
) -> Result<(
|
self.services
|
||||||
Vec<OwnedEventId>,
|
.timeline
|
||||||
HashMap<OwnedEventId, (PduEvent, BTreeMap<String, CanonicalJsonValue>)>,
|
.get_non_outlier_pdu_json(event_id)
|
||||||
)>
|
.await
|
||||||
where
|
.is_ok()
|
||||||
Pdu: Event + Send + Sync,
|
.or(|| event_id.to_owned())
|
||||||
Events: Iterator<Item = &'a EventId> + Clone + Send,
|
})
|
||||||
{
|
.collect::<Vec<_>>()
|
||||||
let num_ids = initial_set.clone().count();
|
.await;
|
||||||
let mut eventid_info = HashMap::new();
|
if missing.is_empty() {
|
||||||
let mut graph: HashMap<OwnedEventId, _> = HashMap::with_capacity(num_ids);
|
debug!(event_id=%incoming_pdu.event_id(), "No missing prev events.");
|
||||||
let mut todo_outlier_stack: VecDeque<OwnedEventId> =
|
return Ok(());
|
||||||
initial_set.map(ToOwned::to_owned).collect();
|
|
||||||
|
|
||||||
let mut amount = 0;
|
|
||||||
|
|
||||||
while let Some(prev_event_id) = todo_outlier_stack.pop_front() {
|
|
||||||
self.services.server.check_running()?;
|
|
||||||
|
|
||||||
match self
|
|
||||||
.fetch_and_handle_outliers(
|
|
||||||
origin,
|
|
||||||
once(prev_event_id.as_ref()),
|
|
||||||
create_event,
|
|
||||||
room_id,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await
|
|
||||||
.pop()
|
|
||||||
{
|
|
||||||
| Some((pdu, mut json_opt)) => {
|
|
||||||
check_room_id(room_id, &pdu)?;
|
|
||||||
|
|
||||||
let limit = self.services.server.config.max_fetch_prev_events;
|
|
||||||
if amount > limit {
|
|
||||||
debug_warn!("Max prev event limit reached! Limit: {limit}");
|
|
||||||
graph.insert(prev_event_id.clone(), HashSet::new());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if json_opt.is_none() {
|
|
||||||
json_opt = self
|
|
||||||
.services
|
|
||||||
.outlier
|
|
||||||
.get_outlier_pdu_json(&prev_event_id)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(json) = json_opt {
|
|
||||||
if pdu.origin_server_ts() > first_ts_in_room {
|
|
||||||
amount = amount.saturating_add(1);
|
|
||||||
for prev_prev in pdu.prev_events() {
|
|
||||||
if !graph.contains_key(prev_prev) {
|
|
||||||
todo_outlier_stack.push_back(prev_prev.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
graph.insert(
|
|
||||||
prev_event_id.clone(),
|
|
||||||
pdu.prev_events().map(ToOwned::to_owned).collect(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Time based check failed
|
|
||||||
graph.insert(prev_event_id.clone(), HashSet::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
eventid_info.insert(prev_event_id.clone(), (pdu, json));
|
|
||||||
} else {
|
|
||||||
// Get json failed, so this was not fetched over federation
|
|
||||||
graph.insert(prev_event_id.clone(), HashSet::new());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
// Fetch and handle failed
|
|
||||||
graph.insert(prev_event_id.clone(), HashSet::new());
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
debug!(%room_id, event_id=%incoming_pdu.event_id(), ?missing, "Fetching previous events");
|
||||||
|
let tail = self
|
||||||
|
.services
|
||||||
|
.state
|
||||||
|
.get_forward_extremities(room_id)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let backfilled = self
|
||||||
|
.get_missing_events(
|
||||||
|
room_id,
|
||||||
|
incoming_pdu,
|
||||||
|
tail,
|
||||||
|
origin,
|
||||||
|
self.services.metadata.get_mindepth(room_id).await,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
debug_info!("Fetched {} missing events", backfilled.len());
|
||||||
|
|
||||||
|
// Persist all fetched events
|
||||||
|
let mapped = backfilled
|
||||||
|
.iter()
|
||||||
|
.map(|(eid, evt)| {
|
||||||
|
let mut obj = evt.to_canonical_object();
|
||||||
|
obj.remove("event_id"); // event_id is inserted by backfill_missing_events
|
||||||
|
(eid.clone(), obj)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
let to_persist = if mapped.len() <= 1 {
|
||||||
|
mapped.keys().map(ToOwned::to_owned).collect()
|
||||||
|
} else {
|
||||||
|
build_local_dag(&mapped).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
for event_id in to_persist {
|
||||||
|
debug_info!("Persisting fetched prev event {event_id}");
|
||||||
|
let obj = mapped.get(&event_id).cloned().unwrap();
|
||||||
|
match self
|
||||||
|
.handle_outlier_pdu(origin, create_event, &event_id, room_id, obj, false)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok((pdu, val)) =>
|
||||||
|
self.upgrade_outlier_to_timeline_pdu(pdu, val, create_event, origin, room_id)
|
||||||
|
.await,
|
||||||
|
| Err(e) => {
|
||||||
|
warn!("Failed to persist prev_event {event_id}: {e}");
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE because i keep forgetting: the caller persists incoming_pdu.
|
||||||
|
// we only care about its prev events
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
let event_fetch = |event_id| {
|
|
||||||
let origin_server_ts = eventid_info
|
|
||||||
.get(&event_id)
|
|
||||||
.map_or_else(|| uint!(0), |info| info.0.origin_server_ts().get());
|
|
||||||
|
|
||||||
// This return value is the key used for sorting events,
|
|
||||||
// events are then sorted by power level, time,
|
|
||||||
// and lexically by event_id.
|
|
||||||
future::ok((int!(0), MilliSecondsSinceUnixEpoch(origin_server_ts)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let sorted = state_res::lexicographical_topological_sort(&graph, &event_fetch)
|
|
||||||
.await
|
|
||||||
.map_err(|e| err!(Database(error!("Error sorting prev events: {e}"))))?;
|
|
||||||
|
|
||||||
Ok((sorted, eventid_info))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::collections::{HashMap, hash_map};
|
use std::collections::{HashMap, hash_map};
|
||||||
|
|
||||||
use conduwuit::{Err, Event, Result, debug, debug_warn, err, implement};
|
use conduwuit::{Err, Event, Result, debug, debug_warn, err, implement};
|
||||||
use futures::FutureExt;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
EventId, OwnedEventId, RoomId, ServerName, api::federation::event::get_room_state_ids,
|
EventId, OwnedEventId, RoomId, ServerName, api::federation::event::get_room_state_ids,
|
||||||
events::StateEventType,
|
events::StateEventType,
|
||||||
@@ -42,7 +41,6 @@ where
|
|||||||
let state_ids = res.pdu_ids.iter().map(AsRef::as_ref);
|
let state_ids = res.pdu_ids.iter().map(AsRef::as_ref);
|
||||||
let state_vec = self
|
let state_vec = self
|
||||||
.fetch_and_handle_outliers(origin, state_ids, create_event, room_id)
|
.fetch_and_handle_outliers(origin, state_ids, create_event, room_id)
|
||||||
.boxed()
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut state: HashMap<ShortStateKey, OwnedEventId> = HashMap::with_capacity(state_vec.len());
|
let mut state: HashMap<ShortStateKey, OwnedEventId> = HashMap::with_capacity(state_vec.len());
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
use std::{
|
use std::{collections::BTreeMap, time::Instant};
|
||||||
collections::{BTreeMap, hash_map},
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Event, PduEvent, Result, debug::INFO_SPAN_LEVEL, debug_error, debug_info, defer, err,
|
Err, Event, PduEvent, Result, debug::INFO_SPAN_LEVEL, debug_error, debug_info, defer, err,
|
||||||
implement, info, trace, utils::stream::IterStream, warn,
|
implement, info, trace, warn,
|
||||||
};
|
};
|
||||||
use futures::{
|
use futures::{
|
||||||
FutureExt, TryFutureExt, TryStreamExt,
|
FutureExt,
|
||||||
future::{OptionFuture, try_join4},
|
future::{OptionFuture, try_join4},
|
||||||
};
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
@@ -236,63 +233,21 @@ pub async fn handle_incoming_pdu<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip old events
|
// Skip old events
|
||||||
let first_ts_in_room = self
|
// let first_ts_in_room = self
|
||||||
.services
|
// .services
|
||||||
.timeline
|
// .timeline
|
||||||
.first_pdu_in_room(room_id)
|
// .first_pdu_in_room(room_id)
|
||||||
.await?
|
// .await?
|
||||||
.origin_server_ts();
|
// .origin_server_ts();
|
||||||
|
|
||||||
// 9. Fetch any missing prev events doing all checks listed here starting at 1.
|
// 9. Fetch any missing prev events doing all checks listed here starting at 1.
|
||||||
// These are timeline events
|
// These are timeline events
|
||||||
let (sorted_prev_events, mut eventid_info) = self
|
debug!("Handling previous events");
|
||||||
.fetch_prev(origin, create_event, room_id, first_ts_in_room, incoming_pdu.prev_events())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!(
|
self.fetch_prevs(room_id, create_event, &incoming_pdu, origin)
|
||||||
events = ?sorted_prev_events,
|
|
||||||
"Handling previous events"
|
|
||||||
);
|
|
||||||
|
|
||||||
sorted_prev_events
|
|
||||||
.iter()
|
|
||||||
.try_stream()
|
|
||||||
.map_ok(AsRef::as_ref)
|
|
||||||
.try_for_each(|prev_id| {
|
|
||||||
self.handle_prev_pdu(
|
|
||||||
origin,
|
|
||||||
event_id,
|
|
||||||
room_id,
|
|
||||||
eventid_info.remove(prev_id),
|
|
||||||
create_event,
|
|
||||||
first_ts_in_room,
|
|
||||||
prev_id,
|
|
||||||
)
|
|
||||||
.inspect_err(move |e| {
|
|
||||||
warn!("Prev {prev_id} failed: {e}");
|
|
||||||
match self
|
|
||||||
.services
|
|
||||||
.globals
|
|
||||||
.bad_event_ratelimiter
|
|
||||||
.write()
|
|
||||||
.entry(prev_id.into())
|
|
||||||
{
|
|
||||||
| hash_map::Entry::Vacant(e) => {
|
|
||||||
e.insert((Instant::now(), 1));
|
|
||||||
},
|
|
||||||
| hash_map::Entry::Occupied(mut e) => {
|
|
||||||
let tries = e.get().1.saturating_add(1);
|
|
||||||
*e.get_mut() = (Instant::now(), tries);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|_| self.services.server.check_running())
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Done with prev events, now handling the incoming event
|
// Done with prev events, now handling the incoming event
|
||||||
self.upgrade_outlier_to_timeline_pdu(incoming_pdu, val, create_event, origin, room_id)
|
self.upgrade_outlier_to_timeline_pdu(incoming_pdu, val, create_event, origin, room_id)
|
||||||
.boxed()
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use std::collections::{BTreeMap, HashMap, hash_map};
|
use std::collections::{BTreeMap, HashMap, hash_map};
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Event, PduEvent, Result, debug, debug_info, debug_warn, err, implement, state_res,
|
Err, Event, PduEvent, Result, debug, debug_info, debug_warn, err, implement, info, state_res,
|
||||||
trace, warn,
|
trace, warn,
|
||||||
};
|
};
|
||||||
use futures::future::ready;
|
use futures::future::ready;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
|
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
|
||||||
events::StateEventType,
|
api::federation::authorization::get_event_authorization, events::StateEventType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{check_room_id, get_room_version_rules};
|
use super::{check_room_id, get_room_version_rules};
|
||||||
@@ -22,7 +22,7 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
event_id: &'a EventId,
|
event_id: &'a EventId,
|
||||||
room_id: &'a RoomId,
|
room_id: &'a RoomId,
|
||||||
mut value: CanonicalJsonObject,
|
mut value: CanonicalJsonObject,
|
||||||
auth_events_known: bool,
|
_auth_events_known: bool,
|
||||||
) -> Result<(PduEvent, BTreeMap<String, CanonicalJsonValue>)>
|
) -> Result<(PduEvent, BTreeMap<String, CanonicalJsonValue>)>
|
||||||
where
|
where
|
||||||
Pdu: Event + Send + Sync,
|
Pdu: Event + Send + Sync,
|
||||||
@@ -107,45 +107,52 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch any missing ones & reject invalid ones
|
// Fetch any missing ones & reject invalid ones
|
||||||
let missing_auth_events = if auth_events_known {
|
if auth_events.len() != pdu_event.auth_events().count() {
|
||||||
pdu_event
|
info!("Missing some auth events, asking remote for auth chain");
|
||||||
.auth_events()
|
let response: get_event_authorization::v1::Response = self
|
||||||
.filter(|id| !auth_events.contains_key(*id))
|
.services
|
||||||
.collect::<Vec<_>>()
|
.sending
|
||||||
} else {
|
.send_federation_request(
|
||||||
pdu_event.auth_events().collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
if !missing_auth_events.is_empty() || !auth_events_known {
|
|
||||||
debug_info!(
|
|
||||||
"Fetching {} missing auth events for outlier event {event_id}",
|
|
||||||
missing_auth_events.len()
|
|
||||||
);
|
|
||||||
for (pdu, _) in self
|
|
||||||
.fetch_and_handle_outliers(
|
|
||||||
origin,
|
origin,
|
||||||
missing_auth_events.iter().copied(),
|
get_event_authorization::v1::Request::new(
|
||||||
create_event,
|
room_id.to_owned(),
|
||||||
room_id,
|
event_id.to_owned(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
.map_err(|e| {
|
||||||
auth_events.insert(pdu.event_id().to_owned(), pdu);
|
err!(Request(Forbidden(
|
||||||
|
"Remote server is not divulging incoming event's auth chain: {e}"
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
let mut auth_chain_map = HashMap::with_capacity(response.auth_chain.len());
|
||||||
|
for auth_pdu_json in response.auth_chain {
|
||||||
|
let (auth_event_room_id, auth_event_id, auth_pdu_json) =
|
||||||
|
self.parse_incoming_pdu(&auth_pdu_json).await?;
|
||||||
|
if auth_event_room_id != room_id {
|
||||||
|
return Err!(Request(BadJson(
|
||||||
|
"Auth event {auth_event_id} is in {auth_event_room_id}, not {room_id}."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let auth_pdu = PduEvent::from_id_val(&auth_event_id, auth_pdu_json)
|
||||||
|
.map_err(|e| err!(Request(BadJson("Invalid PDU {auth_event_id}: {e}"))))?;
|
||||||
|
auth_chain_map.insert(auth_event_id, auth_pdu);
|
||||||
}
|
}
|
||||||
} else {
|
for aid in pdu_event.auth_events() {
|
||||||
debug!("No missing auth events for outlier event {event_id}");
|
if auth_events.contains_key(aid) {
|
||||||
}
|
continue;
|
||||||
// reject if we are still missing some
|
}
|
||||||
let still_missing = pdu_event
|
if let Some(auth_event) = auth_chain_map.get(aid) {
|
||||||
.auth_events()
|
auth_events.insert(aid.to_owned(), auth_event.clone());
|
||||||
.filter(|id| !auth_events.contains_key(*id))
|
} else {
|
||||||
.collect::<Vec<_>>();
|
return Err!(Request(Forbidden(
|
||||||
if !still_missing.is_empty() {
|
"Remote server is not divulging incoming event's auth events (missing: \
|
||||||
// Don't reject: this could be a temporary condition
|
{aid})"
|
||||||
// TODO: use get_missing_events?
|
)));
|
||||||
return Err!(Request(InvalidParam(
|
}
|
||||||
"Could not fetch all auth events for outlier event {event_id}, still missing: \
|
}
|
||||||
{still_missing:?}"
|
// TODO: do events received from auth chain need persisting? that sounds
|
||||||
)));
|
// awfully slow
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Reject "due to auth events" if the event doesn't pass auth based on the
|
// 6. Reject "due to auth events" if the event doesn't pass auth based on the
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
use std::{collections::BTreeMap, time::Instant};
|
|
||||||
|
|
||||||
use conduwuit::{
|
|
||||||
Err, Event, PduEvent, Result, debug::INFO_SPAN_LEVEL, defer, implement,
|
|
||||||
utils::continue_exponential_backoff_secs,
|
|
||||||
};
|
|
||||||
use ruma::{CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, RoomId, ServerName};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
#[implement(super::Service)]
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
#[tracing::instrument(
|
|
||||||
name = "prev",
|
|
||||||
level = INFO_SPAN_LEVEL,
|
|
||||||
skip_all,
|
|
||||||
fields(%prev_id),
|
|
||||||
)]
|
|
||||||
pub(super) async fn handle_prev_pdu<'a, Pdu>(
|
|
||||||
&self,
|
|
||||||
origin: &'a ServerName,
|
|
||||||
event_id: &'a EventId,
|
|
||||||
room_id: &'a RoomId,
|
|
||||||
eventid_info: Option<(PduEvent, BTreeMap<String, CanonicalJsonValue>)>,
|
|
||||||
create_event: &'a Pdu,
|
|
||||||
first_ts_in_room: MilliSecondsSinceUnixEpoch,
|
|
||||||
prev_id: &'a EventId,
|
|
||||||
) -> Result
|
|
||||||
where
|
|
||||||
Pdu: Event + Send + Sync,
|
|
||||||
{
|
|
||||||
// Check for disabled again because it might have changed
|
|
||||||
if self.services.metadata.is_disabled(room_id).await {
|
|
||||||
return Err!(Request(Forbidden(debug_warn!(
|
|
||||||
"Federaton of room {room_id} is currently disabled on this server. Request by \
|
|
||||||
origin {origin} and event ID {event_id}"
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((time, tries)) = self
|
|
||||||
.services
|
|
||||||
.globals
|
|
||||||
.bad_event_ratelimiter
|
|
||||||
.read()
|
|
||||||
.get(prev_id)
|
|
||||||
{
|
|
||||||
// Exponential backoff
|
|
||||||
const MIN_DURATION: u64 = 5 * 60;
|
|
||||||
const MAX_DURATION: u64 = 60 * 60 * 24;
|
|
||||||
if continue_exponential_backoff_secs(MIN_DURATION, MAX_DURATION, time.elapsed(), *tries) {
|
|
||||||
debug!(
|
|
||||||
?tries,
|
|
||||||
duration = ?time.elapsed(),
|
|
||||||
"Backing off from prev_event"
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((pdu, json)) = eventid_info else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Skip old events
|
|
||||||
if pdu.origin_server_ts() < first_ts_in_room {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let start_time = Instant::now();
|
|
||||||
self.federation_handletime
|
|
||||||
.write()
|
|
||||||
.insert(room_id.into(), ((*prev_id).to_owned(), start_time));
|
|
||||||
|
|
||||||
defer! {{
|
|
||||||
self.federation_handletime
|
|
||||||
.write()
|
|
||||||
.remove(room_id);
|
|
||||||
}};
|
|
||||||
|
|
||||||
self.upgrade_outlier_to_timeline_pdu(pdu, json, create_event, origin, room_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
elapsed = ?start_time.elapsed(),
|
|
||||||
"Handled prev_event",
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ mod fetch_prev;
|
|||||||
mod fetch_state;
|
mod fetch_state;
|
||||||
mod handle_incoming_pdu;
|
mod handle_incoming_pdu;
|
||||||
mod handle_outlier_pdu;
|
mod handle_outlier_pdu;
|
||||||
mod handle_prev_pdu;
|
|
||||||
mod parse_incoming_pdu;
|
mod parse_incoming_pdu;
|
||||||
mod policy_server;
|
mod policy_server;
|
||||||
mod resolve_state;
|
mod resolve_state;
|
||||||
@@ -15,6 +14,7 @@ use std::{collections::HashMap, fmt::Write, sync::Arc, time::Instant};
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use conduwuit::{Err, Event, PduEvent, Result, Server, SyncRwLock, utils::MutexMap};
|
use conduwuit::{Err, Event, PduEvent, Result, Server, SyncRwLock, utils::MutexMap};
|
||||||
|
pub use fetch_and_handle_outliers::build_local_dag;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, OwnedRoomId, RoomId, events::room::create::RoomCreateEventContent,
|
OwnedEventId, OwnedRoomId, RoomId, events::room::create::RoomCreateEventContent,
|
||||||
room_version_rules::RoomVersionRules,
|
room_version_rules::RoomVersionRules,
|
||||||
@@ -22,7 +22,6 @@ use ruma::{
|
|||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
use crate::{Dep, globals, rooms, sending, server_keys};
|
use crate::{Dep, globals, rooms, sending, server_keys};
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
pub mutex_federation: RoomMutexMap,
|
pub mutex_federation: RoomMutexMap,
|
||||||
pub federation_handletime: SyncRwLock<HandleTimeMap>,
|
pub federation_handletime: SyncRwLock<HandleTimeMap>,
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ fn extract_room_id(event_type: &str, pdu: &CanonicalJsonObject) -> Result<OwnedR
|
|||||||
|
|
||||||
/// Parses every entry in an array as an event ID, returning an error if any
|
/// Parses every entry in an array as an event ID, returning an error if any
|
||||||
/// step fails.
|
/// step fails.
|
||||||
fn expect_event_id_array(value: &CanonicalJsonObject, field: &str) -> Result<Vec<OwnedEventId>> {
|
pub(super) fn expect_event_id_array(
|
||||||
|
value: &CanonicalJsonObject,
|
||||||
|
field: &str,
|
||||||
|
) -> Result<Vec<OwnedEventId>> {
|
||||||
value
|
value
|
||||||
.get(field)
|
.get(field)
|
||||||
.ok_or_else(|| err!(Request(BadJson("missing field `{field}` on PDU"))))?
|
.ok_or_else(|| err!(Request(BadJson("missing field `{field}` on PDU"))))?
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Result, debug, err, error, implement,
|
Result, debug, debug_error, err, error, implement,
|
||||||
matrix::{Event, StateMap},
|
matrix::{Event, StateMap},
|
||||||
trace,
|
trace,
|
||||||
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, TryWidebandExt},
|
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, TryWidebandExt},
|
||||||
@@ -37,6 +37,7 @@ where
|
|||||||
.pdu_shortstatehash(prev_event)
|
.pdu_shortstatehash(prev_event)
|
||||||
.await
|
.await
|
||||||
else {
|
else {
|
||||||
|
trace!("No shortstatehash for {prev_event}, cannot calculate one-degree state.");
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ where
|
|||||||
.map_ok(move |sstatehash| (sstatehash, prev_event))
|
.map_ok(move |sstatehash| (sstatehash, prev_event))
|
||||||
})
|
})
|
||||||
.try_collect::<HashMap<_, _>>()
|
.try_collect::<HashMap<_, _>>()
|
||||||
|
.inspect_err(|e| debug_error!("failed to calculate N-degree short state hashes: {e}"))
|
||||||
.await
|
.await
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ where
|
|||||||
.get_pdu_id(incoming_pdu.event_id())
|
.get_pdu_id(incoming_pdu.event_id())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
trace!(event_id=%incoming_pdu.event_id(), "Skipping upgrade of already upgraded PDU");
|
||||||
return Ok(Some(pduid));
|
return Ok(Some(pduid));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ where
|
|||||||
"Upgrading PDU from outlier to timeline"
|
"Upgrading PDU from outlier to timeline"
|
||||||
);
|
);
|
||||||
let timer = Instant::now();
|
let timer = Instant::now();
|
||||||
|
let min_depth = self.services.metadata.get_mindepth(room_id).await;
|
||||||
let room_version_rules = get_room_version_rules(create_event)?;
|
let room_version_rules = get_room_version_rules(create_event)?;
|
||||||
|
|
||||||
// 10. Fetch missing state and auth chain events by calling /state_ids at
|
// 10. Fetch missing state and auth chain events by calling /state_ids at
|
||||||
@@ -81,6 +83,7 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
if state_at_incoming_event.is_none() {
|
if state_at_incoming_event.is_none() {
|
||||||
|
trace!("Could not calculate incoming state, asking remote {origin} for it");
|
||||||
state_at_incoming_event = self
|
state_at_incoming_event = self
|
||||||
.fetch_state(origin, create_event, room_id, incoming_pdu.event_id())
|
.fetch_state(origin, create_event, room_id, incoming_pdu.event_id())
|
||||||
.await?;
|
.await?;
|
||||||
@@ -382,6 +385,11 @@ where
|
|||||||
|
|
||||||
// Event has passed all auth/stateres checks
|
// Event has passed all auth/stateres checks
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
|
if incoming_pdu.depth > min_depth {
|
||||||
|
self.services
|
||||||
|
.metadata
|
||||||
|
.set_mindepth(room_id, incoming_pdu.depth.into());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(pdu_id)
|
Ok(pdu_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -626,6 +626,10 @@ impl Service {
|
|||||||
room_id,
|
room_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
self.services
|
||||||
|
.metadata
|
||||||
|
.maybe_set_mindepth(room_id, parsed_join_pdu.depth.into())
|
||||||
|
.await;
|
||||||
|
|
||||||
info!("Setting final room state for new room");
|
info!("Setting final room state for new room");
|
||||||
// We set the room state after inserting the pdu, so that we never have a moment
|
// We set the room state after inserting the pdu, so that we never have a moment
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use conduwuit::{Result, implement, utils::stream::TryIgnore};
|
use conduwuit::{Result, implement, utils::stream::TryIgnore};
|
||||||
use database::Map;
|
use database::{Deserialized, Map};
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use ruma::{OwnedRoomId, RoomId};
|
use ruma::{OwnedRoomId, RoomId, UInt, uint};
|
||||||
|
|
||||||
use crate::{Dep, rooms};
|
use crate::{Dep, rooms};
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ struct Data {
|
|||||||
bannedroomids: Arc<Map>,
|
bannedroomids: Arc<Map>,
|
||||||
roomid_shortroomid: Arc<Map>,
|
roomid_shortroomid: Arc<Map>,
|
||||||
pduid_pdu: Arc<Map>,
|
pduid_pdu: Arc<Map>,
|
||||||
|
roomid_mindepth: Arc<Map>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Services {
|
struct Services {
|
||||||
@@ -31,6 +32,7 @@ impl crate::Service for Service {
|
|||||||
bannedroomids: args.db["bannedroomids"].clone(),
|
bannedroomids: args.db["bannedroomids"].clone(),
|
||||||
roomid_shortroomid: args.db["roomid_shortroomid"].clone(),
|
roomid_shortroomid: args.db["roomid_shortroomid"].clone(),
|
||||||
pduid_pdu: args.db["pduid_pdu"].clone(),
|
pduid_pdu: args.db["pduid_pdu"].clone(),
|
||||||
|
roomid_mindepth: args.db["roomid_mindepth"].clone(),
|
||||||
},
|
},
|
||||||
services: Services {
|
services: Services {
|
||||||
short: args.depend::<rooms::short::Service>("rooms::short"),
|
short: args.depend::<rooms::short::Service>("rooms::short"),
|
||||||
@@ -98,3 +100,27 @@ pub async fn is_disabled(&self, room_id: &RoomId) -> bool {
|
|||||||
pub async fn is_banned(&self, room_id: &RoomId) -> bool {
|
pub async fn is_banned(&self, room_id: &RoomId) -> bool {
|
||||||
self.db.bannedroomids.get(room_id).await.is_ok()
|
self.db.bannedroomids.get(room_id).await.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
pub async fn get_mindepth(&self, room_id: &RoomId) -> UInt {
|
||||||
|
self.db
|
||||||
|
.roomid_mindepth
|
||||||
|
.get(room_id)
|
||||||
|
.await
|
||||||
|
.deserialized::<UInt>()
|
||||||
|
.unwrap_or_else(|_| uint!(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
pub fn set_mindepth(&self, room_id: &RoomId, min_depth: u64) {
|
||||||
|
self.db
|
||||||
|
.roomid_mindepth
|
||||||
|
.put_raw(room_id.as_bytes(), min_depth.to_be_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
pub async fn maybe_set_mindepth(&self, room_id: &RoomId, min_depth: u64) {
|
||||||
|
if min_depth > self.get_mindepth(room_id).await.into() {
|
||||||
|
self.set_mindepth(room_id, min_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ impl Service {
|
|||||||
self.db.get_non_outlier_pdu_json(event_id).await
|
self.db.get_non_outlier_pdu_json(event_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn non_outlier_pdu_exists(&self, event_id: &EventId) -> bool {
|
||||||
|
self.db.non_outlier_pdu_exists(event_id).await.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the pdu's id.
|
/// Returns the pdu's id.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn get_pdu_id(&self, event_id: &EventId) -> Result<RawPduId> {
|
pub async fn get_pdu_id(&self, event_id: &EventId) -> Result<RawPduId> {
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ where
|
|||||||
|
|
||||||
batch
|
batch
|
||||||
});
|
});
|
||||||
|
if server_keys.is_empty() {
|
||||||
debug_assert!(!server_keys.is_empty(), "empty batch request to notary");
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
while let Some(batch) = server_keys
|
while let Some(batch) = server_keys
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use crate::{
|
|||||||
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
||||||
federation, firstrun, globals, key_backups, mailer,
|
federation, firstrun, globals, key_backups, mailer,
|
||||||
manager::Manager,
|
manager::Manager,
|
||||||
media, moderation, oauth, presence, pusher, registration_tokens, resolver, rooms, sending,
|
media, moderation, password_reset, presence, pusher, registration_tokens, resolver, rooms,
|
||||||
server_keys,
|
sending, server_keys,
|
||||||
service::{self, Args, Map, Service},
|
service::{self, Args, Map, Service},
|
||||||
sync, threepid, transactions, uiaa, users,
|
sync, threepid, transactions, uiaa, users,
|
||||||
};
|
};
|
||||||
@@ -27,7 +27,7 @@ pub struct Services {
|
|||||||
pub globals: Arc<globals::Service>,
|
pub globals: Arc<globals::Service>,
|
||||||
pub key_backups: Arc<key_backups::Service>,
|
pub key_backups: Arc<key_backups::Service>,
|
||||||
pub media: Arc<media::Service>,
|
pub media: Arc<media::Service>,
|
||||||
pub oauth: Arc<oauth::Service>,
|
pub password_reset: Arc<password_reset::Service>,
|
||||||
pub mailer: Arc<mailer::Service>,
|
pub mailer: Arc<mailer::Service>,
|
||||||
pub presence: Arc<presence::Service>,
|
pub presence: Arc<presence::Service>,
|
||||||
pub pusher: Arc<pusher::Service>,
|
pub pusher: Arc<pusher::Service>,
|
||||||
@@ -84,7 +84,7 @@ impl Services {
|
|||||||
globals: build!(globals::Service),
|
globals: build!(globals::Service),
|
||||||
key_backups: build!(key_backups::Service),
|
key_backups: build!(key_backups::Service),
|
||||||
media: build!(media::Service),
|
media: build!(media::Service),
|
||||||
oauth: build!(oauth::Service),
|
password_reset: build!(password_reset::Service),
|
||||||
mailer: build!(mailer::Service),
|
mailer: build!(mailer::Service),
|
||||||
presence: build!(presence::Service),
|
presence: build!(presence::Service),
|
||||||
pusher: build!(pusher::Service),
|
pusher: build!(pusher::Service),
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ use ruma::{
|
|||||||
ClientSecret, OwnedClientSecret, OwnedSessionId, SessionId,
|
ClientSecret, OwnedClientSecret, OwnedSessionId, SessionId,
|
||||||
api::error::{ErrorKind, LimitExceededErrorData},
|
api::error::{ErrorKind, LimitExceededErrorData},
|
||||||
};
|
};
|
||||||
use tokio::sync::MutexGuard;
|
|
||||||
|
|
||||||
pub mod session;
|
mod session;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Args, Dep, config,
|
Args, Dep, config,
|
||||||
@@ -27,7 +26,6 @@ pub struct Service {
|
|||||||
ratelimiter: DefaultKeyedRateLimiter<Address>,
|
ratelimiter: DefaultKeyedRateLimiter<Address>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum EmailRequirement {
|
pub enum EmailRequirement {
|
||||||
/// Users may change their email, but cannot remove it entirely.
|
/// Users may change their email, but cannot remove it entirely.
|
||||||
Required,
|
Required,
|
||||||
@@ -221,12 +219,13 @@ impl Service {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a validated validation session.
|
/// Consume a validated validation session, removing it from the database
|
||||||
pub async fn get_valid_session(
|
/// and returning the newly validated email address.
|
||||||
|
pub async fn consume_valid_session(
|
||||||
&self,
|
&self,
|
||||||
session_id: &SessionId,
|
session_id: &SessionId,
|
||||||
client_secret: &ClientSecret,
|
client_secret: &ClientSecret,
|
||||||
) -> Result<ValidSession<'_>, Cow<'static, str>> {
|
) -> Result<Address, Cow<'static, str>> {
|
||||||
let mut sessions = self.sessions.lock().await;
|
let mut sessions = self.sessions.lock().await;
|
||||||
|
|
||||||
let Some(session) = sessions.get_session(session_id) else {
|
let Some(session) = sessions.get_session(session_id) else {
|
||||||
@@ -236,13 +235,9 @@ impl Service {
|
|||||||
if session.client_secret == client_secret
|
if session.client_secret == client_secret
|
||||||
&& matches!(session.validation_state, ValidationState::Validated)
|
&& matches!(session.validation_state, ValidationState::Validated)
|
||||||
{
|
{
|
||||||
let email = session.email.clone();
|
let session = sessions.remove_session(session_id);
|
||||||
|
|
||||||
Ok(ValidSession {
|
Ok(session.email)
|
||||||
email,
|
|
||||||
session_id: session_id.to_owned(),
|
|
||||||
sessions,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
Err("This email address has not been validated. Did you use the link that was sent \
|
Err("This email address has not been validated. Did you use the link that was sent \
|
||||||
to you?"
|
to you?"
|
||||||
@@ -318,20 +313,3 @@ impl Service {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ValidSession<'lock> {
|
|
||||||
pub email: Address,
|
|
||||||
session_id: OwnedSessionId,
|
|
||||||
sessions: MutexGuard<'lock, ValidationSessions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ValidSession<'_> {
|
|
||||||
/// Consume this session, removing it from the database and releasing the
|
|
||||||
/// lock it holds.
|
|
||||||
#[must_use]
|
|
||||||
pub fn consume(mut self) -> Address {
|
|
||||||
self.sessions.remove_session(&self.session_id);
|
|
||||||
|
|
||||||
self.email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ use lettre::Address;
|
|||||||
use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId, SessionId};
|
use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId, SessionId};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ValidationSessions {
|
pub(super) struct ValidationSessions {
|
||||||
sessions: HashMap<OwnedSessionId, ValidationSession>,
|
sessions: HashMap<OwnedSessionId, ValidationSession>,
|
||||||
client_secrets: HashMap<OwnedClientSecret, OwnedSessionId>,
|
client_secrets: HashMap<OwnedClientSecret, OwnedSessionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A pending or completed email validation session.
|
/// A pending or completed email validation session.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ValidationSession {
|
pub(crate) struct ValidationSession {
|
||||||
/// The session's ID
|
/// The session's ID
|
||||||
pub session_id: OwnedSessionId,
|
pub session_id: OwnedSessionId,
|
||||||
/// The client's supplied client secret
|
/// The client's supplied client secret
|
||||||
@@ -28,7 +28,7 @@ pub struct ValidationSession {
|
|||||||
|
|
||||||
/// The state of an email validation session.
|
/// The state of an email validation session.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ValidationState {
|
pub(crate) enum ValidationState {
|
||||||
/// The session is waiting for this validation token to be provided
|
/// The session is waiting for this validation token to be provided
|
||||||
Pending(ValidationToken),
|
Pending(ValidationToken),
|
||||||
/// The session has been validated
|
/// The session has been validated
|
||||||
@@ -36,7 +36,7 @@ pub enum ValidationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ValidationToken {
|
pub(crate) struct ValidationToken {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub issued_at: SystemTime,
|
pub issued_at: SystemTime,
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ impl ValidationSessions {
|
|||||||
const RANDOM_SID_LENGTH: usize = 16;
|
const RANDOM_SID_LENGTH: usize = 16;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn generate_session_id() -> OwnedSessionId {
|
pub(super) fn generate_session_id() -> OwnedSessionId {
|
||||||
SessionId::parse(utils::random_string(Self::RANDOM_SID_LENGTH)).unwrap()
|
SessionId::parse(utils::random_string(Self::RANDOM_SID_LENGTH)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+156
-306
@@ -7,7 +7,7 @@ use std::{
|
|||||||
use conduwuit::{Err, Error, Result, error, utils};
|
use conduwuit::{Err, Error, Result, error, utils};
|
||||||
use lettre::Address;
|
use lettre::Address;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
DeviceId, UserId,
|
UserId,
|
||||||
api::{
|
api::{
|
||||||
client::uiaa::{
|
client::uiaa::{
|
||||||
AuthData, AuthFlow, AuthType, EmailIdentity, EmailUserIdentifier,
|
AuthData, AuthFlow, AuthType, EmailIdentity, EmailUserIdentifier,
|
||||||
@@ -16,19 +16,11 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
error::{ErrorKind, StandardErrorBody},
|
error::{ErrorKind, StandardErrorBody},
|
||||||
},
|
},
|
||||||
assign,
|
|
||||||
};
|
|
||||||
use serde_json::{
|
|
||||||
json,
|
|
||||||
value::{RawValue, to_raw_value},
|
|
||||||
};
|
};
|
||||||
|
use serde_json::value::RawValue;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{Dep, config, globals, registration_tokens, threepid, users};
|
||||||
Dep, config, globals,
|
|
||||||
oauth::{self, OAuthTicket},
|
|
||||||
registration_tokens, threepid, users,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
services: Services,
|
services: Services,
|
||||||
@@ -41,7 +33,6 @@ struct Services {
|
|||||||
config: Dep<config::Service>,
|
config: Dep<config::Service>,
|
||||||
registration_tokens: Dep<registration_tokens::Service>,
|
registration_tokens: Dep<registration_tokens::Service>,
|
||||||
threepid: Dep<threepid::Service>,
|
threepid: Dep<threepid::Service>,
|
||||||
oauth: Dep<oauth::Service>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::Service for Service {
|
impl crate::Service for Service {
|
||||||
@@ -54,7 +45,6 @@ impl crate::Service for Service {
|
|||||||
registration_tokens: args
|
registration_tokens: args
|
||||||
.depend::<registration_tokens::Service>("registration_tokens"),
|
.depend::<registration_tokens::Service>("registration_tokens"),
|
||||||
threepid: args.depend::<threepid::Service>("threepid"),
|
threepid: args.depend::<threepid::Service>("threepid"),
|
||||||
oauth: args.depend::<oauth::Service>("oauth"),
|
|
||||||
},
|
},
|
||||||
uiaa_sessions: Mutex::new(HashMap::new()),
|
uiaa_sessions: Mutex::new(HashMap::new()),
|
||||||
}))
|
}))
|
||||||
@@ -64,56 +54,8 @@ impl crate::Service for Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct UiaaSession {
|
struct UiaaSession {
|
||||||
session_metadata: UiaaSessionMetadata,
|
|
||||||
info: UiaaInfo,
|
info: UiaaInfo,
|
||||||
}
|
identity: Identity,
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
enum UiaaSessionMetadata {
|
|
||||||
Legacy {
|
|
||||||
identity: Identity,
|
|
||||||
},
|
|
||||||
OAuth {
|
|
||||||
localpart: String,
|
|
||||||
ticket: OAuthTicket,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiaaSessionMetadata {
|
|
||||||
fn into_identity(self) -> Identity {
|
|
||||||
match self {
|
|
||||||
| Self::Legacy { identity } => identity,
|
|
||||||
| Self::OAuth { localpart, .. } =>
|
|
||||||
assign!(Identity::default(), { localpart: Some(localpart) }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about the user which is initiating this UIAA session.
|
|
||||||
pub struct UiaaInitiator<'a> {
|
|
||||||
user_id: &'a UserId,
|
|
||||||
device_id: Option<&'a DeviceId>,
|
|
||||||
oauth_ticket: Option<OAuthTicket>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> UiaaInitiator<'a> {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(user_id: &'a UserId, device_id: Option<&'a DeviceId>) -> Self {
|
|
||||||
Self { user_id, device_id, oauth_ticket: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_oauth_ticket(
|
|
||||||
user_id: &'a UserId,
|
|
||||||
device_id: Option<&'a DeviceId>,
|
|
||||||
oauth_ticket: OAuthTicket,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
user_id,
|
|
||||||
device_id,
|
|
||||||
oauth_ticket: Some(oauth_ticket),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about the authenticated user's identity.
|
/// Information about the authenticated user's identity.
|
||||||
@@ -164,7 +106,7 @@ impl Identity {
|
|||||||
/// Create an Identity with the localpart of the provided user ID
|
/// Create an Identity with the localpart of the provided user ID
|
||||||
/// and all other fields set to None.
|
/// and all other fields set to None.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn from_user_id(user_id: &UserId) -> Self {
|
pub fn from_user_id(user_id: &UserId) -> Self {
|
||||||
Self {
|
Self {
|
||||||
localpart: Some(user_id.localpart().to_owned()),
|
localpart: Some(user_id.localpart().to_owned()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -182,11 +124,11 @@ impl Service {
|
|||||||
auth: &Option<AuthData>,
|
auth: &Option<AuthData>,
|
||||||
flows: Vec<AuthFlow>,
|
flows: Vec<AuthFlow>,
|
||||||
params: Box<RawValue>,
|
params: Box<RawValue>,
|
||||||
initiator: Option<UiaaInitiator<'_>>,
|
identity: Option<Identity>,
|
||||||
) -> Result<Identity> {
|
) -> Result<Identity> {
|
||||||
match auth.as_ref() {
|
match auth.as_ref() {
|
||||||
| None => {
|
| None => {
|
||||||
let info = self.create_session(flows, params, initiator).await?;
|
let info = self.create_session(flows, params, identity).await;
|
||||||
|
|
||||||
Err(Error::Uiaa(info))
|
Err(Error::Uiaa(info))
|
||||||
},
|
},
|
||||||
@@ -198,8 +140,8 @@ impl Service {
|
|||||||
// session if they want to start the UIAA exchange with existing
|
// session if they want to start the UIAA exchange with existing
|
||||||
// authentication data. If that happens, we create a new session
|
// authentication data. If that happens, we create a new session
|
||||||
// here.
|
// here.
|
||||||
self.create_session(flows, params, initiator)
|
self.create_session(flows, params, identity)
|
||||||
.await?
|
.await
|
||||||
.session
|
.session
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into()
|
.into()
|
||||||
@@ -219,15 +161,13 @@ impl Service {
|
|||||||
pub async fn authenticate_password(
|
pub async fn authenticate_password(
|
||||||
&self,
|
&self,
|
||||||
auth: &Option<AuthData>,
|
auth: &Option<AuthData>,
|
||||||
user_id: &UserId,
|
identity: Option<Identity>,
|
||||||
device_id: Option<&DeviceId>,
|
|
||||||
oauth_ticket: Option<OAuthTicket>,
|
|
||||||
) -> Result<Identity> {
|
) -> Result<Identity> {
|
||||||
self.authenticate(
|
self.authenticate(
|
||||||
auth,
|
auth,
|
||||||
vec![AuthFlow::new(vec![AuthType::Password])],
|
vec![AuthFlow::new(vec![AuthType::Password])],
|
||||||
Box::default(),
|
Box::default(),
|
||||||
Some(UiaaInitiator { user_id, device_id, oauth_ticket }),
|
identity,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -243,88 +183,20 @@ impl Service {
|
|||||||
&self,
|
&self,
|
||||||
flows: Vec<AuthFlow>,
|
flows: Vec<AuthFlow>,
|
||||||
params: Box<RawValue>,
|
params: Box<RawValue>,
|
||||||
initiator: Option<UiaaInitiator<'_>>,
|
identity: Option<Identity>,
|
||||||
) -> Result<UiaaInfo> {
|
) -> UiaaInfo {
|
||||||
let mut uiaa_sessions = self.uiaa_sessions.lock().await;
|
let mut uiaa_sessions = self.uiaa_sessions.lock().await;
|
||||||
|
|
||||||
let session_id = utils::random_string(Self::SESSION_ID_LENGTH);
|
let session_id = utils::random_string(Self::SESSION_ID_LENGTH);
|
||||||
|
let mut info = assign::assign!(UiaaInfo::new(flows), {params: Some(params)});
|
||||||
|
info.session = Some(session_id.clone());
|
||||||
|
|
||||||
let mut info = assign!(UiaaInfo::new(flows), { params: Some(params), session: Some(session_id.clone()) });
|
uiaa_sessions.insert(session_id, UiaaSession {
|
||||||
|
info: info.clone(),
|
||||||
|
identity: identity.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
|
||||||
let session_metadata = if let Some(initiator) = initiator {
|
info
|
||||||
let is_oauth = if let Some(device_id) = initiator.device_id {
|
|
||||||
self.services
|
|
||||||
.oauth
|
|
||||||
.get_session_info_for_device(initiator.user_id, device_id)
|
|
||||||
.await
|
|
||||||
.is_some()
|
|
||||||
} else {
|
|
||||||
// Appservices never have oauth sessions
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_oauth {
|
|
||||||
if let Some(oauth_ticket) = initiator.oauth_ticket {
|
|
||||||
let ticket_url = self
|
|
||||||
.services
|
|
||||||
.config
|
|
||||||
.get_client_domain()
|
|
||||||
.join(&format!(
|
|
||||||
"{}{}",
|
|
||||||
conduwuit_core::ROUTE_PREFIX,
|
|
||||||
oauth_ticket.ticket_issue_path()
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
info.flows = vec![AuthFlow::new(vec![AuthType::OAuth])];
|
|
||||||
info.params = Some(
|
|
||||||
to_raw_value(&json!({
|
|
||||||
AuthType::OAuth.as_str(): {
|
|
||||||
"url": ticket_url,
|
|
||||||
},
|
|
||||||
// TODO(compat): This is necessary for older versions of matrix-rust-sdk
|
|
||||||
"org.matrix.cross_signing_reset": {
|
|
||||||
"url": ticket_url,
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
UiaaSessionMetadata::OAuth {
|
|
||||||
localpart: initiator.user_id.localpart().to_owned(),
|
|
||||||
ticket: oauth_ticket,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"Clients authorized with OAuth cannot use this route."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
UiaaSessionMetadata::Legacy {
|
|
||||||
identity: Identity::from_user_id(initiator.user_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
UiaaSessionMetadata::Legacy { identity: Identity::default() }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Legacy sessions aren't available if OAuth is required
|
|
||||||
if matches!(&session_metadata, UiaaSessionMetadata::Legacy { .. })
|
|
||||||
&& !self
|
|
||||||
.services
|
|
||||||
.config
|
|
||||||
.oauth
|
|
||||||
.compatibility_mode
|
|
||||||
.uiaa_available()
|
|
||||||
{
|
|
||||||
return Err!(Request(Unrecognized(
|
|
||||||
"User-interactive authentication is unavailable on this server"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
uiaa_sessions.insert(session_id, UiaaSession { session_metadata, info: info.clone() });
|
|
||||||
|
|
||||||
Ok(info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proceed with UIAA authentication given a client's authorization data.
|
/// Proceed with UIAA authentication given a client's authorization data.
|
||||||
@@ -353,7 +225,7 @@ impl Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let completed = {
|
let completed = {
|
||||||
let UiaaSession { session_metadata, info } = session.get_mut();
|
let UiaaSession { info, identity } = session.get_mut();
|
||||||
|
|
||||||
let auth_type = auth.auth_type().expect("auth type should be set");
|
let auth_type = auth.auth_type().expect("auth type should be set");
|
||||||
|
|
||||||
@@ -386,12 +258,12 @@ impl Service {
|
|||||||
|
|
||||||
// If the provided stage hasn't already been completed, check it for completion
|
// If the provided stage hasn't already been completed, check it for completion
|
||||||
if !completed_stages.contains(auth_type.as_str()) {
|
if !completed_stages.contains(auth_type.as_str()) {
|
||||||
match self.check_stage(auth, session_metadata.clone()).await {
|
match self.check_stage(auth, identity.clone()).await {
|
||||||
| Ok((completed_stage, updated_metadata)) => {
|
| Ok((completed_stage, updated_identity)) => {
|
||||||
info.auth_error = None;
|
info.auth_error = None;
|
||||||
completed_stages.insert(completed_stage.to_string());
|
completed_stages.insert(completed_stage.to_string());
|
||||||
info.completed.push(completed_stage);
|
info.completed.push(completed_stage);
|
||||||
*session_metadata = updated_metadata;
|
*identity = updated_identity;
|
||||||
},
|
},
|
||||||
| Err(error) => {
|
| Err(error) => {
|
||||||
info.auth_error = Some(error);
|
info.auth_error = Some(error);
|
||||||
@@ -407,9 +279,9 @@ impl Service {
|
|||||||
|
|
||||||
if completed {
|
if completed {
|
||||||
// This session is complete, remove it and return success
|
// This session is complete, remove it and return success
|
||||||
let (_, UiaaSession { session_metadata, .. }) = session.remove_entry();
|
let (_, UiaaSession { identity, .. }) = session.remove_entry();
|
||||||
|
|
||||||
Ok(Ok(session_metadata.into_identity()))
|
Ok(Ok(identity))
|
||||||
} else {
|
} else {
|
||||||
// The client needs to try again, return the updated session
|
// The client needs to try again, return the updated session
|
||||||
Ok(Err(session.get().info.clone()))
|
Ok(Err(session.get().info.clone()))
|
||||||
@@ -423,174 +295,152 @@ impl Service {
|
|||||||
async fn check_stage(
|
async fn check_stage(
|
||||||
&self,
|
&self,
|
||||||
auth: &AuthData,
|
auth: &AuthData,
|
||||||
mut session_metadata: UiaaSessionMetadata,
|
mut identity: Identity,
|
||||||
) -> Result<(AuthType, UiaaSessionMetadata), StandardErrorBody> {
|
) -> Result<(AuthType, Identity), StandardErrorBody> {
|
||||||
// Note: This function takes ownership of `session_metadata` because mutations
|
// Note: This function takes ownership of `identity` because mutations to the
|
||||||
// to the identity (if it's a legacy session) must not be applied unless
|
// identity must not be applied unless checking the stage succeeds. The
|
||||||
// checking the stage succeeds. The updated identity is returned as part of
|
// updated identity is returned as part of the Ok value, and
|
||||||
// the Ok value, and `continue_session` handles saving it to `uiaa_sessions`.
|
// `continue_session` handles saving it to `uiaa_sessions`.
|
||||||
//
|
//
|
||||||
// This also means it's fine to mutate `identity` at any point in this function,
|
// This also means it's fine to mutate `identity` at any point in this function,
|
||||||
// because those mutations won't be saved unless the function returns Ok.
|
// because those mutations won't be saved unless the function returns Ok.
|
||||||
|
|
||||||
let completed_auth_type = match &mut session_metadata {
|
match auth {
|
||||||
| UiaaSessionMetadata::OAuth { localpart, ticket } => {
|
| AuthData::Dummy(_) => Ok(AuthType::Dummy),
|
||||||
// m.oauth is the only valid stage for oauth sessions
|
| AuthData::EmailIdentity(EmailIdentity {
|
||||||
assert!(
|
thirdparty_id_creds: ThirdpartyIdCredentials { client_secret, sid, .. },
|
||||||
matches!(auth, AuthData::OAuth(_)),
|
..
|
||||||
"got non-oauth auth data for oauth session"
|
}) => {
|
||||||
);
|
match self
|
||||||
|
.services
|
||||||
|
.threepid
|
||||||
|
.consume_valid_session(sid, client_secret)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok(email) => {
|
||||||
|
if let Some(localpart) =
|
||||||
|
self.services.threepid.get_localpart_for_email(&email).await
|
||||||
|
{
|
||||||
|
identity.try_set_localpart(localpart)?;
|
||||||
|
}
|
||||||
|
|
||||||
if self.services.oauth.try_consume_ticket(localpart, *ticket) {
|
identity.try_set_email(email)?;
|
||||||
Ok(AuthType::OAuth)
|
|
||||||
|
Ok(AuthType::EmailIdentity)
|
||||||
|
},
|
||||||
|
| Err(message) => Err(StandardErrorBody::new(
|
||||||
|
ErrorKind::ThreepidAuthFailed,
|
||||||
|
message.into_owned(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
#[allow(clippy::useless_let_if_seq)]
|
||||||
|
| AuthData::Password(Password { identifier, password, .. }) => {
|
||||||
|
let user_id_or_localpart = match identifier {
|
||||||
|
| UserIdentifier::Matrix(MatrixUserIdentifier { user, .. }) =>
|
||||||
|
user.to_owned(),
|
||||||
|
| UserIdentifier::Email(EmailUserIdentifier { address, .. }) => {
|
||||||
|
let Ok(email) = Address::try_from(address.to_owned()) else {
|
||||||
|
return Err(StandardErrorBody::new(
|
||||||
|
ErrorKind::InvalidParam,
|
||||||
|
"Email is malformed".to_owned(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(localpart) =
|
||||||
|
self.services.threepid.get_localpart_for_email(&email).await
|
||||||
|
{
|
||||||
|
identity.try_set_email(email)?;
|
||||||
|
|
||||||
|
localpart
|
||||||
|
} else {
|
||||||
|
return Err(StandardErrorBody::new(
|
||||||
|
ErrorKind::Forbidden,
|
||||||
|
"Invalid identifier or password".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
| _ =>
|
||||||
|
return Err(StandardErrorBody::new(
|
||||||
|
ErrorKind::Unrecognized,
|
||||||
|
"Identifier type not recognized".to_owned(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(user_id) = UserId::parse_with_server_name(
|
||||||
|
user_id_or_localpart,
|
||||||
|
self.services.globals.server_name(),
|
||||||
|
) else {
|
||||||
|
return Err(StandardErrorBody::new(
|
||||||
|
ErrorKind::InvalidParam,
|
||||||
|
"User ID is malformed".to_owned(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.check_password(&user_id, password)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
identity.try_set_localpart(user_id.localpart().to_owned())?;
|
||||||
|
|
||||||
|
Ok(AuthType::Password)
|
||||||
} else {
|
} else {
|
||||||
Err(StandardErrorBody::new(
|
Err(StandardErrorBody::new(
|
||||||
ErrorKind::Forbidden,
|
ErrorKind::Forbidden,
|
||||||
"No OAuth ticket available".to_owned(),
|
"Invalid identifier or password".to_owned(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
| UiaaSessionMetadata::Legacy { identity } => match auth {
|
| AuthData::ReCaptcha(ReCaptcha { response, .. }) => {
|
||||||
| AuthData::Dummy(_) => Ok(AuthType::Dummy),
|
let Some(ref private_site_key) = self.services.config.recaptcha_private_site_key
|
||||||
| AuthData::EmailIdentity(EmailIdentity {
|
else {
|
||||||
thirdparty_id_creds: ThirdpartyIdCredentials { client_secret, sid, .. },
|
return Err(StandardErrorBody::new(
|
||||||
..
|
ErrorKind::Forbidden,
|
||||||
}) => {
|
"ReCaptcha is not configured".to_owned(),
|
||||||
match self
|
));
|
||||||
.services
|
};
|
||||||
.threepid
|
|
||||||
.get_valid_session(sid, client_secret)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok(session) => {
|
|
||||||
let email = session.consume();
|
|
||||||
|
|
||||||
if let Some(localpart) =
|
match recaptcha_verify::verify_v3(private_site_key, response, None).await {
|
||||||
self.services.threepid.get_localpart_for_email(&email).await
|
| Ok(()) => Ok(AuthType::ReCaptcha),
|
||||||
{
|
| Err(e) => {
|
||||||
identity.try_set_localpart(localpart)?;
|
error!("ReCaptcha verification failed: {e:?}");
|
||||||
}
|
|
||||||
|
|
||||||
identity.try_set_email(email)?;
|
|
||||||
|
|
||||||
Ok(AuthType::EmailIdentity)
|
|
||||||
},
|
|
||||||
| Err(message) => Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::ThreepidAuthFailed,
|
|
||||||
message.into_owned(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
#[allow(clippy::useless_let_if_seq)]
|
|
||||||
| AuthData::Password(Password { identifier, password, .. }) => {
|
|
||||||
let user_id_or_localpart = match identifier {
|
|
||||||
| UserIdentifier::Matrix(MatrixUserIdentifier { user, .. }) =>
|
|
||||||
user.to_owned(),
|
|
||||||
| UserIdentifier::Email(EmailUserIdentifier { address, .. }) => {
|
|
||||||
let Ok(email) = Address::try_from(address.to_owned()) else {
|
|
||||||
return Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::InvalidParam,
|
|
||||||
"Email is malformed".to_owned(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(localpart) =
|
|
||||||
self.services.threepid.get_localpart_for_email(&email).await
|
|
||||||
{
|
|
||||||
identity.try_set_email(email)?;
|
|
||||||
|
|
||||||
localpart
|
|
||||||
} else {
|
|
||||||
return Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::Forbidden,
|
|
||||||
"Invalid identifier or password".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| _ =>
|
|
||||||
return Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::Unrecognized,
|
|
||||||
"Identifier type not recognized".to_owned(),
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(user_id) = UserId::parse_with_server_name(
|
|
||||||
user_id_or_localpart,
|
|
||||||
self.services.globals.server_name(),
|
|
||||||
) else {
|
|
||||||
return Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::InvalidParam,
|
|
||||||
"User ID is malformed".to_owned(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
if self
|
|
||||||
.services
|
|
||||||
.users
|
|
||||||
.check_password(&user_id, password)
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
identity.try_set_localpart(user_id.localpart().to_owned())?;
|
|
||||||
|
|
||||||
Ok(AuthType::Password)
|
|
||||||
} else {
|
|
||||||
Err(StandardErrorBody::new(
|
Err(StandardErrorBody::new(
|
||||||
ErrorKind::Forbidden,
|
ErrorKind::Forbidden,
|
||||||
"Invalid identifier or password".to_owned(),
|
"ReCaptcha verification failed".to_owned(),
|
||||||
))
|
))
|
||||||
}
|
},
|
||||||
},
|
}
|
||||||
| AuthData::ReCaptcha(ReCaptcha { response, .. }) => {
|
|
||||||
let Some(ref private_site_key) =
|
|
||||||
self.services.config.recaptcha_private_site_key
|
|
||||||
else {
|
|
||||||
return Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::Forbidden,
|
|
||||||
"ReCaptcha is not configured".to_owned(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
match recaptcha_verify::verify_v3(private_site_key, response, None).await {
|
|
||||||
| Ok(()) => Ok(AuthType::ReCaptcha),
|
|
||||||
| Err(e) => {
|
|
||||||
error!("ReCaptcha verification failed: {e:?}");
|
|
||||||
Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::Forbidden,
|
|
||||||
"ReCaptcha verification failed".to_owned(),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| AuthData::RegistrationToken(RegistrationToken { token, .. }) => {
|
|
||||||
let token = token.trim().to_owned();
|
|
||||||
|
|
||||||
if let Some(valid_token) = self
|
|
||||||
.services
|
|
||||||
.registration_tokens
|
|
||||||
.validate_token(token)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
self.services
|
|
||||||
.registration_tokens
|
|
||||||
.mark_token_as_used(valid_token);
|
|
||||||
|
|
||||||
Ok(AuthType::RegistrationToken)
|
|
||||||
} else {
|
|
||||||
Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::Forbidden,
|
|
||||||
"Invalid registration token".to_owned(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
| AuthData::Terms(_) => Ok(AuthType::Terms),
|
|
||||||
| _ => Err(StandardErrorBody::new(
|
|
||||||
ErrorKind::Unrecognized,
|
|
||||||
"Unsupported stage type".into(),
|
|
||||||
)),
|
|
||||||
},
|
},
|
||||||
}?;
|
| AuthData::RegistrationToken(RegistrationToken { token, .. }) => {
|
||||||
|
let token = token.trim().to_owned();
|
||||||
|
|
||||||
Ok((completed_auth_type, session_metadata))
|
if let Some(valid_token) = self
|
||||||
|
.services
|
||||||
|
.registration_tokens
|
||||||
|
.validate_token(token)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
self.services
|
||||||
|
.registration_tokens
|
||||||
|
.mark_token_as_used(valid_token);
|
||||||
|
|
||||||
|
Ok(AuthType::RegistrationToken)
|
||||||
|
} else {
|
||||||
|
Err(StandardErrorBody::new(
|
||||||
|
ErrorKind::Forbidden,
|
||||||
|
"Invalid registration token".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
| AuthData::Terms(_) => Ok(AuthType::Terms),
|
||||||
|
| _ => Err(StandardErrorBody::new(
|
||||||
|
ErrorKind::Unrecognized,
|
||||||
|
"Unsupported stage type".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
.map(|auth_type| (auth_type, identity))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) ->
|
|||||||
user_id,
|
user_id,
|
||||||
&request.device_id,
|
&request.device_id,
|
||||||
"",
|
"",
|
||||||
None,
|
|
||||||
request.initial_device_display_name.clone(),
|
request.initial_device_display_name.clone(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -139,6 +138,7 @@ pub async fn get_dehydrated_device_id(&self, user_id: &UserId) -> Result<OwnedDe
|
|||||||
level = "debug",
|
level = "debug",
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(%user_id),
|
fields(%user_id),
|
||||||
|
ret,
|
||||||
)]
|
)]
|
||||||
pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result<DehydratedDevice> {
|
pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result<DehydratedDevice> {
|
||||||
self.db
|
self.db
|
||||||
|
|||||||
+17
-336
@@ -1,21 +1,13 @@
|
|||||||
pub(super) mod dehydrated_device;
|
pub(super) mod dehydrated_device;
|
||||||
|
|
||||||
use std::{
|
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
|
||||||
collections::BTreeMap,
|
|
||||||
mem,
|
|
||||||
net::IpAddr,
|
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, Result, debug_error, debug_warn, err, info, trace,
|
Err, Error, Result, Server, debug_error, debug_warn, err, trace,
|
||||||
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
|
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
|
||||||
warn,
|
|
||||||
};
|
};
|
||||||
use database::{Deserialized, Ignore, Interfix, Json, Map};
|
use database::{Deserialized, Ignore, Interfix, Json, Map};
|
||||||
use futures::{FutureExt, Stream, StreamExt, TryFutureExt};
|
use futures::{Stream, StreamExt, TryFutureExt};
|
||||||
use lettre::Address;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName,
|
DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName,
|
||||||
OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedOneTimeKeyId, OwnedUserId, RoomId, UInt, UserId,
|
OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedOneTimeKeyId, OwnedUserId, RoomId, UInt, UserId,
|
||||||
@@ -26,24 +18,15 @@ use ruma::{
|
|||||||
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
|
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
|
||||||
events::{
|
events::{
|
||||||
AnyToDeviceEvent, GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
|
AnyToDeviceEvent, GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
|
||||||
push_rules::PushRulesEvent, room::message::RoomMessageEventContent,
|
|
||||||
},
|
},
|
||||||
push::Ruleset,
|
|
||||||
serde::Raw,
|
serde::Raw,
|
||||||
uint,
|
uint,
|
||||||
};
|
};
|
||||||
use ruminuwuity::invite_permission_config::{FilterLevel, InvitePermissionConfigEvent};
|
use ruminuwuity::invite_permission_config::{FilterLevel, InvitePermissionConfigEvent};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{Dep, account_data, admin, appservice, globals, rooms};
|
||||||
Dep, account_data, admin,
|
|
||||||
appservice::{self, RegistrationInfo},
|
|
||||||
config, firstrun, globals, oauth,
|
|
||||||
rooms::{self, alias, membership},
|
|
||||||
threepid,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserSuspension {
|
pub struct UserSuspension {
|
||||||
@@ -58,7 +41,6 @@ pub struct UserSuspension {
|
|||||||
/// A password hash. This is only for use when setting a user's password,
|
/// A password hash. This is only for use when setting a user's password,
|
||||||
/// if the hash needs to be kept around for a while without keeping the password
|
/// if the hash needs to be kept around for a while without keeping the password
|
||||||
/// in memory.
|
/// in memory.
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct HashedPassword(String);
|
pub struct HashedPassword(String);
|
||||||
|
|
||||||
impl HashedPassword {
|
impl HashedPassword {
|
||||||
@@ -69,30 +51,19 @@ impl HashedPassword {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status of an access token.
|
|
||||||
pub enum AccessTokenStatus {
|
|
||||||
Valid,
|
|
||||||
Expired,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
services: Services,
|
services: Services,
|
||||||
db: Data,
|
db: Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Services {
|
struct Services {
|
||||||
|
server: Arc<Server>,
|
||||||
account_data: Dep<account_data::Service>,
|
account_data: Dep<account_data::Service>,
|
||||||
admin: Dep<admin::Service>,
|
admin: Dep<admin::Service>,
|
||||||
alias: Dep<alias::Service>,
|
|
||||||
appservice: Dep<appservice::Service>,
|
appservice: Dep<appservice::Service>,
|
||||||
config: Dep<config::Service>,
|
|
||||||
firstrun: Dep<firstrun::Service>,
|
|
||||||
globals: Dep<globals::Service>,
|
globals: Dep<globals::Service>,
|
||||||
membership: Dep<membership::Service>,
|
|
||||||
oauth: Dep<oauth::Service>,
|
|
||||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||||
state_cache: Dep<rooms::state_cache::Service>,
|
state_cache: Dep<rooms::state_cache::Service>,
|
||||||
threepid: Dep<threepid::Service>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
@@ -104,7 +75,6 @@ struct Data {
|
|||||||
logintoken_expiresatuserid: Arc<Map>,
|
logintoken_expiresatuserid: Arc<Map>,
|
||||||
todeviceid_events: Arc<Map>,
|
todeviceid_events: Arc<Map>,
|
||||||
token_userdeviceid: Arc<Map>,
|
token_userdeviceid: Arc<Map>,
|
||||||
userdeviceid_tokenexpires: Arc<Map>,
|
|
||||||
userdeviceid_metadata: Arc<Map>,
|
userdeviceid_metadata: Arc<Map>,
|
||||||
userdeviceid_token: Arc<Map>,
|
userdeviceid_token: Arc<Map>,
|
||||||
userfilterid_filter: Arc<Map>,
|
userfilterid_filter: Arc<Map>,
|
||||||
@@ -127,19 +97,14 @@ impl crate::Service for Service {
|
|||||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
services: Services {
|
services: Services {
|
||||||
|
server: args.server.clone(),
|
||||||
account_data: args.depend::<account_data::Service>("account_data"),
|
account_data: args.depend::<account_data::Service>("account_data"),
|
||||||
admin: args.depend::<admin::Service>("admin"),
|
admin: args.depend::<admin::Service>("admin"),
|
||||||
alias: args.depend::<alias::Service>("alias"),
|
|
||||||
appservice: args.depend::<appservice::Service>("appservice"),
|
appservice: args.depend::<appservice::Service>("appservice"),
|
||||||
config: args.depend::<config::Service>("config"),
|
|
||||||
firstrun: args.depend::<firstrun::Service>("firstrun"),
|
|
||||||
globals: args.depend::<globals::Service>("globals"),
|
globals: args.depend::<globals::Service>("globals"),
|
||||||
membership: args.depend::<membership::Service>("membership"),
|
|
||||||
oauth: args.depend::<oauth::Service>("oauth"),
|
|
||||||
state_accessor: args
|
state_accessor: args
|
||||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||||
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
||||||
threepid: args.depend::<threepid::Service>("threepid"),
|
|
||||||
},
|
},
|
||||||
db: Data {
|
db: Data {
|
||||||
keychangeid_userid: args.db["keychangeid_userid"].clone(),
|
keychangeid_userid: args.db["keychangeid_userid"].clone(),
|
||||||
@@ -166,7 +131,6 @@ impl crate::Service for Service {
|
|||||||
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
||||||
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
|
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
|
||||||
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
|
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
|
||||||
userdeviceid_tokenexpires: args.db["userdeviceid_tokenexpires"].clone(),
|
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -228,241 +192,12 @@ impl Service {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new account for a local human or bot user.
|
// /// Create a new account for a local human or bot user.
|
||||||
///
|
// pub async fn create_local_account(
|
||||||
/// Does not automatically join the user to auto join rooms. Use
|
// &self,
|
||||||
/// `join_auto_join_rooms` for that.
|
// username: String,
|
||||||
pub async fn create_local_account(
|
// password:
|
||||||
&self,
|
// )
|
||||||
user_id: &UserId,
|
|
||||||
password: HashedPassword,
|
|
||||||
email: Option<Address>,
|
|
||||||
) {
|
|
||||||
self.create(user_id, Some(password))
|
|
||||||
.await
|
|
||||||
.expect("should be able to save a new local user. what happened?");
|
|
||||||
|
|
||||||
// Set an initial display name
|
|
||||||
{
|
|
||||||
let mut displayname = user_id.localpart().to_owned();
|
|
||||||
|
|
||||||
let suffix = &self.services.config.new_user_displayname_suffix;
|
|
||||||
if !suffix.is_empty() {
|
|
||||||
displayname.push(' ');
|
|
||||||
displayname.push_str(suffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_displayname(user_id, Some(displayname));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set default push rules
|
|
||||||
self.services
|
|
||||||
.account_data
|
|
||||||
.update(
|
|
||||||
None,
|
|
||||||
user_id,
|
|
||||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
|
||||||
&serde_json::to_value(PushRulesEvent::new(
|
|
||||||
Ruleset::server_default(user_id).into(),
|
|
||||||
))
|
|
||||||
.expect("should be able to serialize push rules"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("should be able to update account data");
|
|
||||||
|
|
||||||
// If the user registered with an email, associate it with their account.
|
|
||||||
if let Some(email) = email {
|
|
||||||
// This may fail if the email is already in use, but we should have already
|
|
||||||
// checked that when we sent the validation email, so ignoring the error is
|
|
||||||
// acceptable here in the rare case that an email is sniped by another user
|
|
||||||
// between the validation email being sent and the account being created.
|
|
||||||
let _ = self
|
|
||||||
.services
|
|
||||||
.threepid
|
|
||||||
.associate_localpart_email(user_id.localpart(), &email)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to empower the first user and disable first-run mode.
|
|
||||||
let was_first_user = self.services.firstrun.empower_first_user(user_id).await;
|
|
||||||
|
|
||||||
// If the registering user was not the first and we're suspending users on
|
|
||||||
// register, suspend them.
|
|
||||||
if !was_first_user && self.services.config.suspend_on_register {
|
|
||||||
// Note that we can still do auto joins for suspended users
|
|
||||||
self.suspend_account(user_id, &self.services.globals.server_user)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// And send an @room notice to the admin room, to prompt admins to review the
|
|
||||||
// new user and ideally unsuspend them if deemed appropriate.
|
|
||||||
if self.services.config.admin_room_notices {
|
|
||||||
self.services
|
|
||||||
.admin
|
|
||||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
|
||||||
"User {user_id} has been suspended as they are not the first user on \
|
|
||||||
this server. Please review and unsuspend them if appropriate."
|
|
||||||
)))
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info!("Created new user account for {user_id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Autojoin the user to the configured autojoin rooms
|
|
||||||
pub async fn join_auto_join_rooms(&self, user_id: &UserId) {
|
|
||||||
for room in &self.services.config.auto_join_rooms {
|
|
||||||
let Ok(room_id) = self.services.alias.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 auto-room {room} as we have never joined before.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room_server_name) = room.server_name() {
|
|
||||||
match self
|
|
||||||
.services
|
|
||||||
.membership
|
|
||||||
.join_room(
|
|
||||||
user_id,
|
|
||||||
&room_id,
|
|
||||||
Some("Automatically joining this room upon registration".to_owned()),
|
|
||||||
&[
|
|
||||||
self.services.globals.server_name().to_owned(),
|
|
||||||
room_server_name.to_owned(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.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}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn determine_registration_user_id(
|
|
||||||
&self,
|
|
||||||
supplied_username: Option<String>,
|
|
||||||
email: Option<&Address>,
|
|
||||||
appservice_info: Option<&RegistrationInfo>,
|
|
||||||
) -> Result<OwnedUserId> {
|
|
||||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
|
||||||
|
|
||||||
let emergency_mode_enabled = self.services.config.emergency_password.is_some();
|
|
||||||
|
|
||||||
let supplied_username = supplied_username.or_else(|| {
|
|
||||||
// If the user didn't supply a username but did supply an email, use
|
|
||||||
// the email's user part to avoid falling back to a random username
|
|
||||||
email.map(|address| address.user().to_owned())
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(supplied_username) = supplied_username {
|
|
||||||
// The user gets to pick their username. Do some validation to make sure it's
|
|
||||||
// acceptable.
|
|
||||||
|
|
||||||
// Don't allow registration with forbidden usernames.
|
|
||||||
if self
|
|
||||||
.services
|
|
||||||
.globals
|
|
||||||
.forbidden_usernames()
|
|
||||||
.is_match(&supplied_username)
|
|
||||||
&& !emergency_mode_enabled
|
|
||||||
{
|
|
||||||
return Err!(Request(Forbidden("Username is forbidden")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and validate the user ID
|
|
||||||
let user_id = match UserId::parse_with_server_name(
|
|
||||||
&supplied_username,
|
|
||||||
self.services.globals.server_name(),
|
|
||||||
) {
|
|
||||||
| Ok(user_id) => {
|
|
||||||
if let Err(e) = user_id.validate_strict() {
|
|
||||||
// Unless we are in emergency mode, we should follow synapse's behaviour
|
|
||||||
// on not allowing things like spaces and UTF-8 characters in
|
|
||||||
// usernames
|
|
||||||
if !emergency_mode_enabled {
|
|
||||||
return Err!(Request(InvalidUsername(debug_warn!(
|
|
||||||
"Username {supplied_username} contains disallowed characters or \
|
|
||||||
spaces: {e}"
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow registration with user IDs that aren't local
|
|
||||||
if !self.services.globals.user_is_local(&user_id) {
|
|
||||||
return Err!(Request(InvalidUsername(
|
|
||||||
"Username {supplied_username} is not local to this server"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
user_id
|
|
||||||
},
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(Request(InvalidUsername(debug_warn!(
|
|
||||||
"Username {supplied_username} is not valid: {e}"
|
|
||||||
))));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.exists(&user_id).await {
|
|
||||||
return Err!(Request(UserInUse("User ID is not available.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the user ID is/is not in an appservice's namespace
|
|
||||||
if let Some(appservice_info) = appservice_info {
|
|
||||||
if !appservice_info.is_user_match(&user_id) && !emergency_mode_enabled {
|
|
||||||
return Err!(Request(Exclusive(
|
|
||||||
"Username is not in this appservice's namespace."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
} else if self
|
|
||||||
.services
|
|
||||||
.appservice
|
|
||||||
.is_exclusive_user_id(&user_id)
|
|
||||||
.await && !emergency_mode_enabled
|
|
||||||
{
|
|
||||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(user_id)
|
|
||||||
} else {
|
|
||||||
// The user didn't specify a username. Generate a username for
|
|
||||||
// them.
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let user_id = UserId::parse_with_server_name(
|
|
||||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
|
||||||
self.services.globals.server_name(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if !self.exists(&user_id).await {
|
|
||||||
break Ok(user_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deactivate account
|
/// Deactivate account
|
||||||
pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
|
pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
|
||||||
@@ -602,42 +337,8 @@ impl Service {
|
|||||||
pub async fn count(&self) -> usize { self.db.userid_password.count().await }
|
pub async fn count(&self) -> usize { self.db.userid_password.count().await }
|
||||||
|
|
||||||
/// Find out which user an access token belongs to.
|
/// Find out which user an access token belongs to.
|
||||||
pub async fn find_from_token(
|
pub async fn find_from_token(&self, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> {
|
||||||
&self,
|
self.db.token_userdeviceid.get(token).await.deserialized()
|
||||||
token: &str,
|
|
||||||
) -> Option<(OwnedUserId, OwnedDeviceId, AccessTokenStatus)> {
|
|
||||||
let user = self
|
|
||||||
.db
|
|
||||||
.token_userdeviceid
|
|
||||||
.get(token)
|
|
||||||
.await
|
|
||||||
.deserialized()
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Check if the token has expired
|
|
||||||
if let Some((user_id, device_id)) = user {
|
|
||||||
if let Some(expires) = self
|
|
||||||
.db
|
|
||||||
.userdeviceid_tokenexpires
|
|
||||||
.qry(&(&user_id, &device_id))
|
|
||||||
.await
|
|
||||||
.deserialized::<u64>()
|
|
||||||
.ok()
|
|
||||||
.map(Duration::from_secs)
|
|
||||||
{
|
|
||||||
let expires_at = SystemTime::UNIX_EPOCH
|
|
||||||
.checked_add(expires)
|
|
||||||
.expect("expiry time should not overflow SystemTime");
|
|
||||||
|
|
||||||
if SystemTime::now() > expires_at {
|
|
||||||
return Some((user_id, device_id, AccessTokenStatus::Expired));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((user_id, device_id, AccessTokenStatus::Valid))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an iterator over all users on this homeserver.
|
/// Returns an iterator over all users on this homeserver.
|
||||||
@@ -733,7 +434,6 @@ impl Service {
|
|||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
device_id: &DeviceId,
|
device_id: &DeviceId,
|
||||||
token: &str,
|
token: &str,
|
||||||
token_max_age: Option<Duration>,
|
|
||||||
initial_device_display_name: Option<String>,
|
initial_device_display_name: Option<String>,
|
||||||
client_ip: Option<String>,
|
client_ip: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -751,8 +451,7 @@ impl Service {
|
|||||||
|
|
||||||
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
|
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
|
||||||
self.db.userdeviceid_metadata.put(key, Json(device));
|
self.db.userdeviceid_metadata.put(key, Json(device));
|
||||||
self.set_token(user_id, device_id, token, token_max_age)
|
self.set_token(user_id, device_id, token).await
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a device from a user.
|
/// Removes a device from a user.
|
||||||
@@ -768,7 +467,6 @@ impl Service {
|
|||||||
if let Ok(old_token) = self.db.userdeviceid_token.qry(&userdeviceid).await {
|
if let Ok(old_token) = self.db.userdeviceid_token.qry(&userdeviceid).await {
|
||||||
self.db.userdeviceid_token.del(userdeviceid);
|
self.db.userdeviceid_token.del(userdeviceid);
|
||||||
self.db.token_userdeviceid.remove(&old_token);
|
self.db.token_userdeviceid.remove(&old_token);
|
||||||
self.db.userdeviceid_tokenexpires.del(userdeviceid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove todevice events
|
// Remove todevice events
|
||||||
@@ -782,9 +480,6 @@ impl Service {
|
|||||||
|
|
||||||
// TODO: Remove onetimekeys
|
// TODO: Remove onetimekeys
|
||||||
|
|
||||||
// Remove OAuth session information
|
|
||||||
self.services.oauth.remove_session(user_id, device_id).await;
|
|
||||||
|
|
||||||
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
|
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
|
||||||
|
|
||||||
self.db.userdeviceid_metadata.del(userdeviceid);
|
self.db.userdeviceid_metadata.del(userdeviceid);
|
||||||
@@ -840,7 +535,6 @@ impl Service {
|
|||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
device_id: &DeviceId,
|
device_id: &DeviceId,
|
||||||
token: &str,
|
token: &str,
|
||||||
token_max_age: Option<Duration>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let key = (user_id, device_id);
|
let key = (user_id, device_id);
|
||||||
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||||
@@ -867,7 +561,6 @@ impl Service {
|
|||||||
// Remove old token
|
// Remove old token
|
||||||
if let Ok(old_token) = self.db.userdeviceid_token.qry(&key).await {
|
if let Ok(old_token) = self.db.userdeviceid_token.qry(&key).await {
|
||||||
self.db.token_userdeviceid.remove(&old_token);
|
self.db.token_userdeviceid.remove(&old_token);
|
||||||
self.db.userdeviceid_tokenexpires.remove(&old_token);
|
|
||||||
// It will be removed from userdeviceid_token by the insert later
|
// It will be removed from userdeviceid_token by the insert later
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,18 +568,6 @@ impl Service {
|
|||||||
self.db.userdeviceid_token.put_raw(key, token);
|
self.db.userdeviceid_token.put_raw(key, token);
|
||||||
self.db.token_userdeviceid.raw_put(token, key);
|
self.db.token_userdeviceid.raw_put(token, key);
|
||||||
|
|
||||||
if let Some(max_age) = token_max_age {
|
|
||||||
let expires = SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.expect("system time should not be before the epoch")
|
|
||||||
.saturating_add(max_age)
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
self.db.userdeviceid_tokenexpires.put(key, expires);
|
|
||||||
} else {
|
|
||||||
self.db.userdeviceid_tokenexpires.del(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1557,7 +1238,7 @@ impl Service {
|
|||||||
pub fn create_openid_token(&self, user_id: &UserId, token: &str) -> Result<u64> {
|
pub fn create_openid_token(&self, user_id: &UserId, token: &str) -> Result<u64> {
|
||||||
use std::num::Saturating as Sat;
|
use std::num::Saturating as Sat;
|
||||||
|
|
||||||
let expires_in = self.services.config.openid_token_ttl;
|
let expires_in = self.services.server.config.openid_token_ttl;
|
||||||
let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in) * Sat(1000);
|
let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in) * Sat(1000);
|
||||||
|
|
||||||
let mut value = expires_at.0.to_be_bytes().to_vec();
|
let mut value = expires_at.0.to_be_bytes().to_vec();
|
||||||
@@ -1601,7 +1282,7 @@ impl Service {
|
|||||||
pub fn create_login_token(&self, user_id: &UserId, token: &str) -> u64 {
|
pub fn create_login_token(&self, user_id: &UserId, token: &str) -> u64 {
|
||||||
use std::num::Saturating as Sat;
|
use std::num::Saturating as Sat;
|
||||||
|
|
||||||
let expires_in = self.services.config.login_token_ttl;
|
let expires_in = self.services.server.config.login_token_ttl;
|
||||||
let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in);
|
let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in);
|
||||||
|
|
||||||
let value = (expires_at.0, user_id);
|
let value = (expires_at.0, user_id);
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ crate-type = [
|
|||||||
conduwuit-build-metadata.workspace = true
|
conduwuit-build-metadata.workspace = true
|
||||||
conduwuit-service.workspace = true
|
conduwuit-service.workspace = true
|
||||||
conduwuit-core.workspace = true
|
conduwuit-core.workspace = true
|
||||||
conduwuit-database.workspace = true
|
|
||||||
conduwuit-api.workspace = true
|
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
askama.workspace = true
|
askama.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
@@ -37,18 +35,9 @@ ruma.workspace = true
|
|||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tower-http.workspace = true
|
tower-http.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
|
||||||
lettre.workspace = true
|
|
||||||
memory-serve = "2.1.0"
|
memory-serve = "2.1.0"
|
||||||
validator = { version = "0.20.0", features = ["derive"] }
|
validator = { version = "0.20.0", features = ["derive"] }
|
||||||
tower-sec-fetch = { version = "0.1.2", features = ["tracing"] }
|
tower-sec-fetch = { version = "0.1.2", features = ["tracing"] }
|
||||||
tower-sessions = { version = "0.15.0", default-features = false, features = ["axum-core"] }
|
|
||||||
tower-sessions-core = { version = "0.15.0", features = ["deletion-task"] }
|
|
||||||
serde_urlencoded.workspace = true
|
|
||||||
url.workspace = true
|
|
||||||
recaptcha-verify = { version = "0.2.0", default-features = false }
|
|
||||||
reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated
|
|
||||||
form_urlencoded = "1.2.2"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
memory-serve = "2.1.0"
|
memory-serve = "2.1.0"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user