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 endpoints = kittybox::frontend::IndiewebEndpoints { authorization_endpoint: authorization_endpoint.to_string(), token_endpoint: token_endpoint.to_string(), webmention: None, microsub: None, }; /*let micropub = warp::path("micropub") .and(warp::path::end() .and(kittybox::micropub::micropub( database.clone(), token_endpoint.to_string(), http.clone() )) .or(warp::get() .and(warp::path("client")) .and(warp::path::end()) .map(|| warp::reply::html(kittybox::MICROPUB_CLIENT)))); let static_files = warp::path("static") .and(kittybox::frontend::static_files()); let media = warp::path("media") .and(warp::path::end() .and(kittybox::media::media()) .or(kittybox::util::require_host() .and(warp::path::param()) .map(|_host: Authority, path: String| format!("media file {}", path)))); let technical = warp::path(".kittybox") .and(warp::path("onboarding") .and(warp::path::end()) .and(kittybox::frontend::onboarding( database.clone(), endpoints.clone(), http.clone() )) .or(warp::path("health") .and(warp::path::end()) .and(warp::get()) // TODO make healthcheck report on database status too .map(|| "OK")) .or(warp::path("metrics") .and(warp::path::end()) .and(warp::get()) .map(kittybox::metrics::gather)) .or(micropub) .or(media) .or(static_files) .or(warp::path("login") .and(warp::path("callback") .map(|| "callback!") // TODO form on GET and handler on POST .or(warp::path::end() .map(|| "login page!")) ) ) ); // TODO prettier error response let coffee = warp::path("coffee") .map(|| warp::reply::with_status("I'm a teapot!", warp::http::StatusCode::IM_A_TEAPOT)); et catchall = ; let app = homepage .or(technical) .or(coffee) .or(catchall) .with(warp::log("kittybox")) .with(kittybox::metrics::metrics(vec![ ".kittybox".to_string() ])) ; let svc = warp::service(app); */ 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(|| async { todo!() }), ) .route("/:filename", axum::routing::get(|| async { todo!() })), ) .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( 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); } }