use hyperx::header::{Charset, ContentDisposition, DispositionParam, DispositionType}; use rocket::figment::Figment; use rocket::fs::NamedFile; use rocket::http::{Header, Status}; use rocket::outcome::Outcome; use rocket::request; use rocket::response::status::Custom; use rocket::response::{Responder, Response}; use rocket_dyn_templates::{Engines, Template}; use rocket_i18n::I18n; use rocket_prometheus::PrometheusMetrics; use gettext_macros::{compile_i18n, include_i18n}; use serde::Serialize; use std::path::PathBuf; use crate::counters; use crate::i18n::I18NHelper; use crate::i18n_helpers::describe_query_error; use crate::mail; use crate::rate_limiter::RateLimiter; use crate::template_helpers::TemplateOverrides; use crate::tokens; use crate::database::types::Fingerprint; use crate::database::{Database, KeyDatabase, Query}; use crate::Result; use std::convert::TryInto; mod debug_web; mod hkp; mod maintenance; mod manage; mod vks; mod vks_api; mod vks_web; mod wkd; use crate::web::maintenance::MaintenanceMode; pub struct HagridTemplate(&'static str, serde_json::Value, I18n, RequestOrigin); impl<'r> Responder<'r, 'static> for HagridTemplate { fn respond_to( self, req: &'r rocket::Request, ) -> std::result::Result, Status> { let HagridTemplate(tmpl, ctx, i18n, origin) = self; let template_overrides: &TemplateOverrides = req .rocket() .state() .expect("TemplateOverrides must be in managed state"); let template_override = template_overrides.get_template_override(i18n.lang, tmpl); let layout_context = templates::HagridLayout::new(ctx, i18n, origin); if let Some(template_override) = template_override { Template::render(template_override, layout_context) } else { Template::render(tmpl, layout_context) } .respond_to(req) } } #[derive(Responder)] pub enum MyResponse { #[response(status = 200, content_type = "html")] Success(HagridTemplate), #[response(status = 200, content_type = "plain")] Plain(String), #[response(status = 200, content_type = "xml")] Xml(HagridTemplate), #[response(status = 200, content_type = "application/pgp-keys")] Key(String, Header<'static>), #[response(status = 200, content_type = "application/octet-stream")] WkdKey(Vec, Header<'static>), #[response(status = 500, content_type = "html")] ServerError(Template), #[response(status = 404, content_type = "html")] NotFound(HagridTemplate), #[response(status = 404, content_type = "html")] NotFoundPlain(String), #[response(status = 400, content_type = "html")] BadRequest(HagridTemplate), #[response(status = 400, content_type = "html")] BadRequestPlain(String), #[response(status = 501, content_type = "html")] NotImplementedPlain(String), #[response(status = 503, content_type = "html")] Maintenance(Template), #[response(status = 503, content_type = "json")] MaintenanceJson(serde_json::Value), #[response(status = 503, content_type = "plain")] MaintenancePlain(String), } impl MyResponse { pub fn ok(tmpl: &'static str, ctx: impl Serialize, i18n: I18n, origin: RequestOrigin) -> Self { let context_json = serde_json::to_value(ctx).unwrap(); MyResponse::Success(HagridTemplate(tmpl, context_json, i18n, origin)) } pub fn ok_bare(tmpl: &'static str, i18n: I18n, origin: RequestOrigin) -> Self { let context_json = serde_json::to_value(templates::Bare { dummy: () }).unwrap(); MyResponse::Success(HagridTemplate(tmpl, context_json, i18n, origin)) } pub fn xml(tmpl: &'static str, i18n: I18n, origin: RequestOrigin) -> Self { let context_json = serde_json::to_value(templates::Bare { dummy: () }).unwrap(); MyResponse::Xml(HagridTemplate(tmpl, context_json, i18n, origin)) } pub fn plain(s: String) -> Self { MyResponse::Plain(s) } pub fn key(armored_key: String, fp: &Fingerprint) -> Self { let content_disposition = Header::new( rocket::http::hyper::header::CONTENT_DISPOSITION.as_str(), ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename( Charset::Us_Ascii, None, (fp.to_string() + ".asc").into_bytes(), )], } .to_string(), ); MyResponse::Key(armored_key, content_disposition) } pub fn wkd(binary_key: Vec, wkd_hash: &str) -> Self { let content_disposition = Header::new( rocket::http::hyper::header::CONTENT_DISPOSITION.as_str(), ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename( Charset::Us_Ascii, None, (wkd_hash.to_string() + ".pgp").into_bytes(), )], } .to_string(), ); MyResponse::WkdKey(binary_key, content_disposition) } pub fn ise(e: anyhow::Error) -> Self { eprintln!("Internal error: {:?}", e); let ctx = templates::FiveHundred { internal_error: e.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(), lang: "en".to_string(), }; MyResponse::ServerError(Template::render("500", ctx)) } pub fn bad_request( template: &'static str, e: anyhow::Error, i18n: I18n, origin: RequestOrigin, ) -> Self { let ctx = templates::Error { error: format!("{}", e), }; let context_json = serde_json::to_value(ctx).unwrap(); MyResponse::BadRequest(HagridTemplate(template, context_json, i18n, origin)) } pub fn bad_request_plain(message: impl Into) -> Self { MyResponse::BadRequestPlain(message.into()) } pub fn not_found_plain(message: impl Into) -> Self { MyResponse::NotFoundPlain(message.into()) } pub fn not_implemented_plain(message: impl Into) -> Self { MyResponse::NotImplementedPlain(message.into()) } pub fn not_found( tmpl: Option<&'static str>, message: impl Into>, i18n: I18n, origin: RequestOrigin, ) -> Self { let ctx = templates::Error { error: message.into().unwrap_or_else(|| "Key not found".to_owned()), }; let context_json = serde_json::to_value(ctx).unwrap(); MyResponse::NotFound(HagridTemplate( tmpl.unwrap_or("index"), context_json, i18n, origin, )) } } mod templates { use super::{I18n, RequestOrigin}; #[derive(Serialize)] pub struct FiveHundred { pub internal_error: String, pub commit: String, pub version: String, pub lang: String, } #[derive(Serialize)] pub struct HagridLayout { pub error: Option, pub commit: String, pub version: String, pub base_uri: String, pub lang: String, pub htmldir: String, pub htmlclass: String, pub page: T, } #[derive(Serialize)] pub struct Error { pub error: String, } #[derive(Serialize)] pub struct Bare { // Dummy value to make sure {{#with page}} always passes pub dummy: (), } impl HagridLayout { pub fn new(page: T, i18n: I18n, origin: RequestOrigin) -> Self { let is_rtl = (i18n.lang) == "ar"; Self { error: None, version: env!("CARGO_PKG_VERSION").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(), base_uri: origin.get_base_uri().to_string(), page, lang: i18n.lang.to_string(), htmldir: if is_rtl { "rtl".to_owned() } else { "ltr".to_owned() }, htmlclass: if is_rtl { "rtl".to_owned() } else { "".to_owned() }, } } } } pub struct HagridState { /// Assets directory, mounted to /assets, served by hagrid or nginx assets_dir: PathBuf, /// XXX base_uri: String, base_uri_onion: String, } #[derive(Debug)] pub enum RequestOrigin { Direct(String), OnionService(String), } #[async_trait] impl<'r> request::FromRequest<'r> for RequestOrigin { type Error = (); async fn from_request( request: &'r request::Request<'_>, ) -> request::Outcome { let hagrid_state = request.rocket().state::().unwrap(); let result = match request.headers().get("x-is-onion").next() { Some(_) => RequestOrigin::OnionService(hagrid_state.base_uri_onion.clone()), None => RequestOrigin::Direct(hagrid_state.base_uri.clone()), }; Outcome::Success(result) } } impl RequestOrigin { fn get_base_uri(&self) -> &str { match self { RequestOrigin::Direct(uri) => uri.as_str(), RequestOrigin::OnionService(uri) => uri.as_str(), } } } pub fn key_to_response_plain( db: &rocket::State, i18n: I18n, query: Query, ) -> MyResponse { if query.is_invalid() { return MyResponse::bad_request_plain(describe_query_error(&i18n, &query)); } let fp = if let Some(fp) = db.lookup_primary_fingerprint(&query) { fp } else { return MyResponse::not_found_plain(describe_query_error(&i18n, &query)); }; match db.by_fpr(&fp) { Some(armored) => MyResponse::key(armored, &fp), None => MyResponse::not_found_plain(describe_query_error(&i18n, &query)), } } #[get("/assets/")] async fn files(file: PathBuf, state: &rocket::State) -> Option { NamedFile::open(state.assets_dir.join(file)).await.ok() } #[get("/")] fn root(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("index", i18n, origin) } #[get("/about")] fn about(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("about/about", i18n, origin) } #[get("/about/news")] fn news(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("about/news", i18n, origin) } #[get("/atom.xml")] fn news_atom(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::xml("atom", i18n, origin) } #[get("/about/faq")] fn faq(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("about/faq", i18n, origin) } #[get("/about/usage")] fn usage(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("about/usage", i18n, origin) } #[get("/about/privacy")] fn privacy(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("about/privacy", i18n, origin) } #[get("/about/api")] fn apidoc(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("about/api", i18n, origin) } #[get("/about/stats")] fn stats(origin: RequestOrigin, i18n: I18n) -> MyResponse { MyResponse::ok_bare("about/stats", i18n, origin) } #[get("/errors//