use kittybox::database::FileStorage;
use std::{env, time::Duration};
use tracing::{debug, error, info};
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);
kittybox::media::router::<_, _>(blobstore, auth_backend)
},
other => unimplemented!("Unsupported backend: {other}")
}
}
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);
}
}
};
// 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."),
#[cfg(feature = "postgres")]
"postgres" => {
use kittybox::database::PostgresStorage;
let database = {
match PostgresStorage::new(backend_uri).await {
Ok(db) => db,
Err(err) => {
error!("Error creating database: {:?}", err);
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::<PostgresStorage>
)
.layer(axum::Extension(database.clone()));
let fallback = axum::routing::get(
kittybox::frontend::catchall::<PostgresStorage>
)
.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::PostgresStorage>)
.layer(axum::Extension(database))
)
},
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!(
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()
};
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)
};
compose_kittybox_with_auth(http, auth_backend, backend_uri, blobstore_uri).await
}
other => unimplemented!("Unsupported backend: {other}")
};
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!")
})
}
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"))
}
#[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())
.with(
#[cfg(debug_assertions)]
tracing_tree::HierarchicalLayer::new(2),
#[cfg(not(debug_assertions))]
tracing_subscriber::fmt::layer().json()
)
.init();
//let _ = tracing_log::LogTracer::init();
info!("Starting the kittybox server...");
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);
});
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);
});
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);
}
}