diff --git a/Cargo.toml b/Cargo.toml index a683d2058..18e8c7c80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -392,6 +392,7 @@ features = [ "unstable-msc4406", "unstable-msc4439", "unstable-msc4354", + "unstable-msc4388", "unstable-msc4480", "unstable-msc4466", "unstable-msc4494", diff --git a/conduwuit-example.toml b/conduwuit-example.toml index e52fb7463..c39909a70 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -812,15 +812,30 @@ # env: CONTINUWUITY_FORGET_FORCED_UPON_LEAVE #forget_forced_upon_leave = false -# Set this to true to require authentication on the normally -# unauthenticated profile retrieval endpoints (GET) -# "/_matrix/client/v3/profile/{userId}". +# Set this to true to require authentication when looking up user profiles +# through this server. # -# This can prevent profile scraping. +# By default this is false, allowing tools such as matrix.to to access +# profiles of users on this server and on other servers. +# Setting this to true will block those tools, and may block automated +# crawlers from scraping users' profiles, although it will not block +# other homeservers from accessing users' profiles. # # env: CONTINUWUITY_REQUIRE_AUTH_FOR_PROFILE_REQUESTS #require_auth_for_profile_requests = false +# Set this to false to allow rendezvous sessions to be created without +# authentication. Rendezvous sessions are used for QR code sign-in. +# +# By default this is true, meaning that a user must use their signed-in +# device to generate a QR code when signing in. Setting this to false will +# allow signed-out devices to generate QR codes using this homeserver, +# which may be desirable for public-registration homeservers. +#a user +# +# env: CONTINUWUITY_REQUIRE_AUTH_FOR_RENDEZVOUS +#require_auth_for_rendezvous = true + # Set this to true to allow your server's public room directory to be # federated. Set this to false to protect against /publicRooms spiders, # but will forbid external users from viewing your server's public room diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index c0bb2dc98..1b2fdb622 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -24,6 +24,7 @@ pub(super) mod push; pub(super) mod read_marker; pub(super) mod redact; pub(super) mod relations; +pub(super) mod rendezvous; pub(super) mod report; pub(super) mod room; pub(super) mod search; @@ -71,6 +72,7 @@ pub(super) use push::*; pub(super) use read_marker::*; pub(super) use redact::*; pub(super) use relations::*; +pub(super) use rendezvous::*; pub(super) use report::*; pub(super) use room::*; pub(super) use search::*; diff --git a/src/api/client/rendezvous.rs b/src/api/client/rendezvous.rs new file mode 100644 index 000000000..d779b74a3 --- /dev/null +++ b/src/api/client/rendezvous.rs @@ -0,0 +1,76 @@ +use axum::extract::State; +use conduwuit::{Err, Result}; +use http::HeaderMap; +use ruma::api::client::rendezvous::{ + create_rendezvous_session, delete_rendezvous_session, discover_rendezvous, + get_rendezvous_session, update_rendezvous_session, +}; +use service::Services; + +use crate::{Ruma, router::ClientIdentity}; + +/// `GET /_matrix/client/msc4388/io.element.msc4388/rendezvous` +pub(crate) async fn discover_rendezvous_route( + State(services): State, + body: Ruma, +) -> Result { + Ok(discover_rendezvous::msc4388::Response::new(rendezvous_available( + &services, + body.identity.as_ref(), + ))) +} + +/// `POST /_matrix/client/msc4388/io.element.msc4388/rendezvous/` +pub(crate) async fn create_rendezvous_session_route( + State(services): State, + body: Ruma, +) -> Result { + if !rendezvous_available(&services, body.identity.as_ref()) { + return Err!(Request(Forbidden("You are not authorized to create a rendezvous session"))); + } + + services.rendezvous.create_session(body.data.clone()) +} + +/// `PUT /_matrix/client/msc4388/io.element.msc4388/rendezvous/{id}/{txn_id}` +pub(crate) async fn update_rendezvous_session_route( + State(services): State, + body: Ruma, +) -> Result { + services.rendezvous.update_session( + &body.id, + body.txn_id.clone(), + body.sequence_token.clone(), + body.data.clone(), + ) +} + +/// `GET /_matrix/client/msc4388/io.element.msc4388/rendezvous/{id}` +pub(crate) async fn get_rendezvous_session_route( + State(services): State, + headers: HeaderMap, + body: Ruma, +) -> Result { + if let Some(mode) = headers.get("Sec-Fetch-Mode") + && let Ok(mode) = mode.to_str() + && mode == "navigate" + { + return Err!(Request(Forbidden( + "Rendezvous sessions do not accept browser navigation requests." + ))); + } + + services.rendezvous.get_session(&body.id) +} + +/// `DELETE /_matrix/client/msc4388/io.element.msc4388/rendezvous/{id}` +pub(crate) async fn delete_rendezvous_session_route( + State(services): State, + body: Ruma, +) -> Result { + services.rendezvous.delete_session(&body.id) +} + +fn rendezvous_available(services: &Services, identity: Option<&ClientIdentity>) -> bool { + !services.config.require_auth_for_rendezvous || identity.is_some() +} diff --git a/src/api/router.rs b/src/api/router.rs index 09681c0b1..2ab8022ac 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -194,6 +194,11 @@ pub fn build(router: Router, state: State) -> Router { .ruma_route(&client::well_known_policy_server) .ruma_route(&client::get_rtc_transports) .ruma_route(&client::room_initial_sync_route) + .ruma_route(&client::discover_rendezvous_route) + .ruma_route(&client::create_rendezvous_session_route) + .ruma_route(&client::update_rendezvous_session_route) + .ruma_route(&client::get_rendezvous_session_route) + .ruma_route(&client::delete_rendezvous_session_route) .ruma_route(&client::get_authorization_server_metadata_route) .merge(client::oauth::router(state)) .route("/_continuwuity/server_version", get(client::continuwuity_server_version)) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 2c58cd14f..6b0d8f6f1 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -918,14 +918,31 @@ pub struct Config { #[serde(default)] pub forget_forced_upon_leave: bool, - /// Set this to true to require authentication on the normally - /// unauthenticated profile retrieval endpoints (GET) - /// "/_matrix/client/v3/profile/{userId}". + /// Set this to true to require authentication when looking up user profiles + /// through this server. /// - /// This can prevent profile scraping. + /// By default this is false, allowing tools such as matrix.to to access + /// profiles of users on this server and on other servers. + /// Setting this to true will block those tools, and may block automated + /// crawlers from scraping users' profiles, although it will not block + /// other homeservers from accessing users' profiles. + /// + /// default: false #[serde(default)] pub require_auth_for_profile_requests: bool, + /// Set this to false to allow rendezvous sessions to be created without + /// authentication. Rendezvous sessions are used for QR code sign-in. + /// + /// By default this is true, meaning that a user must use their signed-in + /// device to generate a QR code when signing in. Setting this to false will + /// allow signed-out devices to generate QR codes using this homeserver, + /// which may be desirable for public-registration homeservers. + /// + /// default: true + #[serde(default = "true_fn")] + pub require_auth_for_rendezvous: bool, + /// Set this to true to allow your server's public room directory to be /// federated. Set this to false to protect against /publicRooms spiders, /// but will forbid external users from viewing your server's public room diff --git a/src/core/mod.rs b/src/core/mod.rs index f176593c4..d815bc545 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -37,7 +37,8 @@ pub use error::Error; pub use info::version::*; pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu, state_res}; pub use parking_lot::{ - Mutex as SyncMutex, RwLock as SyncRwLock, RwLockReadGuard as SyncRwLockReadGuard, + MappedMutexGuard as SyncMappedMutexGuard, Mutex as SyncMutex, MutexGuard as SyncMutexGuard, + RwLock as SyncRwLock, RwLockReadGuard as SyncRwLockReadGuard, RwLockWriteGuard as SyncRwLockWriteGuard, }; pub use server::Server; diff --git a/src/service/mod.rs b/src/service/mod.rs index 1a5505583..ff825e354 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -33,6 +33,7 @@ pub mod oidc; pub mod presence; pub mod pusher; pub mod registration_tokens; +pub mod rendezvous; pub mod rooms; pub mod sending; pub mod server_keys; diff --git a/src/service/rendezvous/mod.rs b/src/service/rendezvous/mod.rs new file mode 100644 index 000000000..e877b1ef8 --- /dev/null +++ b/src/service/rendezvous/mod.rs @@ -0,0 +1,158 @@ +use std::{ + sync::Arc, + time::{Duration, SystemTime}, +}; + +use conduwuit::{Err, Result, SyncMappedMutexGuard, SyncMutex, SyncMutexGuard, at, err, utils}; +use lru_cache::LruCache; +use ruma::{ + OwnedTransactionId, + api::client::rendezvous::{ + create_rendezvous_session, delete_rendezvous_session, get_rendezvous_session, + update_rendezvous_session, + }, +}; + +pub struct Service { + sessions: SyncMutex>, +} + +struct Session { + data: String, + created_at: SystemTime, + last_sequence_token: String, + transaction_sequences: LruCache, +} + +impl Session { + const SAVED_TRANSACTION_SEQUENCES: usize = 256; + + fn new(data: String, sequence_token: String) -> Self { + Self { + data, + created_at: SystemTime::now(), + last_sequence_token: sequence_token, + transaction_sequences: LruCache::new(Self::SAVED_TRANSACTION_SEQUENCES), + } + } +} + +impl crate::Service for Service { + fn build(_: crate::Args<'_>) -> Result> { + Ok(Arc::new(Self { + sessions: SyncMutex::new(LruCache::new(Self::MAX_SESSIONS)), + })) + } + + fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } +} + +impl Service { + const MAX_DATA_SIZE_BYTES: usize = 4096; + const MAX_SESSIONS: usize = 256; + const SESSION_MAX_AGE: Duration = Duration::from_secs(300); + const TOKEN_LENGTH: usize = 32; + + fn create_token() -> String { utils::random_string(Self::TOKEN_LENGTH) } + + fn lock_session(&self, id: &str) -> Result> { + SyncMutexGuard::try_map_or_err(self.sessions.lock(), |sessions| { + sessions + .get_mut(id) + .filter(|session| { + SystemTime::now() + .duration_since(session.created_at) + .is_ok_and(|duration| duration < Self::SESSION_MAX_AGE) + }) + .ok_or_else(|| err!(Request(NotFound("Unknown session")))) + }) + .map_err(at!(1)) + } + + pub fn create_session( + &self, + data: String, + ) -> Result { + if data.len() > Self::MAX_DATA_SIZE_BYTES { + return Err!(Request(TooLarge("Data too large"))); + } + + let id = Self::create_token(); + let sequence_token = Self::create_token(); + + self.sessions + .lock() + .insert(id.clone(), Session::new(data, sequence_token.clone())); + + Ok(create_rendezvous_session::msc4388::Response::new( + id, + sequence_token, + Self::SESSION_MAX_AGE, + )) + } + + pub fn update_session( + &self, + id: &str, + transaction: OwnedTransactionId, + sequence_token: String, + data: String, + ) -> Result { + let mut session = self.lock_session(id)?; + + if data.len() > Self::MAX_DATA_SIZE_BYTES { + return Err!(Request(TooLarge("Data too large"))); + } + + let mut session = self.lock_session(id)?; + + if let Some(sequence_token) = session.transaction_sequences.get_mut(&transaction) { + return Ok(update_rendezvous_session::msc4388::Response::new( + sequence_token.to_owned(), + )); + } + + if session.last_sequence_token != sequence_token { + return Err!(Request(ConcurrentWrite("Sequence token mismatch"))); + } + + let new_sequence_token = Self::create_token(); + + session.data = data; + session.last_sequence_token.clone_from(&new_sequence_token); + session + .transaction_sequences + .insert(transaction, sequence_token); + + Ok(update_rendezvous_session::msc4388::Response::new(new_sequence_token)) + } + + pub fn get_session(&self, id: &str) -> Result { + let session = self.lock_session(id)?; + + let session_age = SystemTime::now() + .duration_since(session.created_at) + .unwrap_or(Duration::MAX); + + let session_time_remaining = Self::SESSION_MAX_AGE.saturating_sub(session_age); + + Ok(get_rendezvous_session::msc4388::Response::new( + session.last_sequence_token.clone(), + session.data.clone(), + session_time_remaining, + )) + } + + pub fn delete_session( + &self, + id: &str, + ) -> Result { + let mut sessions = self.sessions.lock(); + + if sessions.remove(id).is_some() { + Ok(delete_rendezvous_session::msc4388::Response::new()) + } else { + Err!(Request(NotFound("Unknown session"))) + } + } +} diff --git a/src/service/services.rs b/src/service/services.rs index 96df263f7..79a1ced78 100644 --- a/src/service/services.rs +++ b/src/service/services.rs @@ -11,8 +11,8 @@ use crate::{ account_data, admin, announcements, antispam, appservice, client, config, emergency, federation, firstrun, globals, key_backups, mailer, manager::Manager, - media, moderation, oauth, oidc, presence, pusher, registration_tokens, rooms, sending, - server_keys, + media, moderation, oauth, oidc, presence, pusher, registration_tokens, rendezvous, rooms, + sending, server_keys, service::{self, Args, Map, Service}, sync, threepid, transactions, uiaa, users, }; @@ -33,6 +33,7 @@ pub struct Services { pub presence: Arc, pub pusher: Arc, pub registration_tokens: Arc, + pub rendezvous: Arc, pub rooms: rooms::Service, pub federation: Arc, pub firstrun: Arc, @@ -89,6 +90,7 @@ impl Services { presence: build!(presence::Service), pusher: build!(pusher::Service), registration_tokens: build!(registration_tokens::Service), + rendezvous: build!(rendezvous::Service), rooms: rooms::Service { alias: build!(rooms::alias::Service), auth_chain: build!(rooms::auth_chain::Service),