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},
|
2021-01-11 04:09:47 +00:00
|
|
|
web, App, HttpRequest, HttpResponse, HttpServer,
|
2020-12-16 02:40:41 +00:00
|
|
|
};
|
2021-01-10 01:49:33 +00:00
|
|
|
use hyaenidae_accounts::{Auth, User};
|
2021-01-11 04:09:47 +00:00
|
|
|
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;
|
2021-01-11 04:09:47 +00:00
|
|
|
use uuid::Uuid;
|
2020-12-16 02:40:41 +00:00
|
|
|
|
|
|
|
mod accounts;
|
2021-01-06 08:21:37 +00:00
|
|
|
mod apub;
|
2021-01-12 03:59:42 +00:00
|
|
|
mod comments;
|
2020-12-16 02:40:41 +00:00
|
|
|
mod error;
|
2021-01-06 08:21:37 +00:00
|
|
|
mod images;
|
|
|
|
mod jobs;
|
2021-01-10 01:49:33 +00:00
|
|
|
mod nav;
|
2021-01-06 08:21:37 +00:00
|
|
|
mod profiles;
|
2021-01-10 01:49:33 +00:00
|
|
|
mod submissions;
|
2020-12-16 02:40:41 +00:00
|
|
|
|
2021-01-09 04:35:35 +00:00
|
|
|
use error::{Error, OptionExt};
|
2021-01-10 01:49:33 +00:00
|
|
|
use nav::NavState;
|
2021-01-11 04:09:47 +00:00
|
|
|
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()
|
2021-01-07 05:42:08 +00:00
|
|
|
.wrap(CurrentProfile(state.clone()))
|
2021-01-06 08:21:37 +00:00
|
|
|
.data(state)
|
2020-12-16 04:14:01 +00:00
|
|
|
.wrap(Auth(accounts_state.clone()))
|
2020-12-16 02:40:41 +00:00
|
|
|
.data(accounts_state)
|
|
|
|
.data(SystemTime::now())
|
2021-01-11 04:09:47 +00:00
|
|
|
.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())
|
2021-01-10 01:49:33 +00:00
|
|
|
.service(submissions::scope())
|
2021-01-12 03:59:42 +00:00
|
|
|
.service(comments::scope())
|
2021-01-09 04:35:35 +00:00
|
|
|
.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()
|
|
|
|
}
|
|
|
|
|
2021-01-11 04:09:47 +00:00
|
|
|
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(
|
2021-01-11 04:09:47 +00:00
|
|
|
req: HttpRequest,
|
|
|
|
page: Option<web::Query<SubmissionPage>>,
|
2021-01-08 04:44:43 +00:00
|
|
|
user: Option<User>,
|
|
|
|
profile: Option<Profile>,
|
2021-01-10 01:49:33 +00:00
|
|
|
nav_state: NavState,
|
2021-01-11 04:09:47 +00:00
|
|
|
state: web::Data<State>,
|
2021-01-09 04:35:35 +00:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2021-01-08 04:44:43 +00:00
|
|
|
if user.is_some() && profile.is_none() {
|
2021-01-11 04:09:47 +00:00
|
|
|
return Ok(profiles::to_change_profile_page());
|
2021-01-08 04:44:43 +00:00
|
|
|
}
|
2021-01-11 04:09:47 +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| {
|
2021-01-11 04:09:47 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-01-11 04:09:47 +00:00
|
|
|
async fn not_found(nav_state: NavState) -> Result<HttpResponse, Error> {
|
2020-12-16 02:40:41 +00:00
|
|
|
rendered(HttpResponse::NotFound(), |cursor| {
|
2021-01-11 04:09:47 +00:00
|
|
|
templates::not_found(cursor, nav_state.dark())
|
2020-12-16 02:40:41 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-11 04:09:47 +00:00
|
|
|
async fn serve_error(nav_state: NavState) -> Result<HttpResponse, Error> {
|
2020-12-16 02:40:41 +00:00
|
|
|
rendered(HttpResponse::InternalServerError(), |cursor| {
|
2021-01-11 04:09:47 +00:00
|
|
|
templates::error(
|
|
|
|
cursor,
|
|
|
|
"Hyaenidae encountered a problem".to_owned(),
|
|
|
|
nav_state.dark(),
|
|
|
|
)
|
2021-01-08 04:44:43 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-10 01:49:33 +00:00
|
|
|
async fn settings(nav_state: NavState) -> Result<HttpResponse, Error> {
|
2021-01-08 04:44:43 +00:00
|
|
|
rendered(HttpResponse::Ok(), |cursor| {
|
2021-01-10 01:49:33 +00:00
|
|
|
templates::settings(cursor, &nav_state)
|
2021-01-08 04:44:43 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-12 03:59:42 +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,
|
2021-01-12 03:59:42 +00:00
|
|
|
f: impl FnOnce(&mut MinifyWriter) -> std::io::Result<()>,
|
2020-12-16 02:40:41 +00:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2021-01-12 03:59:42 +00:00
|
|
|
MINIFIER.with(|writer| {
|
|
|
|
writer.borrow_mut().reset();
|
|
|
|
(f)(&mut *writer.borrow_mut()).map_err(Error::Render)?;
|
2020-12-16 02:40:41 +00:00
|
|
|
|
2021-01-12 03:59:42 +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
|
|
|
}
|