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
";
const VERSION: &str = env!("CARGO_PKG_VERSION");
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"));
}
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);
}
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() {
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))
}