use log::{debug, error, info}; use std::{convert::Infallible, env, time::Duration}; use url::Url; use warp::{Filter, host::Authority}; #[tokio::main] async fn main() { // TODO turn into a feature so I can enable and disable it #[cfg(debug_assertions)] console_subscriber::init(); // TODO use tracing instead of log let logger_env = env_logger::Env::new().filter_or("RUST_LOG", "info"); env_logger::init_from_env(logger_env); 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); } }; // This thing handles redirects automatically but is type-incompatible with hyper::Client // Bonus: less generics to be aware of, this thing hides its complexity 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 homepage = warp::get() .and(warp::path::end()) .and(kittybox::frontend::homepage(database.clone(), endpoints.clone())); 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)); let catchall = kittybox::frontend::catchall( database.clone(), endpoints.clone() ); 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); // A little dance to turn a potential file descriptor into an async network socket let mut listenfd = listenfd::ListenFd::from_env(); let tcp_listener: std::net::TcpListener = 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(); 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(hyper::service::make_service_fn(move |_| { let service = svc.clone(); async move { Ok::<_, Infallible>(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); } }