Skip to main content

rustio_core/
defaults.rs

1//! Default routes that scaffolded projects mount via [`with_defaults`]:
2//! `/` (homepage) and `/docs` (placeholder).
3//!
4//! `/admin` is intentionally **not** registered here — it is owned by the
5//! admin layer (see [`crate::admin::Admin::register`]). If no admin models
6//! are registered, `/admin` is simply absent.
7
8use crate::error::Error;
9use crate::http::{html, text, Request, Response, MAX_REQUEST_BODY_BYTES};
10use crate::middleware::Next;
11use crate::router::{Params, Router};
12
13const HOME_HTML: &str = include_str!("../assets/home.html");
14
15pub fn homepage() -> Response {
16    html(HOME_HTML)
17}
18
19pub fn docs_placeholder() -> Response {
20    text("RustIO docs — coming soon.")
21}
22
23/// Reject requests whose `Content-Length` exceeds
24/// [`MAX_REQUEST_BODY_BYTES`] before any handler runs.
25///
26/// This is a cheap upfront defence — clients that advertise a
27/// multi-megabyte body are refused with HTTP 413 immediately. Clients
28/// that under-report or use chunked transfer still pay the ceiling at
29/// the body-reader layer (see `admin::read_form`, which wraps the body
30/// in `http_body_util::Limited`). Both paths end in
31/// `Error::PayloadTooLarge`.
32///
33/// `with_defaults` wraps every router with this middleware so custom
34/// handlers that don't explicitly limit their bodies still benefit.
35pub async fn body_limit(req: Request, next: Next) -> Result<Response, Error> {
36    if let Some(header) = req.headers().get(hyper::header::CONTENT_LENGTH) {
37        // A `Content-Length` header that doesn't parse is a malformed
38        // request; the router's downstream body reader will reject it,
39        // but we can also short-circuit here. We conservatively
40        // *forward* on parse failure rather than rejecting — a bad
41        // header is a 400 concern, not ours.
42        if let Ok(s) = header.to_str() {
43            if let Ok(n) = s.parse::<u64>() {
44                if n as u128 > MAX_REQUEST_BODY_BYTES as u128 {
45                    return Err(Error::PayloadTooLarge);
46                }
47            }
48        }
49    }
50    next.run(req).await
51}
52
53pub fn with_defaults(mut router: Router) -> Router {
54    // Register each default only if the project hasn't already claimed
55    // that path. Without this check, registering `with_defaults` after
56    // your own `/` handler would silently shadow it (router matches in
57    // registration order), which is a nasty footgun. Ordering now
58    // doesn't matter: whichever `/` or `/docs` the project registers
59    // first takes precedence, the framework fills in any gap.
60    if !router.has_route(&hyper::Method::GET, "/") {
61        router = router.get("/", |_req: Request, _p: Params| async {
62            Ok::<Response, Error>(homepage())
63        });
64    }
65    if !router.has_route(&hyper::Method::GET, "/docs") {
66        router = router.get("/docs", |_req: Request, _p: Params| async {
67            Ok::<Response, Error>(docs_placeholder())
68        });
69    }
70    // `wrap` adds middleware that runs on every request — so the
71    // body-size cap applies uniformly to admin, user, and default
72    // routes without each handler having to opt in.
73    router.wrap(body_limit)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    /// Re-parse a Content-Length value against the same check the
81    /// middleware uses. This unit-level test avoids spinning up a
82    /// server — the integration test in `tests/login_flow.rs` covers
83    /// the end-to-end wiring.
84    fn check_content_length(value: &str) -> Result<(), ()> {
85        let n: u64 = value.parse().map_err(|_| ())?;
86        if n as u128 > MAX_REQUEST_BODY_BYTES as u128 {
87            Err(())
88        } else {
89            Ok(())
90        }
91    }
92
93    #[test]
94    fn content_length_at_limit_is_accepted() {
95        let at_limit = MAX_REQUEST_BODY_BYTES.to_string();
96        assert!(check_content_length(&at_limit).is_ok());
97    }
98
99    #[test]
100    fn content_length_over_limit_is_rejected() {
101        let over = (MAX_REQUEST_BODY_BYTES + 1).to_string();
102        assert!(check_content_length(&over).is_err());
103    }
104
105    #[test]
106    fn content_length_way_over_limit_is_rejected() {
107        // Even obviously-huge values don't overflow the u128 compare.
108        let huge = format!("{}", u64::MAX);
109        assert!(check_content_length(&huge).is_err());
110    }
111}