Skip to main content

laterite_admin/
upload.rs

1//! Reading an uploaded form, with its request token checked on the way in.
2//!
3//! An ordinary admin form carries its token in a body the guard reads before the
4//! handler runs. An upload cannot work that way: reading the body to find one
5//! field would mean holding a whole file in memory, which is the thing streaming
6//! ingest exists to avoid. The reference system never faces this, because its
7//! runtime parses the form before the handler is reached.
8//!
9//! So the token travels in the body as usual, and this extractor checks it while
10//! parsing rather than before. Two rules keep that honest:
11//!
12//! - **The token must be the first field.** A later one would mean buffering
13//!   everything ahead of it, and the whole point is not to.
14//! - **A handler cannot skip the check.** The guard marks the check outstanding
15//!   and refuses the response if it is still outstanding when the handler
16//!   returns, so taking the upload with a bare parser fails loudly rather than
17//!   running unguarded.
18
19use axum::extract::{FromRequest, Multipart, Request};
20use axum::response::{IntoResponse, Response};
21
22use crate::error::csrf_rejected;
23use crate::session::{self, SessionHandle};
24
25/// An upload whose request token has been verified.
26///
27/// Obtain fields from it exactly as from `axum::extract::Multipart`; the first
28/// field has already been consumed and checked.
29pub struct VerifiedUpload {
30    inner: Multipart,
31}
32
33impl VerifiedUpload {
34    /// The next form field, or `None` at the end.
35    pub async fn next_field(
36        &mut self,
37    ) -> Result<Option<axum::extract::multipart::Field<'_>>, axum::extract::multipart::MultipartError>
38    {
39        self.inner.next_field().await
40    }
41}
42
43impl<S> FromRequest<S> for VerifiedUpload
44where
45    S: Send + Sync,
46{
47    type Rejection = Response;
48
49    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
50        // The guard put these here: the session's token to compare against, and
51        // the marker it will check after the handler returns.
52        let expected = req
53            .extensions()
54            .get::<SessionHandle>()
55            .map(|h| h.csrf_token())
56            .unwrap_or_default();
57        let pending = req.extensions().get::<session::CsrfPending>().cloned();
58
59        let mut inner = Multipart::from_request(req, state)
60            .await
61            .map_err(|e| e.into_response())?;
62
63        // Already checked beside the body (a header, or the action's query
64        // string), so the first field is the handler's to read.
65        let Some(pending) = pending else {
66            return Ok(Self { inner });
67        };
68
69        let submitted = match inner.next_field().await {
70            Ok(Some(field)) if field.name() == Some(session::CSRF_FIELD) => field.text().await.ok(),
71            // Anything else means the token was not where it has to be. Reading
72            // on to look for it would buffer the upload, so this is refused.
73            _ => None,
74        };
75
76        if !session::token_matches(&expected, submitted.as_deref()) {
77            tracing::warn!("upload rejected: request token missing or wrong");
78            return Err(csrf_rejected());
79        }
80        pending.satisfy();
81        Ok(Self { inner })
82    }
83}