holger-ui 3.1.1

Holger's web console: `cargo install holger-ui`, point it at a Holger with HOLGER_UI_HOLGER, open http://127.0.0.1:3210/ and sign in. It serves the console bundle and a same-origin pass-through to that Holger, and nothing else.
//! `holger-ui` (`cargo install holger-ui`): Holger's web console. It serves the console
//! bundle (built into this program, or `HOLGER_UI_DIR`) and a same-origin pass-through to
//! the Holger it points at. holger's estate front links the same router (`holger-ui-server`).

use std::net::SocketAddr;
use std::path::PathBuf;

use holger_ui_server::{router, Upstreams};

const USAGE: &str = "\
holger-ui — Holger's web console: the console bundle, and a same-origin
pass-through to the Holger it points at. Open http://<HOLGER_UI_BIND>/ and sign in.

the environment:
  HOLGER_UI_BIND          where to listen (127.0.0.1:3210)
  HOLGER_UI_HOLGER        holger's gRPC door (127.0.0.1:50051)
  HOLGER_UI_DOOR          holger's HTTP door, its passkey plane (127.0.0.1:8443)
  HOLGER_UI_DIR           a console bundle on disk (none: the one built into this
                          program, if it carries one)
  HOLGER_UI_TOKEN         file holding this console's bearer — the proof behind
                          a ceremony's client hint, never anyone's credential
  HOLGER_UI_TOKEN_INLINE  …or that bearer inline
";

/// This program's own version (lane HOL-310-CHECK): 3.1.0 printed holger-ui-server's
/// `VERSION`, a library on its own line (3.0.0), so `--version` named the wrong release.
const VERSION: &str = env!("CARGO_PKG_VERSION");
/// `holger-ui --version`: `holger-ui v<version> (<commit>)`, as `holger --version` prints it.
const VERSION_LINE: &str = concat!(
    "holger-ui v",
    env!("CARGO_PKG_VERSION"),
    " (",
    env!("HOLGER_UI_GIT_HASH"),
    ")"
);

fn env(key: &str, default: &str) -> String {
    std::env::var(key)
        .ok()
        .filter(|v| !v.trim().is_empty())
        .unwrap_or_else(|| default.to_owned())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
        )
        .init();
    match std::env::args().nth(1).as_deref() {
        None => {}
        Some("-V" | "--version" | "version") => {
            println!("{VERSION_LINE}");
            return Ok(());
        }
        Some(_) => {
            print!("{USAGE}");
            return Ok(());
        }
    }
    let proof = match std::env::var("HOLGER_UI_TOKEN")
        .ok()
        .filter(|p| !p.is_empty())
    {
        Some(file) => Some(std::fs::read_to_string(file)?),
        None => std::env::var("HOLGER_UI_TOKEN_INLINE").ok(),
    };
    let up = Upstreams::new(
        &env("HOLGER_UI_HOLGER", "127.0.0.1:50051"),
        &env("HOLGER_UI_DOOR", "127.0.0.1:8443"),
        proof,
    );
    let dir = match std::env::var("HOLGER_UI_DIR")
        .ok()
        .filter(|d| !d.is_empty())
    {
        Some(d) => Some(PathBuf::from(d)),
        None => built_in_console()?,
    };
    let bind: SocketAddr = env("HOLGER_UI_BIND", "127.0.0.1:3210").parse()?;
    tracing::info!(%bind, grpc = %up.grpc, door = %up.door, bundle = ?dir, "holger-ui: serving");
    let app = router(dir, up);
    let listener = tokio::net::TcpListener::bind(bind).await?;
    axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
    .with_graceful_shutdown(async {
        let _ = tokio::signal::ctrl_c().await;
    })
    .await?;
    Ok(())
}

mod console {
    include!(concat!(env!("OUT_DIR"), "/console.rs"));
}

/// The console bundle built into this program, laid out once in the cache directory
/// (`$XDG_CACHE_HOME` or `~/.cache`, `holger-ui/console-<version>-<digest>`) so the router
/// serves it like any bundle on disk. `None` when the build carried no bundle.
fn built_in_console() -> std::io::Result<Option<PathBuf>> {
    if console::CONSOLE.is_empty() {
        tracing::warn!("this holger-ui carries no console bundle (build one with `cargo xtask console`, or set HOLGER_UI_DIR): serving the pass-through only");
        return Ok(None);
    }
    // FNV-1a over every path and its bytes: the same bundle lays out to the same directory.
    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
    for (path, bytes) in console::CONSOLE {
        for b in path.as_bytes().iter().chain(bytes.iter()) {
            h = (h ^ u64::from(*b)).wrapping_mul(0x0000_0100_0000_01b3);
        }
    }
    let cache = std::env::var_os("XDG_CACHE_HOME")
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))
        .unwrap_or_else(std::env::temp_dir);
    let dir = cache
        .join("holger-ui")
        .join(format!("console-{VERSION}-{h:016x}"));
    if !dir.join("index.html").is_file() {
        let tmp = dir.with_extension(format!("tmp-{}", std::process::id()));
        for (path, bytes) in console::CONSOLE {
            let p = tmp.join(path);
            if let Some(parent) = p.parent() {
                std::fs::create_dir_all(parent)?;
            }
            std::fs::write(&p, bytes)?;
        }
        if std::fs::rename(&tmp, &dir).is_err() {
            // Another start laid it out first: theirs is the same bundle.
            let _ = std::fs::remove_dir_all(&tmp);
        }
    }
    tracing::info!(files = console::CONSOLE.len(), built_from = console::CONSOLE_FROM, at = %dir.display(), "the console bundle built into this program");
    Ok(Some(dir))
}