hyaenidae/server/src/main.rs

316 lines
8.7 KiB
Rust
Raw Normal View History

2020-12-16 02:40:41 +00:00
use actix_web::{
2021-01-06 08:21:37 +00:00
client::Client,
2020-12-16 02:40:41 +00:00
dev::HttpResponseBuilder,
http::header::{CacheControl, CacheDirective, ContentType, LastModified},
middleware::{Compress, Logger},
web, App, HttpResponse, HttpServer,
};
use hyaenidae_accounts::{Auth, Authenticated};
2020-12-16 02:40:41 +00:00
use sled::Db;
use std::{fmt, time::SystemTime};
2021-01-06 08:21:37 +00:00
use structopt::StructOpt;
2020-12-16 02:40:41 +00:00
mod accounts;
2021-01-06 08:21:37 +00:00
mod apub;
2020-12-16 02:40:41 +00:00
mod error;
2021-01-06 08:21:37 +00:00
mod images;
mod jobs;
mod profiles;
2020-12-16 02:40:41 +00:00
2021-01-06 08:21:37 +00:00
use error::{Error, OptionExt, ResultExt, StateError};
2020-12-16 02:40:41 +00:00
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<()> {
2021-01-06 08:21:37 +00:00
let config = Config::from_args();
2020-12-16 02:40:41 +00:00
if std::env::var("RUST_LOG").is_err() {
2021-01-06 08:21:37 +00:00
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");
}
2020-12-16 02:40:41 +00:00
}
env_logger::init();
2021-01-06 08:21:37 +00:00
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<u8> = bytes.into();
let secret_key_str = base64::encode(&secret_key);
log::warn!("No secret key set! Generated '{}'", secret_key_str);
secret_key
};
2020-12-16 02:40:41 +00:00
let accounts_config = hyaenidae_accounts::Config {
toolkit_path: format!(
"/toolkit/{}",
hyaenidae_toolkit::templates::statics::toolkit_css.name
),
2021-01-06 08:21:37 +00:00
domain: config
.base_url
.domain()
.expect("Invalid domain for base url")
.to_owned(),
key: secret_key,
2020-12-16 02:40:41 +00:00
https: false,
pages: std::sync::Arc::new(accounts::Pages),
};
2021-01-06 08:21:37 +00:00
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())?;
2020-12-16 02:40:41 +00:00
2021-01-06 08:21:37 +00:00
let config_clone = config.clone();
2020-12-16 02:40:41 +00:00
HttpServer::new(move || {
2021-01-06 08:21:37 +00:00
let config = config_clone.clone();
let state = State::new(
spawner.clone(),
config.base_url,
config.pictrs_upstream,
&db.clone(),
)
.unwrap();
2020-12-16 02:40:41 +00:00
let accounts_config = accounts_config.clone();
let accounts_state = accounts_state.clone();
App::new()
.wrap(Logger::default())
.wrap(Compress::default())
2021-01-06 08:21:37 +00:00
.data(state)
.wrap(Auth(accounts_state.clone()))
2020-12-16 02:40:41 +00:00
.data(accounts_state)
.data(SystemTime::now())
.wrap(hyaenidae_accounts::cookie_middlware(&accounts_config))
.route("/", web::get().to(home))
.route("/404", web::get().to(not_found))
.route("/500", web::get().to(serve_error))
.route("/toolkit/{name}", web::get().to(toolkit))
.route("/static/{name}", web::get().to(statics))
.service(accounts::scope())
2021-01-06 08:21:37 +00:00
.service(images::scope())
.service(apub::scope())
.service(profiles::scope())
2020-12-16 02:40:41 +00:00
})
2021-01-06 08:21:37 +00:00
.bind(config.bind_address)?
2020-12-16 02:40:41 +00:00
.run()
.await?;
Ok(())
}
2021-01-06 08:21:37 +00:00
#[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<SecretKey>,
#[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<u8>,
}
impl std::str::FromStr for SecretKey {
type Err = base64::DecodeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(SecretKey {
inner: base64::decode(s)?,
})
}
}
2020-12-16 02:40:41 +00:00
#[derive(Clone)]
struct State {
2021-01-06 08:21:37 +00:00
profiles: hyaenidae_profiles::State,
spawn: jobs::Spawn,
apub: apub::Apub,
images: images::Images,
domain: String,
base_url: url::Url,
client: Client,
2020-12-16 02:40:41 +00:00
db: Db,
}
impl State {
2021-01-06 08:21:37 +00:00
fn new(
spawn: jobs::Spawn,
base_url: url::Url,
pict_rs_upstream: url::Url,
db: &Db,
) -> Result<Self, Error> {
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(),
})
2020-12-16 02:40:41 +00:00
}
}
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<String>, startup: web::Data<SystemTime>) -> 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<String>, startup: web::Data<SystemTime>) -> 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(
state: web::Data<State>,
authenticated: Option<Authenticated>,
logout_args: Option<hyaenidae_accounts::LogoutPageArgs>,
) -> Result<HttpResponse, StateError> {
let logout_opt = logout_args.map(|args| hyaenidae_accounts::logout_page(args));
let authenticated_opt = authenticated.and_then(|a| logout_opt.map(|l| (a.user(), l)));
rendered(HttpResponse::Ok(), |cursor| {
templates::index(cursor, authenticated_opt)
})
.state(&state)
}
2021-01-06 08:21:37 +00:00
fn to_404() -> HttpResponse {
HttpResponse::SeeOther().header("Location", "/404").finish()
}
2020-12-16 02:40:41 +00:00
async fn not_found(state: web::Data<State>) -> Result<HttpResponse, StateError> {
rendered(HttpResponse::NotFound(), |cursor| {
templates::not_found(cursor)
})
.state(&state)
}
async fn serve_error(state: web::Data<State>) -> Result<HttpResponse, StateError> {
rendered(HttpResponse::InternalServerError(), |cursor| {
templates::error(cursor, "Hyaenidae encountered a problem".to_owned())
})
.state(&state)
}
fn rendered(
mut builder: HttpResponseBuilder,
f: impl FnOnce(&mut std::io::Cursor<Vec<u8>>) -> std::io::Result<()>,
) -> Result<HttpResponse, Error> {
let mut cursor = std::io::Cursor::new(vec![]);
(f)(&mut cursor).map_err(Error::Render)?;
let mut html = cursor.into_inner();
let len = minify_html::in_place(&mut html, &minify_html::Cfg { minify_js: false })?;
html.truncate(len);
Ok(builder
.content_type(mime::TEXT_HTML.essence_str())
.body(html))
}