//! Oxygenhub: a forge for Oxygen repositories (`docs/spec.md`). //! //! One process: an axum router serving pages, the JSON API and the wire //! protocol; SQLite for hub state; repositories as bare Oxygen stores. pub mod api; pub mod auth; pub mod config; pub mod db; pub mod events; pub mod hooks; pub mod keys; pub mod mirror; pub mod notify; pub mod protocol; pub mod render; pub mod repos; pub mod web; use crate::config::Config; use crate::repos::Repos; use std::sync::Arc; /// Everything a request handler can reach. pub struct App { pub config: Config, pub db: db::Pool, pub repos: Arc, pub server: Arc, pub hub_key: keys::HubKey, pub cookie_key: auth::CookieKey, pub events: events::Events, pub started: std::time::Instant, } pub type Shared = Arc; /// Open the data directory and build the application state. `init` must /// have run once; `serve` and the tests call this. pub async fn build(config: Config) -> anyhow::Result { std::fs::create_dir_all(config.repos_dir())?; let db = db::open(&config.db_path()).await?; let repos = Arc::new(Repos::new(config.repos_dir())); let hub_key = keys::ensure_identity(&config.keys_dir(), &config.site.name, config.base_url())?; let cookie_key = auth::CookieKey::load_or_create(&config.keys_dir())?; let backend = protocol::HubBackend::new(repos.clone(), db.clone(), config.auth.push_tokens.clone(), tokio::runtime::Handle::current()); let slot = backend.app_slot(); let server = oxy_server::Server::new(backend); let app = Arc::new(App { config, db, repos, server, hub_key, cookie_key, events: events::Events::default(), started: std::time::Instant::now(), }); *slot.lock().unwrap() = Some(Arc::downgrade(&app)); tokio::spawn(hooks::worker(app.clone())); Ok(app) } /// The full router: pages, API, protocol and static assets. pub fn router(app: Shared) -> axum::Router { web::router(app) } /// Serve on the configured addresses until the process is told to stop. pub async fn serve(app: Shared) -> anyhow::Result<()> { let router = router(app.clone()); let http = app.config.bind_addr()?; let https = app.config.tls_bind_addr()?; if http.is_none() && https.is_none() { anyhow::bail!("nothing to bind: set server.bind or server.tls_bind"); } let mut tasks = tokio::task::JoinSet::new(); if let Some(addr) = http { let listener = tokio::net::TcpListener::bind(addr).await?; tracing::info!("http on {addr}"); let r = router.clone(); tasks.spawn(async move { axum::serve(listener, r).await.map_err(anyhow::Error::from) }); } if let Some(addr) = https { let _ = rustls::crypto::ring::default_provider().install_default(); let tls = axum_server::tls_rustls::RustlsConfig::from_pem_file(app.config.tls_cert(), app.config.tls_key()) .await .map_err(|e| anyhow::anyhow!("loading TLS files: {e}"))?; tracing::info!("https on {addr}"); let r = router.clone(); tasks.spawn(async move { axum_server::bind_rustls(addr, tls).serve(r.into_make_service()).await.map_err(anyhow::Error::from) }); } tokio::select! { r = tasks.join_next() => { r.transpose()?.transpose()?; Ok(()) } _ = tokio::signal::ctrl_c() => { tracing::info!("stopping"); Ok(()) } } }