mod alerts; mod api; mod auth; mod db; mod health; mod term; mod vault; mod web; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; pub struct App { pub db: Mutex, /// Derived vault key; only present while at least one session is logged in. pub vault_key: RwLock>, /// session token -> expiry (unix seconds) pub sessions: Mutex>, /// single-use websocket tickets -> expiry pub tickets: Mutex>, pub live: RwLock>, pub login_guard: Mutex, pub http_strict: reqwest::Client, pub http_insecure: reqwest::Client, pub wake: tokio::sync::Notify, pub started: i64, } fn build_client(insecure: bool) -> reqwest::Client { reqwest::Client::builder() .user_agent(concat!("labdeck/", env!("CARGO_PKG_VERSION"))) .redirect(reqwest::redirect::Policy::limited(5)) .danger_accept_invalid_certs(insecure) .tls_info(true) .pool_max_idle_per_host(0) .build() .expect("http client") } #[tokio::main] async fn main() { let addr: SocketAddr = std::env::var("LABDECK_ADDR") .unwrap_or_else(|_| "127.0.0.1:8787".into()) .parse() .expect("LABDECK_ADDR must look like 127.0.0.1:8787"); let data_dir = std::env::var("LABDECK_DATA").unwrap_or_else(|_| "./data".into()); std::fs::create_dir_all(&data_dir).expect("cannot create data dir"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let _ = std::fs::set_permissions(&data_dir, std::fs::Permissions::from_mode(0o700)); } let db_path = format!("{}/labdeck.db", data_dir.trim_end_matches('/')); let conn = db::open(&db_path).unwrap_or_else(|e| { eprintln!("cannot open database {db_path}: {e}"); eprintln!("hint: the data directory must be writable by this process."); eprintln!(" Docker bind mount: sudo chown 10001:10001 (or use a named volume)"); std::process::exit(1); }); let app = Arc::new(App { db: Mutex::new(conn), vault_key: RwLock::new(None), sessions: Mutex::new(HashMap::new()), tickets: Mutex::new(HashMap::new()), live: RwLock::new(HashMap::new()), login_guard: Mutex::new(auth::LoginGuard::default()), http_strict: build_client(false), http_insecure: build_client(true), wake: tokio::sync::Notify::new(), started: db::now(), }); let router = api::router(app.clone()); let listener = tokio::net::TcpListener::bind(addr) .await .unwrap_or_else(|e| panic!("cannot bind {addr}: {e}")); // start background work only once we are reachable (self-checks, alerts) tokio::spawn(health::scheduler(app.clone())); tokio::spawn(auth::janitor(app.clone())); eprintln!( "labdeck {} listening on http://{} (data: {})", env!("CARGO_PKG_VERSION"), addr, db_path ); if !addr.ip().is_loopback() { eprintln!("note: not bound to loopback - put this behind HTTPS (reverse proxy / tunnel)."); } axum::serve(listener, router) .with_graceful_shutdown(async { let _ = tokio::signal::ctrl_c().await; eprintln!("shutting down"); tokio::time::sleep(Duration::from_millis(100)).await; }) .await .expect("server error"); }