use kittybox::database::FileStorage; use std::{env, time::Duration}; use tracing::{debug, error, info}; use url::Url; #[tokio::main] async fn main() { // TODO use tracing instead of log use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; Registry::default() .with(EnvFilter::from_default_env()) .with(tracing_subscriber::fmt::layer().json()) .init(); info!("Starting the kittybox server..."); let backend_uri: String = match env::var("BACKEND_URI") { Ok(val) => { debug!("Backend URI: {}", val); val } Err(_) => { error!("BACKEND_URI is not set, cannot find a database"); std::process::exit(1); } }; let token_endpoint: Url = match env::var("TOKEN_ENDPOINT") { Ok(val) => { debug!("Token endpoint: {}", val); match Url::parse(&val) { Ok(val) => val, _ => { error!("Token endpoint URL cannot be parsed, aborting."); std::process::exit(1) } } } Err(_) => { error!("TOKEN_ENDPOINT is not set, will not be able to authorize users!"); std::process::exit(1) } }; let authorization_endpoint: Url = match env::var("AUTHORIZATION_ENDPOINT") { Ok(val) => { debug!("Auth endpoint: {}", val); match Url::parse(&val) { Ok(val) => val, _ => { error!("Authorization endpoint URL cannot be parsed, aborting."); std::process::exit(1) } } } Err(_) => { error!("AUTHORIZATION_ENDPOINT is not set, will not be able to confirm token and ID requests using IndieAuth!"); std::process::exit(1) } }; let listen_at = match env::var("SERVE_AT") .ok() .unwrap_or_else(|| "[::]:8080".to_string()) .parse::() { Ok(addr) => addr, Err(e) => { error!("Cannot parse SERVE_AT: {}", e); std::process::exit(1); } }; let http: reqwest::Client = { #[allow(unused_mut)] let mut builder = reqwest::Client::builder().user_agent(concat!( env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION") )); // TODO: add a root certificate if there's an environment variable pointing at it //builder = builder.add_root_certificate(reqwest::Certificate::from_pem(todo!())); builder.build().unwrap() }; if backend_uri.starts_with("redis") { println!("The Redis backend is deprecated."); std::process::exit(1); } else if backend_uri.starts_with("file") { let database = { let folder = backend_uri.strip_prefix("file://").unwrap(); let path = std::path::PathBuf::from(folder); match kittybox::database::FileStorage::new(path).await { Ok(db) => db, Err(err) => { error!("Error creating database: {:?}", err); std::process::exit(1); } } }; let blobstore = { let variable = std::env::var("BLOBSTORE_URI") .unwrap(); let folder = variable .strip_prefix("file://") .unwrap(); let path = std::path::PathBuf::from(folder); kittybox::media::storage::file::FileStore::new(path) }; let svc = axum::Router::new() .route( "/", axum::routing::get(kittybox::frontend::homepage::), ) .route( "/.kittybox/coffee", axum::routing::get(|| async { use axum::http::{header, StatusCode}; ( StatusCode::IM_A_TEAPOT, [(header::CONTENT_TYPE, "text/plain")], "Sorry, can't brew coffee yet!", ) }), ) .route( "/.kittybox/onboarding", axum::routing::get(kittybox::frontend::onboarding::get) .post(kittybox::frontend::onboarding::post::), ) .route( "/.kittybox/micropub", axum::routing::get(kittybox::micropub::query::) .post(kittybox::micropub::post::) .layer( tower_http::cors::CorsLayer::new() .allow_methods([axum::http::Method::GET, axum::http::Method::POST]) .allow_origin(tower_http::cors::Any), ), ) .route( "/.kittybox/micropub/client", axum::routing::get(|| { std::future::ready(axum::response::Html(kittybox::MICROPUB_CLIENT)) }), ) .route( "/.kittybox/health", axum::routing::get(|| async { // TODO health-check the database "OK" }), ) .route( "/.kittybox/metrics", axum::routing::get(|| async { todo!() }), ) .nest( "/.kittybox/media", axum::Router::new() .route( "/", axum::routing::get(|| async { todo!() }) .post( kittybox::media::upload:: ), ) .route("/uploads/*file", axum::routing::get( kittybox::media::serve:: )), ) .route( "/.kittybox/static/:path", axum::routing::get(kittybox::frontend::statics), ) .fallback(axum::routing::get( kittybox::frontend::catchall::, )) .layer(axum::Extension(database)) .layer(axum::Extension(http)) .layer(axum::Extension(kittybox::indieauth::TokenEndpoint( token_endpoint, ))) .layer(axum::Extension(blobstore)) .layer( tower::ServiceBuilder::new() .layer(tower_http::trace::TraceLayer::new_for_http()) .into_inner(), ); // A little dance to turn a potential file descriptor into a guaranteed async network socket let tcp_listener: std::net::TcpListener = { let mut listenfd = listenfd::ListenFd::from_env(); let tcp_listener = if let Ok(Some(listener)) = listenfd.take_tcp_listener(0) { listener } else { std::net::TcpListener::bind(listen_at).unwrap() }; // Set the socket to non-blocking so tokio can work with it properly // This is the async magic tcp_listener.set_nonblocking(true).unwrap(); tcp_listener }; info!("Listening on {}", tcp_listener.local_addr().unwrap()); let server = hyper::server::Server::from_tcp(tcp_listener) .unwrap() // Otherwise Chrome keeps connections open for too long .tcp_keepalive(Some(Duration::from_secs(30 * 60))) .serve(svc.into_make_service()) .with_graceful_shutdown(async move { // Defer to C-c handler whenever we're not on Unix // TODO consider using a diverging future here #[cfg(not(unix))] return tokio::signal::ctrl_c().await.unwrap(); #[cfg(unix)] { use tokio::signal::unix::{signal, SignalKind}; signal(SignalKind::terminate()) .unwrap() .recv() .await .unwrap() } }); if let Err(err) = server.await { error!("Error serving requests: {}", err); std::process::exit(1); } } else { println!("Unknown backend, not starting."); std::process::exit(1); } }