use rocket; use rocket::http::{Header, Status}; use rocket::request; use rocket::outcome::Outcome; use rocket::response::NamedFile; use rocket::config::Config; use rocket_contrib::templates::Template; use rocket::http::uri::Uri; use rocket_contrib::json::JsonValue; use rocket::response::status::Custom; 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::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; #[derive(Responder)] pub enum MyResponse { #[response(status = 200, content_type = "html")] Success(Template), #[response(status = 200, content_type = "plain")] Plain(String), #[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(Template), #[response(status = 404, content_type = "html")] NotFoundPlain(String), #[response(status = 400, content_type = "html")] BadRequest(Template), #[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: S) -> Self { MyResponse::Success(Template::render(tmpl, ctx)) } 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: failure::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(), }; MyResponse::ServerError(Template::render("500", ctx)) } pub fn bad_request(template: &'static str, e: failure::Error) -> Self { let ctx = templates::General { error: Some(format!("{}", e)), version: env!("VERGEN_SEMVER").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(), }; MyResponse::BadRequest(Template::render(template, ctx)) } 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 { MyResponse::NotFound( Template::render( tmpl.unwrap_or("index"), templates::General::new( Some(message.into() .unwrap_or_else(|| "Key not found".to_owned()))))) } } mod templates { #[derive(Serialize)] pub struct FiveHundred { pub internal_error: String, pub commit: String, pub version: String, } #[derive(Serialize)] pub struct General { pub error: Option, pub commit: String, pub version: String, } #[derive(Serialize)] pub struct About { pub base_uri: String, pub commit: String, pub version: String, } impl About { pub fn new(base_uri: impl Into) -> Self { Self { base_uri: base_uri.into(), version: env!("VERGEN_SEMVER").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(), } } } impl General { pub fn new(error: Option) -> Self { Self { error: error, version: env!("VERGEN_SEMVER").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(), } } } impl Default for General { fn default() -> Self { Self::new(None) } } } 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() -> Template { Template::render("index", templates::General::default()) } #[get("/about")] fn about() -> Template { Template::render("about/about", templates::General::default()) } #[get("/about/news")] fn news() -> Template { Template::render("about/news", templates::General::default()) } #[get("/about/faq")] fn faq() -> Template { Template::render("about/faq", templates::General::default()) } #[get("/about/usage")] fn usage(state: rocket::State) -> Template { Template::render("about/usage", templates::About::new(state.base_uri.clone())) } #[get("/about/privacy")] fn privacy() -> Template { Template::render("about/privacy", templates::General::default()) } #[get("/about/api")] fn apidoc() -> Template { Template::render("about/api", templates::General::default()) } #[get("/about/stats")] fn stats() -> Template { Template::render("about/stats", templates::General::default()) } #[get("/errors//