Skip to main content

ling_http/
app.rs

1use axum::Router;
2use std::net::SocketAddr;
3use std::time::Duration;
4use tower_http::cors::CorsLayer;
5use tower_http::limit::RequestBodyLimitLayer;
6use tower_http::timeout::TimeoutLayer;
7use tower_http::trace::TraceLayer;
8
9/// Prints a startup banner to stdout — plain `println!`, not `tracing`, so
10/// it's visible even in a binary that never wired up a tracing subscriber
11/// (the common case for a small `.ling`/`lingfu` app). Raw ANSI codes rather
12/// than a colour crate dependency; modern Windows Terminal/PowerShell and
13/// every Unix terminal render these natively.
14fn print_listening_banner(scheme: &str, addr: SocketAddr) {
15    let url = format!("{scheme}://{addr}");
16    println!();
17    println!("  \x1b[1;36m⚡ ling-http\x1b[0m");
18    println!("  \x1b[32m➜\x1b[0m  Running on \x1b[1;32m{url}\x1b[0m");
19    println!("  \x1b[32m➜\x1b[0m  Press \x1b[1mCtrl+C\x1b[0m to quit");
20    println!();
21}
22
23#[cfg(feature = "dev-certs")]
24use crate::tls::TlsMaterial;
25
26/// A ling-http application: an axum `Router` pre-wired with the middleware
27/// every service on this framework wants (tracing, timeouts, a body-size
28/// cap, CORS), plus HTTPS serving helpers.
29///
30/// `S` is your application state (typically a struct holding a [`crate::db::Db`]
31/// and any config), shared across handlers via axum's `State` extractor.
32pub struct App<S> {
33    router: Router<S>,
34}
35
36impl<S> App<S>
37where
38    S: Clone + Send + Sync + 'static,
39{
40    /// Starts from an empty router with sane default middleware:
41    /// request tracing, a 60s timeout, and a 25 MiB request body cap
42    /// (override with `.max_body_bytes` if a route needs to accept larger
43    /// uploads, e.g. crate tarballs).
44    pub fn new() -> Self {
45        let router = Router::new()
46            .layer(TraceLayer::new_for_http())
47            .layer(TimeoutLayer::with_status_code(
48                axum::http::StatusCode::REQUEST_TIMEOUT,
49                Duration::from_secs(60),
50            ))
51            .layer(RequestBodyLimitLayer::new(25 * 1024 * 1024))
52            .layer(CorsLayer::permissive());
53        Self { router }
54    }
55
56    /// Raises the request body size cap. Call this before merging in routes
57    /// that need to accept large uploads.
58    pub fn max_body_bytes(mut self, bytes: usize) -> Self {
59        self.router = self.router.layer(RequestBodyLimitLayer::new(bytes));
60        self
61    }
62
63    /// Merges routes (from `.route(...)` calls or [`crate::mvc::resource`])
64    /// into the app.
65    pub fn merge(mut self, routes: Router<S>) -> Self {
66        self.router = self.router.merge(routes);
67        self
68    }
69
70    /// Attaches the shared application state, finishing the router.
71    pub fn with_state(self, state: S) -> Router {
72        self.router.with_state(state)
73    }
74}
75
76impl<S> Default for App<S>
77where
78    S: Clone + Send + Sync + 'static,
79{
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85/// Serves `router` over plain HTTP. Use this only behind a reverse proxy
86/// that terminates TLS (matches how the other linglin.art sites are
87/// deployed); for a service that must terminate TLS itself, use
88/// [`serve_tls`] or [`serve_dev_tls`] instead.
89pub async fn serve_http(router: Router, addr: SocketAddr) -> anyhow::Result<()> {
90    let listener = tokio::net::TcpListener::bind(addr).await?;
91    let bound_addr = listener.local_addr().unwrap_or(addr);
92    tracing::info!(%bound_addr, "ling-http listening (plain HTTP)");
93    print_listening_banner("http", bound_addr);
94    axum::serve(listener, router.into_make_service()).await?;
95    Ok(())
96}
97
98/// Serves `router` over HTTPS using a certificate/key pair loaded from disk
99/// (PEM format). This is real TLS termination — use it when there's no
100/// reverse proxy in front of the service.
101pub async fn serve_tls(
102    router: Router,
103    addr: SocketAddr,
104    cert_pem: impl AsRef<std::path::Path>,
105    key_pem: impl AsRef<std::path::Path>,
106) -> anyhow::Result<()> {
107    let config = crate::tls::load_rustls_config(cert_pem, key_pem).await?;
108    tracing::info!(%addr, "ling-http listening (HTTPS)");
109    print_listening_banner("https", addr);
110    axum_server::bind_rustls(addr, config)
111        .serve(router.into_make_service())
112        .await?;
113    Ok(())
114}
115
116/// Serves `router` over HTTPS using a throwaway self-signed certificate
117/// generated at startup. **Local development only** — browsers and `cargo`/
118/// `curl` will reject the cert as untrusted unless told to ignore it
119/// (`curl -k`). Requires the `dev-certs` feature.
120#[cfg(feature = "dev-certs")]
121pub async fn serve_dev_tls(router: Router, addr: SocketAddr) -> anyhow::Result<()> {
122    let TlsMaterial { config, .. } = crate::tls::generate_dev_cert(addr).await?;
123    tracing::warn!(%addr, "ling-http listening (HTTPS, SELF-SIGNED DEV CERT — do not use in production)");
124    print_listening_banner("https", addr);
125    println!("  \x1b[33m⚠\x1b[0m  self-signed dev cert — browsers/curl will reject it as untrusted\n");
126    axum_server::bind_rustls(addr, config)
127        .serve(router.into_make_service())
128        .await?;
129    Ok(())
130}