use rocket; use rocket::http::{Header, Status}; use rocket::request; use rocket::outcome::Outcome; use rocket::response::{NamedFile, Responder, Response}; use rocket::config::Config; use rocket_contrib::templates::{Template, Engines}; use rocket::http::uri::Uri; use rocket_contrib::json::JsonValue; use rocket::response::status::Custom; use rocket_i18n::I18n; use rocket_prometheus::PrometheusMetrics; use gettext_macros::{compile_i18n, include_i18n}; use serde::Serialize; use std::path::PathBuf; use crate::mail; use crate::tokens; use crate::counters; use crate::template_helpers::TemplateOverrides; use crate::i18n::I18NHelper; use crate::rate_limiter::RateLimiter; use crate::database::{Database, KeyDatabase, Query}; use crate::database::types::Fingerprint; use crate::Result; use std::convert::TryInto; mod hkp; mod manage; mod maintenance; mod vks; mod vks_web; mod vks_api; mod debug_web; use crate::web::maintenance::MaintenanceMode; use rocket::http::hyper::header::ContentDisposition; pub struct HagridTemplate(&'static str, serde_json::Value); impl Responder<'static> for HagridTemplate { fn respond_to(self, req: &rocket::Request) -> std::result::Result, Status> { let HagridTemplate(tmpl, ctx) = self; let i18n: I18n = req.guard().expect("Error parsing language"); let template_overrides: rocket::State = req.guard().expect("TemplateOverrides must be in managed state"); let template_override = template_overrides.get_template_override(i18n.lang, tmpl); let origin: RequestOrigin = req.guard().expect("Error determining request origin"); 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, ContentDisposition), #[response(status = 200, content_type = "application/pgp-keys")] XAccelRedirect(&'static str, Header<'static>, ContentDisposition), #[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 = 503, content_type = "html")] Maintenance(Template), #[response(status = 503, content_type = "json")] MaintenanceJson(JsonValue), #[response(status = 503, content_type = "plain")] MaintenancePlain(String), } impl MyResponse { pub fn ok(tmpl: &'static str, ctx: impl Serialize) -> Self { let context_json = serde_json::to_value(ctx).unwrap(); MyResponse::Success(HagridTemplate(tmpl, context_json)) } pub fn ok_bare(tmpl: &'static str) -> Self { let context_json = serde_json::to_value(templates::Bare { dummy: () }).unwrap(); MyResponse::Success(HagridTemplate(tmpl, context_json)) } pub fn xml(tmpl: &'static str) -> Self { let context_json = serde_json::to_value(templates::Bare { dummy: () }).unwrap(); MyResponse::Xml(HagridTemplate(tmpl, context_json)) } pub fn plain(s: String) -> Self { MyResponse::Plain(s) } pub fn key(armored_key: String, fp: &Fingerprint) -> Self { use rocket::http::hyper::header::{DispositionType, DispositionParam, Charset}; MyResponse::Key( armored_key, ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![ DispositionParam::Filename( Charset::Us_Ascii, None, (fp.to_string() + ".asc").into_bytes()), ], }) } pub fn x_accel_redirect(x_accel_path: String, fp: &Fingerprint) -> Self { use rocket::http::hyper::header::{DispositionType, DispositionParam, Charset}; // nginx expects percent-encoded URIs let x_accel_path = Uri::percent_encode(&x_accel_path).into_owned(); MyResponse::XAccelRedirect( "", Header::new("X-Accel-Redirect", x_accel_path), ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![ DispositionParam::Filename( Charset::Us_Ascii, None, (fp.to_string() + ".asc").into_bytes()), ], }) } pub fn ise(e: anyhow::Error) -> Self { eprintln!("Internal error: {:?}", e); let ctx = templates::FiveHundred { internal_error: e.to_string(), version: env!("VERGEN_SEMVER").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) -> Self { let ctx = templates::Error { error: format!("{}", e) }; let context_json = serde_json::to_value(ctx).unwrap(); MyResponse::BadRequest(HagridTemplate(template, context_json)) } 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_found( tmpl: Option<&'static str>, message: impl Into>, ) -> 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)) } } 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 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 { Self { error: None, version: env!("VERGEN_SEMVER").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(), base_uri: origin.get_base_uri().to_string(), page: page, lang: i18n.lang.to_string(), } } } } pub struct HagridState { /// Assets directory, mounted to /assets, served by hagrid or nginx assets_dir: PathBuf, /// The keys directory, where keys are located, served by hagrid or nginx keys_external_dir: PathBuf, /// XXX base_uri: String, base_uri_onion: String, /// x_accel_redirect: bool, x_accel_prefix: Option, } #[derive(Debug)] pub enum RequestOrigin { Direct(String), OnionService(String), } impl<'a, 'r> request::FromRequest<'a, 'r> for RequestOrigin { type Error = (); fn from_request(request: &'a request::Request<'r>) -> request::Outcome { let hagrid_state = request.guard::>().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( state: rocket::State, db: rocket::State, query: Query, ) -> MyResponse { let fp = if let Some(fp) = db.lookup_primary_fingerprint(&query) { fp } else { return MyResponse::not_found_plain(query.describe_error()); }; if state.x_accel_redirect { if let Some(key_path) = db.lookup_path(&query) { let mut x_accel_path = state.keys_external_dir.join(&key_path); if let Some(prefix) = state.x_accel_prefix.as_ref() { x_accel_path = x_accel_path.strip_prefix(&prefix).unwrap().to_path_buf(); } // prepend a / to make path relative to nginx root let x_accel_path = format!("/{}", x_accel_path.to_string_lossy()); return MyResponse::x_accel_redirect(x_accel_path, &fp); } } return match db.by_fpr(&fp) { Some(armored) => MyResponse::key(armored, &fp.into()), None => MyResponse::not_found_plain(query.describe_error()), } } #[get("/assets/")] fn files(file: PathBuf, state: rocket::State) -> Option { NamedFile::open(state.assets_dir.join(file)).ok() } #[get("/")] fn root() -> MyResponse { MyResponse::ok_bare("index") } #[get("/about")] fn about() -> MyResponse { MyResponse::ok_bare("about/about") } #[get("/about/news")] fn news() -> MyResponse { MyResponse::ok_bare("about/news") } #[get("/atom.xml")] fn news_atom() -> MyResponse { MyResponse::xml("atom") } #[get("/about/faq")] fn faq() -> MyResponse { MyResponse::ok_bare("about/faq") } #[get("/about/usage")] fn usage() -> MyResponse { MyResponse::ok_bare("about/usage") } #[get("/about/privacy")] fn privacy() -> MyResponse { MyResponse::ok_bare("about/privacy") } #[get("/about/api")] fn apidoc() -> MyResponse { MyResponse::ok_bare("about/api") } #[get("/about/stats")] fn stats() -> MyResponse { MyResponse::ok_bare("about/stats") } #[get("/errors//