atagen

malice

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