//! MakeNotWork library — shared between the binary and integration tests. pub mod access_gate; pub mod auth; pub mod background; pub mod build_runner; pub mod config; pub mod constants; pub mod csrf; pub mod db; pub mod email; pub mod error; pub mod git; pub mod git_ssh; pub mod license_templates; pub mod crypto; pub mod formatting; pub mod helpers; pub mod rate_limit; pub mod import; pub mod markdown; pub mod metrics; pub mod monitor; pub mod openapi; pub mod mt_client; pub mod wam_client; pub mod payments; pub mod pricing; pub mod synckit_billing; pub mod scheduler; pub mod routes; pub mod rss; pub mod scanning; pub mod storage; pub mod synckit_auth; pub mod templates; pub mod tier_prices; pub mod types; pub mod validation; pub mod wordlist; // Test-only lint: enforce that every Caddy site block proxying the app declares // a safe IP-trust posture. Compiled only under `cargo test`. #[cfg(test)] mod deploy_lint; use axum::{http::HeaderValue, middleware, Router}; use std::time::Instant; use tower_http::limit::RequestBodyLimitLayer; use tower_http::services::ServeDir; use tower_http::set_header::SetResponseHeaderLayer; use tower_sessions::SessionManagerLayer; use tower_sessions_sqlx_store::PostgresStore; use std::sync::Arc; use dashmap::DashMap; use db::{SyncAppId, UserSessionId, UserId}; use config::Config; use docengine::DocLoader; use email::EmailClient; use payments::PaymentProvider; use routes::{ admin_routes, api_routes, auth_routes, build_routes, git_routes, git_issue_routes, oauth_routes, ota_routes, page_routes, postmark_routes, sso_routes, storage_routes, stripe_routes, synckit_routes, }; use scanning::ScanPipeline; use storage::StorageBackend; use webauthn_rs::Webauthn; /// Application state shared across all handlers #[derive(Clone)] pub struct AppState { pub db: sqlx::PgPool, pub config: Config, pub s3: Option>, pub synckit_s3: Option>, pub stripe: Option>, pub email: EmailClient, pub docs: Arc, pub tier_prices: tier_prices::TierPrices, pub cost_allocation: tier_prices::CostAllocation, pub runway_config: tier_prices::RunwayConfig, pub scanner: Option>, pub webauthn: Arc, pub syntax: Option>, pub started_at: chrono::DateTime, pub start_instant: Instant, /// Cache of recently-validated session tracking IDs to skip per-request DB touch. /// Maps session tracking ID → last validated instant. Entries older than /// SESSION_TOUCH_CACHE_SECS are treated as expired. pub session_cache: Arc>, /// HTTP client for the Multithreaded internal API (community/thread provisioning). pub mt_client: Option, /// HTTP client for the WAM ticket manager (operational alerts). pub wam: Option, /// Cache of verified custom domains → user IDs (populated on startup, updated on verify/delete). pub domain_cache: Arc>, /// Limits concurrent file scans to prevent memory exhaustion (each scan /// downloads up to SCAN_MAX_MEMORY_BYTES into RAM). pub scan_semaphore: Arc, /// Caps concurrent cache-miss DB lookups in `caddy-ask` so a flood of /// unknown-domain queries can't saturate the pool or drive ACME issuance. pub caddy_ask_semaphore: Arc, /// Unix timestamp when the server will restart (0 = no restart pending). /// Set by the deploy script via the internal API before uploading a new binary. pub restart_at: Arc, /// SSE push notification channels for SyncKit subscribers. /// Key: (app_id, user_id), Value: broadcast sender that SSE connections subscribe to. pub sync_notify: Arc>>, /// Concurrent SSE connection count per user (for rate limiting). pub sse_connections: Arc>, /// Prometheus metrics handle for rendering the admin dashboard. `None` in tests. pub metrics_handle: Option, /// Bounded batcher for page-view UPSERTs. Replaces the previous /// `tokio::spawn(record_view(...))` per request which under burst saturated /// the DB pool. `try_record` is non-blocking; drops on overflow. pub page_view_tx: db::page_views::PageViewTx, /// Bounded background-task queue for fire-and-forget work (email sends, /// mailing-list subscriptions, etc.). Replaces per-request `tokio::spawn` /// for low-priority work; bounded queue + bounded concurrent execution /// prevent burst traffic from starving the DB pool. See `background.rs`. pub bg: background::BackgroundTx, } impl AppState { /// Get the main S3 storage backend, or error if not configured. pub fn require_s3(&self) -> error::Result<&Arc> { self.s3 .as_ref() .ok_or_else(|| error::AppError::ServiceUnavailable("File storage is not configured".to_string())) } /// Get the SyncKit S3 storage backend, or error if not configured. pub fn require_synckit_s3(&self) -> error::Result<&Arc> { self.synckit_s3 .as_ref() .ok_or_else(|| error::AppError::ServiceUnavailable("SyncKit storage is not configured".to_string())) } } /// Build the app router with all routes and middleware (minus tracing/TCP). pub fn build_app( state: AppState, session_layer: SessionManagerLayer, ) -> Router { let metrics_handle = state.metrics_handle.clone(); // All mutation-bearing sub-routers register through `CsrfRouter`, whose // `route` method only accepts `PostureMethodRouter` values produced by // the `csrf::*_csrf*` helpers. Finalising the merged tree drops the // structural envelope so global middleware, static-file mounts, and // the few bare GETs below can attach to a plain `Router`. let csrf_routes = csrf::CsrfRouter::new() .merge(auth_routes()) .merge(api_routes()) .merge(storage_routes()) .merge(stripe_routes()) .merge(admin_routes()) .merge(synckit_routes(state.config.synckit_jwt_secret.clone().map(std::sync::Arc::new))) .merge(oauth_routes()) .merge(postmark_routes()) .merge(git_issue_routes()) .merge(ota_routes()) .merge(build_routes()) .finalize(); let mut app = Router::new() .merge(page_routes()) .merge(sso_routes()) .merge(csrf_routes) .merge(git_routes()) .merge(routes::embed::embed_routes()) .route("/api/openapi.json", axum::routing::get(openapi::openapi_json)) .nest_service( "/static", tower::ServiceBuilder::new() .layer(SetResponseHeaderLayer::overriding( axum::http::header::CACHE_CONTROL, HeaderValue::from_static( "public, max-age=604800, stale-while-revalidate=86400", ), )) .service(ServeDir::new("static")), ) .nest_service( "/rustdoc", tower::ServiceBuilder::new() .layer(SetResponseHeaderLayer::overriding( axum::http::header::CACHE_CONTROL, HeaderValue::from_static( "public, max-age=86400, stale-while-revalidate=3600", ), )) .service(ServeDir::new("rustdoc")), ) .fallback(routes::custom_domain::custom_domain_fallback) .with_state(state.clone()); // /metrics endpoint (Prometheus scrape target). Only available when the // recorder is installed (i.e. in the real server, not in integration tests). // Protected by Bearer token matching cli_service_token. if let Some(handle) = metrics_handle { let metrics_state = state.clone(); app = app.merge( Router::new() .route("/metrics", axum::routing::get(move | axum::extract::State(prom_handle): axum::extract::State, headers: axum::http::HeaderMap, | async move { use axum::response::IntoResponse; let token = headers .get("authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")); match (token, metrics_state.config.cli_service_token.as_deref()) { (Some(t), Some(expected)) if crate::helpers::constant_time_compare(t, expected) => { prom_handle.render().into_response() } _ => axum::http::StatusCode::UNAUTHORIZED.into_response(), } })) .with_state(handle), ); } // All rate limiters were registered as they were built above; start the // periodic GC that sweeps their bucket maps so they don't grow unbounded for // process lifetime (Run #14 CHRONIC 1). Guarded by `Once` internally. crate::rate_limit::start_governor_sweeper(); app.layer(middleware::from_fn_with_state(state.clone(), access_gate::access_gate_middleware)) .layer(middleware::from_fn_with_state(state.clone(), security_headers_middleware)) .layer(middleware::from_fn(metrics::cache_control_middleware)) .layer(middleware::from_fn(metrics::metrics_middleware)) .layer(middleware::from_fn_with_state(state.clone(), metrics::idempotency_middleware)) .layer(session_layer) .layer(RequestBodyLimitLayer::new(1024 * 1024)) } /// Middleware that sets security headers on all responses. /// Embed routes (`/embed/`) get permissive frame headers for iframe embedding. async fn security_headers_middleware( axum::extract::State(state): axum::extract::State, request: axum::http::Request, next: middleware::Next, ) -> axum::response::Response { let is_embed = request.uri().path().starts_with("/embed/"); let mut response = next.run(request).await; let headers = response.headers_mut(); if is_embed { // Embed routes: framable from any origin, but otherwise locked down. // `frame-ancestors *` alone (the old value) left default-src/script-src // unrestricted, so an embed XSS would have had no CSP backstop. We keep // inline script/style (the audio-player embed uses an inline