diff --git a/Cargo.lock b/Cargo.lock index 1fd4cd711..a4d91afae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,6 +298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -1225,6 +1226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -2306,12 +2308,16 @@ version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64", + "ed25519-dalek", "getrandom 0.2.17", + "hmac", "js-sys", "pem", "serde", "serde_json", + "sha2", "signature", "simple_asn1", ] @@ -3747,14 +3753,14 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] [[package]] name = "ruma" version = "0.13.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "assign", "js_int", @@ -3773,7 +3779,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.13.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "js_int", "ruma-common", @@ -3785,7 +3791,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.21.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "as_variant", "assign", @@ -3810,7 +3816,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.16.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "as_variant", "base64", @@ -3844,7 +3850,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.31.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "as_variant", "indexmap", @@ -3871,7 +3877,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.12.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "bytes", "headers", @@ -3894,7 +3900,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.11.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "js_int", "thiserror 2.0.18", @@ -3903,7 +3909,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.16.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "cfg-if", "proc-macro-crate", @@ -3918,7 +3924,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.12.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "js_int", "ruma-common", @@ -3930,7 +3936,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.18.0" -source = "git+https://github.com/matrix-construct/ruma?rev=3cb939f5c8a67197433cbb3dc7e256f0ddaee978#3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +source = "git+https://github.com/matrix-construct/ruma?rev=30d063c4503c3b630cdd55eda71a0bc3504518a2#30d063c4503c3b630cdd55eda71a0bc3504518a2" dependencies = [ "base64", "ed25519-dalek", @@ -4094,7 +4100,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -5517,6 +5523,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index c4969e65e..b4edbaf11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ authors = [ ] categories = ["network-programming"] description = "High Performance Matrix Homeserver in Rust!" +# also update edition in rustfmt.toml edition = "2024" homepage = "https://tuwunel.chat" keywords = [ @@ -231,7 +232,13 @@ rev = "93795449913f65ab533b7fa482333eef63fc3ae0" [workspace.dependencies.jsonwebtoken] version = "10.3" default-features = false -features = ["use_pem"] +features = [ + "aws_lc_rs", + "ed25519-dalek", + "hmac", + "sha2", + "use_pem", +] [workspace.dependencies.ldap3] git = "https://github.com/matrix-construct/ldap3" @@ -267,7 +274,7 @@ version = "0.6" features = ["std"] [workspace.dependencies.nix] -version = "0.30" +version = "0" default-features = false features = [ "resource", @@ -320,7 +327,7 @@ default-features = false [workspace.dependencies.ruma] git = "https://github.com/matrix-construct/ruma" -rev = "3cb939f5c8a67197433cbb3dc7e256f0ddaee978" +rev = "30d063c4503c3b630cdd55eda71a0bc3504518a2" features = [ "__compat", "appservice-api-c", diff --git a/rustfmt.toml b/rustfmt.toml index 9538c3d60..e092f691c 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,9 +1,11 @@ +edition = "2024" +style_edition = "2024" + array_width = 80 attr_fn_like_width = 60 chain_width = 50 comment_width = 80 condense_wildcard_suffixes = true -style_edition = "2024" fn_call_width = 80 fn_single_line = true format_code_in_doc_comments = true diff --git a/src/admin/federation/commands.rs b/src/admin/federation/commands.rs index 8178311d3..770eee434 100644 --- a/src/admin/federation/commands.rs +++ b/src/admin/federation/commands.rs @@ -96,6 +96,6 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result .collect::>() .join("\n"); - self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```",)) + self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```")) .await } diff --git a/src/admin/media/commands.rs b/src/admin/media/commands.rs index 4eea9880d..92350e71f 100644 --- a/src/admin/media/commands.rs +++ b/src/admin/media/commands.rs @@ -190,7 +190,7 @@ pub(super) async fn delete_past_remote_media( ) .await?; - self.write_str(&format!("Deleted {deleted_count} total files.",)) + self.write_str(&format!("Deleted {deleted_count} total files.")) .await } @@ -204,7 +204,7 @@ pub(super) async fn delete_all_from_user(&self, username: String) -> Result { .delete_from_user(&user_id) .await?; - self.write_str(&format!("Deleted {deleted_count} total files.",)) + self.write_str(&format!("Deleted {deleted_count} total files.")) .await } @@ -257,7 +257,7 @@ pub(super) async fn delete_all_from_server( } } - self.write_str(&format!("Deleted {deleted_count} total files.",)) + self.write_str(&format!("Deleted {deleted_count} total files.")) .await } diff --git a/src/admin/room/commands.rs b/src/admin/room/commands.rs index 967448919..29a4d8f37 100644 --- a/src/admin/room/commands.rs +++ b/src/admin/room/commands.rs @@ -55,7 +55,7 @@ pub(super) async fn room_list( .collect::>() .join("\n"); - self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len(),)) + self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len())) .await } @@ -132,3 +132,52 @@ pub(super) async fn room_prune_empty(&self, force: bool) -> Result { Ok(()) } + +#[admin_command] +pub(super) async fn room_delete_rooms( + &self, + page: Option, + room_index: Vec, + force: bool, + exclude_disabled: bool, + exclude_banned: bool, +) -> Result { + let page = page.unwrap_or(1); + let mut rooms = self + .services + .metadata + .iter_ids() + .filter_map(async |room_id| { + (!exclude_disabled || !self.services.metadata.is_disabled(room_id).await) + .then_some(room_id) + }) + .filter_map(async |room_id| { + (!exclude_banned || !self.services.metadata.is_banned(room_id).await) + .then_some(room_id) + }) + .then(|room_id| get_room_info(self.services, room_id)) + .collect::>() + .await; + + rooms.sort_by_key(|r| r.1); + rooms.reverse(); + let rooms = rooms + .into_iter() + .skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE)) + .take(PAGE_SIZE) + .collect::>(); + // get room_id from room_index + let rooms = room_index + .iter() + .filter_map(|i| { + i.checked_sub(1) + .and_then(|idx| rooms.get(idx).cloned()) + }) + .collect::>(); + // call the delete_room function for each room + for room in rooms { + self.room_delete(room.0, force).await?; + } + + Ok(()) +} diff --git a/src/admin/room/directory.rs b/src/admin/room/directory.rs index fcf74b44d..655c4059e 100644 --- a/src/admin/room/directory.rs +++ b/src/admin/room/directory.rs @@ -70,7 +70,7 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Context<'_> .join("\n"); context - .write_str(&format!("Rooms (page {page}):\n```\n{body}\n```",)) + .write_str(&format!("Rooms (page {page}):\n```\n{body}\n```")) .await }, } diff --git a/src/admin/room/info.rs b/src/admin/room/info.rs index 09a1a19a0..afdbddbad 100644 --- a/src/admin/room/info.rs +++ b/src/admin/room/info.rs @@ -65,7 +65,7 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R .collect::>() .join("\n"); - self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```",)) + self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```")) .await } diff --git a/src/admin/room/mod.rs b/src/admin/room/mod.rs index 32359eeab..4f62ecadd 100644 --- a/src/admin/room/mod.rs +++ b/src/admin/room/mod.rs @@ -69,4 +69,21 @@ pub(super) enum RoomCommand { #[arg(short, long)] force: bool, }, + + /// - Delete rooms + DeleteRooms { + #[arg(long)] + page: Option, + + room_index: Vec, + + #[arg(short, long)] + force: bool, + + #[arg(long)] + exclude_disabled: bool, + + #[arg(long)] + exclude_banned: bool, + }, } diff --git a/src/admin/room/moderation.rs b/src/admin/room/moderation.rs index 60952ba2f..d016f3b35 100644 --- a/src/admin/room/moderation.rs +++ b/src/admin/room/moderation.rs @@ -233,6 +233,6 @@ async fn list_banned_rooms(&self, no_details: bool) -> Result { .collect::>() .join("\n"); - self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```",)) + self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```")) .await } diff --git a/src/admin/token/commands.rs b/src/admin/token/commands.rs index 0ab29a58f..b5b0e055d 100644 --- a/src/admin/token/commands.rs +++ b/src/admin/token/commands.rs @@ -26,7 +26,7 @@ pub(super) async fn issue( .issue_token(expires) .await?; - self.write_str(&format!("New registration token issued: `{token}` - {info}",)) + self.write_str(&format!("New registration token issued: `{token}` - {info}")) .await } diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index a01434baf..f18453a60 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -265,7 +265,7 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result { .collect::>() .join("\n"); - self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len(),)) + self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len())) .await } @@ -520,7 +520,7 @@ pub(super) async fn force_join_room(&self, user_id: String, room: OwnedRoomOrAli drop(state_lock); - self.write_str(&format!("{user_id} has been joined to {room_id}.",)) + self.write_str(&format!("{user_id} has been joined to {room_id}.")) .await } @@ -561,7 +561,7 @@ pub(super) async fn force_leave_room( drop(state_lock); - self.write_str(&format!("{user_id} has left {room_id}.",)) + self.write_str(&format!("{user_id} has left {room_id}.")) .await } @@ -728,7 +728,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result { .boxed() .await?; - self.write_str(&format!("{user_id} has been granted admin privileges.",)) + self.write_str(&format!("{user_id} has been granted admin privileges.")) .await } diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 52c6bc50d..4525fba6c 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -280,7 +280,7 @@ pub(crate) async fn get_profile_route( .into_iter() .filter_map(|(key, val)| val.map(|val| (key, val))) .map(|(key, val)| (key.to_owned(), val.into())) - .chain(response.custom_profile_fields.into_iter()); + .chain(response.custom_profile_fields); return Ok(response.collect::()); } diff --git a/src/api/client/session/mod.rs b/src/api/client/session/mod.rs index edc505ff0..f8ca21c45 100644 --- a/src/api/client/session/mod.rs +++ b/src/api/client/session/mod.rs @@ -78,7 +78,7 @@ pub(crate) async fn get_login_types_route( | LoginType::Sso(SsoLoginType { identity_providers }) if list_idps && identity_providers.is_empty() => false, - + | LoginType::Password(_) => services.config.login_with_password, | _ => true, }) .collect(), diff --git a/src/api/client/session/sso.rs b/src/api/client/session/sso.rs index 1c5dd0e40..b615aa7fc 100644 --- a/src/api/client/session/sso.rs +++ b/src/api/client/session/sso.rs @@ -301,24 +301,28 @@ pub(crate) async fn sso_callback_route( return Err!(Request(Unauthorized("Authorization grant session has expired."))); } - let cookie = body - .cookie - .get(GRANT_SESSION_COOKIE) - .map(Cookie::value) - .map(serde_html_form::from_str::>) - .transpose()? - .ok_or_else(|| err!(Request(Unauthorized("Missing cookie {GRANT_SESSION_COOKIE:?}"))))?; - - if cookie.client_id.as_ref() != client_id.as_str() { - return Err!(Request(Unauthorized("Client ID {client_id:?} cookie mismatch."))); - } + if provider.check_cookie { + let cookie = body + .cookie + .get(GRANT_SESSION_COOKIE) + .map(Cookie::value) + .map(serde_html_form::from_str::>) + .transpose()? + .ok_or_else(|| { + err!(Request(Unauthorized("Missing cookie {GRANT_SESSION_COOKIE:?}"))) + })?; + + if cookie.client_id.as_ref() != client_id.as_str() { + return Err!(Request(Unauthorized("Client ID {client_id:?} cookie mismatch."))); + } - if Some(cookie.nonce.as_ref()) != session.cookie_nonce.as_deref() { - return Err!(Request(Unauthorized("Cookie nonce does not match session state."))); - } + if Some(cookie.nonce.as_ref()) != session.cookie_nonce.as_deref() { + return Err!(Request(Unauthorized("Cookie nonce does not match session state."))); + } - if cookie.state.as_ref() != sess_id { - return Err!(Request(Unauthorized("Session ID {sess_id:?} cookie mismatch."))); + if cookie.state.as_ref() != sess_id { + return Err!(Request(Unauthorized("Session ID {sess_id:?} cookie mismatch."))); + } } // Request access token. @@ -502,7 +506,7 @@ async fn register_user( // log in conduit admin channel if a non-guest user registered let notice = - format!("New user \"{user_id}\" registered on this server via {idp_name} ({idp_id})",); + format!("New user \"{user_id}\" registered on this server via {idp_name} ({idp_id})"); info!("{notice}"); if services.server.config.admin_room_notices { @@ -593,10 +597,14 @@ async fn decide_user_id( return Ok(user_id); } - let allowed = - |claim: &str| provider.userid_claims.is_empty() || provider.userid_claims.contains(claim); + let explicit = |claim: &str| provider.userid_claims.contains(claim); + + let allowed = |claim: &str| provider.userid_claims.is_empty() || explicit(claim); let choices = [ + explicit("sub") + .then_some(userinfo.sub.as_str()) + .map(str::to_lowercase), userinfo .preferred_username .as_deref() diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 1345bd45b..c72d49ce3 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -984,6 +984,16 @@ pub struct Config { #[serde(default = "true_fn")] pub login_via_token: bool, + /// Whether to enable login using traditional user/password authorization + /// flow. + /// + /// Set this option to false if you intend to allow logging in only using + /// other mechanisms, such as SSO. + /// + /// default: true + #[serde(default = "true_fn")] + pub login_with_password: bool, + /// Login token expiration/TTL in milliseconds. /// /// These are short-lived tokens for the m.login.token endpoint. @@ -2714,9 +2724,17 @@ pub struct IdentityProvider { /// compute a Matrix UserId for new registrations. Reviewing Tuwunel's /// documentation will be necessary for a complete description in detail. An /// empty array imposes no restriction here, avoiding generated fallbacks as - /// much as possible. For simplicity we reserve a claim called "unique" - /// which can be listed alone to ensure *only* generated ID's are used for - /// registrations. + /// much as possible. + /// + /// For simplicity we reserve a claim called "unique" which can be listed + /// alone to ensure *only* generated ID's are used for registrations. + /// + /// Note that listing the claim "sub" has special significance and will take + /// precedence over all other claims, listed or unlisted. "sub" is not + /// normally used to determine a UserId unless explicitly listed here. + /// + /// As of now arbitrary claims cannot be listed here, we only recognize + /// specific hard-coded claims. /// /// default: [] #[serde(default)] @@ -2768,6 +2786,15 @@ pub struct IdentityProvider { /// default: 300 #[serde(default = "default_sso_grant_session_duration")] pub grant_session_duration: Option, + + /// Whether to check the redirect cookie during the callback. This is a + /// security feature and should remain enabled. This is available for + /// developers or deployments which cannot tolerate cookies and are willing + /// to tolerate the risks. + /// + /// default: true + #[serde(default = "true_fn")] + pub check_cookie: bool, } impl IdentityProvider { diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index 4314cab93..ab4b2b336 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -52,6 +52,9 @@ pub enum Error { CargoToml(#[from] cargo_toml::Error), #[error(transparent)] Clap(#[from] clap::error::Error), + #[cfg(unix)] + #[error(transparent)] + Errno(#[from] nix::errno::Errno), #[error(transparent)] Extension(#[from] axum::extract::rejection::ExtensionRejection), #[error(transparent)] diff --git a/src/core/utils/sys.rs b/src/core/utils/sys.rs index 541ee8101..437124870 100644 --- a/src/core/utils/sys.rs +++ b/src/core/utils/sys.rs @@ -1,32 +1,12 @@ pub mod compute; +pub mod limits; pub mod storage; +pub mod usage; use std::path::PathBuf; -pub use compute::available_parallelism; - -use crate::{Result, at, debug}; - -/// This is needed for opening lots of file descriptors, which tends to -/// happen more often when using RocksDB and making lots of federation -/// connections at startup. The soft limit is usually 1024, and the hard -/// limit is usually 512000; I've personally seen it hit >2000. -/// -/// * -/// * -#[cfg(unix)] -pub fn maximize_fd_limit() -> Result<(), nix::errno::Errno> { - use nix::sys::resource::{Resource::RLIMIT_NOFILE as NOFILE, getrlimit, setrlimit}; - - let (soft_limit, hard_limit) = getrlimit(NOFILE)?; - if soft_limit < hard_limit { - setrlimit(NOFILE, hard_limit, hard_limit)?; - assert_eq!((hard_limit, hard_limit), getrlimit(NOFILE)?, "getrlimit != setrlimit"); - debug!(to = hard_limit, from = soft_limit, "Raised RLIMIT_NOFILE",); - } - - Ok(()) -} +pub use self::{compute::available_parallelism, limits::*, usage::*}; +use crate::{Result, at}; /// Return a possibly corrected std::env::current_exe() even if the path is /// marked deleted. diff --git a/src/core/utils/sys/limits.rs b/src/core/utils/sys/limits.rs new file mode 100644 index 000000000..aa34a143a --- /dev/null +++ b/src/core/utils/sys/limits.rs @@ -0,0 +1,101 @@ +#[cfg(unix)] +use nix::sys::resource::{Resource, getrlimit}; + +use crate::{Result, apply, debug, utils::math::usize_from_u64_truncated}; + +#[cfg(unix)] +/// This is needed for opening lots of file descriptors, which tends to +/// happen more often when using RocksDB and making lots of federation +/// connections at startup. The soft limit is usually 1024, and the hard +/// limit is usually 512000; I've personally seen it hit >2000. +/// +/// * +/// * +pub fn maximize_fd_limit() -> Result { + use nix::sys::resource::setrlimit; + + let (soft_limit, hard_limit) = max_file_descriptors()?; + if soft_limit < hard_limit { + let new_limit = hard_limit.try_into()?; + setrlimit(Resource::RLIMIT_NOFILE, new_limit, new_limit)?; + assert_eq!((hard_limit, hard_limit), max_file_descriptors()?, "getrlimit != setrlimit"); + debug!(to = hard_limit, from = soft_limit, "Raised RLIMIT_NOFILE"); + } + + Ok(()) +} + +#[cfg(not(unix))] +pub fn maximize_fd_limit() -> Result { Ok(()) } + +#[cfg(unix)] +/// Some distributions ship with very low defaults for thread counts; similar to +/// low default file descriptor limits. But unlike fd's, thread limit is rarely +/// reached, though on large systems (32+ cores) shipping with defaults of +/// ~1024 as have been observed are problematic. +pub fn maximize_thread_limit() -> Result { + use nix::sys::resource::setrlimit; + + let (soft_limit, hard_limit) = max_threads()?; + if soft_limit < hard_limit { + let new_limit = hard_limit.try_into()?; + setrlimit(Resource::RLIMIT_NPROC, new_limit, new_limit)?; + assert_eq!((hard_limit, hard_limit), max_threads()?, "getrlimit != setrlimit"); + debug!(to = hard_limit, from = soft_limit, "Raised RLIMIT_NPROC"); + } + + Ok(()) +} + +#[cfg(not(unix))] +pub fn maximize_thread_limit() -> Result { Ok(()) } + +#[cfg(unix)] +pub fn max_file_descriptors() -> Result<(usize, usize)> { + getrlimit(Resource::RLIMIT_NOFILE) + .map(apply!(2, usize_from_u64_truncated)) + .map_err(Into::into) +} + +#[cfg(not(unix))] +pub fn max_file_descriptors() -> Result<(usize, usize)> { Ok((usize::MAX, usize::MAX)) } + +#[cfg(unix)] +pub fn max_stack_size() -> Result<(usize, usize)> { + getrlimit(Resource::RLIMIT_STACK) + .map(apply!(2, usize_from_u64_truncated)) + .map_err(Into::into) +} + +#[cfg(not(unix))] +pub fn max_stack_size() -> Result<(usize, usize)> { Ok((usize::MAX, usize::MAX)) } + +#[cfg(any(linux_android, netbsdlike, target_os = "freebsd",))] +pub fn max_memory_locked() -> Result<(usize, usize)> { + getrlimit(Resource::RLIMIT_MEMLOCK) + .map(apply!(2, usize_from_u64_truncated)) + .map_err(Into::into) +} + +#[cfg(not(any(linux_android, netbsdlike, target_os = "freebsd",)))] +pub fn max_memory_locked() -> Result<(usize, usize)> { Ok((usize::MIN, usize::MIN)) } + +#[cfg(any( + linux_android, + netbsdlike, + target_os = "aix", + target_os = "freebsd", +))] +pub fn max_threads() -> Result<(usize, usize)> { + getrlimit(Resource::RLIMIT_NPROC) + .map(apply!(2, usize_from_u64_truncated)) + .map_err(Into::into) +} + +#[cfg(not(any( + linux_android, + netbsdlike, + target_os = "aix", + target_os = "freebsd", +)))] +pub fn max_threads() -> Result<(usize, usize)> { Ok((usize::MAX, usize::MAX)) } diff --git a/src/core/utils/sys/usage.rs b/src/core/utils/sys/usage.rs new file mode 100644 index 000000000..6bf6f01a3 --- /dev/null +++ b/src/core/utils/sys/usage.rs @@ -0,0 +1,25 @@ +use nix::sys::resource::Usage; +#[cfg(unix)] +use nix::sys::resource::{UsageWho, getrusage}; + +use crate::Result; + +#[cfg(unix)] +pub fn usage() -> Result { getrusage(UsageWho::RUSAGE_SELF).map_err(Into::into) } + +#[cfg(not(unix))] +pub fn usage() -> Result { Ok(Usage::default()) } + +#[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" +))] +pub fn thread_usage() -> Result { getrusage(UsageWho::RUSAGE_THREAD).map_err(Into::into) } + +#[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" +)))] +pub fn thread_usage() -> Result { Ok(Usage::default()) } diff --git a/src/core/utils/two_phase_counter.rs b/src/core/utils/two_phase_counter.rs index 9489790ab..fecebb942 100644 --- a/src/core/utils/two_phase_counter.rs +++ b/src/core/utils/two_phase_counter.rs @@ -139,7 +139,7 @@ impl Result + Sync> State { /// Retire the sequence number `id`. fn retire(&mut self, id: u64) { - debug_assert!(self.check_pending(id), "sequence number must be currently pending",); + debug_assert!(self.check_pending(id), "sequence number must be currently pending"); let index = self .pending_index(id) diff --git a/src/database/pool/configure.rs b/src/database/pool/configure.rs index fb7b0d4ed..4bc619103 100644 --- a/src/database/pool/configure.rs +++ b/src/database/pool/configure.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, sync::Arc}; use tuwunel_core::{ - Server, debug, + Server, at, debug, debug::INFO_SPAN_LEVEL, debug_info, debug_warn, expected, info, is_equal_to, utils::{ @@ -12,7 +12,7 @@ use tuwunel_core::{ stream::{AMPLIFICATION_LIMIT, WIDTH_LIMIT}, sys::{ compute::{available_parallelism, cores_available, is_core_available}, - storage, + max_threads, storage, }, }, }; @@ -116,6 +116,13 @@ pub(super) fn configure(server: &Arc) -> (Vec, Vec, Vec) -> (Vec, Vec, Vec = OnceLock::new(); static GC_ON_PARK: OnceLock> = OnceLock::new(); static GC_MUZZY: OnceLock> = OnceLock::new(); +static CORES_OCCUPIED: AtomicUsize = AtomicUsize::new(0); +static THREAD_SPAWNS: AtomicUsize = AtomicUsize::new(0); + pub fn new(args: Option<&Args>) -> Result { let args_default = args.is_none().then(Args::default); let args = args.unwrap_or_else(|| args_default.as_ref().expect("default arguments")); @@ -47,14 +54,20 @@ pub fn new(args: Option<&Args>) -> Result { .set(args.gc_muzzy) .expect("set GC_MUZZY from program argument"); + let max_blocking_threads = max_threads() + .expect("obtained RLIMIT_NPROC or default") + .0 + .saturating_div(3) + .clamp(WORKER_THREAD_MIN, BLOCKING_THREAD_MAX); + let mut builder = Builder::new_multi_thread(); builder .enable_io() .enable_time() - .thread_name(WORKER_NAME) - .worker_threads(args.worker_threads.max(WORKER_MIN)) - .max_blocking_threads(MAX_BLOCKING_THREADS) - .thread_keep_alive(Duration::from_secs(WORKER_KEEPALIVE)) + .thread_name_fn(thread_name) + .worker_threads(args.worker_threads.max(WORKER_THREAD_MIN)) + .max_blocking_threads(max_blocking_threads) + .thread_keep_alive(Duration::from_secs(BLOCKING_THREAD_KEEPALIVE)) .global_queue_interval(args.global_event_interval) .event_interval(args.kernel_event_interval) .max_io_events_per_tick(args.kernel_events_per_tick) @@ -95,20 +108,22 @@ pub fn shutdown(server: &Arc, runtime: Runtime) -> Result { // The final metrics output is promoted to INFO when tokio_unstable is active in // a release/bench mode and DEBUG is likely optimized out - const LEVEL: Level = if cfg!(debug_assertions) { + const LEVEL: Level = if cfg!(not(any(tokio_unstable, feature = "release_max_log_level"))) { Level::DEBUG } else { Level::INFO }; wait_shutdown(server, runtime); - let runtime_metrics = server - .server - .metrics - .runtime_interval() - .unwrap_or_default(); - tuwunel_core::event!(LEVEL, ?runtime_metrics, "Final runtime metrics"); + if let Some(runtime_metrics) = server.server.metrics.runtime_interval() { + tuwunel_core::event!(LEVEL, ?runtime_metrics, "Final runtime metrics."); + } + + if let Ok(resource_usage) = tuwunel_core::utils::sys::usage() { + tuwunel_core::event!(LEVEL, ?resource_usage, "Final resource usage."); + } + Ok(()) } @@ -121,11 +136,11 @@ pub fn shutdown(server: &Arc, runtime: Runtime) -> Result { fn wait_shutdown(_server: &Arc, runtime: Runtime) { debug!( - timeout = ?SHUTDOWN_TIMEOUT, + timeout = ?RUNTIME_SHUTDOWN_TIMEOUT, "Waiting for runtime..." ); - runtime.shutdown_timeout(SHUTDOWN_TIMEOUT); + runtime.shutdown_timeout(RUNTIME_SHUTDOWN_TIMEOUT); // Join any jemalloc threads so they don't appear in use at exit. #[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))] @@ -134,33 +149,45 @@ fn wait_shutdown(_server: &Arc, runtime: Runtime) { .ok(); } +fn thread_name() -> String { + let handle = Handle::current(); + let num_workers = handle.metrics().num_workers(); + let i = THREAD_SPAWNS.load(Ordering::Acquire); + + if i >= num_workers { + BLOCKING_THREAD_NAME.into() + } else { + WORKER_THREAD_NAME.into() + } +} + #[tracing::instrument( name = "fork", level = "debug", skip_all, fields( - id = ?thread::current().id(), + tid = ?thread::current().id(), name = %thread::current().name().unwrap_or("None"), ), )] fn thread_start() { - debug_assert_eq!( - Some(WORKER_NAME), - thread::current().name(), + debug_assert!( + thread::current().name() == Some(WORKER_THREAD_NAME) + || thread::current().name() == Some(BLOCKING_THREAD_NAME), "tokio worker name mismatch at thread start" ); if WORKER_AFFINITY.get().is_some_and(is_true!()) { set_worker_affinity(); } + + THREAD_SPAWNS.fetch_add(1, Ordering::AcqRel); } fn set_worker_affinity() { - static CORES_OCCUPIED: AtomicUsize = AtomicUsize::new(0); - let handle = Handle::current(); let num_workers = handle.metrics().num_workers(); - let i = CORES_OCCUPIED.fetch_add(1, Ordering::Relaxed); + let i = CORES_OCCUPIED.fetch_add(1, Ordering::AcqRel); if i >= num_workers { return; } @@ -197,18 +224,24 @@ fn set_worker_mallctl(_: usize) {} level = "debug", skip_all, fields( - id = ?thread::current().id(), + tid = ?thread::current().id(), name = %thread::current().name().unwrap_or("None"), ), )] -fn thread_stop() {} +fn thread_stop() { + if cfg!(any(tokio_unstable, not(feature = "release_max_log_level"))) + && let Ok(resource_usage) = tuwunel_core::utils::sys::thread_usage() + { + tuwunel_core::debug!(?resource_usage, "Thread resource usage."); + } +} #[tracing::instrument( name = "work", level = "trace", skip_all, fields( - id = ?thread::current().id(), + tid = ?thread::current().id(), name = %thread::current().name().unwrap_or("None"), ), )] @@ -219,7 +252,7 @@ fn thread_unpark() {} level = "trace", skip_all, fields( - id = ?thread::current().id(), + tid = ?thread::current().id(), name = %thread::current().name().unwrap_or("None"), ), )] @@ -248,6 +281,7 @@ fn gc_on_park() { skip_all, fields( id = %meta.id(), + tid = ?thread::current().id(), ), )] fn task_spawn(meta: &tokio::runtime::TaskMeta<'_>) {} @@ -258,7 +292,8 @@ fn task_spawn(meta: &tokio::runtime::TaskMeta<'_>) {} level = "trace", skip_all, fields( - id = %meta.id() + id = %meta.id(), + tid = ?thread::current().id() ), )] fn task_terminate(meta: &tokio::runtime::TaskMeta<'_>) {} @@ -269,7 +304,8 @@ fn task_terminate(meta: &tokio::runtime::TaskMeta<'_>) {} level = "trace", skip_all, fields( - id = %meta.id() + id = %meta.id(), + tid = ?thread::current().id() ), )] fn task_enter(meta: &tokio::runtime::TaskMeta<'_>) {} @@ -280,7 +316,8 @@ fn task_enter(meta: &tokio::runtime::TaskMeta<'_>) {} level = "trace", skip_all, fields( - id = %meta.id() + id = %meta.id(), + tid = ?thread::current().id() ), )] fn task_leave(meta: &tokio::runtime::TaskMeta<'_>) {} diff --git a/src/main/server.rs b/src/main/server.rs index b8717c227..c707ac043 100644 --- a/src/main/server.rs +++ b/src/main/server.rs @@ -51,9 +51,9 @@ pub fn new(args: Option<&Args>, runtime: Option<&runtime::Handle>) -> Result( warn!("Prev {prev_id} failed: {e}"); self.back_off(prev_id); }) + .inspect_ok(|()| { + self.cancel_back_off(prev_id); + }) .map(|_| self.services.server.check_running()) }) .boxed() diff --git a/src/service/rooms/event_handler/handle_outlier_pdu.rs b/src/service/rooms/event_handler/handle_outlier_pdu.rs index 5a4d42b01..ebad12ace 100644 --- a/src/service/rooms/event_handler/handle_outlier_pdu.rs +++ b/src/service/rooms/event_handler/handle_outlier_pdu.rs @@ -103,7 +103,7 @@ pub(super) async fn handle_outlier_pdu( let auth_events: Vec<_> = event .auth_events() - .chain(hydra_create_id.as_deref().into_iter()) + .chain(hydra_create_id.as_deref()) .stream() .filter_map(|auth_event_id| { self.event_fetch(auth_event_id) diff --git a/src/service/rooms/event_handler/mod.rs b/src/service/rooms/event_handler/mod.rs index b34300b75..ae77eae57 100644 --- a/src/service/rooms/event_handler/mod.rs +++ b/src/service/rooms/event_handler/mod.rs @@ -76,7 +76,16 @@ impl crate::Service for Service { } #[implement(Service)] -fn back_off(&self, event_id: &EventId) { +fn cancel_back_off(&self, event_id: &EventId) -> bool { + self.bad_event_ratelimiter + .write() + .expect("locked") + .remove(event_id) + .is_some() +} + +#[implement(Service)] +fn back_off(&self, event_id: &EventId) -> bool { use hash_map::Entry::{Occupied, Vacant}; match self @@ -87,9 +96,11 @@ fn back_off(&self, event_id: &EventId) { { | Vacant(e) => { e.insert((Instant::now(), 1)); + true }, | Occupied(mut e) => { *e.get_mut() = (Instant::now(), e.get().1.saturating_add(1)); + false }, } } @@ -106,7 +117,11 @@ fn is_backed_off(&self, event_id: &EventId, range: Range) -> bool { return false; }; - continue_exponential_backoff(range.start, range.end, time.elapsed(), tries) + if !continue_exponential_backoff(range.start, range.end, time.elapsed(), tries) { + return false; + } + + true } #[implement(Service)] diff --git a/src/service/server_keys/acquire.rs b/src/service/server_keys/acquire.rs index cf6730856..46c654702 100644 --- a/src/service/server_keys/acquire.rs +++ b/src/service/server_keys/acquire.rs @@ -251,7 +251,7 @@ async fn acquire_notary_result(&self, missing: &mut Batch, server_keys: ServerSi fn keys_count(batch: &Batch) -> usize { batch - .iter() - .flat_map(|(_, key_ids)| key_ids.iter()) + .values() + .flat_map(|key_ids| key_ids.iter()) .count() } diff --git a/src/service/server_keys/mod.rs b/src/service/server_keys/mod.rs index 28affe4ad..79f86dd5e 100644 --- a/src/service/server_keys/mod.rs +++ b/src/service/server_keys/mod.rs @@ -165,7 +165,7 @@ pub async fn verify_keys_for(&self, origin: &ServerName) -> VerifyKeys { .unwrap_or(BTreeMap::new()); if self.services.globals.server_is_ours(origin) { - keys.extend(self.verify_keys.clone().into_iter()); + keys.extend(self.verify_keys.clone()); } keys diff --git a/src/service/server_keys/request.rs b/src/service/server_keys/request.rs index a3757d32d..c815dc501 100644 --- a/src/service/server_keys/request.rs +++ b/src/service/server_keys/request.rs @@ -41,8 +41,8 @@ where }); let total_keys = server_keys - .iter() - .flat_map(|(_, ids)| ids.iter()) + .values() + .flat_map(|ids| ids.iter()) .count(); debug_assert!(total_keys > 0, "empty batch request to notary"); @@ -64,7 +64,7 @@ where .rev() .step_by(batch_max.saturating_sub(1)) .skip(1) - .chain(server_keys.keys().next().into_iter()) + .chain(server_keys.keys().next()) .cloned() .collect(); diff --git a/tests/complement/results.jsonl b/tests/complement/results.jsonl index 87fae8d33..810bb0218 100644 --- a/tests/complement/results.jsonl +++ b/tests/complement/results.jsonl @@ -664,11 +664,11 @@ {"Action":"pass","Test":"TestRoomSpecificUsernameChange/Eve_can_find_Alice_by_mxid"} {"Action":"pass","Test":"TestRoomSpecificUsernameChange/Eve_can_find_Alice_by_profile_display_name"} {"Action":"pass","Test":"TestRoomSpecificUsernameChange/Eve_cannot_find_Alice_by_room-specific_name_that_Eve_is_not_privy_to"} -{"Action":"fail","Test":"TestRoomState"} -{"Action":"fail","Test":"TestRoomState/Parallel"} +{"Action":"pass","Test":"TestRoomState"} +{"Action":"pass","Test":"TestRoomState/Parallel"} {"Action":"pass","Test":"TestRoomState/Parallel/GET_/directory/room/:room_alias_yields_room_ID"} {"Action":"pass","Test":"TestRoomState/Parallel/GET_/joined_rooms_lists_newly-created_room"} -{"Action":"fail","Test":"TestRoomState/Parallel/GET_/rooms/:room_id/joined_members_fetches_my_membership"} +{"Action":"pass","Test":"TestRoomState/Parallel/GET_/rooms/:room_id/joined_members_fetches_my_membership"} {"Action":"pass","Test":"TestRoomState/Parallel/GET_/rooms/:room_id/joined_members_is_forbidden_after_leaving_room"} {"Action":"pass","Test":"TestRoomState/Parallel/GET_/rooms/:room_id/state/m.room.member/:user_id?format=event_fetches_my_membership_event"} {"Action":"pass","Test":"TestRoomState/Parallel/GET_/rooms/:room_id/state/m.room.member/:user_id_fetches_my_membership"} diff --git a/tuwunel-example.toml b/tuwunel-example.toml index 9749f4eb4..fa02a7a54 100644 --- a/tuwunel-example.toml +++ b/tuwunel-example.toml @@ -810,6 +810,14 @@ # #login_via_token = true +# Whether to enable login using traditional user/password authorization +# flow. +# +# Set this option to false if you intend to allow logging in only using +# other mechanisms, such as SSO. +# +#login_with_password = true + # Login token expiration/TTL in milliseconds. # # These are short-lived tokens for the m.login.token endpoint. @@ -2320,9 +2328,17 @@ # compute a Matrix UserId for new registrations. Reviewing Tuwunel's # documentation will be necessary for a complete description in detail. An # empty array imposes no restriction here, avoiding generated fallbacks as -# much as possible. For simplicity we reserve a claim called "unique" -# which can be listed alone to ensure *only* generated ID's are used for -# registrations. +# much as possible. +# +# For simplicity we reserve a claim called "unique" which can be listed +# alone to ensure *only* generated ID's are used for registrations. +# +# Note that listing the claim "sub" has special significance and will take +# precedence over all other claims, listed or unlisted. "sub" is not +# normally used to determine a UserId unless explicitly listed here. +# +# As of now arbitrary claims cannot be listed here, we only recognize +# specific hard-coded claims. # #userid_claims = [] @@ -2378,6 +2394,13 @@ # #grant_session_duration = 300 +# Whether to check the redirect cookie during the callback. This is a +# security feature and should remain enabled. This is available for +# developers or deployments which cannot tolerate cookies and are willing +# to tolerate the risks. +# +#check_cookie = true + #[global.appservice.]