mas_handlers/compat/
mod.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use axum::{
8    Json,
9    body::Bytes,
10    extract::{
11        Request,
12        rejection::{BytesRejection, FailedToBufferBody},
13    },
14    response::IntoResponse,
15};
16use hyper::{StatusCode, header};
17use mas_axum_utils::record_error;
18use serde::{Serialize, de::DeserializeOwned};
19use thiserror::Error;
20
21pub(crate) mod login;
22pub(crate) mod login_sso_complete;
23pub(crate) mod login_sso_redirect;
24pub(crate) mod logout;
25pub(crate) mod logout_all;
26pub(crate) mod refresh;
27
28#[derive(Debug, Serialize)]
29struct MatrixError {
30    errcode: &'static str,
31    error: &'static str,
32    #[serde(skip)]
33    status: StatusCode,
34}
35
36impl IntoResponse for MatrixError {
37    fn into_response(self) -> axum::response::Response {
38        (self.status, Json(self)).into_response()
39    }
40}
41
42#[derive(Debug, Clone, Copy, Default)]
43#[must_use]
44pub struct MatrixJsonBody<T>(pub T);
45
46#[derive(Debug, Error)]
47pub enum MatrixJsonBodyRejection {
48    #[error("Invalid Content-Type header: expected application/json")]
49    InvalidContentType,
50
51    #[error("Invalid Content-Type header: expected application/json, got {0}")]
52    ContentTypeNotJson(mime::Mime),
53
54    #[error("Failed to read request body")]
55    BytesRejection(#[from] BytesRejection),
56
57    #[error("Invalid JSON document")]
58    Json(#[from] serde_json::Error),
59}
60
61impl IntoResponse for MatrixJsonBodyRejection {
62    fn into_response(self) -> axum::response::Response {
63        let sentry_event_id = record_error!(self, !);
64        let response = match self {
65            Self::InvalidContentType | Self::ContentTypeNotJson(_) => MatrixError {
66                errcode: "M_NOT_JSON",
67                error: "Invalid Content-Type header: expected application/json",
68                status: StatusCode::BAD_REQUEST,
69            },
70
71            Self::BytesRejection(BytesRejection::FailedToBufferBody(
72                FailedToBufferBody::LengthLimitError(_),
73            )) => MatrixError {
74                errcode: "M_TOO_LARGE",
75                error: "Request body too large",
76                status: StatusCode::PAYLOAD_TOO_LARGE,
77            },
78
79            Self::BytesRejection(BytesRejection::FailedToBufferBody(
80                FailedToBufferBody::UnknownBodyError(_),
81            )) => MatrixError {
82                errcode: "M_UNKNOWN",
83                error: "Failed to read request body",
84                status: StatusCode::BAD_REQUEST,
85            },
86
87            Self::BytesRejection(_) => MatrixError {
88                errcode: "M_UNKNOWN",
89                error: "Unknown error while reading request body",
90                status: StatusCode::BAD_REQUEST,
91            },
92
93            Self::Json(err) if err.is_data() => MatrixError {
94                errcode: "M_BAD_JSON",
95                error: "JSON fields are not valid",
96                status: StatusCode::BAD_REQUEST,
97            },
98
99            Self::Json(_) => MatrixError {
100                errcode: "M_NOT_JSON",
101                error: "Body is not a valid JSON document",
102                status: StatusCode::BAD_REQUEST,
103            },
104        };
105
106        (sentry_event_id, response).into_response()
107    }
108}
109
110impl<T, S> axum::extract::FromRequest<S> for MatrixJsonBody<T>
111where
112    T: DeserializeOwned,
113    S: Send + Sync,
114{
115    type Rejection = MatrixJsonBodyRejection;
116
117    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
118        // Matrix spec says it's optional to send a Content-Type header, so we
119        // only check it if it's present
120        if let Some(content_type) = req.headers().get(header::CONTENT_TYPE) {
121            let Ok(content_type) = content_type.to_str() else {
122                return Err(MatrixJsonBodyRejection::InvalidContentType);
123            };
124
125            let Ok(mime) = content_type.parse::<mime::Mime>() else {
126                return Err(MatrixJsonBodyRejection::InvalidContentType);
127            };
128
129            let is_json_content_type = mime.type_() == "application"
130                && (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"));
131
132            if !is_json_content_type {
133                return Err(MatrixJsonBodyRejection::ContentTypeNotJson(mime));
134            }
135        }
136
137        let bytes = Bytes::from_request(req, state).await?;
138
139        let value: T = serde_json::from_slice(&bytes)?;
140
141        Ok(Self(value))
142    }
143}
144
145impl<T, S> axum::extract::OptionalFromRequest<S> for MatrixJsonBody<T>
146where
147    T: DeserializeOwned,
148    S: Send + Sync,
149{
150    type Rejection = MatrixJsonBodyRejection;
151
152    async fn from_request(req: Request, state: &S) -> Result<Option<Self>, Self::Rejection> {
153        if req.headers().contains_key(header::CONTENT_TYPE) {
154            // If there is a Content-Type header, handle it as normal
155            let result = <Self as axum::extract::FromRequest<S>>::from_request(req, state).await?;
156            return Ok(Some(result));
157        }
158
159        // Else, we poke at the body, and deserialize it only if it's JSON
160        let bytes = <Bytes as axum::extract::FromRequest<S>>::from_request(req, state).await?;
161        if bytes.is_empty() {
162            return Ok(None);
163        }
164
165        let value: T = serde_json::from_slice(&bytes)?;
166
167        Ok(Some(Self(value)))
168    }
169}