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.rs392
1 files changed, 200 insertions, 192 deletions
diff --git a/kittybox-rs/src/main.rs b/kittybox-rs/src/main.rs
index 395bb31..3136924 100644
--- a/kittybox-rs/src/main.rs
+++ b/kittybox-rs/src/main.rs
@@ -2,39 +2,88 @@ use kittybox::database::FileStorage;
 use std::{env, time::Duration};
 use tracing::{debug, error, info};
 
-#[tokio::main]
-async fn main() {
-    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
-    Registry::default()
-        .with(EnvFilter::from_default_env())
-        .with(tracing_subscriber::fmt::layer().json())
-        .init();
+fn init_media<A: kittybox::indieauth::backend::AuthBackend>(auth_backend: A, blobstore_uri: &str) -> axum::Router {
+    match blobstore_uri.split_once(':').unwrap().0 {
+        "file" => {
+            let folder = std::path::PathBuf::from(
+                blobstore_uri.strip_prefix("file://").unwrap()
+            );
+            let blobstore = kittybox::media::storage::file::FileStore::new(folder);
 
-    info!("Starting the kittybox server...");
+            kittybox::media::router::<_, _>(blobstore, auth_backend)
+        },
+        other => unimplemented!("Unsupported backend: {other}")
+    }
+}
 
-    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);
-        }
-    };
+async fn compose_kittybox_with_auth<A>(
+    http: reqwest::Client,
+    auth_backend: A,
+    backend_uri: &str,
+    blobstore_uri: &str
+) -> axum::Router
+where A: kittybox::indieauth::backend::AuthBackend
+{
+    match backend_uri.split_once(':').unwrap().0 {
+        "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 listen_addr = 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);
-        }
-    };
+            // Technically, if we don't construct the micropub router,
+            // we could use some wrapper that makes the database
+            // read-only.
+            //
+            // This would allow to exclude all code to write to the
+            // database and separate reader and writer processes of
+            // Kittybox to improve security.
+            let homepage: axum::routing::MethodRouter<_> = axum::routing::get(
+                kittybox::frontend::homepage::<FileStorage>
+            )
+                .layer(axum::Extension(database.clone()));
+            let fallback = axum::routing::get(
+                kittybox::frontend::catchall::<FileStorage>
+            )
+                .layer(axum::Extension(database.clone()));
+
+            let micropub = kittybox::micropub::router(
+                database.clone(),
+                http.clone(),
+                auth_backend.clone()
+            );
+            let onboarding = kittybox::frontend::onboarding::router(
+                database.clone(), http.clone()
+            );
+
+            axum::Router::new()
+                .route("/", homepage)
+                .fallback(fallback)
+                .route("/.kittybox/micropub", micropub)
+                .route("/.kittybox/onboarding", onboarding)
+                .nest("/.kittybox/media", init_media(auth_backend.clone(), blobstore_uri))
+                .merge(kittybox::indieauth::router(auth_backend.clone(), database.clone(), http.clone()))
+                .route(
+                    "/.kittybox/health",
+                    axum::routing::get(health_check::<kittybox::database::FileStorage>)
+                        .layer(axum::Extension(database))
+                )
+        },
+        "redis" => unimplemented!("Redis backend is not supported."),
 
