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,
|
|
|
|
};
|
2020-12-16 04:14:01 +00:00
|
|
|
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)
|
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())
|
|
|
|
.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))
|
|
|
|
}
|