mas_handlers/compat/
mod.rs

1// Copyright 2024 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::sentry::SentryEventID;
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 refresh;
26
27#[derive(Debug, Serialize)]
28struct MatrixError {
29    errcode: &'static str,
30    error: &'static str,
31    #[serde(skip)]
32    status: StatusCode,
33}
34
35impl IntoResponse for MatrixError {
36    fn into_response(self) -> axum::response::Response {
37        (self.status, Json(self)).into_response()
38    }
39}
40
41#[derive(Debug, Clone, Copy, Default)]
42#[must_use]
43pub struct MatrixJsonBody<T>(pub T);
44
45#[derive(Debug, Error)]
46pub enum MatrixJsonBodyRejection {
47    #[error("Invalid Content-Type header: expected application/json")]
48    InvalidContentType,
49
50    #[error("Invalid Content-Type header: expected application/json, got {0}")]
51    ContentTypeNotJson(mime::Mime),
52
53    #[error("Failed to read request body")]
54    BytesRejection(#[from] BytesRejection),
55
56    #[error("Invalid JSON document")]
57    Json(#[from] serde_json::Error),
58}
59
60impl IntoResponse for MatrixJsonBodyRejection {
61    fn into_response(self) -> axum::response::Response {
62        let event_id = sentry::capture_error(&self);
63        let response = match self {
64            Self::InvalidContentType | Self::ContentTypeNotJson(_) => MatrixError {
65                errcode: "M_NOT_JSON",
66                error: "Invalid Content-Type header: expected application/json",
67                status: StatusCode::BAD_REQUEST,
68            },
69
70            Self::BytesRejection(BytesRejection::FailedToBufferBody(
71                FailedToBufferBody::LengthLimitError(_),
72            )) => MatrixError {
73                errcode: "M_TOO_LARGE",
74                error: "Request body too large",
75                status: StatusCode::PAYLOAD_TOO_LARGE,
76            },
77
78            Self::BytesRejection(BytesRejection::FailedToBufferBody(
79                FailedToBufferBody::UnknownBodyError(_),
80            )) => MatrixError {
81                errcode: "M_UNKNOWN",
82                error: "Failed to read request body",
83                status: StatusCode::BAD_REQUEST,
84            },
85
86            Self::BytesRejection(_) => MatrixError {
87                errcode: "M_UNKNOWN",
88                error: "Unknown error while reading request body",
89                status: StatusCode::BAD_REQUEST,
90            },
91
92            Self::Json(err) if err.is_data() => MatrixError {
93                errcode: "M_BAD_JSON",
94                error: "JSON fields are not valid",
95                status: StatusCode::BAD_REQUEST,
96            },
97
98            Self::Json(_) => MatrixError {
99                errcode: "M_NOT_JSON",
100                error: "Body is not a valid JSON document",
101                status: StatusCode::BAD_REQUEST,
102            },
103        };
104
105        (SentryEventID::from(event_id), response).into_response()
106    }
107}
108
109impl<T, S> axum::extract::FromRequest<S> for MatrixJsonBody<T>
110where
111    T: DeserializeOwned,
112    S: Send + Sync,
113{
114    type Rejection = MatrixJsonBodyRejection;
115
116    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
117        // Matrix spec says it's optional to send a Content-Type header, so we
118        // only check it if it's present
119        if let Some(content_type) = req.headers().get(header::CONTENT_TYPE) {
120            let Ok(content_type) = content_type.to_str() else {
121                return Err(MatrixJsonBodyRejection::InvalidContentType);
122            };
123
124            let Ok(mime) = content_type.parse::<mime::Mime>() else {
125                return Err(MatrixJsonBodyRejection::InvalidContentType);
126            };
127
128            let is_json_content_type = mime.type_() == "application"
129                && (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"));
130
131            if !is_json_content_type {
132                return Err(MatrixJsonBodyRejection::ContentTypeNotJson(mime));
133            }
134        }
135
136        let bytes = Bytes::from_request(req, state).await?;
137
138        let value: T = serde_json::from_slice(&bytes)?;
139
140        Ok(Self(value))
141    }
142}