//! Router construction. //! //! §6 stage 0/1 body caps are applied here as per-route `DefaultBodyLimit` //! layers: Axum rejects on `Content-Length` before reading a body *and* caps the //! stream for chunked or mis-declared uploads, which is what makes a lying //! header and a chunked upload both safe. Per-route means the bundle endpoint //! gets its larger limit without widening the others (§6 stage 1). use std::time::Duration; use axum::extract::DefaultBodyLimit; use axum::routing::{get, post}; use axum::Router; use tower_http::timeout::TimeoutLayer; use tower_http::trace::TraceLayer; use crate::api::{exists, federation, fetch, report, upload}; use crate::state::AppState; use crate::validate::limits; /// Small cap for endpoints that take a short JSON body. A read endpoint has no /// business accepting a large payload, and the batch `exists` form is bounded at /// 100 items. const SMALL_BODY_LIMIT: usize = 256 * 1024; /// TRACES: DR-009, DR-013 | SR-004 pub fn router(state: AppState) -> Router { let timeout = state.config.request_timeout; let v1 = Router::new() // UR-1 — existence probes. .route("/manifests/exists", get(exists::exists).post(exists::exists_batch)) // Reads. .route("/manifests/movie", get(fetch::get_movie)) .route("/manifests/episode", get(fetch::get_episode)) .route("/manifests/series/{series_tmdb_id}", get(fetch::get_series)) .route("/manifests/{id}", get(fetch::get_by_id)) .route("/manifests/{id}/status", get(fetch::get_status)) .route("/manifests/{id}/report", post(report::post_report)) // UR-2 — contribution. .route( "/manifests", post(upload::post_manifest).layer(DefaultBodyLimit::max(limits::BODY_LIMIT_MANIFEST)), ) // UR-6 — whole-series contribution, with its own larger cap. .route( "/manifests/bundle", post(upload::post_bundle).layer(DefaultBodyLimit::max(limits::BODY_LIMIT_BUNDLE)), ) // §5a — anonymous bearer capability, not an account. .route("/tokens", post(upload::post_token)) // §9a federation. Pull-based: a peer chooses what it ingests and when, // so every route here is a read. There is deliberately no push endpoint. .route("/federation/changes", get(federation::get_changes)) .route("/federation/manifests/{content_id}", get(federation::get_manifest_by_content_id)) .route("/federation/have", post(federation::post_have)) .route("/federation/peers", get(federation::get_peers)) .route("/federation/capabilities", get(federation::get_capabilities)) .layer(DefaultBodyLimit::max(SMALL_BODY_LIMIT)); Router::new() .route("/health", get(report::health)) .route("/ready", get(report::ready)) .nest("/api/v1", v1) // §8: a request timeout so a slow bundle query fails fast. .layer(TimeoutLayer::with_status_code(axum::http::StatusCode::REQUEST_TIMEOUT, timeout)) .layer(TraceLayer::new_for_http()) .with_state(state) } /// Convenience for tests and `main`. pub fn default_timeout() -> Duration { Duration::from_secs(30) }