use actix_web::{ client::Client, dev::HttpResponseBuilder, http::header::{CacheControl, CacheDirective, ContentType, LastModified}, middleware::{Compress, Logger}, web, App, HttpResponse, HttpServer, }; use hyaenidae_accounts::{Auth, LogoutState, User}; use sled::Db; use std::{fmt, time::SystemTime}; use structopt::StructOpt; mod accounts; mod apub; mod back; mod error; mod images; mod jobs; mod profiles; use back::{Back, BackPage}; use error::{Error, OptionExt}; use profiles::{CurrentProfile, Profile}; include!(concat!(env!("OUT_DIR"), "/templates.rs")); const HOURS: u32 = 60 * 60; const DAYS: u32 = 24 * HOURS; #[actix_web::main] async fn main() -> anyhow::Result<()> { let config = Config::from_args(); if std::env::var("RUST_LOG").is_err() { if config.debug { std::env::set_var("RUST_LOG", "hyaenidae_profiles=debug,hyaenidae_accounts=debug,hyaenidae_toolkit=debug,hyaenidae_server=debug,info"); } else { std::env::set_var("RUST_LOG", "info"); } } env_logger::init(); let secret_key = if let Some(secret_key) = &config.secret_key { secret_key.inner.to_owned() } else { use rand::{thread_rng, RngCore}; let mut bytes = [0u8; 64]; thread_rng().fill_bytes(&mut bytes); let secret_key: Vec = bytes.into(); let secret_key_str = base64::encode(&secret_key); log::warn!("No secret key set! Generated '{}'", secret_key_str); secret_key }; let accounts_config = hyaenidae_accounts::Config { toolkit_path: format!( "/toolkit/{}", hyaenidae_toolkit::templates::statics::toolkit_css.name ), domain: config .base_url .domain() .expect("Invalid domain for base url") .to_owned(), key: secret_key, https: false, pages: std::sync::Arc::new(accounts::Pages), }; let db = sled::open(&config.sled_path)?; let spawner = jobs::build( config.base_url.clone(), config.pictrs_upstream.clone(), db.clone(), )?; let accounts_state = hyaenidae_accounts::state(&accounts_config, db.clone())?; let config_clone = config.clone(); HttpServer::new(move || { let config = config_clone.clone(); let state = State::new( spawner.clone(), config.base_url, config.pictrs_upstream, &db.clone(), ) .unwrap(); let accounts_config = accounts_config.clone(); let accounts_state = accounts_state.clone(); App::new() .wrap(BackPage) .wrap(CurrentProfile(state.clone())) .data(state) .wrap(Auth(accounts_state.clone())) .data(accounts_state) .data(SystemTime::now()) .wrap(hyaenidae_accounts::cookie_middlware(&accounts_config)) .wrap(Compress::default()) .wrap(Logger::default()) .route("/", web::get().to(home)) .route("/nav", web::get().to(nav_page)) .route("/404", web::get().to(not_found)) .route("/500", web::get().to(serve_error)) .route("/settings", web::get().to(settings)) .route("/toolkit/{name}", web::get().to(toolkit)) .route("/static/{name}", web::get().to(statics)) .service(accounts::scope()) .service(images::scope()) .service(apub::scope()) .service(profiles::scope()) .default_service(web::route().to(|| async move { to_404() })) }) .bind(config.bind_address)? .run() .await?; Ok(()) } #[derive(Clone, StructOpt)] struct Config { #[structopt( short, long, default_value = "0.0.0.0:8085", env = "HYAENIDAE_BIND_ADDR" )] bind_address: std::net::SocketAddr, #[structopt( short = "u", long, default_value = "http://localhost:8085", env = "HYAENIDAE_BASE_URL" )] base_url: url::Url, #[structopt( short, long, default_value = "http://localhost:8080", env = "HYAENIDAE_PICTRS_UPSTREAM" )] pictrs_upstream: url::Url, #[structopt( short, long, default_value = "./sled/db-0.34", env = "HYAENIDAE_SLED_PATH" )] sled_path: std::path::PathBuf, #[structopt(long, env = "HYAENIDAE_SECRET_KEY")] secret_key: Option, #[structopt( long, env = "HYAENIDAE_SKIP_SIGNATURE_VALIDATION", about = "don't validate HTTP Signatures on requests" )] skip_signature_validation: bool, #[structopt(short, long, about = "enable debug logging")] debug: bool, } #[derive(Clone)] struct SecretKey { inner: Vec, } impl std::str::FromStr for SecretKey { type Err = base64::DecodeError; fn from_str(s: &str) -> Result { Ok(SecretKey { inner: base64::decode(s)?, }) } } #[derive(Clone)] struct State { profiles: hyaenidae_profiles::State, spawn: jobs::Spawn, apub: apub::Apub, images: images::Images, domain: String, base_url: url::Url, client: Client, db: Db, } impl State { fn new( spawn: jobs::Spawn, base_url: url::Url, pict_rs_upstream: url::Url, db: &Db, ) -> Result { let client = Client::builder() .header("User-Agent", "hyaenidae-0.1.0") .finish(); let images = images::Images::new(base_url.clone()); let apub = apub::Apub::build(base_url.clone(), db)?; let domain = base_url.domain().req()?.to_owned(); Ok(State { profiles: hyaenidae_profiles::State::build( pict_rs_upstream, images.clone(), apub.clone(), spawn.clone(), client.clone(), db.clone(), )?, spawn, apub, images, base_url, domain, client, db: db.clone(), }) } } impl fmt::Debug for State { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("State").field("db", &"Db").finish() } } fn toolkit_path(name: &str) -> String { format!("/toolkit/{}", name) } fn statics_path(name: &str) -> String { format!("/static/{}", name) } async fn toolkit(path: web::Path, startup: web::Data) -> HttpResponse { if let Some(file) = hyaenidae_toolkit::templates::statics::StaticFile::get(&path) { return HttpResponse::Ok() .set(LastModified(SystemTime::clone(&startup).into())) .set(CacheControl(vec![ CacheDirective::Public, CacheDirective::MaxAge(365 * DAYS), CacheDirective::Extension("immutable".to_owned(), None), ])) .set(ContentType(file.mime.clone())) .body(file.content); } HttpResponse::NotFound().finish() } async fn statics(path: web::Path, startup: web::Data) -> HttpResponse { if let Some(file) = templates::statics::StaticFile::get(&path) { return HttpResponse::Ok() .set(LastModified(SystemTime::clone(&startup).into())) .set(CacheControl(vec![ CacheDirective::Public, CacheDirective::MaxAge(365 * DAYS), CacheDirective::Extension("immutable".to_owned(), None), ])) .set(ContentType(file.mime.clone())) .body(file.content); } HttpResponse::NotFound().finish() } async fn home( user: Option, profile: Option, logout: Option, ) -> Result { if user.is_some() && profile.is_none() { return Ok(profiles::to_create()); } rendered(HttpResponse::Ok(), |cursor| { templates::index(cursor, &logout, &profile) }) } fn to_404() -> HttpResponse { redirect("/404") } fn to_home() -> HttpResponse { redirect("/") } fn redirect(path: &str) -> HttpResponse { HttpResponse::SeeOther().header("Location", path).finish() } async fn not_found() -> Result { rendered(HttpResponse::NotFound(), |cursor| { templates::not_found(cursor) }) } async fn serve_error() -> Result { rendered(HttpResponse::InternalServerError(), |cursor| { templates::error(cursor, "Hyaenidae encountered a problem".to_owned()) }) } async fn nav_page(logout: Option, back: Back) -> Result { rendered(HttpResponse::Ok(), |cursor| { templates::nav::page(cursor, &logout, &back) }) } async fn settings(logout: LogoutState) -> Result { rendered(HttpResponse::Ok(), |cursor| { templates::settings(cursor, logout) }) } fn rendered( mut builder: HttpResponseBuilder, f: impl FnOnce(&mut std::io::Cursor<&mut Vec>) -> std::io::Result<()>, ) -> Result { let mut bytes = vec![]; (f)(&mut std::io::Cursor::new(&mut bytes)).map_err(Error::Render)?; minify_html::truncate( &mut bytes, &minify_html::Cfg { minify_js: false, minify_css: false, }, )?; Ok(builder .content_type(mime::TEXT_HTML.essence_str()) .body(bytes)) }