feat: Add support for MSC4388

This commit is contained in:
Ginger committed 2026-11-11 09:33:55 -05:00
1 parent aef031c9d7
commit 64de61c2d2
10 files changed
+289 -11

No files matched your search

+1
View File
@@ -392,6 +392,7 @@ features = [
"unstable-msc4406",
"unstable-msc4439",
"unstable-msc4354",
"unstable-msc4388",
"unstable-msc4480",
"unstable-msc4466",
"unstable-msc4494",
+19 -4
View File
@@ -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
+2
View File
@@ -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::*;
+76
View File
@@ -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<crate::State>,
body: Ruma<discover_rendezvous::msc4388::Request>,
) -> Result<discover_rendezvous::msc4388::Response> {
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<crate::State>,
body: Ruma<create_rendezvous_session::msc4388::Request>,
) -> Result<create_rendezvous_session::msc4388::Response> {
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<crate::State>,
body: Ruma<update_rendezvous_session::msc4388::Request>,
) -> Result<update_rendezvous_session::msc4388::Response> {
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<crate::State>,
headers: HeaderMap,
body: Ruma<get_rendezvous_session::msc4388::Request>,
) -> Result<get_rendezvous_session::msc4388::Response> {
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<crate::State>,
body: Ruma<delete_rendezvous_session::msc4388::Request>,
) -> Result<delete_rendezvous_session::msc4388::Response> {
services.rendezvous.delete_session(&body.id)
}
fn rendezvous_available(services: &Services, identity: Option<&ClientIdentity>) -> bool {
!services.config.require_auth_for_rendezvous || identity.is_some()
}
+5
View File
@@ -194,6 +194,11 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
.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))
+21 -4
View File
@@ -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
+2 -1
View File
@@ -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;
+1
View File
@@ -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;
+158
View File
@@ -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<LruCache<String, Session>>,
}
struct Session {
data: String,
created_at: SystemTime,
last_sequence_token: String,
transaction_sequences: LruCache<OwnedTransactionId, String>,
}
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<Arc<Self>> {
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<SyncMappedMutexGuard<'_, Session>> {
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<create_rendezvous_session::msc4388::Response> {
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<update_rendezvous_session::msc4388::Response> {
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<get_rendezvous_session::unstable::Response> {
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<delete_rendezvous_session::msc4388::Response> {
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")))
}
}
}
+4 -2
View File
@@ -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<presence::Service>,
pub pusher: Arc<pusher::Service>,
pub registration_tokens: Arc<registration_tokens::Service>,
pub rendezvous: Arc<rendezvous::Service>,
pub rooms: rooms::Service,
pub federation: Arc<federation::Service>,
pub firstrun: Arc<firstrun::Service>,
@@ -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),