| 1 | //! HTTP server for anvil: the web UI and the smart-HTTP git endpoints |
| 2 | //! (`/<owner>/<repo>.git/info/refs`, `/git-upload-pack`, `/git-receive-pack`), |
| 3 | //! plus cookie-session auth. |
| 4 | |
| 5 | // Handlers return `Result<_, Response>` — the idiomatic axum pattern where the |
| 6 | // error arm is a ready-made HTTP response (404/500/redirect). `Response` is |
| 7 | // intrinsically large, so `result_large_err` fires across the crate; the |
| 8 | // pattern is intentional and the responses are never hot-path allocated en masse. |
| 9 | #![allow(clippy::result_large_err)] |
| 10 | |
| 11 | use anvil_core::{ |
| 12 | App, |
| 13 | Result, |
| 14 | }; |
| 15 | use axum::{ |
| 16 | Router, |
| 17 | routing::{ |
| 18 | get, |
| 19 | post, |
| 20 | }, |
| 21 | }; |
| 22 | |
| 23 | pub mod admin; |
| 24 | pub mod agent; |
| 25 | pub mod artifacts; |
| 26 | pub mod attachments; |
| 27 | pub mod auth; |
| 28 | pub mod git_http; |
| 29 | pub mod oidc; |
| 30 | pub mod pages; |
| 31 | pub mod runner; |
| 32 | pub mod secrets; |
| 33 | pub mod todomd; |
| 34 | pub mod ui; |
| 35 | |
| 36 | /// Build the application router. |
| 37 | pub fn router(app: App) -> Router { |
| 38 | let mut router = Router::new() |
| 39 | .route("/-/healthz", get(healthz)) |
| 40 | .route("/-/login", get(auth::login_form).post(auth::login_submit)) |
| 41 | .route("/-/logout", post(auth::logout)); |
| 42 | router = ui::routes(router); // web UI, including `/` |
| 43 | router = admin::routes(router); // admin-only dashboard |
| 44 | router = agent::routes(router); // agent sessions + the browser terminal |
| 45 | router = pages::routes(router); // static sites from `pages` branches |
| 46 | router = artifacts::routes(router); // CI artifact downloads + sites |
| 47 | router = attachments::routes( |
| 48 | router, |
| 49 | app.config.http.attachment_max_mb.saturating_mul(1 << 20), |
| 50 | ); // uploaded image attachments |
| 51 | router = oidc::routes(router); // single sign-on, when configured |
| 52 | router = secrets::routes(router); // sealed per-repo secrets + unlock API |
| 53 | router = runner::routes(router); // job runners claiming and reporting CI |
| 54 | router = git_http::routes(router); // smart-HTTP git endpoints |
| 55 | router |
| 56 | // Derives the per-request CSRF token so the layout can attach it to |
| 57 | // htmx requests (hx-headers). Runs for all routes; cheap. |
| 58 | .layer(axum::middleware::from_fn_with_state( |
| 59 | app.clone(), |
| 60 | auth::csrf_context, |
| 61 | )) |
| 62 | .with_state(app) |
| 63 | } |
| 64 | |
| 65 | /// Bind to the configured HTTP address and serve until shutdown. |
| 66 | pub async fn serve(app: App) -> Result<()> { |
| 67 | let listen = app.config.http.listen.clone(); |
| 68 | let router = router(app); |
| 69 | |
| 70 | let listener = tokio::net::TcpListener::bind(&listen).await?; |
| 71 | tracing::info!("anvil web server listening on http://{listen}"); |
| 72 | axum::serve(listener, router).await?; |
| 73 | Ok(()) |
| 74 | } |
| 75 | |
| 76 | async fn healthz() -> &'static str { |
| 77 | "ok" |
| 78 | } |