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();
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 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);
}
};
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 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)
};
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)
};
// 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()
));
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!() }),
);
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();
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()
}
});
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);
}
}