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 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}