tuwunel_api_client/rendezvous/msc4388/
mod.rs1mod 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 ruma::TransactionId;
18use serde::Serialize;
19use tuwunel_core::{
20 Error as CoreError, Result as CoreResult, err,
21 utils::{BoolExt, string::is_opaque_id},
22};
23use tuwunel_service::Services;
24
25pub(crate) use self::{
26 create::create_msc4388_route, delete::delete_msc4388_route, discover::discover_msc4388_route,
27 get::get_msc4388_route, put::put_msc4388_route,
28};
29use crate::Ruma;
30
31pub(crate) type Result<T> = StdResult<T, Error>;
32
33#[derive(Debug)]
34pub(crate) enum Error {
35 Core(Box<CoreError>),
36 ConcurrentWrite,
37}
38
39#[derive(Serialize)]
40struct ConcurrentWriteResponse {
41 errcode: &'static str,
42 error: &'static str,
43}
44
45const MAX_DATA_BYTES: usize = 4_096;
46const SEC_FETCH_DEST: &str = "sec-fetch-dest";
47const SEC_FETCH_MODE: &str = "sec-fetch-mode";
48const SEC_FETCH_SITE: &str = "sec-fetch-site";
49const SEC_FETCH_USER: &str = "sec-fetch-user";
50
51impl From<CoreError> for Error {
52 fn from(error: CoreError) -> Self { Self::Core(Box::new(error)) }
53}
54
55impl IntoResponse for Error {
56 fn into_response(self) -> Response {
57 match self {
58 | Self::Core(error) => (*error).into_response(),
59 | Self::ConcurrentWrite => {
60 let body = ConcurrentWriteResponse {
61 errcode: "IO_ELEMENT_MSC4388_CONCURRENT_WRITE",
62 error: "sequence_token does not match",
63 };
64
65 (StatusCode::CONFLICT, Json(body)).into_response()
66 },
67 }
68 }
69}
70
71pub(super) fn ensure_available(services: &Services, client: IpAddr) -> CoreResult {
72 super::ensure_enabled(services)?;
73
74 services.rendezvous.check_rate_limit(client)
75}
76
77pub(super) fn ensure_create_available<T>(services: &Services, body: &Ruma<T>) -> CoreResult {
78 create_permitted(services, body)?
79 .into_option()
80 .ok_or_else(|| {
81 err!(Request(Forbidden("Rendezvous session creation requires authentication")))
82 })
83}
84
85pub(super) fn create_permitted<T>(services: &Services, body: &Ruma<T>) -> CoreResult<bool> {
86 services.oauth.get_server()?;
87
88 let authenticated = body.sender_user.is_some() || body.appservice_info.is_some();
89
90 Ok(!services.config.rendezvous.authenticated_only || authenticated)
91}
92
93pub(super) fn ensure_txn_id(txn_id: &TransactionId) -> CoreResult {
94 BoolExt::ok_or_else(is_opaque_id(txn_id.as_str()), || {
95 err!(Request(InvalidParam("Transaction ID is not an opaque identifier")))
96 })
97}
98
99pub(super) fn ensure_data_size(services: &Services, data: &str) -> CoreResult {
100 let max_bytes = max_data_bytes(services.config.rendezvous.session_max_bytes);
101
102 BoolExt::ok_or_else(data.len() <= max_bytes, || {
103 err!(Request(TooLarge("Rendezvous payload is too large")))
104 })
105}
106
107fn max_data_bytes(configured: usize) -> usize { configured.min(MAX_DATA_BYTES) }
108
109pub(super) fn ensure_safe_get(headers: &HeaderMap) -> CoreResult {
110 let destination = headers
111 .get(SEC_FETCH_DEST)
112 .is_some_and(|value| value.as_bytes().ne(b"empty"));
113
114 let navigation = headers
115 .get(SEC_FETCH_MODE)
116 .is_some_and(|value| value.as_bytes().eq(b"navigate"));
117
118 let user_activation = headers
119 .get(SEC_FETCH_USER)
120 .is_some_and(|value| value.as_bytes().eq(b"?1"));
121
122 let direct_request = headers
123 .get(SEC_FETCH_SITE)
124 .is_some_and(|value| value.as_bytes().eq(b"none"));
125
126 let safe = !destination && !navigation && !user_activation && !direct_request;
127
128 BoolExt::ok_or_else(safe, || {
129 err!(Request(Forbidden("Rendezvous payload is unavailable to browser navigation")))
130 })
131}
132
133pub(super) fn data_to_string(data: &Bytes) -> CoreResult<String> {
134 str::from_utf8(data)
135 .map(ToOwned::to_owned)
136 .map_err(|_| err!(Request(Unknown("Rendezvous payload is not valid UTF-8"))))
137}