+        other => unimplemented!("Unsupported backend: {other}")
+    }
+}
+
+async fn compose_kittybox(backend_uri: &str, blobstore_uri: &str, authstore_uri: &str) -> axum::Router {
     let http: reqwest::Client = {
         #[allow(unused_mut)]
         let mut builder = reqwest::Client::builder().user_agent(concat!(
@@ -48,178 +97,137 @@ async fn main() {
         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 router = match authstore_uri.split_once(':').unwrap().0 {
+        "file" => {
+            let auth_backend = {
+                let folder = authstore_uri
+                    .strip_prefix("file://")
+                    .unwrap();
+                kittybox::indieauth::backend::fs::FileBackend::new(folder)
+            };
 
-        let blobstore = {
-            let variable = std::env::var("BLOBSTORE_URI")
-                .unwrap();
-            let folder = variable
-                .strip_prefix("file://")
-                .unwrap();
-            let path = std::path::PathBuf::from(folder);
-            kittybox::media::storage::file::FileStore::new(path)
-        };
+            compose_kittybox_with_auth(http, auth_backend, backend_uri, blobstore_uri).await
+        }
+        other => unimplemented!("Unsupported backend: {other}")
+    };
 
-        let auth_backend = {
-            let variable = std::env::var("AUTH_STORE_URI")
-                .unwrap();
-            let folder = variable
-                .strip_prefix("file://")
-                .unwrap();
-            kittybox::indieauth::backend::fs::FileBackend::new(folder)
-        };
+    router
+        .route(
+            "/.kittybox/static/:path",
+            axum::routing::get(kittybox::frontend::statics)
+        )
+        .route("/.kittybox/coffee", teapot_route())
+        .nest("/.kittybox/micropub/client", kittybox::companion::router())
+        .layer(tower::ServiceBuilder::new()
+            .layer(tower_http::trace::TraceLayer::new_for_http())
+            .into_inner())
+        .layer(tower_http::catch_panic::CatchPanicLayer::new())
+}
 
+fn teapot_route() -> axum::routing::MethodRouter {
+    axum::routing::get(|| async {
+        use axum::http::{header, StatusCode};
+        (StatusCode::IM_A_TEAPOT, [(header::CONTENT_TYPE, "text/plain")], "Sorry, can't brew coffee yet!")
+    })
+}
 
-        // This code proves that different components of Kittybox can
-        // be split up without hurting the app
-        //
-        // If needed, some features could be omitted from the binary
-        // or just not spun up in the future
-        //
-        // For example, the frontend code could run spearately from
-        // Micropub and only have read access to the database folder
-        let frontend = axum::Router::new()
-            .route(
-                "/",
-                axum::routing::get(kittybox::frontend::homepage::<FileStorage>)
-                    .layer(axum::Extension(database.clone())))
-            .route("/.kittybox/static/:path", axum::routing::get(kittybox::frontend::statics))
-            .fallback(
-                axum::routing::get(kittybox::frontend::catchall::<FileStorage>)
-                    .layer(axum::Extension(database.clone())));
-
-        // Onboarding is a bit of a special case. One might argue that
-        // the onboarding makes Kittybox a monolith. This is wrong.
-        // The "onboarding receiver" doesn't need any code from the
-        // onboarding form - they're grouped in a single module for
-        // convenience only, since modifying one usually requires
-        // updating the other to match.
-        //
-        // For example, this "router" just groups two separate methods
-        // in one request, because logically they live in the same
-        // subtree. But one could manually construct only one but not
-        // the other, to receive a "frontend-only" application. Of
-        // course, in this scenario, one must employ a reverse proxy
-        // to distinguish between GET and POST requests to the same
-        // path, and route them to the correct set of endpoints with
-        // write access.
-        let onboarding = axum::Router::new()
-            .route("/.kittybox/onboarding", kittybox::frontend::onboarding::router(
-                database.clone(), http.clone()
-            ));
+async fn health_check</*A, B, */D>(
+    //axum::Extension(auth): axum::Extension<A>,
+    //axum::Extension(blob): axum::Extension<B>,
+    axum::Extension(data): axum::Extension<D>,
+) -> impl axum::response::IntoResponse
+where
+    //A: kittybox::indieauth::backend::AuthBackend,
+    //B: kittybox::media::storage::MediaStore,
+    D: kittybox::database::Storage
+{
+    (axum::http::StatusCode::OK, std::borrow::Cow::Borrowed("OK"))
+}
 
-        let micropub = axum::Router::new()
-            .route("/.kittybox/micropub", kittybox::micropub::router(
-                database.clone(),
-                http.clone(),
-                auth_backend.clone()
-            ))
-            .nest("/.kittybox/micropub/client", kittybox::companion::router());
-
-        let media = axum::Router::new()
-            .nest("/.kittybox/media", kittybox::media::router(blobstore, auth_backend.clone()));
-
-        let indieauth = kittybox::indieauth::router(auth_backend, database.clone(), http.clone());
-
-        let technical = axum::Router::new()
-            .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/health",
-                axum::routing::get(
-                    |axum::Extension(db): axum::Extension<FileStorage>| async move {
-                        // TODO health-check the database
-                        "OK"
-                    }
-                )
-                    .layer(axum::Extension(database))
-            )
-            .route(
-                "/.kittybox/metrics",
-                axum::routing::get(|| async { todo!() }),
-            );
+#[tokio::main]
+async fn main() {
+    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
+    Registry::default()
+        .with(EnvFilter::from_default_env())
+        .with(tracing_subscriber::fmt::layer().json())
+        .init();
 
-        let svc = axum::Router::new()
-            .merge(frontend)
-            .merge(onboarding)
-            .merge(micropub)
-            .merge(media)
-            .merge(indieauth)
-            .merge(technical)
-            .layer(tower::ServiceBuilder::new()
-                   .layer(tower_http::trace::TraceLayer::new_for_http())
-                   .into_inner())
-            .layer(tower_http::catch_panic::CatchPanicLayer::new());
-
-        // 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_addr).unwrap()
-            };
-            // Set the socket to non-blocking so tokio can poll it
-            // properly -- this is the async magic!
-            tcp_listener.set_nonblocking(true).unwrap();
+    info!("Starting the kittybox server...");
 
-            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()
-                }
-            });
+    let backend_uri: String = env::var("BACKEND_URI")
+        .unwrap_or_else(|_| {
+            error!("BACKEND_URI is not set, cannot find a database");
+            std::process::exit(1);
+        });
+    let blobstore_uri: String = env::var("BLOBSTORE_URI")
+        .unwrap_or_else(|_| {
+            error!("BLOBSTORE_URI is not set, can't find media store");
+            std::process::exit(1);
+        });
 
-        if let Err(err) = server.await {
-            error!("Error serving requests: {}", err);
+    let authstore_uri: String = env::var("AUTH_STORE_URI")
+        .unwrap_or_else(|_| {
+            error!("AUTH_STORE_URI is not set, can't find authentication store");
             std::process::exit(1);
-        }
-    } else {
-        println!("Unknown backend, not starting.");
+        });
+
+    let listen_addr = env::var("SERVE_AT")
+        .ok()
+        .unwrap_or_else(|| "[::]:8080".to_string())
+        .parse::<std::net::SocketAddr>()
+        .unwrap_or_else(|e| {
+            error!("Cannot parse SERVE_AT: {}", e);
+            std::process::exit(1);
+        });
+
+    let router = compose_kittybox(
+        backend_uri.as_str(),
+        blobstore_uri.as_str(),
+        authstore_uri.as_str()
+    ).await;
+
+    // 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_addr).unwrap()
+        };
+        // Set the socket to non-blocking so tokio can poll 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(router.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);
     }
 }