diff options
Diffstat (limited to 'kittybox-rs')
-rw-r--r-- | kittybox-rs/src/indieauth/mod.rs | 1 | ||||
-rw-r--r-- | kittybox-rs/src/lib.rs | 1 | ||||
-rw-r--r-- | kittybox-rs/src/main.rs | 392 | ||||
-rw-r--r-- | kittybox-rs/src/media/mod.rs | 1 | ||||
-rw-r--r-- | kittybox-rs/src/micropub/mod.rs | 1 |
5 files changed, 204 insertions, 192 deletions
diff --git a/kittybox-rs/src/indieauth/mod.rs b/kittybox-rs/src/indieauth/mod.rs index cf3278a..a86100d 100644 --- a/kittybox-rs/src/indieauth/mod.rs +++ b/kittybox-rs/src/indieauth/mod.rs @@ -789,6 +789,7 @@ async fn userinfo_endpoint_get<A: AuthBackend, D: Storage + 'static>( } } +#[must_use] pub fn router<A: AuthBackend, D: Storage + 'static>(backend: A, db: D, http: reqwest::Client) -> axum::Router { use axum::routing::{Router, get, post}; diff --git a/kittybox-rs/src/lib.rs b/kittybox-rs/src/lib.rs index 05c1ed2..3d5638a 100644 --- a/kittybox-rs/src/lib.rs +++ b/kittybox-rs/src/lib.rs @@ -50,6 +50,7 @@ pub mod companion { } } + #[must_use] pub fn router() -> axum::Router { let resources: ResourceTable = { let mut map = HashMap::new(); 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); } } diff --git a/kittybox-rs/src/media/mod.rs b/kittybox-rs/src/media/mod.rs index ce08704..297184a 100644 --- a/kittybox-rs/src/media/mod.rs +++ b/kittybox-rs/src/media/mod.rs @@ -131,6 +131,7 @@ pub(crate) async fn serve<S: MediaStore>( } } +#[must_use] pub fn router<S: MediaStore, A: AuthBackend>(blobstore: S, auth: A) -> axum::Router { axum::Router::new() .route("/", axum::routing::post(upload::<S, A>)) diff --git a/kittybox-rs/src/micropub/mod.rs b/kittybox-rs/src/micropub/mod.rs index 7ef55ab..ee4d334 100644 --- a/kittybox-rs/src/micropub/mod.rs +++ b/kittybox-rs/src/micropub/mod.rs @@ -594,6 +594,7 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>( } } +#[must_use] pub fn router<S, A>( storage: S, http: reqwest::Client, |