atagen

malice

public
Star0
Watch0
77eddb6b710ffe2b884b8bdf7b0611041f7358e6
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: crates/malice-static/src/lib.rs

dffd63b58af4atagen2026-09-28 09:10 UTC1
pub mod idle;
dffd63b58af4atagen2026-09-28 09:10 UTC2
dffd63b58af4atagen2026-09-28 09:10 UTC3
use std::{future::Future, net::SocketAddr, time::Duration};
dffd63b58af4atagen2026-09-28 09:10 UTC4
dffd63b58af4atagen2026-09-28 09:10 UTC5
use axum::{
dffd63b58af4atagen2026-09-28 09:10 UTC6
    Router,
dffd63b58af4atagen2026-09-28 09:10 UTC7
    http::{HeaderName, HeaderValue, StatusCode, header},
dffd63b58af4atagen2026-09-28 09:10 UTC8
    middleware,
dffd63b58af4atagen2026-09-28 09:10 UTC9
    middleware::Next,
dffd63b58af4atagen2026-09-28 09:10 UTC10
    response::{IntoResponse, Response},
dffd63b58af4atagen2026-09-28 09:10 UTC11
    routing::any,
dffd63b58af4atagen2026-09-28 09:10 UTC12
};
dffd63b58af4atagen2026-09-28 09:10 UTC13
use listenfd::ListenFd;
dffd63b58af4atagen2026-09-28 09:10 UTC14
use tower_http::{
dffd63b58af4atagen2026-09-28 09:10 UTC15
    services::{ServeDir, ServeFile},
dffd63b58af4atagen2026-09-28 09:10 UTC16
    trace::TraceLayer,
dffd63b58af4atagen2026-09-28 09:10 UTC17
};
dffd63b58af4atagen2026-09-28 09:10 UTC18
dffd63b58af4atagen2026-09-28 09:10 UTC19
use crate::idle::IdleTracker;
dffd63b58af4atagen2026-09-28 09:10 UTC20
dffd63b58af4atagen2026-09-28 09:10 UTC21
fn bind_addr() -> SocketAddr {
dffd63b58af4atagen2026-09-28 09:10 UTC22
    std::env::var("MALICE_BIND")
dffd63b58af4atagen2026-09-28 09:10 UTC23
        .unwrap_or_else(|_| "127.0.0.1:3000".to_owned())
dffd63b58af4atagen2026-09-28 09:10 UTC24
        .parse()
dffd63b58af4atagen2026-09-28 09:10 UTC25
        .expect("MALICE_BIND must be host:port")
dffd63b58af4atagen2026-09-28 09:10 UTC26
}
dffd63b58af4atagen2026-09-28 09:10 UTC27
dffd63b58af4atagen2026-09-28 09:10 UTC28
fn idle_exit_secs() -> Option<u64> {
dffd63b58af4atagen2026-09-28 09:10 UTC29
    std::env::var("MALICE_IDLE_EXIT_SECS")
dffd63b58af4atagen2026-09-28 09:10 UTC30
        .ok()
dffd63b58af4atagen2026-09-28 09:10 UTC31
        .and_then(|value| value.trim().parse().ok())
dffd63b58af4atagen2026-09-28 09:10 UTC32
}
dffd63b58af4atagen2026-09-28 09:10 UTC33
dffd63b58af4atagen2026-09-28 09:10 UTC34
/// Takes over a systemd-activated listener from fd 3 when present, so the
dffd63b58af4atagen2026-09-28 09:10 UTC35
/// socket unit can hold the port before the process ever starts. Falls back
dffd63b58af4atagen2026-09-28 09:10 UTC36
/// to a plain bind otherwise.
dffd63b58af4atagen2026-09-28 09:10 UTC37
pub async fn bind_listener() -> tokio::net::TcpListener {
dffd63b58af4atagen2026-09-28 09:10 UTC38
    let mut listenfd = ListenFd::from_env();
dffd63b58af4atagen2026-09-28 09:10 UTC39
    if let Some(listener) = listenfd
dffd63b58af4atagen2026-09-28 09:10 UTC40
        .take_tcp_listener(0)
dffd63b58af4atagen2026-09-28 09:10 UTC41
        .expect("inspecting inherited sockets")
dffd63b58af4atagen2026-09-28 09:10 UTC42
    {
dffd63b58af4atagen2026-09-28 09:10 UTC43
        listener
dffd63b58af4atagen2026-09-28 09:10 UTC44
            .set_nonblocking(true)
dffd63b58af4atagen2026-09-28 09:10 UTC45
            .expect("setting inherited socket nonblocking");
dffd63b58af4atagen2026-09-28 09:10 UTC46
        return tokio::net::TcpListener::from_std(listener)
dffd63b58af4atagen2026-09-28 09:10 UTC47
            .expect("adopting inherited socket into tokio");
dffd63b58af4atagen2026-09-28 09:10 UTC48
    }
dffd63b58af4atagen2026-09-28 09:10 UTC49
    let addr = bind_addr();
dffd63b58af4atagen2026-09-28 09:10 UTC50
    tracing::info!("listening on {addr}");
dffd63b58af4atagen2026-09-28 09:10 UTC51
    tokio::net::TcpListener::bind(addr)
dffd63b58af4atagen2026-09-28 09:10 UTC52
        .await
dffd63b58af4atagen2026-09-28 09:10 UTC53
        .expect("binding the listener must succeed")
dffd63b58af4atagen2026-09-28 09:10 UTC54
}
dffd63b58af4atagen2026-09-28 09:10 UTC55
dffd63b58af4atagen2026-09-28 09:10 UTC56
/// Serves `app` on `listener`, exiting on ctrl-c or, when
dffd63b58af4atagen2026-09-28 09:10 UTC57
/// `MALICE_IDLE_EXIT_SECS` is set, after that long with no request in flight
dffd63b58af4atagen2026-09-28 09:10 UTC58
/// or started. `on_shutdown` runs after the exit signal either way.
dffd63b58af4atagen2026-09-28 09:10 UTC59
pub async fn serve(
dffd63b58af4atagen2026-09-28 09:10 UTC60
    listener: tokio::net::TcpListener,
dffd63b58af4atagen2026-09-28 09:10 UTC61
    app: Router,
dffd63b58af4atagen2026-09-28 09:10 UTC62
    on_shutdown: impl Future<Output = ()> + Send + 'static,
dffd63b58af4atagen2026-09-28 09:10 UTC63
) {
dffd63b58af4atagen2026-09-28 09:10 UTC64
    let idle = idle_exit_secs().map(|secs| (IdleTracker::new(), secs));
dffd63b58af4atagen2026-09-28 09:10 UTC65
    let app = match &idle {
dffd63b58af4atagen2026-09-28 09:10 UTC66
        Some((tracker, _)) => {
dffd63b58af4atagen2026-09-28 09:10 UTC67
            app.layer(middleware::from_fn_with_state(tracker.clone(), idle::track))
dffd63b58af4atagen2026-09-28 09:10 UTC68
        }
dffd63b58af4atagen2026-09-28 09:10 UTC69
        None => app,
dffd63b58af4atagen2026-09-28 09:10 UTC70
    };
dffd63b58af4atagen2026-09-28 09:10 UTC71
    axum::serve(listener, app)
dffd63b58af4atagen2026-09-28 09:10 UTC72
        .with_graceful_shutdown(async move {
dffd63b58af4atagen2026-09-28 09:10 UTC73
            match idle {
dffd63b58af4atagen2026-09-28 09:10 UTC74
                Some((tracker, secs)) => {
dffd63b58af4atagen2026-09-28 09:10 UTC75
                    tokio::select! {
dffd63b58af4atagen2026-09-28 09:10 UTC76
                        _ = tokio::signal::ctrl_c() => {}
dffd63b58af4atagen2026-09-28 09:10 UTC77
                        _ = tracker.wait_idle(Duration::from_secs(secs)) => {
dffd63b58af4atagen2026-09-28 09:10 UTC78
                            tracing::info!("idle for {secs}s with no requests, exiting");
dffd63b58af4atagen2026-09-28 09:10 UTC79
                        }
dffd63b58af4atagen2026-09-28 09:10 UTC80
                    }
dffd63b58af4atagen2026-09-28 09:10 UTC81
                }
dffd63b58af4atagen2026-09-28 09:10 UTC82
                None => {
dffd63b58af4atagen2026-09-28 09:10 UTC83
                    let _ = tokio::signal::ctrl_c().await;
dffd63b58af4atagen2026-09-28 09:10 UTC84
                }
dffd63b58af4atagen2026-09-28 09:10 UTC85
            }
dffd63b58af4atagen2026-09-28 09:10 UTC86
            on_shutdown.await;
dffd63b58af4atagen2026-09-28 09:10 UTC87
        })
dffd63b58af4atagen2026-09-28 09:10 UTC88
        .await
dffd63b58af4atagen2026-09-28 09:10 UTC89
        .expect("serving must succeed");
dffd63b58af4atagen2026-09-28 09:10 UTC90
}
dffd63b58af4atagen2026-09-28 09:10 UTC91
dffd63b58af4atagen2026-09-28 09:10 UTC92
async fn api_not_found() -> StatusCode {
dffd63b58af4atagen2026-09-28 09:10 UTC93
    StatusCode::NOT_FOUND
dffd63b58af4atagen2026-09-28 09:10 UTC94
}
dffd63b58af4atagen2026-09-28 09:10 UTC95
dffd63b58af4atagen2026-09-28 09:10 UTC96
async fn missing_frontend() -> Response {
dffd63b58af4atagen2026-09-28 09:10 UTC97
    (
dffd63b58af4atagen2026-09-28 09:10 UTC98
        StatusCode::SERVICE_UNAVAILABLE,
dffd63b58af4atagen2026-09-28 09:10 UTC99
        "frontend assets missing; set MALICE_WEB_DIR or use the Nix package",
dffd63b58af4atagen2026-09-28 09:10 UTC100
    )
dffd63b58af4atagen2026-09-28 09:10 UTC101
        .into_response()
dffd63b58af4atagen2026-09-28 09:10 UTC102
}
dffd63b58af4atagen2026-09-28 09:10 UTC103
dffd63b58af4atagen2026-09-28 09:10 UTC104
fn is_no_cache_path(path: &str) -> bool {
dffd63b58af4atagen2026-09-28 09:10 UTC105
    matches!(path, "/" | "/index.html" | "/sw.js")
dffd63b58af4atagen2026-09-28 09:10 UTC106
}
dffd63b58af4atagen2026-09-28 09:10 UTC107
dffd63b58af4atagen2026-09-28 09:10 UTC108
/// index.html and sw.js must never be cached, or a stale service worker or
dffd63b58af4atagen2026-09-28 09:10 UTC109
/// app shell survives a rebuild; the rest of the bundle is hashed and can be
dffd63b58af4atagen2026-09-28 09:10 UTC110
/// cached freely.
dffd63b58af4atagen2026-09-28 09:10 UTC111
async fn static_headers(request: axum::extract::Request, next: Next) -> Response {
dffd63b58af4atagen2026-09-28 09:10 UTC112
    let no_cache = is_no_cache_path(request.uri().path());
dffd63b58af4atagen2026-09-28 09:10 UTC113
    let mut response = next.run(request).await;
dffd63b58af4atagen2026-09-28 09:10 UTC114
    if no_cache {
dffd63b58af4atagen2026-09-28 09:10 UTC115
        let headers = response.headers_mut();
dffd63b58af4atagen2026-09-28 09:10 UTC116
        headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
dffd63b58af4atagen2026-09-28 09:10 UTC117
        headers.insert(
dffd63b58af4atagen2026-09-28 09:10 UTC118
            HeaderName::from_static("service-worker-allowed"),
dffd63b58af4atagen2026-09-28 09:10 UTC119
            HeaderValue::from_static("/"),
dffd63b58af4atagen2026-09-28 09:10 UTC120
        );
dffd63b58af4atagen2026-09-28 09:10 UTC121
    }
dffd63b58af4atagen2026-09-28 09:10 UTC122
    response
dffd63b58af4atagen2026-09-28 09:10 UTC123
}
dffd63b58af4atagen2026-09-28 09:10 UTC124
dffd63b58af4atagen2026-09-28 09:10 UTC125
/// Serves only the built frontend: no `/api` routes, no session state, no
dffd63b58af4atagen2026-09-28 09:10 UTC126
/// data dir. Used for the socket-activated on-device build.
dffd63b58af4atagen2026-09-28 09:10 UTC127
pub fn static_router(web_dir: Option<String>) -> Router {
dffd63b58af4atagen2026-09-28 09:10 UTC128
    let router = Router::new()
dffd63b58af4atagen2026-09-28 09:10 UTC129
        .route("/api", any(api_not_found))
dffd63b58af4atagen2026-09-28 09:10 UTC130
        .route("/api/{*rest}", any(api_not_found))
dffd63b58af4atagen2026-09-28 09:10 UTC131
        .layer(TraceLayer::new_for_http());
dffd63b58af4atagen2026-09-28 09:10 UTC132
    match web_dir {
dffd63b58af4atagen2026-09-28 09:10 UTC133
        Some(dir) => {
dffd63b58af4atagen2026-09-28 09:10 UTC134
            let files =
dffd63b58af4atagen2026-09-28 09:10 UTC135
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
dffd63b58af4atagen2026-09-28 09:10 UTC136
            router
dffd63b58af4atagen2026-09-28 09:10 UTC137
                .fallback_service(files)
dffd63b58af4atagen2026-09-28 09:10 UTC138
                .layer(middleware::from_fn(static_headers))
dffd63b58af4atagen2026-09-28 09:10 UTC139
        }
dffd63b58af4atagen2026-09-28 09:10 UTC140
        None => router.fallback(missing_frontend),
dffd63b58af4atagen2026-09-28 09:10 UTC141
    }
dffd63b58af4atagen2026-09-28 09:10 UTC142
}