about summary refs log tree commit diff
path: root/kittybox-rs/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'kittybox-rs/src/main.rs')
-rw-r--r--kittybox-rs/src/main.rs256
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);
+    }
+}