use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;
use kovra_core::{Confirmer, FileConfirmer};
use kovra_webui::{AppState, parse_master_key, serve};
#[tokio::main]
async fn main() -> ExitCode {
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("kovra-ui: {e}");
ExitCode::FAILURE
}
}
}
async fn run() -> Result<(), String> {
let root = env_required("KOVRA_VAULT_DIR")?;
let key_file = env_required("KOVRA_MASTER_KEY_FILE")?;
let bind: IpAddr = std::env::var("KOVRA_UI_BIND")
.ok()
.unwrap_or_else(|| "0.0.0.0".to_string())
.parse()
.map_err(|_| "KOVRA_UI_BIND is not a valid IP address".to_string())?;
let port: u16 = match std::env::var("KOVRA_UI_PORT") {
Ok(p) => p
.parse()
.map_err(|_| "KOVRA_UI_PORT is not a valid port".to_string())?,
Err(_) => kovra_webui::DEFAULT_PORT,
};
let idle = Duration::from_secs(
std::env::var("KOVRA_UI_IDLE_SECS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(300),
);
let raw = std::fs::read(&key_file)
.map_err(|e| format!("reading master key file {key_file:?}: {e}"))?;
let master = parse_master_key(&raw)?;
drop(raw);
let root_path = PathBuf::from(root);
let confirmer: Arc<dyn Confirmer + Send + Sync> =
Arc::new(FileConfirmer::under_root(&root_path));
let state = match std::env::var("KOVRA_UI_SESSION") {
Ok(token) if !token.is_empty() => {
AppState::new_with_session(root_path, master, token, confirmer)
}
_ => AppState::new(root_path, master, confirmer),
};
let addr = SocketAddr::new(bind, port);
let listener = tokio::net::TcpListener::bind(addr)
.await
.map_err(|e| format!("binding {addr}: {e}"))?;
eprintln!(
"kovra-ui: serving on {addr} (idle shutdown {}s). Reach it via the host's loopback publish.",
idle.as_secs()
);
serve(listener, state, idle)
.await
.map_err(|e| format!("serving: {e}"))
}
fn env_required(name: &str) -> Result<String, String> {
std::env::var(name).map_err(|_| format!("{name} is required (set by `kovra ui --docker`)"))
}