diff options
Diffstat (limited to 'kittybox-rs/src/main.rs')
-rw-r--r-- | kittybox-rs/src/main.rs | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/kittybox-rs/src/main.rs b/kittybox-rs/src/main.rs new file mode 100644 index 0000000..eb70885 --- /dev/null +++ b/kittybox-rs/src/main.rs @@ -0,0 +1,256 @@ +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 internal_token: Option<String> = 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::<std::net::SocketAddr>() { + 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 onboarding = warp::path("onboarding") + .and(warp::path::end()) + .and(kittybox::frontend::onboarding( + database.clone(), + endpoints.clone(), + http.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 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)))); + + // 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); + } +} |