use log::{debug, error, info};
use std::{convert::Infallible, env, time::Duration};
use url::Url;
use hyper::client::{HttpConnector,connect::dns::GaiResolver};
use hyper_rustls::HttpsConnector;
use warp::{Filter, host::Authority};
#[tokio::main]
async fn main() {
console_subscriber::init();
// TODO json logging in the future?
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);
backend_uri = 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) => token_endpoint = 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) => authorization_endpoint = 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);
}
};
// TODO remove this and see what screams to replace it with reqwest
let http_client: hyper::Client<HttpsConnector<HttpConnector<GaiResolver>>, hyper::Body> = {
let builder = hyper::Client::builder();
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_webpki_roots()
.https_only()
.enable_http1()
.enable_http2()
.build();
builder.build(https)
};
// 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_client.clone()));
let micropub = warp::path("micropub")
.and(warp::path::end()
.and(kittybox::micropub::micropub(
database.clone(),
token_endpoint.to_string(),
http_client.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::micropub::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 tcp_listener: std::net::TcpListener;
let mut listenfd = listenfd::ListenFd::from_env();
if let Ok(Some(listener)) = listenfd.take_tcp_listener(0) {
tcp_listener = listener;
} else {
tcp_listener = 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);
}
}