104 lines
3.4 KiB
Rust
104 lines
3.4 KiB
Rust
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<rusqlite::Connection>,
|
|
/// Derived vault key; only present while at least one session is logged in.
|
|
pub vault_key: RwLock<Option<vault::VaultKey>>,
|
|
/// session token -> expiry (unix seconds)
|
|
pub sessions: Mutex<HashMap<String, i64>>,
|
|
/// single-use websocket tickets -> expiry
|
|
pub tickets: Mutex<HashMap<String, i64>>,
|
|
pub live: RwLock<HashMap<i64, health::Live>>,
|
|
pub login_guard: Mutex<auth::LoginGuard>,
|
|
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 <dir> (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");
|
|
}
|