Skip to main content

tuwunel_api/client/rendezvous/msc4388/
mod.rs

1mod create;
2mod delete;
3mod discover;
4mod get;
5mod put;
6#[cfg(test)]
7mod tests;
8
9use std::{net::IpAddr, result::Result as StdResult, str};
10
11use axum::{
12	Json,
13	response::{IntoResponse, Response},
14};
15use bytes::Bytes;
16use http::{HeaderMap, StatusCode};
17use serde::Serialize;
18use tuwunel_core::{Error as CoreError, Result as CoreResult, err};
19use tuwunel_service::Services;
20
21pub(crate) use self::{
22	create::create_msc4388_route, delete::delete_msc4388_route, discover::discover_msc4388_route,
23	get::get_msc4388_route, put::put_msc4388_route,
24};
25use crate::Ruma;
26
27pub(crate) type Result<T> = StdResult<T, Error>;
28
29#[derive(Debug)]
30pub(crate) enum Error {
31	Core(Box<CoreError>),
32	ConcurrentWrite,
33}
34
35#[derive(Serialize)]
36struct ConcurrentWriteResponse {
37	errcode: &'static str,
38	error: &'static str,
39}
40
41const MAX_DATA_BYTES: usize = 4_096;
42const SEC_FETCH_DEST: &str = "sec-fetch-dest";
43const SEC_FETCH_MODE: &str = "sec-fetch-mode";
44const SEC_FETCH_SITE: &str = "sec-fetch-site";
45const SEC_FETCH_USER: &str = "sec-fetch-user";
46
47impl From<CoreError> for Error {
48	fn from(error: CoreError) -> Self { Self::Core(Box::new(error)) }
49}
50
51impl IntoResponse for Error {
52	fn into_response(self) -> Response {
53		match self {
54			| Self::Core(error) => (*error).into_response(),
55			| Self::ConcurrentWrite => {
56				let body = ConcurrentWriteResponse {
57					errcode: "IO_ELEMENT_MSC4388_CONCURRENT_WRITE",
58					error: "sequence_token does not match",
59				};
60
61				(StatusCode::CONFLICT, Json(body)).into_response()
62			},
63		}
64	}
65}
66
67pub(super) fn ensure_available(services: &Services, client: IpAddr) -> CoreResult {
68	super::ensure_enabled(services)?;
69
70	services.rendezvous.check_rate_limit(client)
71}
72
73pub(super) fn ensure_create_available<T>(services: &Services, body: &Ruma<T>) -> CoreResult {
74	services.oauth.get_server()?;
75
76	let authenticated = body.sender_user.is_some() || body.appservice_info.is_some();
77
78	(!services.config.rendezvous_authenticated_only || authenticated)
79		.then_some(())
80		.ok_or_else(|| {
81			err!(Request(Forbidden("Rendezvous session creation requires authentication")))
82		})
83}
84
85pub(super) fn ensure_data_size(services: &Services, data: &str) -> CoreResult {
86	let max_bytes = max_data_bytes(services.config.rendezvous_session_max_bytes);
87
88	(data.len() <= max_bytes)
89		.then_some(())
90		.ok_or_else(|| err!(Request(TooLarge("Rendezvous payload is too large"))))
91}
92
93fn max_data_bytes(configured: usize) -> usize { configured.min(MAX_DATA_BYTES) }
94
95pub(super) fn ensure_safe_get(headers: &HeaderMap) -> CoreResult {
96	let destination = headers
97		.get(SEC_FETCH_DEST)
98		.is_some_and(|value| value.as_bytes().ne(b"empty"));
99
100	let navigation = headers
101		.get(SEC_FETCH_MODE)
102		.is_some_and(|value| value.as_bytes().eq(b"navigate"));
103
104	let user_activation = headers
105		.get(SEC_FETCH_USER)
106		.is_some_and(|value| value.as_bytes().eq(b"?1"));
107
108	let direct_request = headers
109		.get(SEC_FETCH_SITE)
110		.is_some_and(|value| value.as_bytes().eq(b"none"));
111
112	(!destination && !navigation && !user_activation && !direct_request)
113		.then_some(())
114		.ok_or_else(|| {
115			err!(Request(Forbidden("Rendezvous payload is unavailable to browser navigation")))
116		})
117}
118
119pub(super) fn data_to_string(data: &Bytes) -> CoreResult<String> {
120	str::from_utf8(data)
121		.map(ToOwned::to_owned)
122		.map_err(|_| err!(Request(Unknown("Rendezvous payload is not valid UTF-8"))))
123}