juiceback/routes/
mod.rs

1//! Glues every HTTP handler together into one router with middleware.
2
3use axum::{
4    body::Body,
5    extract::{DefaultBodyLimit, State},
6    http::{header, HeaderValue, Request},
7    middleware,
8    response::IntoResponse,
9    routing::{delete, get, post},
10    Json, Router,
11};
12use serde_json::json;
13use std::sync::Arc;
14use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer};
15use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer, trace::TraceLayer};
16use utoipa::OpenApi;
17
18use crate::routes::api_doc::ApiDoc;
19use crate::state::AppState;
20
21pub mod admin;
22pub mod api_doc;
23pub mod manage;
24pub mod noscript;
25pub mod register;
26pub mod tus;
27pub mod upload;
28
29/// Middleware that applies security-related response headers.
30async fn add_security_headers(req: Request<Body>, next: middleware::Next) -> impl IntoResponse {
31    let mut response = next.run(req).await;
32    let headers = response.headers_mut();
33
34    headers.insert(
35        header::X_CONTENT_TYPE_OPTIONS,
36        HeaderValue::from_static("nosniff"),
37    );
38    headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
39    headers.insert(
40        header::CONTENT_SECURITY_POLICY,
41        HeaderValue::from_static(
42            "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'",
43        ),
44    );
45
46    response
47}
48
49/// Build the main router with every route and middleware layer.
50///
51/// Available routes:
52/// POST /upload -- multipart upload (rate limited and size limited)
53/// POST /api/tus and /api/tus/:id -- TUS resumable upload
54/// GET /file/:id/info -- get file info
55/// POST /file/:id/renew -- rotate the file ID
56/// DELETE /file/:id -- delete a file
57/// POST /file/:id/delete -- delete via HTML form (no JS)
58/// GET and POST /api/report -- report form
59/// POST /api/feedback -- feedback form
60/// GET /api/health -- health check
61/// GET /api/config -- public config values
62/// POST /api/owned-files -- bulk lookup of owned files
63/// POST /api/register -- internal registration for juicehost
64/// /api/admin/* -- admin panel
65pub fn build_router(state: Arc<AppState>) -> Router {
66    let upload_governor_conf = Arc::new(
67        GovernorConfigBuilder::default()
68            .period(std::time::Duration::from_secs(60))
69            .burst_size(state.config.rate_limit_per_minute)
70            .finish()
71            .unwrap(),
72    );
73
74    let cors = CorsLayer::permissive();
75
76    let upload_router = Router::new()
77        .route("/", post(upload::upload_handler))
78        .layer(GovernorLayer {
79            config: upload_governor_conf,
80        })
81        .layer(RequestBodyLimitLayer::new(
82            state.config.max_file_size_bytes as usize + 1024 * 1024,
83        ))
84        .layer(DefaultBodyLimit::max(
85            state.config.max_file_size_bytes as usize + 1024 * 1024,
86        ));
87
88    let tus_router = tus::tus_routes().layer(DefaultBodyLimit::max(
89        state.config.max_file_size_bytes as usize + 1024 * 1024,
90    ));
91
92    Router::new()
93        .nest("/upload", upload_router)
94        .merge(tus_router)
95        .route("/file/:id/info", get(manage::file_info_handler))
96        .route("/file/:id/renew", post(manage::renew_file_id_handler))
97        .route("/file/:id", delete(manage::delete_file_handler))
98        // no-JS HTML endpoints
99        .route("/file/:id/delete", post(manage::delete_file_form_handler))
100        .route(
101            "/api/report",
102            get(noscript::report_form_handler).post(noscript::report_submit_handler),
103        )
104        .route("/api/feedback", post(noscript::feedback_submit_handler))
105        .route("/api/health", get(health_handler))
106        .route("/api/config", get(config_handler))
107        .route("/api/owned-files", post(manage::owned_files_handler))
108        .route("/api/openapi.json", get(openapi_json_handler))
109        .route("/api/docs", get(openapi_json_handler))
110        .route("/api/register", post(register::register_handler))
111        .merge(admin::admin_routes())
112        .layer(middleware::from_fn(add_security_headers))
113        .layer(TraceLayer::new_for_http())
114        .layer(cors)
115        .with_state(state)
116}
117
118#[utoipa::path(
119    get,
120    path = "/api/health",
121    responses(
122        (status = 200, description = "Service health status (juicehost is always unknown since we check passively)", body = serde_json::Value),
123    ),
124    tag = "General",
125)]
126pub async fn health_handler() -> Json<serde_json::Value> {
127    // Simple local health check, no cross-service calls to avoid
128    // circular health check loops (juiceback to juicehost to juiceback).
129    // juicehost liveness is tracked passively via push/delete failures.
130    Json(json!({
131        "status": "ok",
132        "juiceback": "ok",
133        "juicehost": "unknown",
134    }))
135}
136
137#[utoipa::path(
138    get,
139    path = "/api/config",
140    responses(
141        (status = 200, description = "Public server configuration", body = serde_json::Value),
142    ),
143    tag = "General",
144)]
145pub async fn config_handler(
146    State(state): State<Arc<AppState>>,
147) -> Json<serde_json::Value> {
148    Json(json!({
149        "max_file_size_bytes": state.config.max_file_size_bytes,
150        "max_ttl_hours": state.config.max_ttl_hours,
151        "default_ttl_hours": state.config.default_ttl_hours,
152        "public_base_url": state.config.public_base_url,
153        "cleanup_interval_minutes": state.config.cleanup_interval_minutes,
154        "rate_limit_per_minute": state.config.rate_limit_per_minute,
155    }))
156}
157
158async fn openapi_json_handler() -> (axum::http::header::HeaderMap, String) {
159    let json = serde_json::to_string_pretty(&ApiDoc::openapi()).unwrap_or_default();
160    let mut headers = axum::http::header::HeaderMap::new();
161    headers.insert(
162        axum::http::header::CONTENT_TYPE,
163        axum::http::HeaderValue::from_static("application/json"),
164    );
165    (headers, json)
166}