use log::{debug, error, info}; use std::{convert::Infallible, env, time::Duration}; use url::Url; use hyper::client::{HttpConnector,connect::dns::GaiResolver}; use hyper_rustls::HttpsConnector; use warp::{Filter, host::Authority}; #[tokio::main] async fn main() { console_subscriber::init(); // TODO json logging in the future? 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) => token_endpoint = 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) => authorization_endpoint = 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 internal_token: Option = env::var("KITTYBOX_INTERNAL_TOKEN").ok(); /*let cookie_secret: String = match env::var("COOKIE_SECRET").ok() { Some(value) => value, None => { if let Ok(filename) = env::var("COOKIE_SECRET_FILE") { use tokio::io::AsyncReadExt; let mut file = tokio::fs::File::open(filename).await.map_err(|e| { error!("Couldn't open the cookie secret file: {}", e); std::process::exit(1); }).unwrap(); let mut temp_string = String::new(); file.read_to_string(&mut temp_string).await.map_err(|e| { error!("Couldn't read the cookie secret from file: {}", e); std::process::exit(1); }).unwrap(); temp_string } else { error!("COOKIE_SECRET or COOKIE_SECRET_FILE is not set, will not be able to log in users securely!"); 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); } }; // TODO remove this and see what screams to replace it with reqwest let http_client: hyper::Client>, hyper::Body> = { let builder = hyper::Client::builder(); let https = hyper_rustls::HttpsConnectorBuilder::new() .with_webpki_roots() .https_only() .enable_http1() .enable_http2() .build(); builder.build(https) }; // 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 onboarding = warp::path("onboarding") .and(warp::path::end()) .and(kittybox::frontend::onboarding(database.clone(), endpoints.clone(), http_client.clone())); let micropub = warp::path("micropub") .and(warp::path::end() .and(kittybox::micropub::micropub( database.clone(), token_endpoint.to_string(), http_client.clone() )) .or(warp::get() .and(warp::path("client")) .and(warp::path::end()) .map(|| warp::reply::html(kittybox::MICROPUB_CLIENT)))); let media = warp::path("media") .and(warp::path::end() .and(kittybox::micropub::media::media()) .or(kittybox::util::require_host() .and(warp::path::param()) .map(|_host: Authority, path: String| format!("media file {}", path)))); // TODO remember how login logic works because I forgor let login = 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 static_files = warp::path("static") .and(kittybox::frontend::static_files()); let catchall = kittybox::frontend::catchall( database.clone(), endpoints.clone() ); let health = warp::path("health").and(warp::path::end()).map(|| "OK"); let metrics = warp::path("metrics").and(warp::path::end()).map(kittybox::metrics::gather); let app = homepage .or(onboarding) .or(metrics .or(health)) .or(static_files) .or(login) .or(coffee) .or(micropub) .or(media) .or(catchall) .with(warp::log("kittybox")) .with(kittybox::metrics::metrics(vec![ "health".to_string(), "micropub".to_string(), "static".to_string(), "media".to_string(), "metrics".to_string() ])) ; let svc = warp::service(app); 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() }; tcp_listener.set_nonblocking(true).unwrap(); info!("Listening on {}", tcp_listener.local_addr().unwrap()); let server = hyper::server::Server::from_tcp(tcp_listener) .unwrap() .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); } }