hyaenidae/server/src/main.rs

426 lines
11 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, HttpRequest, HttpResponse, HttpServer,
2020-12-16 02:40:41 +00:00
};
use hyaenidae_accounts::{Auth, User};
use hyaenidae_toolkit::Button;
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;
use uuid::Uuid;
2020-12-16 02:40:41 +00:00
mod accounts;
2021-01-06 08:21:37 +00:00
mod apub;
mod comments;
2020-12-16 02:40:41 +00:00
mod error;
2021-01-06 08:21:37 +00:00
mod images;
mod jobs;
mod nav;
2021-01-06 08:21:37 +00:00
mod profiles;
mod submissions;
2020-12-16 02:40:41 +00:00
use error::{Error, OptionExt};
use nav::NavState;
use profiles::{CurrentProfile, Profile, SubmissionPage, SubmissionView};
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(CurrentProfile(state.clone()))
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_middleware(&accounts_config))
2021-01-08 04:44:43 +00:00
.wrap(Compress::default())
.wrap(Logger::default())
2020-12-16 02:40:41 +00:00
.route("/", web::get().to(home))
.route("/404", web::get().to(not_found))
.route("/500", web::get().to(serve_error))
2021-01-08 04:44:43 +00:00
.route("/settings", web::get().to(settings))
2020-12-16 02:40:41 +00:00
.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())
.service(submissions::scope())
.service(comments::scope())
.default_service(web::route().to(|| async move { to_404() }))
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()
}
pub struct HomeView {
pub(crate) submissions: Vec<SubmissionView>,
submission_nav: Vec<Button>,
pub(crate) viewer: Option<Uuid>,
}
impl HomeView {
pub(crate) fn nav(&self) -> Vec<&Button> {
self.submission_nav.iter().collect()
}
}
fn body_class(dark: bool) -> &'static str {
if dark {
"toolkit-dark"
} else {
""
}
}
2020-12-16 02:40:41 +00:00
async fn home(
req: HttpRequest,
page: Option<web::Query<SubmissionPage>>,
2021-01-08 04:44:43 +00:00
user: Option<User>,
profile: Option<Profile>,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
2021-01-08 04:44:43 +00:00
if user.is_some() && profile.is_none() {
return Ok(profiles::to_change_profile_page());
2021-01-08 04:44:43 +00:00
}
let viewer = profile.map(|p| p.id());
let submission_pages = profiles::build_submissions(
req.uri().path(),
false,
None,
viewer,
page.map(|q| q.into_inner()),
6,
nav_state.dark(),
&state,
);
let view = HomeView {
submissions: submission_pages.submissions,
submission_nav: submission_pages.nav,
viewer,
};
2020-12-16 02:40:41 +00:00
rendered(HttpResponse::Ok(), |cursor| {
templates::index(cursor, &view, &nav_state)
2020-12-16 02:40:41 +00:00
})
}
2021-01-06 08:21:37 +00:00
fn to_404() -> HttpResponse {
2021-01-08 04:44:43 +00:00
redirect("/404")
}
fn to_home() -> HttpResponse {
redirect("/")
}
fn redirect(path: &str) -> HttpResponse {
HttpResponse::SeeOther().header("Location", path).finish()
2021-01-06 08:21:37 +00:00
}
async fn not_found(nav_state: NavState) -> Result<HttpResponse, Error> {
2020-12-16 02:40:41 +00:00
rendered(HttpResponse::NotFound(), |cursor| {
templates::not_found(cursor, nav_state.dark())
2020-12-16 02:40:41 +00:00
})
}
async fn serve_error(nav_state: NavState) -> Result<HttpResponse, Error> {
2020-12-16 02:40:41 +00:00
rendered(HttpResponse::InternalServerError(), |cursor| {
templates::error(
cursor,
"Hyaenidae encountered a problem".to_owned(),
nav_state.dark(),
)
2021-01-08 04:44:43 +00:00
})
}
async fn settings(nav_state: NavState) -> Result<HttpResponse, Error> {
2021-01-08 04:44:43 +00:00
rendered(HttpResponse::Ok(), |cursor| {
templates::settings(cursor, &nav_state)
2021-01-08 04:44:43 +00:00
})
}
struct MinifyWriter {
inner: html_minifier::HTMLMinifier,
}
impl MinifyWriter {
fn new() -> Self {
let mut minifier = html_minifier::HTMLMinifier::new();
minifier.set_remove_comments(true);
minifier.set_minify_code(false);
MinifyWriter { inner: minifier }
}
fn reset(&mut self) {
self.inner.reset();
}
fn get_html(&mut self) -> Vec<u8> {
self.inner.get_html().to_vec()
}
}
impl std::io::Write for MinifyWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let len = buf.len();
self.inner
.digest(buf)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
.map(|_| len)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
use once_cell::unsync::Lazy;
use std::cell::RefCell;
thread_local! {
static MINIFIER: RefCell<Lazy<MinifyWriter>> = RefCell::new(Lazy::new(|| MinifyWriter::new()));
}
2020-12-16 02:40:41 +00:00
fn rendered(
mut builder: HttpResponseBuilder,
f: impl FnOnce(&mut MinifyWriter) -> std::io::Result<()>,
2020-12-16 02:40:41 +00:00
) -> Result<HttpResponse, Error> {
MINIFIER.with(|writer| {
writer.borrow_mut().reset();
(f)(&mut *writer.borrow_mut()).map_err(Error::Render)?;
2020-12-16 02:40:41 +00:00
Ok(builder
.content_type(mime::TEXT_HTML.essence_str())
.body(writer.borrow_mut().get_html().to_vec()))
})
2020-12-16 02:40:41 +00:00
}