use rocket; use rocket::http::Header; use rocket::response::NamedFile; use rocket::config::Config; use rocket_contrib::templates::Template; use rocket::http::uri::Uri; use serde::Serialize; use handlebars::Handlebars; use std::path::PathBuf; pub mod upload; use mail; use tokens; use rate_limiter::RateLimiter; use database::{Database, KeyDatabase, Query}; use database::types::{Email, Fingerprint, KeyID}; use Result; use std::str::FromStr; use std::convert::TryInto; mod hkp; mod manage; mod maintenance; use 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 = 400, content_type = "html")] BadRequest(Template), } 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::{ContentDisposition, 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::{ContentDisposition, 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 { println!("Internal error: {:?}", e); let ctx = templates::FiveHundred{ error: format!("{}", e), 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 not_found(tmpl: Option<&'static str>, message: M) -> Self where M: Into>, { 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 Search { pub query: String, pub gpg_options: Option<&'static str>, pub fpr: String, pub base_uri: String, pub commit: String, pub version: String, } #[derive(Serialize)] pub struct FiveHundred { pub error: String, pub commit: String, pub version: String, } #[derive(Serialize)] pub struct General { pub error: Option, pub commit: String, pub version: 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, /// x_accel_redirect: bool, x_accel_prefix: Option, } fn key_to_response<'a>(state: rocket::State, db: rocket::State, query_string: String, query: Query, machine_readable: bool) -> MyResponse { let fp = if let Some(fp) = db.lookup_primary_fingerprint(&query) { fp } else { return MyResponse::not_found(None, None); }; if machine_readable { 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(None, None), } } let has_uids = match key_has_uids(&state, &db, &query) { Ok(x) => x, Err(e) => return MyResponse::ise(e), }; let context = templates::Search{ query: query_string, gpg_options: if has_uids { None } else { Some("--keyserver-options import-drop-uids ") }, base_uri: state.base_uri.clone(), fpr: fp.to_string(), version: env!("VERGEN_SEMVER").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(), }; MyResponse::ok("found", context) } fn key_has_uids(state: &HagridState, db: &KeyDatabase, query: &Query) -> Result { use sequoia_openpgp::Packet; use sequoia_openpgp::parse::{Parse, PacketParser, PacketParserResult}; let mut ppr = match db.lookup_path(query) { Some(path) => PacketParser::from_file(&state.keys_external_dir.join(path))?, None => return Err(failure::err_msg("key vanished")), }; while let PacketParserResult::Some(pp) = ppr { if let Packet::UserID(_) = pp.packet { return Ok(true); } ppr = pp.recurse()?.1; } Ok(false) } #[get("/vks/v1/by-fingerprint/")] fn vks_v1_by_fingerprint(state: rocket::State, db: rocket::State, fpr: String) -> MyResponse { let query = match Fingerprint::from_str(&fpr) { Ok(fpr) => Query::ByFingerprint(fpr), Err(e) => return MyResponse::bad_request("index", e), }; key_to_response(state, db, fpr, query, true) } #[get("/vks/v1/by-email/")] fn vks_v1_by_email(state: rocket::State, db: rocket::State, email: String) -> MyResponse { let query = match Email::from_str(&email) { Ok(email) => Query::ByEmail(email), Err(e) => return MyResponse::bad_request("index", e), }; key_to_response(state, db, email, query, true) } #[get("/vks/v1/by-keyid/")] fn vks_v1_by_keyid(state: rocket::State, db: rocket::State, kid: String) -> MyResponse { let query = match KeyID::from_str(&kid) { Ok(keyid) => Query::ByKeyID(keyid), Err(e) => return MyResponse::bad_request("index", e), }; key_to_response(state, db, kid, query, true) } #[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/privacy")] fn privacy() -> Template { Template::render("about/privacy", templates::General::default()) } #[get("/about/api")] fn apidoc() -> Template { Template::render("about/api", templates::General::default()) } pub fn serve() -> Result<()> { Err(rocket_factory(rocket::ignite())?.launch().into()) } fn rocket_factory(rocket: rocket::Rocket) -> Result { let routes = routes![ // infra root, about, privacy, apidoc, files, // VKSv1 vks_v1_by_email, vks_v1_by_fingerprint, vks_v1_by_keyid, upload::vks_v1_publish_post, // User interaction. upload::publish, upload::publish_verify, upload::vks_publish_verify, // HKP hkp::pks_lookup, hkp::pks_add, // EManage manage::vks_manage, manage::vks_manage_key, manage::vks_manage_post, manage::vks_manage_unpublish, // Maintenance error page maintenance::maintenance_error, ]; let db_service = configure_db_service(rocket.config())?; let hagrid_state = configure_hagrid_state(rocket.config())?; let stateful_token_service = configure_stateful_token_service(rocket.config())?; let stateless_token_service = configure_stateless_token_service(rocket.config())?; let mail_service = configure_mail_service(rocket.config())?; let rate_limiter = configure_rate_limiter(rocket.config())?; let maintenance_mode = configure_maintenance_mode(rocket.config())?; Ok(rocket .attach(Template::fairing()) .attach(maintenance_mode) .manage(hagrid_state) .manage(stateless_token_service) .manage(stateful_token_service) .manage(mail_service) .manage(db_service) .manage(rate_limiter) .mount("/", routes) ) } fn configure_db_service(config: &Config) -> Result { let keys_internal_dir: PathBuf = config.get_str("keys_internal_dir")?.into(); let keys_external_dir: PathBuf = config.get_str("keys_external_dir")?.into(); let tmp_dir: PathBuf = config.get_str("tmp_dir")?.into(); let fs_db = KeyDatabase::new(keys_internal_dir, keys_external_dir, tmp_dir)?; Ok(fs_db) } fn configure_hagrid_state(config: &Config) -> Result { let assets_dir: PathBuf = config.get_str("assets_dir")?.into(); let keys_external_dir: PathBuf = config.get_str("keys_external_dir")?.into(); let x_accel_prefix: Option = config.get_string("x_accel_prefix").map(|prefix| prefix.into()).ok(); // State let base_uri = config.get_str("base-URI")?.to_string(); Ok(HagridState { assets_dir, keys_external_dir: keys_external_dir, base_uri: base_uri.clone(), x_accel_redirect: config.get_bool("x-accel-redirect")?, x_accel_prefix, }) } fn configure_stateful_token_service(config: &Config) -> Result { let token_dir: PathBuf = config.get_str("token_dir")?.into(); database::StatefulTokens::new(token_dir) } fn configure_stateless_token_service(config: &Config) -> Result { use std::convert::TryFrom; let secret = config.get_str("token_secret")?.to_string(); let validity = config.get_int("token_validity")?; let validity = u64::try_from(validity)?; Ok(tokens::Service::init(&secret, validity)) } fn configure_mail_service(config: &Config) -> Result { // Mail service let template_dir: PathBuf = config.get_str("template_dir")?.into(); let base_uri = config.get_str("base-URI")?.to_string(); let from = config.get_str("from")?.to_string(); let verify_html = template_dir.join("email/publish-html.hbs"); let verify_txt = template_dir.join("email/publish-txt.hbs"); let manage_html = template_dir.join("email/manage-html.hbs"); let manage_txt = template_dir.join("email/manage-txt.hbs"); let mut handlebars = Handlebars::new(); handlebars.register_template_file("verify-html", verify_html)?; handlebars.register_template_file("verify-txt", verify_txt)?; handlebars.register_template_file("manage-html", manage_html)?; handlebars.register_template_file("manage-txt", manage_txt)?; let filemail_into = config.get_str("filemail_into") .ok().map(|p| PathBuf::from(p)); if let Some(path) = filemail_into { mail::Service::filemail(from, base_uri, handlebars, path) } else { mail::Service::sendmail(from, base_uri, handlebars) } } fn configure_rate_limiter(config: &Config) -> Result { let timeout_secs = config.get_int("mail_rate_limit").unwrap_or(60); let timeout_secs = timeout_secs.try_into()?; Ok(RateLimiter::new(timeout_secs)) } fn configure_maintenance_mode(config: &Config) -> Result { let maintenance_file: PathBuf = config.get_str("maintenance_file") .unwrap_or("maintenance").into(); Ok(MaintenanceMode::new(maintenance_file)) } #[cfg(test)] pub mod tests { use regex; use std::fs; use std::path::Path; use tempfile::{tempdir, TempDir}; use super::rocket; use rocket::local::{Client, LocalResponse}; use rocket::http::Status; use rocket::http::ContentType; use lettre::{SendableEmail, SimpleSendableEmail}; use sequoia_openpgp::TPK; use sequoia_openpgp::tpk::TPKBuilder; use sequoia_openpgp::parse::Parse; use sequoia_openpgp::serialize::Serialize; use database::*; use super::*; /// Fake base URI to use in tests. const BASE_URI: &'static str = "http://local.connection"; /// Creates a configuration and empty state dir for testing purposes. /// /// Note that you need to keep the returned TempDir alive for the /// duration of your test. To debug the test, mem::forget it to /// prevent cleanup. pub fn configuration() -> Result<(TempDir, rocket::Config)> { use rocket::config::{Config, Environment}; let root = tempdir()?; let filemail = root.path().join("filemail"); ::std::fs::create_dir_all(&filemail)?; let base_dir: PathBuf = root.path().into(); let config = Config::build(Environment::Staging) .root(root.path().to_path_buf()) .extra("template_dir", ::std::env::current_dir().unwrap().join("dist/templates") .to_str().unwrap()) .extra("assets_dir", ::std::env::current_dir().unwrap().join("dist/assets") .to_str().unwrap()) .extra("keys_internal_dir", base_dir.join("keys_internal").to_str().unwrap()) .extra("keys_external_dir", base_dir.join("keys_external").to_str().unwrap()) .extra("tmp_dir", base_dir.join("tmp").to_str().unwrap()) .extra("token_dir", base_dir.join("tokens").to_str().unwrap()) .extra("base-URI", BASE_URI) .extra("from", "from") .extra("token_secret", "hagrid") .extra("token_validity", 3600) .extra("filemail_into", filemail.into_os_string().into_string() .expect("path is valid UTF8")) .extra("x-accel-redirect", false) .finalize()?; Ok((root, config)) } pub fn client() -> Result<(TempDir, Client)> { let (tmpdir, config) = configuration()?; let rocket = rocket_factory(rocket::custom(config))?; Ok((tmpdir, Client::new(rocket)?)) } pub fn assert_consistency(rocket: &rocket::Rocket) { let db = rocket.state::().unwrap(); db.check_consistency().unwrap(); } #[test] fn basics() { let (_tmpdir, config) = configuration().unwrap(); let rocket = rocket_factory(rocket::custom(config)).unwrap(); let client = Client::new(rocket).expect("valid rocket instance"); // Check that we see the landing page. let mut response = client.get("/").dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::HTML)); assert!(response.body_string().unwrap().contains("Hagrid")); // Check that we see the privacy policy. let mut response = client.get("/about").dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::HTML)); assert!(response.body_string().unwrap().contains("distribution and discovery")); // Check that we see the privacy policy. let mut response = client.get("/about/privacy").dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::HTML)); assert!(response.body_string().unwrap().contains("Public Key Data")); // Check that we see the API docs. let mut response = client.get("/about/api").dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::HTML)); assert!(response.body_string().unwrap().contains("/vks/v1/by-keyid")); // Check that we see the upload form. let mut response = client.get("/publish").dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::HTML)); assert!(response.body_string().unwrap().contains("upload")); // Check that we see the deletion form. let mut response = client.get("/manage").dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::HTML)); assert!(response.body_string().unwrap().contains("any verified address")); assert_consistency(client.rocket()); } #[test] fn upload_verify_single() { let (tmpdir, client) = client().unwrap(); let filemail_into = tmpdir.path().join("filemail"); // Generate a key and upload it. let (tpk, _) = TPKBuilder::autocrypt( None, Some("foo@invalid.example.com".into())) .generate().unwrap(); let mut tpk_serialized = Vec::new(); tpk.serialize(&mut tpk_serialized).unwrap(); let token = vks_publish_submit_get_token(&client, &tpk_serialized); // Prior to email confirmation, we should not be able to look // it up by email address. check_null_responses_by_email(&client, "foo@invalid.example.com"); // And check that we can get it back via the machine readable // interface. check_mr_responses_by_fingerprint(&client, &tpk, 0); // And check that we can see the human-readable result page. check_hr_responses_by_fingerprint(&client, &tpk, 0); // Check the verification link check_verify_link(&client, &token, "foo@invalid.example.com"); // Now check for the verification mail. check_mails_and_verify_email(&client, filemail_into.as_path()); // Now lookups using the mail address should work. check_responses_by_email(&client, "foo@invalid.example.com", &tpk, 1); // And check that we can see the human-readable result page. check_hr_responses_by_fingerprint(&client, &tpk, 1); // Request deletion of the binding. vks_manage(&client, "foo@invalid.example.com"); // Confirm deletion. check_mails_and_confirm_deletion(&client, filemail_into.as_path(), "foo@invalid.example.com"); // Now, we should no longer be able to look it up by email // address. check_null_responses_by_email(&client, "foo@invalid.example.com"); // But lookup by fingerprint should still work. check_mr_responses_by_fingerprint(&client, &tpk, 0); // And check that we can see the human-readable result page. check_hr_responses_by_fingerprint(&client, &tpk, 0); assert_consistency(client.rocket()); } #[test] fn upload_two() { let (_tmpdir, config) = configuration().unwrap(); let rocket = rocket_factory(rocket::custom(config)).unwrap(); let client = Client::new(rocket).expect("valid rocket instance"); // Generate two keys and upload them. let tpk_0 = TPKBuilder::autocrypt( None, Some("foo@invalid.example.com".into())) .generate().unwrap().0; let tpk_1 = TPKBuilder::autocrypt( None, Some("bar@invalid.example.com".into())) .generate().unwrap().0; let mut tpk_serialized = Vec::new(); tpk_0.serialize(&mut tpk_serialized).unwrap(); tpk_1.serialize(&mut tpk_serialized).unwrap(); vks_publish_submit_multiple(&client, &tpk_serialized); // Prior to email confirmation, we should not be able to look // them up by email address. check_null_responses_by_email(&client, "foo@invalid.example.com"); check_null_responses_by_email(&client, "bar@invalid.example.com"); // And check that we can get them back via the machine readable // interface. check_mr_responses_by_fingerprint(&client, &tpk_0, 0); check_mr_responses_by_fingerprint(&client, &tpk_1, 0); // And check that we can see the human-readable result page. check_hr_responses_by_fingerprint(&client, &tpk_0, 0); check_hr_responses_by_fingerprint(&client, &tpk_1, 0); } #[test] fn upload_verify_two() { let (tmpdir, config) = configuration().unwrap(); let filemail_into = tmpdir.path().join("filemail"); let rocket = rocket_factory(rocket::custom(config)).unwrap(); let client = Client::new(rocket).expect("valid rocket instance"); // Generate two keys and upload them. let tpk_1 = TPKBuilder::autocrypt( None, Some("foo@invalid.example.com".into())) .generate().unwrap().0; let tpk_2 = TPKBuilder::autocrypt( None, Some("bar@invalid.example.com".into())) .generate().unwrap().0; let mut tpk_serialized_1 = Vec::new(); tpk_1.serialize(&mut tpk_serialized_1).unwrap(); let token_1 = vks_publish_submit_get_token(&client, &tpk_serialized_1); let mut tpk_serialized_2 = Vec::new(); tpk_2.serialize(&mut tpk_serialized_2).unwrap(); let token_2 = vks_publish_submit_get_token(&client, &tpk_serialized_2); // Prior to email confirmation, we should not be able to look // them up by email address. check_null_responses_by_email(&client, "foo@invalid.example.com"); check_null_responses_by_email(&client, "bar@invalid.example.com"); // And check that we can get them back via the machine readable // interface. check_mr_responses_by_fingerprint(&client, &tpk_1, 0); check_mr_responses_by_fingerprint(&client, &tpk_2, 0); // And check that we can see the human-readable result page. check_hr_responses_by_fingerprint(&client, &tpk_1, 0); check_hr_responses_by_fingerprint(&client, &tpk_2, 0); // Check the verification link check_verify_link(&client, &token_1, "foo@invalid.example.com"); check_verify_link(&client, &token_2, "bar@invalid.example.com"); // Now check for the verification mails. check_mails_and_verify_email(&client, &filemail_into); check_mails_and_verify_email(&client, &filemail_into); // Now lookups using the mail address should work. check_responses_by_email(&client, "foo@invalid.example.com", &tpk_1, 1); check_responses_by_email(&client, "bar@invalid.example.com", &tpk_2, 1); // Request deletion of the bindings. vks_manage(&client, "foo@invalid.example.com"); check_mails_and_confirm_deletion(&client, &filemail_into, "foo@invalid.example.com"); vks_manage(&client, "bar@invalid.example.com"); check_mails_and_confirm_deletion(&client, &filemail_into, "bar@invalid.example.com"); // Now, we should no longer be able to look it up by email // address. check_null_responses_by_email(&client, "foo@invalid.example.com"); check_null_responses_by_email(&client, "bar@invalid.example.com"); // But lookup by fingerprint should still work. check_mr_responses_by_fingerprint(&client, &tpk_1, 0); check_mr_responses_by_fingerprint(&client, &tpk_2, 0); // And check that we can see the human-readable result page. check_hr_responses_by_fingerprint(&client, &tpk_1, 0); check_hr_responses_by_fingerprint(&client, &tpk_2, 0); assert_consistency(client.rocket()); } #[test] fn upload_no_key() { let (_tmpdir, client) = client().unwrap(); let response = vks_publish_submit_response(&client, b""); assert_eq!(response.status(), Status::BadRequest); } /// Asserts that the given URI 404s. pub fn check_null_response(client: &Client, uri: &str) { let response = client.get(uri).dispatch(); assert_eq!(response.status(), Status::NotFound); } /// Asserts that lookups by the given email 404. pub fn check_null_responses_by_email(client: &Client, addr: &str) { check_null_response( &client, &format!("/vks/v1/by-email/{}", addr)); check_null_response( &client, &format!("/pks/lookup?op=get&search={}", addr)); check_null_response( &client, &format!("/pks/lookup?op=get&options=mr&search={}", addr)); } /// Asserts that lookups by the given email are successful. pub fn check_responses_by_email(client: &Client, addr: &str, tpk: &TPK, nr_uids: usize) { check_mr_response( &client, &format!("/vks/v1/by-email/{}", addr), &tpk, nr_uids); check_mr_response( &client, &format!("/vks/v1/by-email/{}", addr.replace("@", "%40")), &tpk, nr_uids); check_mr_response( &client, &format!("/pks/lookup?op=get&options=mr&search={}", addr), &tpk, nr_uids); check_hr_response( &client, &format!("/pks/lookup?op=get&search={}", addr), &tpk, nr_uids); } /// Asserts that the given URI returns a TPK matching the given /// one, with the given number of userids. pub fn check_mr_response(client: &Client, uri: &str, tpk: &TPK, nr_uids: usize) { let mut response = client.get(uri).dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::new("application", "pgp-keys"))); let body = response.body_string().unwrap(); assert!(body.contains("END PGP PUBLIC KEY BLOCK")); let tpk_ = TPK::from_bytes(body.as_bytes()).unwrap(); assert_eq!(tpk.fingerprint(), tpk_.fingerprint()); assert_eq!(tpk.subkeys().map(|skb| skb.subkey().fingerprint()) .collect::>(), tpk_.subkeys().map(|skb| skb.subkey().fingerprint()) .collect::>()); assert_eq!(tpk_.userids().count(), nr_uids); } /// Asserts that we can get the given TPK back using the various /// by-fingerprint or by-keyid lookup mechanisms. pub fn check_mr_responses_by_fingerprint(client: &Client, tpk: &TPK, nr_uids: usize) { let fp = tpk.fingerprint().to_hex(); let keyid = tpk.fingerprint().to_keyid().to_hex(); check_mr_response( &client, &format!("/vks/v1/by-keyid/{}", keyid), &tpk, nr_uids); check_mr_response( &client, &format!("/vks/v1/by-fingerprint/{}", fp), &tpk, nr_uids); check_mr_response( &client, &format!("/pks/lookup?op=get&options=mr&search={}", fp), &tpk, nr_uids); check_mr_response( &client, &format!("/pks/lookup?op=get&options=mr&search=0x{}", fp), &tpk, nr_uids); check_mr_response( &client, &format!("/pks/lookup?op=get&options=mr&search={}", keyid), &tpk, nr_uids); check_mr_response( &client, &format!("/pks/lookup?op=get&options=mr&search=0x{}", keyid), &tpk, nr_uids); } /// Asserts that the given URI returns human readable response /// page that contains a URI pointing to the TPK. pub fn check_hr_response(client: &Client, uri: &str, tpk: &TPK, nr_uids: usize) { let mut response = client.get(uri).dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.content_type(), Some(ContentType::HTML)); let body = response.body_string().unwrap(); assert!(body.contains("found")); assert!(body.contains(&tpk.fingerprint().to_hex())); // Extract the links. let link_re = regex::Regex::new( &format!("{}(/vks/[^ \t\n\"<]*)", BASE_URI)).unwrap(); let mut n = 0; for link in link_re.captures_iter(&body) { check_mr_response(client, link.get(1).unwrap().as_str(), tpk, nr_uids); n += 1; } assert!(n > 0); } /// Asserts that we can get the given TPK back using the various /// by-fingerprint or by-keyid lookup mechanisms. pub fn check_hr_responses_by_fingerprint(client: &Client, tpk: &TPK, nr_uids: usize) { let fp = tpk.fingerprint().to_hex(); let keyid = tpk.fingerprint().to_keyid().to_hex(); check_hr_response( &client, &format!("/pks/lookup?op=get&search={}", fp), &tpk, nr_uids); check_hr_response( &client, &format!("/pks/lookup?op=get&search=0x{}", fp), &tpk, nr_uids); check_hr_response( &client, &format!("/pks/lookup?op=get&search={}", keyid), &tpk, nr_uids); check_hr_response( &client, &format!("/pks/lookup?op=get&search=0x{}", keyid), &tpk, nr_uids); } fn check_verify_link(client: &Client, token: &str, address: &str) { let encoded = ::url::form_urlencoded::Serializer::new(String::new()) .append_pair("token", token) .append_pair("address", address) .finish(); let response = client.post("/publish/verify") .header(ContentType::Form) .body(encoded.as_bytes()) .dispatch(); assert_eq!(response.status(), Status::Ok); } fn check_mails_and_verify_email(client: &Client, filemail_path: &Path) { let pattern = format!("{}(/publish/[^ \t\n]*)", BASE_URI); let confirm_uri = pop_mail_capture_pattern(filemail_path, &pattern); let response = client.get(&confirm_uri).dispatch(); assert_eq!(response.status(), Status::Ok); } fn check_mails_and_confirm_deletion(client: &Client, filemail_path: &Path, address: &str) { let pattern = format!("{}/manage/([^ \t\n]*)", BASE_URI); let token = pop_mail_capture_pattern(filemail_path, &pattern); vks_manage_delete(client, &token, address); } fn pop_mail_capture_pattern(filemail_path: &Path, pattern: &str) -> String { let mail_message = pop_mail(filemail_path).unwrap().unwrap(); let mail_content = mail_message.message(); let capture_re = regex::bytes::Regex::new(pattern).unwrap(); let capture_content = capture_re .captures(&mail_content).unwrap() .get(1).unwrap().as_bytes(); String::from_utf8_lossy(capture_content).to_string() } /// Returns and removes the first mail it finds from the given /// directory. pub fn pop_mail(dir: &Path) -> Result> { for entry in fs::read_dir(dir)? { let entry = entry?; if entry.file_type()?.is_file() { let fh = fs::File::open(entry.path())?; fs::remove_file(entry.path())?; let mail: SimpleSendableEmail = ::serde_json::from_reader(fh)?; return Ok(Some(mail)); } } Ok(None) } fn vks_publish_submit_multiple<'a>(client: &'a Client, data: &[u8]) { let mut response = vks_publish_submit_response(client, data); let response_body = response.body_string().unwrap(); assert_eq!(response.status(), Status::Ok); assert!(response_body.contains("you must upload them individually")); } fn vks_publish_submit_get_token<'a>(client: &'a Client, data: &[u8]) -> String { let mut response = vks_publish_submit_response(client, data); let response_body = response.body_string().unwrap(); let pattern = "name=\"token\" value=\"([^\"]*)\""; let capture_re = regex::bytes::Regex::new(pattern).unwrap(); let capture_content = capture_re .captures(response_body.as_bytes()).unwrap() .get(1).unwrap().as_bytes(); let token = String::from_utf8_lossy(capture_content).to_string(); assert_eq!(response.status(), Status::Ok); token } fn vks_publish_submit_response<'a>(client: &'a Client, data: &[u8]) -> LocalResponse<'a> { let ct = ContentType::with_params( "multipart", "form-data", ("boundary", "---------------------------14733842173518794281682249499")); let header = b"-----------------------------14733842173518794281682249499\r\n\ Content-Disposition: form-data; name=\"csrf\"\r\n\ \r\n\ \r\n\ -----------------------------14733842173518794281682249499\r\n\ Content-Disposition: form-data; name=\"keytext\"; filename=\".k\"\r\n\ Content-Type: application/octet-stream\r\n\ \r\n"; let footer = b"\r\n-----------------------------14733842173518794281682249499--"; let mut body = Vec::new(); body.extend_from_slice(header); body.extend_from_slice(data); body.extend_from_slice(footer); client.post("/vks/v1/publish") .header(ct) .body(&body[..]) .dispatch() } fn vks_manage<'a>(client: &'a Client, search_term: &str) { let encoded = ::url::form_urlencoded::Serializer::new(String::new()) .append_pair("search_term", search_term) .finish(); let response = client.post("/manage") .header(ContentType::Form) .body(encoded.as_bytes()) .dispatch(); assert_eq!(response.status(), Status::Ok); } fn vks_manage_delete(client: &Client, token: &str, address: &str) { let encoded = ::url::form_urlencoded::Serializer::new(String::new()) .append_pair("token", token) .append_pair("address", address) .finish(); let response = client.post("/manage/unpublish") .header(ContentType::Form) .body(encoded.as_bytes()) .dispatch(); assert_eq!(response.status(), Status::Ok); } }