cargo: apply cargo fmt --all

This commit is contained in:
Vincent Breitmoser 2022-02-26 16:54:07 +01:00
parent 961559e154
commit b29845b893
32 changed files with 2684 additions and 1866 deletions

View File

@ -4,6 +4,5 @@ use vergen::{generate_cargo_keys, ConstantsFlags};
fn main() { fn main() {
// Generate the 'cargo:' key output // Generate the 'cargo:' key output
generate_cargo_keys(ConstantsFlags::all()) generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!");
.expect("Unable to generate the cargo keys!");
} }

View File

@ -1,19 +1,21 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fs::{OpenOptions, File, create_dir_all, read_link, remove_file, rename, set_permissions, Permissions}; use std::fs::{
create_dir_all, read_link, remove_file, rename, set_permissions, File, OpenOptions, Permissions,
};
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf};
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use tempfile;
use url::form_urlencoded;
use pathdiff::diff_paths; use pathdiff::diff_paths;
use std::time::SystemTime; use std::time::SystemTime;
use tempfile;
use url::form_urlencoded;
use {Database, Query};
use types::{Email, Fingerprint, KeyID};
use sync::FlockMutexGuard; use sync::FlockMutexGuard;
use types::{Email, Fingerprint, KeyID};
use Result; use Result;
use {Database, Query};
use wkd; use wkd;
@ -51,7 +53,6 @@ fn ensure_parent(path: &Path) -> Result<&Path> {
Ok(path) Ok(path)
} }
impl Filesystem { impl Filesystem {
pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self> { pub fn new_from_base(base_dir: impl Into<PathBuf>) -> Result<Self> {
let base_dir: PathBuf = base_dir.into(); let base_dir: PathBuf = base_dir.into();
@ -164,22 +165,23 @@ impl Filesystem {
/// Returns the path to the given Email. /// Returns the path to the given Email.
fn link_by_email(&self, email: &Email) -> PathBuf { fn link_by_email(&self, email: &Email) -> PathBuf {
let email = form_urlencoded::byte_serialize(email.as_str().as_bytes()) let email = form_urlencoded::byte_serialize(email.as_str().as_bytes()).collect::<String>();
.collect::<String>();
self.links_dir_by_email.join(path_split(&email)) self.links_dir_by_email.join(path_split(&email))
} }
/// Returns the WKD path to the given Email. /// Returns the WKD path to the given Email.
fn link_wkd_by_email(&self, email: &Email) -> PathBuf { fn link_wkd_by_email(&self, email: &Email) -> PathBuf {
let (encoded_local_part, domain) = wkd::encode_wkd(email.as_str()).unwrap(); let (encoded_local_part, domain) = wkd::encode_wkd(email.as_str()).unwrap();
let encoded_domain = form_urlencoded::byte_serialize(domain.as_bytes()) let encoded_domain =
.collect::<PathBuf>(); form_urlencoded::byte_serialize(domain.as_bytes()).collect::<PathBuf>();
[ [
&self.links_dir_wkd_by_email, &self.links_dir_wkd_by_email,
&encoded_domain, &encoded_domain,
&path_split(&encoded_local_part) &path_split(&encoded_local_part),
].iter().collect() ]
.iter()
.collect()
} }
/// Returns the WKD path to the given url-encoded domain and wkd-encoded local part. /// Returns the WKD path to the given url-encoded domain and wkd-encoded local part.
@ -187,16 +189,19 @@ impl Filesystem {
[ [
&self.links_dir_wkd_by_email, &self.links_dir_wkd_by_email,
Path::new(&domain), Path::new(&domain),
&path_split(hash) &path_split(hash),
].iter().collect() ]
.iter()
.collect()
} }
#[allow(clippy::nonminimal_bool)] #[allow(clippy::nonminimal_bool)]
fn read_from_path(&self, path: &Path, allow_internal: bool) -> Option<String> { fn read_from_path(&self, path: &Path, allow_internal: bool) -> Option<String> {
use std::fs; use std::fs;
if !path.starts_with(&self.keys_external_dir) && if !path.starts_with(&self.keys_external_dir)
!(allow_internal && path.starts_with(&self.keys_internal_dir)) { && !(allow_internal && path.starts_with(&self.keys_internal_dir))
{
panic!("Attempted to access file outside expected dirs!"); panic!("Attempted to access file outside expected dirs!");
} }
@ -211,8 +216,9 @@ impl Filesystem {
fn read_from_path_bytes(&self, path: &Path, allow_internal: bool) -> Option<Vec<u8>> { fn read_from_path_bytes(&self, path: &Path, allow_internal: bool) -> Option<Vec<u8>> {
use std::fs; use std::fs;
if !path.starts_with(&self.keys_external_dir) && if !path.starts_with(&self.keys_external_dir)
!(allow_internal && path.starts_with(&self.keys_internal_dir)) { && !(allow_internal && path.starts_with(&self.keys_internal_dir))
{
panic!("Attempted to access file outside expected dirs!"); panic!("Attempted to access file outside expected dirs!");
} }
@ -286,8 +292,9 @@ impl Filesystem {
let expected = diff_paths( let expected = diff_paths(
&self.fingerprint_to_path_published(fpr), &self.fingerprint_to_path_published(fpr),
link.parent().unwrap() link.parent().unwrap(),
).unwrap(); )
.unwrap();
symlink_unlink_with_check(&link, &expected) symlink_unlink_with_check(&link, &expected)
} }
@ -297,8 +304,9 @@ impl Filesystem {
let expected = diff_paths( let expected = diff_paths(
&self.fingerprint_to_path_published_wkd(fpr), &self.fingerprint_to_path_published_wkd(fpr),
link.parent().unwrap() link.parent().unwrap(),
).unwrap(); )
.unwrap();
symlink_unlink_with_check(&link, &expected) symlink_unlink_with_check(&link, &expected)
} }
@ -317,8 +325,8 @@ impl Filesystem {
tpks: &mut HashMap<Fingerprint, Cert>, tpks: &mut HashMap<Fingerprint, Cert>,
check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<()>, check: impl Fn(&Path, &Cert, &Fingerprint) -> Result<()>,
) -> Result<()> { ) -> Result<()> {
use walkdir::WalkDir;
use std::fs; use std::fs;
use walkdir::WalkDir;
for entry in WalkDir::new(checks_dir) { for entry in WalkDir::new(checks_dir) {
let entry = entry?; let entry = entry?;
@ -331,23 +339,19 @@ impl Filesystem {
// Compute the corresponding primary fingerprint just // Compute the corresponding primary fingerprint just
// by looking at the paths. // by looking at the paths.
let primary_fp = Filesystem::path_to_primary(path) let primary_fp = Filesystem::path_to_primary(path)
.ok_or_else( .ok_or_else(|| format_err!("Malformed path: {:?}", path.read_link().unwrap()))?;
|| format_err!("Malformed path: {:?}",
path.read_link().unwrap()))?;
// Load into cache. // Load into cache.
if ! tpks.contains_key(&primary_fp) { if !tpks.contains_key(&primary_fp) {
tpks.insert( tpks.insert(
primary_fp.clone(), primary_fp.clone(),
self.lookup(&Query::ByFingerprint(primary_fp.clone())) self.lookup(&Query::ByFingerprint(primary_fp.clone()))?
?.ok_or_else( .ok_or_else(|| format_err!("No Cert with fingerprint {:?}", primary_fp))?,
|| format_err!("No Cert with fingerprint {:?}", );
primary_fp))?);
} }
let tpk = tpks.get(&primary_fp) let tpk = tpks.get(&primary_fp).ok_or_else(|| {
.ok_or_else( format_err!("Broken symlink {:?}: No such Key {}", path, primary_fp)
|| format_err!("Broken symlink {:?}: No such Key {}", })?;
path, primary_fp))?;
check(path, tpk, &primary_fp)?; check(path, tpk, &primary_fp)?;
} }
@ -433,7 +437,11 @@ impl Database for Filesystem {
Ok(()) Ok(())
} }
fn move_tmp_to_published_wkd(&self, file: Option<Self::TempCert>, fpr: &Fingerprint) -> Result<()> { fn move_tmp_to_published_wkd(
&self,
file: Option<Self::TempCert>,
fpr: &Fingerprint,
) -> Result<()> {
if self.dry_run { if self.dry_run {
return Ok(()); return Ok(());
} }
@ -461,7 +469,11 @@ impl Database for Filesystem {
Ok(()) Ok(())
} }
fn check_link_fpr(&self, fpr: &Fingerprint, fpr_target: &Fingerprint) -> Result<Option<Fingerprint>> { fn check_link_fpr(
&self,
fpr: &Fingerprint,
fpr_target: &Fingerprint,
) -> Result<Option<Fingerprint>> {
let link_keyid = self.link_by_keyid(&fpr.into()); let link_keyid = self.link_by_keyid(&fpr.into());
let link_fpr = self.link_by_fingerprint(fpr); let link_fpr = self.link_by_fingerprint(fpr);
@ -477,8 +489,10 @@ impl Database for Filesystem {
if let Ok(link_keyid_target) = link_keyid.canonicalize() { if let Ok(link_keyid_target) = link_keyid.canonicalize() {
if !link_keyid_target.ends_with(&path_published) { if !link_keyid_target.ends_with(&path_published) {
info!("KeyID points to different key for {} (expected {:?} to be suffix of {:?})", info!(
fpr, &path_published, &link_keyid_target); "KeyID points to different key for {} (expected {:?} to be suffix of {:?})",
fpr, &path_published, &link_keyid_target
);
return Err(anyhow!(format!("KeyID collision for key {}", fpr))); return Err(anyhow!(format!("KeyID collision for key {}", fpr)));
} }
} }
@ -496,7 +510,7 @@ impl Database for Filesystem {
ByFingerprint(ref fp) => self.link_by_fingerprint(fp), ByFingerprint(ref fp) => self.link_by_fingerprint(fp),
ByKeyID(ref keyid) => self.link_by_keyid(keyid), ByKeyID(ref keyid) => self.link_by_keyid(keyid),
ByEmail(ref email) => self.link_by_email(email), ByEmail(ref email) => self.link_by_email(email),
_ => return None _ => return None,
}; };
path.read_link() path.read_link()
.ok() .ok()
@ -527,8 +541,11 @@ impl Database for Filesystem {
let link_fpr = self.link_by_fingerprint(from); let link_fpr = self.link_by_fingerprint(from);
let link_keyid = self.link_by_keyid(&from.into()); let link_keyid = self.link_by_keyid(&from.into());
let target = diff_paths(&self.fingerprint_to_path_published(primary_fpr), let target = diff_paths(
link_fpr.parent().unwrap()).unwrap(); &self.fingerprint_to_path_published(primary_fpr),
link_fpr.parent().unwrap(),
)
.unwrap();
symlink(&target, ensure_parent(&link_fpr)?)?; symlink(&target, ensure_parent(&link_fpr)?)?;
symlink(&target, ensure_parent(&link_keyid)?) symlink(&target, ensure_parent(&link_keyid)?)
@ -537,8 +554,11 @@ impl Database for Filesystem {
fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> { fn unlink_fpr(&self, from: &Fingerprint, primary_fpr: &Fingerprint) -> Result<()> {
let link_fpr = self.link_by_fingerprint(from); let link_fpr = self.link_by_fingerprint(from);
let link_keyid = self.link_by_keyid(&from.into()); let link_keyid = self.link_by_keyid(&from.into());
let expected = diff_paths(&self.fingerprint_to_path_published(primary_fpr), let expected = diff_paths(
link_fpr.parent().unwrap()).unwrap(); &self.fingerprint_to_path_published(primary_fpr),
link_fpr.parent().unwrap(),
)
.unwrap();
if let Ok(target) = read_link(&link_fpr) { if let Ok(target) = read_link(&link_fpr) {
if target == expected { if target == expected {
@ -604,7 +624,9 @@ impl Database for Filesystem {
// A cache of all Certs, for quick lookups. // A cache of all Certs, for quick lookups.
let mut tpks = HashMap::new(); let mut tpks = HashMap::new();
self.perform_checks(&self.keys_dir_published, &mut tpks, self.perform_checks(
&self.keys_dir_published,
&mut tpks,
|path, _, primary_fp| { |path, _, primary_fp| {
// The KeyID corresponding with this path. // The KeyID corresponding with this path.
let fp = Filesystem::path_to_fingerprint(path) let fp = Filesystem::path_to_fingerprint(path)
@ -614,14 +636,16 @@ impl Database for Filesystem {
return Err(format_err!( return Err(format_err!(
"{:?} points to the wrong Cert, expected {} \ "{:?} points to the wrong Cert, expected {} \
but found {}", but found {}",
path, fp, primary_fp)); path,
fp,
primary_fp
));
} }
Ok(()) Ok(())
} },
)?; )?;
self.perform_checks(&self.keys_dir_published, &mut tpks, self.perform_checks(&self.keys_dir_published, &mut tpks, |_, tpk, primary_fp| {
|_, tpk, primary_fp| {
// check that certificate exists in published wkd path // check that certificate exists in published wkd path
let path_wkd = self.fingerprint_to_path_published_wkd(primary_fp); let path_wkd = self.fingerprint_to_path_published_wkd(primary_fp);
let should_wkd_exist = tpk.userids().next().is_some(); let should_wkd_exist = tpk.userids().next().is_some();
@ -633,12 +657,10 @@ impl Database for Filesystem {
return Err(format_err!("Incorrectly present wkd for fp {}", primary_fp)); return Err(format_err!("Incorrectly present wkd for fp {}", primary_fp));
}; };
Ok(()) Ok(())
} })?;
)?;
// check that all subkeys are linked // check that all subkeys are linked
self.perform_checks(&self.keys_dir_published, &mut tpks, self.perform_checks(&self.keys_dir_published, &mut tpks, |_, tpk, primary_fp| {
|_, tpk, primary_fp| {
let policy = &POLICY; let policy = &POLICY;
let fingerprints = tpk let fingerprints = tpk
.keys() .keys()
@ -652,16 +674,17 @@ impl Database for Filesystem {
for fpr in fingerprints { for fpr in fingerprints {
if let Some(missing_fpr) = self.check_link_fpr(&fpr, primary_fp)? { if let Some(missing_fpr) = self.check_link_fpr(&fpr, primary_fp)? {
return Err(format_err!( return Err(format_err!(
"Missing link to key {} for sub {}", primary_fp, missing_fpr)); "Missing link to key {} for sub {}",
primary_fp,
missing_fpr
));
} }
} }
Ok(()) Ok(())
} })?;
)?;
// check that all published uids are linked // check that all published uids are linked
self.perform_checks(&self.keys_dir_published, &mut tpks, self.perform_checks(&self.keys_dir_published, &mut tpks, |_, tpk, primary_fp| {
|_, tpk, primary_fp| {
let emails = tpk let emails = tpk
.userids() .userids()
.map(|binding| binding.userid().clone()) .map(|binding| binding.userid().clone())
@ -671,72 +694,81 @@ impl Database for Filesystem {
let email_path = self.link_by_email(&email); let email_path = self.link_by_email(&email);
if !email_path.exists() { if !email_path.exists() {
return Err(format_err!( return Err(format_err!(
"Missing link to key {} for email {}", primary_fp, email)); "Missing link to key {} for email {}",
primary_fp,
email
));
} }
let email_wkd_path = self.link_wkd_by_email(&email); let email_wkd_path = self.link_wkd_by_email(&email);
if !email_wkd_path.exists() { if !email_wkd_path.exists() {
return Err(format_err!( return Err(format_err!(
"Missing wkd link to key {} for email {}", primary_fp, email)); "Missing wkd link to key {} for email {}",
primary_fp,
email
));
} }
} }
Ok(()) Ok(())
} })?;
)?;
self.perform_checks(&self.links_dir_by_fingerprint, &mut tpks, |path, tpk, _| {
self.perform_checks(&self.links_dir_by_fingerprint, &mut tpks,
|path, tpk, _| {
// The KeyID corresponding with this path. // The KeyID corresponding with this path.
let id = Filesystem::path_to_keyid(path) let id = Filesystem::path_to_keyid(path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?; .ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let found = tpk.keys() let found = tpk
.keys()
.map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap()) .map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap())
.any(|key_fp| key_fp == id); .any(|key_fp| key_fp == id);
if ! found { if !found {
return Err(format_err!( return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \ "{:?} points to the wrong Cert, the Cert does not \
contain the (sub)key {}", path, id)); contain the (sub)key {}",
path,
id
));
} }
Ok(()) Ok(())
} })?;
)?;
self.perform_checks(&self.links_dir_by_keyid, &mut tpks, self.perform_checks(&self.links_dir_by_keyid, &mut tpks, |path, tpk, _| {
|path, tpk, _| {
// The KeyID corresponding with this path. // The KeyID corresponding with this path.
let id = Filesystem::path_to_keyid(path) let id = Filesystem::path_to_keyid(path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?; .ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let found = tpk.keys() let found = tpk
.keys()
.map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap()) .map(|amalgamation| KeyID::try_from(amalgamation.key().fingerprint()).unwrap())
.any(|key_fp| key_fp == id); .any(|key_fp| key_fp == id);
if ! found { if !found {
return Err(format_err!( return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \ "{:?} points to the wrong Cert, the Cert does not \
contain the (sub)key {}", path, id)); contain the (sub)key {}",
path,
id
));
} }
Ok(()) Ok(())
} })?;
)?;
self.perform_checks(&self.links_dir_by_email, &mut tpks, self.perform_checks(&self.links_dir_by_email, &mut tpks, |path, tpk, _| {
|path, tpk, _| {
// The Email corresponding with this path. // The Email corresponding with this path.
let email = Filesystem::path_to_email(path) let email = Filesystem::path_to_email(path)
.ok_or_else(|| format_err!("Malformed path: {:?}", path))?; .ok_or_else(|| format_err!("Malformed path: {:?}", path))?;
let mut found = false; let mut found = false;
for uidb in tpk.userids() { for uidb in tpk.userids() {
if Email::try_from(uidb.userid()).unwrap() == email if Email::try_from(uidb.userid()).unwrap() == email {
{
found = true; found = true;
break; break;
} }
} }
if ! found { if !found {
return Err(format_err!( return Err(format_err!(
"{:?} points to the wrong Cert, the Cert does not \ "{:?} points to the wrong Cert, the Cert does not \
contain the email {}", path, email)); contain the email {}",
path,
email
));
} }
Ok(()) Ok(())
})?; })?;
@ -754,7 +786,13 @@ fn path_split(path: &str) -> PathBuf {
} }
fn path_merge(path: &Path) -> String { fn path_merge(path: &Path) -> String {
let comps = path.iter().rev().take(3).collect::<Vec<_>>().into_iter().rev(); let comps = path
.iter()
.rev()
.take(3)
.collect::<Vec<_>>()
.into_iter()
.rev();
let comps: Vec<_> = comps.map(|os| os.to_string_lossy()).collect(); let comps: Vec<_> = comps.map(|os| os.to_string_lossy()).collect();
comps.join("") comps.join("")
} }
@ -762,9 +800,9 @@ fn path_merge(path: &Path) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use test;
use openpgp::cert::CertBuilder; use openpgp::cert::CertBuilder;
use tempfile::TempDir; use tempfile::TempDir;
use test;
#[test] #[test]
fn init() { fn init() {
@ -783,18 +821,48 @@ mod tests {
#[test] #[test]
fn new() { fn new() {
let (_tmp_dir, db, _log_path) = open_db(); let (_tmp_dir, db, _log_path) = open_db();
let k1 = CertBuilder::new().add_userid("a@invalid.example.org") let k1 = CertBuilder::new()
.generate().unwrap().0; .add_userid("a@invalid.example.org")
let k2 = CertBuilder::new().add_userid("b@invalid.example.org") .generate()
.generate().unwrap().0; .unwrap()
let k3 = CertBuilder::new().add_userid("c@invalid.example.org") .0;
.generate().unwrap().0; let k2 = CertBuilder::new()
.add_userid("b@invalid.example.org")
.generate()
.unwrap()
.0;
let k3 = CertBuilder::new()
.add_userid("c@invalid.example.org")
.generate()
.unwrap()
.0;
assert!(db.merge(k1).unwrap().into_tpk_status().email_status.len() > 0); assert!(db.merge(k1).unwrap().into_tpk_status().email_status.len() > 0);
assert!(db.merge(k2.clone()).unwrap().into_tpk_status().email_status.len() > 0); assert!(
db.merge(k2.clone())
.unwrap()
.into_tpk_status()
.email_status
.len()
> 0
);
assert!(!db.merge(k2).unwrap().into_tpk_status().email_status.len() > 0); assert!(!db.merge(k2).unwrap().into_tpk_status().email_status.len() > 0);
assert!(db.merge(k3.clone()).unwrap().into_tpk_status().email_status.len() > 0); assert!(
assert!(!db.merge(k3.clone()).unwrap().into_tpk_status().email_status.len() > 0); db.merge(k3.clone())
.unwrap()
.into_tpk_status()
.email_status
.len()
> 0
);
assert!(
!db.merge(k3.clone())
.unwrap()
.into_tpk_status()
.email_status
.len()
> 0
);
assert!(!db.merge(k3).unwrap().into_tpk_status().email_status.len() > 0); assert!(!db.merge(k3).unwrap().into_tpk_status().email_status.len() > 0);
} }
@ -915,11 +983,12 @@ mod tests {
let tmpdir = TempDir::new().unwrap(); let tmpdir = TempDir::new().unwrap();
let db = Filesystem::new_from_base(tmpdir.path()).unwrap(); let db = Filesystem::new_from_base(tmpdir.path()).unwrap();
let fp: Fingerprint = let fp: Fingerprint = "CBCD8F030588653EEDD7E2659B7DD433F254904A".parse().unwrap();
"CBCD8F030588653EEDD7E2659B7DD433F254904A".parse().unwrap();
assert_eq!(Filesystem::path_to_fingerprint(&db.link_by_fingerprint(&fp)), assert_eq!(
Some(fp.clone())); Filesystem::path_to_fingerprint(&db.link_by_fingerprint(&fp)),
Some(fp.clone())
);
db.check_consistency().expect("inconsistent database"); db.check_consistency().expect("inconsistent database");
} }

View File

@ -14,6 +14,8 @@ extern crate fs2;
extern crate idna; extern crate idna;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate chrono;
extern crate hex;
extern crate pathdiff; extern crate pathdiff;
extern crate rand; extern crate rand;
extern crate serde; extern crate serde;
@ -21,24 +23,17 @@ extern crate serde_json;
extern crate tempfile; extern crate tempfile;
extern crate time; extern crate time;
extern crate url; extern crate url;
extern crate hex;
extern crate walkdir; extern crate walkdir;
extern crate chrono;
extern crate zbase32; extern crate zbase32;
extern crate sequoia_openpgp as openpgp; extern crate sequoia_openpgp as openpgp;
use openpgp::{ use openpgp::{packet::UserID, parse::Parse, types::KeyFlags, Cert};
Cert,
packet::UserID,
parse::Parse,
types::KeyFlags,
};
pub mod types; pub mod types;
use types::{Email, Fingerprint, KeyID}; use types::{Email, Fingerprint, KeyID};
pub mod wkd;
pub mod sync; pub mod sync;
pub mod wkd;
mod fs; mod fs;
pub use self::fs::Filesystem as KeyDatabase; pub use self::fs::Filesystem as KeyDatabase;
@ -47,7 +42,7 @@ mod stateful_tokens;
pub use stateful_tokens::StatefulTokens; pub use stateful_tokens::StatefulTokens;
mod openpgp_utils; mod openpgp_utils;
use openpgp_utils::{tpk_filter_alive_emails, tpk_to_string, tpk_clean, is_status_revoked, POLICY}; use openpgp_utils::{is_status_revoked, tpk_clean, tpk_filter_alive_emails, tpk_to_string, POLICY};
#[cfg(test)] #[cfg(test)]
mod test; mod test;
@ -74,8 +69,8 @@ impl FromStr for Query {
fn from_str(term: &str) -> Result<Self> { fn from_str(term: &str) -> Result<Self> {
use self::Query::*; use self::Query::*;
let looks_like_short_key_id = !term.contains('@') && let looks_like_short_key_id =
(term.starts_with("0x") && term.len() < 16 || term.len() == 8); !term.contains('@') && (term.starts_with("0x") && term.len() < 16 || term.len() == 8);
if looks_like_short_key_id { if looks_like_short_key_id {
Ok(InvalidShort()) Ok(InvalidShort())
} else if let Ok(fp) = Fingerprint::from_str(term) { } else if let Ok(fp) = Fingerprint::from_str(term) {
@ -90,7 +85,7 @@ impl FromStr for Query {
} }
} }
#[derive(Debug,PartialEq,Eq,PartialOrd,Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum EmailAddressStatus { pub enum EmailAddressStatus {
Published, Published,
NotPublished, NotPublished,
@ -113,10 +108,10 @@ impl ImportResult {
} }
} }
#[derive(Debug,PartialEq)] #[derive(Debug, PartialEq)]
pub struct TpkStatus { pub struct TpkStatus {
pub is_revoked: bool, pub is_revoked: bool,
pub email_status: Vec<(Email,EmailAddressStatus)>, pub email_status: Vec<(Email, EmailAddressStatus)>,
pub unparsed_uids: usize, pub unparsed_uids: usize,
} }
@ -151,7 +146,11 @@ pub trait Database: Sync + Send {
fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>>; fn by_email_wkd(&self, email: &Email) -> Option<Vec<u8>>;
fn by_domain_and_hash_wkd(&self, domain: &str, hash: &str) -> Option<Vec<u8>>; fn by_domain_and_hash_wkd(&self, domain: &str, hash: &str) -> Option<Vec<u8>>;
fn check_link_fpr(&self, fpr: &Fingerprint, target: &Fingerprint) -> Result<Option<Fingerprint>>; fn check_link_fpr(
&self,
fpr: &Fingerprint,
target: &Fingerprint,
) -> Result<Option<Fingerprint>>;
fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String>; fn by_fpr_full(&self, fpr: &Fingerprint) -> Option<String>;
fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String>; fn by_primary_fpr(&self, fpr: &Fingerprint) -> Option<String>;
@ -159,7 +158,11 @@ pub trait Database: Sync + Send {
fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert>; fn write_to_temp(&self, content: &[u8]) -> Result<Self::TempCert>;
fn move_tmp_to_full(&self, content: Self::TempCert, fpr: &Fingerprint) -> Result<()>; fn move_tmp_to_full(&self, content: Self::TempCert, fpr: &Fingerprint) -> Result<()>;
fn move_tmp_to_published(&self, content: Self::TempCert, fpr: &Fingerprint) -> Result<()>; fn move_tmp_to_published(&self, content: Self::TempCert, fpr: &Fingerprint) -> Result<()>;
fn move_tmp_to_published_wkd(&self, content: Option<Self::TempCert>, fpr: &Fingerprint) -> Result<()>; fn move_tmp_to_published_wkd(
&self,
content: Option<Self::TempCert>,
fpr: &Fingerprint,
) -> Result<()>;
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()>; fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()>;
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()>; fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()>;
@ -204,7 +207,8 @@ pub trait Database: Sync + Send {
.map(|binding| binding.userid().clone()) .map(|binding| binding.userid().clone())
.collect(); .collect();
let full_tpk_old = self.by_fpr_full(&fpr_primary) let full_tpk_old = self
.by_fpr_full(&fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok()); .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok());
let is_update = full_tpk_old.is_some(); let is_update = full_tpk_old.is_some();
let (full_tpk_new, full_tpk_unchanged) = if let Some(full_tpk_old) = full_tpk_old { let (full_tpk_new, full_tpk_unchanged) = if let Some(full_tpk_old) = full_tpk_old {
@ -217,9 +221,9 @@ pub trait Database: Sync + Send {
let is_revoked = is_status_revoked(full_tpk_new.revocation_status(&POLICY, None)); let is_revoked = is_status_revoked(full_tpk_new.revocation_status(&POLICY, None));
let is_ok = is_revoked || let is_ok = is_revoked
full_tpk_new.keys().subkeys().next().is_some() || || full_tpk_new.keys().subkeys().next().is_some()
full_tpk_new.userids().next().is_some(); || full_tpk_new.userids().next().is_some();
if !is_ok { if !is_ok {
// self.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?; // self.write_to_quarantine(&fpr_primary, &tpk_to_string(&full_tpk_new)?)?;
return Err(anyhow!("Not a well-formed key!")); return Err(anyhow!("Not a well-formed key!"));
@ -228,7 +232,10 @@ pub trait Database: Sync + Send {
let published_tpk_old = self let published_tpk_old = self
.by_fpr(&fpr_primary) .by_fpr(&fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok()); .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok());
let published_emails = published_tpk_old.as_ref().map(tpk_get_emails).unwrap_or_default(); let published_emails = published_tpk_old
.as_ref()
.map(tpk_get_emails)
.unwrap_or_default();
let unparsed_uids = full_tpk_new let unparsed_uids = full_tpk_new
.userids() .userids()
@ -246,7 +253,9 @@ pub trait Database: Sync + Send {
} }
}) })
.flatten() .flatten()
.filter(|(binding, email)| known_uids.contains(binding.userid()) || published_emails.contains(email)) .filter(|(binding, email)| {
known_uids.contains(binding.userid()) || published_emails.contains(email)
})
.flat_map(|(binding, email)| { .flat_map(|(binding, email)| {
if is_status_revoked(binding.revocation_status(&POLICY, None)) { if is_status_revoked(binding.revocation_status(&POLICY, None)) {
Some((email, EmailAddressStatus::Revoked)) Some((email, EmailAddressStatus::Revoked))
@ -264,7 +273,11 @@ pub trait Database: Sync + Send {
// Abort if no changes were made // Abort if no changes were made
if full_tpk_unchanged { if full_tpk_unchanged {
return Ok(ImportResult::Unchanged(TpkStatus { is_revoked, email_status, unparsed_uids })); return Ok(ImportResult::Unchanged(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}));
} }
let published_tpk_new = if is_revoked { let published_tpk_new = if is_revoked {
@ -284,7 +297,8 @@ pub trait Database: Sync + Send {
.flatten() .flatten()
.any(|unrevoked_email| &unrevoked_email == *email); .any(|unrevoked_email| &unrevoked_email == *email);
!has_unrevoked_userid !has_unrevoked_userid
}).collect(); })
.collect();
let fingerprints = tpk_get_linkable_fprs(&published_tpk_new); let fingerprints = tpk_get_linkable_fprs(&published_tpk_new);
@ -321,22 +335,31 @@ pub trait Database: Sync + Send {
for fpr in fpr_not_linked { for fpr in fpr_not_linked {
if let Err(e) = self.link_fpr(&fpr, &fpr_primary) { if let Err(e) = self.link_fpr(&fpr, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}", info!("Error ensuring symlink! {} {} {:?}", &fpr, &fpr_primary, e);
&fpr, &fpr_primary, e);
} }
} }
for revoked_email in newly_revoked_emails { for revoked_email in newly_revoked_emails {
if let Err(e) = self.unlink_email(revoked_email, &fpr_primary) { if let Err(e) = self.unlink_email(revoked_email, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}", info!(
&fpr_primary, &revoked_email, e); "Error ensuring symlink! {} {} {:?}",
&fpr_primary, &revoked_email, e
);
} }
} }
if is_update { if is_update {
Ok(ImportResult::Updated(TpkStatus { is_revoked, email_status, unparsed_uids })) Ok(ImportResult::Updated(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}))
} else { } else {
Ok(ImportResult::New(TpkStatus { is_revoked, email_status, unparsed_uids })) Ok(ImportResult::New(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
}))
} }
} }
@ -352,8 +375,13 @@ pub trait Database: Sync + Send {
Utc::now().format("%Y-%m-%d").to_string() Utc::now().format("%Y-%m-%d").to_string()
} }
fn get_tpk_status(&self, fpr_primary: &Fingerprint, known_addresses: &[Email]) -> Result<TpkStatus> { fn get_tpk_status(
let tpk_full = self.by_fpr_full(fpr_primary) &self,
fpr_primary: &Fingerprint,
known_addresses: &[Email],
) -> Result<TpkStatus> {
let tpk_full = self
.by_fpr_full(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!")) .ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?; .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
@ -368,10 +396,12 @@ pub trait Database: Sync + Send {
let published_uids: Vec<UserID> = self let published_uids: Vec<UserID> = self
.by_fpr(fpr_primary) .by_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok()) .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.map(|tpk| tpk.userids() .map(|tpk| {
tpk.userids()
.map(|binding| binding.userid().clone()) .map(|binding| binding.userid().clone())
.collect() .collect()
).unwrap_or_default(); })
.unwrap_or_default();
let mut email_status: Vec<_> = tpk_full let mut email_status: Vec<_> = tpk_full
.userids() .userids()
@ -397,7 +427,11 @@ pub trait Database: Sync + Send {
// the same address, we keep the first. // the same address, we keep the first.
email_status.dedup_by(|(e1, _), (e2, _)| e1 == e2); email_status.dedup_by(|(e1, _), (e2, _)| e1 == e2);
Ok(TpkStatus { is_revoked, email_status, unparsed_uids }) Ok(TpkStatus {
is_revoked,
email_status,
unparsed_uids,
})
} }
/// Complex operation that publishes some user id for a Cert already in the database. /// Complex operation that publishes some user id for a Cert already in the database.
@ -418,18 +452,22 @@ pub trait Database: Sync + Send {
self.nolock_unlink_email_if_other(fpr_primary, email_new)?; self.nolock_unlink_email_if_other(fpr_primary, email_new)?;
let full_tpk = self.by_fpr_full(fpr_primary) let full_tpk = self
.by_fpr_full(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!")) .ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?; .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
let published_uids_old: Vec<UserID> = self let published_uids_old: Vec<UserID> = self
.by_fpr(fpr_primary) .by_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok()) .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.map(|tpk| tpk.userids() .map(|tpk| {
tpk.userids()
.map(|binding| binding.userid().clone()) .map(|binding| binding.userid().clone())
.collect() .collect()
).unwrap_or_default(); })
let published_emails_old: Vec<Email> = published_uids_old.iter() .unwrap_or_default();
let published_emails_old: Vec<Email> = published_uids_old
.iter()
.map(|uid| Email::try_from(uid).ok()) .map(|uid| Email::try_from(uid).ok())
.flatten() .flatten()
.collect(); .collect();
@ -449,7 +487,8 @@ pub trait Database: Sync + Send {
.userids() .userids()
.map(|binding| Email::try_from(binding.userid())) .map(|binding| Email::try_from(binding.userid()))
.flatten() .flatten()
.any(|email| email == *email_new) { .any(|email| email == *email_new)
{
return Err(anyhow!("Requested UserID not found!")); return Err(anyhow!("Requested UserID not found!"));
} }
@ -462,8 +501,10 @@ pub trait Database: Sync + Send {
self.update_write_log(fpr_primary); self.update_write_log(fpr_primary);
if let Err(e) = self.link_email(email_new, fpr_primary) { if let Err(e) = self.link_email(email_new, fpr_primary) {
info!("Error ensuring email symlink! {} -> {} {:?}", info!(
&email_new, &fpr_primary, e); "Error ensuring email symlink! {} -> {} {:?}",
&email_new, &fpr_primary, e
);
} }
Ok(()) Ok(())
@ -474,13 +515,15 @@ pub trait Database: Sync + Send {
fpr_primary: &Fingerprint, fpr_primary: &Fingerprint,
unlink_email: &Email, unlink_email: &Email,
) -> Result<()> { ) -> Result<()> {
let current_link_fpr = self.lookup_primary_fingerprint( let current_link_fpr =
&Query::ByEmail(unlink_email.clone())); self.lookup_primary_fingerprint(&Query::ByEmail(unlink_email.clone()));
if let Some(current_fpr) = current_link_fpr { if let Some(current_fpr) = current_link_fpr {
if current_fpr != *fpr_primary { if current_fpr != *fpr_primary {
self.nolock_set_email_unpublished_filter(&current_fpr, self.nolock_set_email_unpublished_filter(&current_fpr, |uid| {
|uid| Email::try_from(uid).map(|email| email != *unlink_email) Email::try_from(uid)
.unwrap_or(false))?; .map(|email| email != *unlink_email)
.unwrap_or(false)
})?;
} }
} }
Ok(()) Ok(())
@ -513,7 +556,8 @@ pub trait Database: Sync + Send {
fpr_primary: &Fingerprint, fpr_primary: &Fingerprint,
email_remove: impl Fn(&UserID) -> bool, email_remove: impl Fn(&UserID) -> bool,
) -> Result<()> { ) -> Result<()> {
let published_tpk_old = self.by_fpr(fpr_primary) let published_tpk_old = self
.by_fpr(fpr_primary)
.ok_or_else(|| anyhow!("Key not in database!")) .ok_or_else(|| anyhow!("Key not in database!"))
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?; .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()))?;
@ -523,8 +567,7 @@ pub trait Database: Sync + Send {
.flatten() .flatten()
.collect(); .collect();
let published_tpk_new = published_tpk_old.retain_userids( let published_tpk_new = published_tpk_old.retain_userids(|uid| email_remove(uid.userid()));
|uid| email_remove(uid.userid()));
let published_emails_new: Vec<Email> = published_tpk_new let published_emails_new: Vec<Email> = published_tpk_new
.userids() .userids()
@ -546,37 +589,31 @@ pub trait Database: Sync + Send {
for unpublished_email in unpublished_emails { for unpublished_email in unpublished_emails {
if let Err(e) = self.unlink_email(unpublished_email, fpr_primary) { if let Err(e) = self.unlink_email(unpublished_email, fpr_primary) {
info!("Error deleting email symlink! {} -> {} {:?}", info!(
&unpublished_email, &fpr_primary, e); "Error deleting email symlink! {} -> {} {:?}",
&unpublished_email, &fpr_primary, e
);
} }
} }
Ok(()) Ok(())
} }
fn set_email_unpublished( fn set_email_unpublished(&self, fpr_primary: &Fingerprint, email_remove: &Email) -> Result<()> {
&self, self.set_email_unpublished_filter(fpr_primary, |uid| {
fpr_primary: &Fingerprint,
email_remove: &Email,
) -> Result<()> {
self.set_email_unpublished_filter(fpr_primary, |uid|
Email::try_from(uid) Email::try_from(uid)
.map(|email| email != *email_remove) .map(|email| email != *email_remove)
.unwrap_or(false)) .unwrap_or(false)
})
} }
fn set_email_unpublished_all( fn set_email_unpublished_all(&self, fpr_primary: &Fingerprint) -> Result<()> {
&self,
fpr_primary: &Fingerprint,
) -> Result<()> {
self.set_email_unpublished_filter(fpr_primary, |_| false) self.set_email_unpublished_filter(fpr_primary, |_| false)
} }
fn regenerate_links( fn regenerate_links(&self, fpr_primary: &Fingerprint) -> Result<RegenerateResult> {
&self, let tpk = self
fpr_primary: &Fingerprint, .by_primary_fpr(fpr_primary)
) -> Result<RegenerateResult> {
let tpk = self.by_primary_fpr(fpr_primary)
.and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok()) .and_then(|bytes| Cert::from_bytes(bytes.as_bytes()).ok())
.ok_or_else(|| anyhow!("Key not in database!"))?; .ok_or_else(|| anyhow!("Key not in database!"))?;
@ -619,11 +656,7 @@ pub trait Database: Sync + Send {
} }
} }
fn regenerate_wkd( fn regenerate_wkd(&self, fpr_primary: &Fingerprint, published_tpk: &Cert) -> Result<()> {
&self,
fpr_primary: &Fingerprint,
published_tpk: &Cert
) -> Result<()> {
let published_wkd_tpk_tmp = if published_tpk.userids().next().is_some() { let published_wkd_tpk_tmp = if published_tpk.userids().next().is_some() {
Some(self.write_to_temp(&published_tpk.export_to_vec()?)?) Some(self.write_to_temp(&published_tpk.export_to_vec()?)?)
} else { } else {
@ -636,30 +669,33 @@ pub trait Database: Sync + Send {
} }
fn tpk_get_emails(cert: &Cert) -> Vec<Email> { fn tpk_get_emails(cert: &Cert) -> Vec<Email> {
cert cert.userids()
.userids()
.map(|binding| Email::try_from(binding.userid())) .map(|binding| Email::try_from(binding.userid()))
.flatten() .flatten()
.collect() .collect()
} }
pub fn tpk_get_linkable_fprs(tpk: &Cert) -> Vec<Fingerprint> { pub fn tpk_get_linkable_fprs(tpk: &Cert) -> Vec<Fingerprint> {
let signing_capable = &KeyFlags::empty() let signing_capable = &KeyFlags::empty().set_signing().set_certification();
.set_signing()
.set_certification();
let fpr_primary = &Fingerprint::try_from(tpk.fingerprint()).unwrap(); let fpr_primary = &Fingerprint::try_from(tpk.fingerprint()).unwrap();
tpk tpk.keys()
.keys()
.into_iter() .into_iter()
.flat_map(|bundle| { .flat_map(|bundle| {
Fingerprint::try_from(bundle.key().fingerprint()) Fingerprint::try_from(bundle.key().fingerprint()).map(|fpr| {
.map(|fpr| (fpr, bundle.binding_signature(&POLICY, None).ok().and_then(|sig| sig.key_flags()))) (
fpr,
bundle
.binding_signature(&POLICY, None)
.ok()
.and_then(|sig| sig.key_flags()),
)
})
}) })
.filter(|(fpr, flags)| { .filter(|(fpr, flags)| {
fpr == fpr_primary || fpr == fpr_primary
flags.is_none() || || flags.is_none()
!(signing_capable & flags.as_ref().unwrap()).is_empty() || !(signing_capable & flags.as_ref().unwrap()).is_empty()
}) })
.map(|(fpr,_)| fpr) .map(|(fpr, _)| fpr)
.collect() .collect()
} }

View File

@ -2,11 +2,8 @@ use openpgp::Result;
use std::convert::TryFrom; use std::convert::TryFrom;
use openpgp::{ use openpgp::{
Cert, cert::prelude::*, policy::StandardPolicy, serialize::SerializeInto as _,
types::RevocationStatus, types::RevocationStatus, Cert,
cert::prelude::*,
serialize::SerializeInto as _,
policy::StandardPolicy,
}; };
use Email; use Email;
@ -33,24 +30,42 @@ pub fn tpk_clean(tpk: &Cert) -> Result<Cert> {
// The primary key and related signatures. // The primary key and related signatures.
let pk_bundle = tpk.primary_key().bundle(); let pk_bundle = tpk.primary_key().bundle();
acc.push(pk_bundle.key().clone().into()); acc.push(pk_bundle.key().clone().into());
for s in pk_bundle.self_signatures() { acc.push(s.clone().into()) } for s in pk_bundle.self_signatures() {
for s in pk_bundle.self_revocations() { acc.push(s.clone().into()) } acc.push(s.clone().into())
for s in pk_bundle.other_revocations() { acc.push(s.clone().into()) } }
for s in pk_bundle.self_revocations() {
acc.push(s.clone().into())
}
for s in pk_bundle.other_revocations() {
acc.push(s.clone().into())
}
// The subkeys and related signatures. // The subkeys and related signatures.
for skb in tpk.keys().subkeys() { for skb in tpk.keys().subkeys() {
acc.push(skb.key().clone().into()); acc.push(skb.key().clone().into());
for s in skb.self_signatures() { acc.push(s.clone().into()) } for s in skb.self_signatures() {
for s in skb.self_revocations() { acc.push(s.clone().into()) } acc.push(s.clone().into())
for s in skb.other_revocations() { acc.push(s.clone().into()) } }
for s in skb.self_revocations() {
acc.push(s.clone().into())
}
for s in skb.other_revocations() {
acc.push(s.clone().into())
}
} }
// The UserIDs. // The UserIDs.
for uidb in tpk.userids() { for uidb in tpk.userids() {
acc.push(uidb.userid().clone().into()); acc.push(uidb.userid().clone().into());
for s in uidb.self_signatures() { acc.push(s.clone().into()) } for s in uidb.self_signatures() {
for s in uidb.self_revocations() { acc.push(s.clone().into()) } acc.push(s.clone().into())
for s in uidb.other_revocations() { acc.push(s.clone().into()) } }
for s in uidb.self_revocations() {
acc.push(s.clone().into())
}
for s in uidb.other_revocations() {
acc.push(s.clone().into())
}
// Reasoning about the currently attested certifications // Reasoning about the currently attested certifications
// requires a policy. // requires a policy.

View File

@ -1,6 +1,6 @@
use std::io::{Read,Write};
use std::path::PathBuf;
use std::fs::{create_dir_all, remove_file, File}; use std::fs::{create_dir_all, remove_file, File};
use std::io::{Read, Write};
use std::path::PathBuf;
use std::str; use std::str;

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,10 @@ use std::fmt;
use std::result; use std::result;
use std::str::FromStr; use std::str::FromStr;
use anyhow::Error;
use openpgp::packet::UserID; use openpgp::packet::UserID;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use anyhow::Error; use Result;
use {Result};
/// Holds a normalized email address. /// Holds a normalized email address.
/// ///
@ -86,8 +86,7 @@ impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> { fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
match fpr { match fpr {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)), sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
sequoia_openpgp::Fingerprint::Invalid(_) => sequoia_openpgp::Fingerprint::Invalid(_) => Err(anyhow!("invalid fingerprint")),
Err(anyhow!("invalid fingerprint")),
_ => Err(anyhow!("unknown fingerprint type")), _ => Err(anyhow!("unknown fingerprint type")),
} }
} }
@ -95,7 +94,7 @@ impl TryFrom<sequoia_openpgp::Fingerprint> for Fingerprint {
impl fmt::Display for Fingerprint { impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ::hex::ToHex; use hex::ToHex;
self.0.write_hex_upper(f) self.0.write_hex_upper(f)
} }
} }
@ -116,8 +115,7 @@ impl<'de> Deserialize<'de> for Fingerprint {
{ {
use serde::de::Error; use serde::de::Error;
String::deserialize(deserializer).and_then(|string| { String::deserialize(deserializer).and_then(|string| {
Self::from_str(&string) Self::from_str(&string).map_err(|err| Error::custom(err.to_string()))
.map_err(|err| Error::custom(err.to_string()))
}) })
} }
} }
@ -128,8 +126,9 @@ impl FromStr for Fingerprint {
fn from_str(s: &str) -> Result<Fingerprint> { fn from_str(s: &str) -> Result<Fingerprint> {
match sequoia_openpgp::Fingerprint::from_hex(s)? { match sequoia_openpgp::Fingerprint::from_hex(s)? {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)), sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
sequoia_openpgp::Fingerprint::Invalid(_) => sequoia_openpgp::Fingerprint::Invalid(_) => {
Err(anyhow!("'{}' is not a valid fingerprint", s)), Err(anyhow!("'{}' is not a valid fingerprint", s))
}
_ => Err(anyhow!("unknown fingerprint type")), _ => Err(anyhow!("unknown fingerprint type")),
} }
} }
@ -144,9 +143,7 @@ impl TryFrom<sequoia_openpgp::Fingerprint> for KeyID {
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> { fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
match fpr { match fpr {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a).into()), sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a).into()),
sequoia_openpgp::Fingerprint::Invalid(_) => { sequoia_openpgp::Fingerprint::Invalid(_) => Err(anyhow!("invalid fingerprint")),
Err(anyhow!("invalid fingerprint"))
},
_ => Err(anyhow!("unknown fingerprint type")), _ => Err(anyhow!("unknown fingerprint type")),
} }
} }
@ -172,7 +169,7 @@ impl From<Fingerprint> for KeyID {
impl fmt::Display for KeyID { impl fmt::Display for KeyID {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ::hex::ToHex; use hex::ToHex;
self.0.write_hex_upper(f) self.0.write_hex_upper(f)
} }
} }
@ -183,8 +180,9 @@ impl FromStr for KeyID {
fn from_str(s: &str) -> Result<KeyID> { fn from_str(s: &str) -> Result<KeyID> {
match sequoia_openpgp::KeyID::from_hex(s)? { match sequoia_openpgp::KeyID::from_hex(s)? {
sequoia_openpgp::KeyID::V4(a) => Ok(KeyID(a)), sequoia_openpgp::KeyID::V4(a) => Ok(KeyID(a)),
sequoia_openpgp::KeyID::Invalid(_) => sequoia_openpgp::KeyID::Invalid(_) => {
Err(anyhow!("'{}' is not a valid long key ID", s)), Err(anyhow!("'{}' is not a valid long key ID", s))
}
_ => Err(anyhow!("unknown keyid type")), _ => Err(anyhow!("unknown keyid type")),
} }
} }
@ -203,10 +201,11 @@ mod tests {
assert_eq!(c("Foo Bar <foo@example.org>").as_str(), "foo@example.org"); assert_eq!(c("Foo Bar <foo@example.org>").as_str(), "foo@example.org");
// FIXME gotta fix this // FIXME gotta fix this
// assert_eq!(c("foo@example.org <foo@example.org>").as_str(), "foo@example.org"); // assert_eq!(c("foo@example.org <foo@example.org>").as_str(), "foo@example.org");
assert_eq!(c("\"Foo Bar\" <foo@example.org>").as_str(), assert_eq!(
"foo@example.org"); c("\"Foo Bar\" <foo@example.org>").as_str(),
assert_eq!(c("foo@👍.example.org").as_str(), "foo@example.org"
"foo@xn--yp8h.example.org"); );
assert_eq!(c("foo@👍.example.org").as_str(), "foo@xn--yp8h.example.org");
assert_eq!(c("Foo@example.org").as_str(), "foo@example.org"); assert_eq!(c("Foo@example.org").as_str(), "foo@example.org");
assert_eq!(c("foo@EXAMPLE.ORG").as_str(), "foo@example.org"); assert_eq!(c("foo@EXAMPLE.ORG").as_str(), "foo@example.org");
} }

View File

@ -1,11 +1,11 @@
use super::Result;
use crate::openpgp::types::HashAlgorithm; use crate::openpgp::types::HashAlgorithm;
use zbase32; use zbase32;
use super::Result;
// cannibalized from // cannibalized from
// https://gitlab.com/sequoia-pgp/sequoia/blob/master/net/src/wkd.rs // https://gitlab.com/sequoia-pgp/sequoia/blob/master/net/src/wkd.rs
pub fn encode_wkd(address: impl AsRef<str>) -> Result<(String,String)> { pub fn encode_wkd(address: impl AsRef<str>) -> Result<(String, String)> {
let (local_part, domain) = split_address(address)?; let (local_part, domain) = split_address(address)?;
let local_part_encoded = encode_local_part(local_part); let local_part_encoded = encode_local_part(local_part);
@ -13,7 +13,7 @@ pub fn encode_wkd(address: impl AsRef<str>) -> Result<(String,String)> {
Ok((local_part_encoded, domain)) Ok((local_part_encoded, domain))
} }
fn split_address(email_address: impl AsRef<str>) -> Result<(String,String)> { fn split_address(email_address: impl AsRef<str>) -> Result<(String, String)> {
let email_address = email_address.as_ref(); let email_address = email_address.as_ref();
let v: Vec<&str> = email_address.split('@').collect(); let v: Vec<&str> = email_address.split('@').collect();
if v.len() != 2 { if v.len() != 2 {

View File

@ -1,22 +1,22 @@
use std::path::{Path,PathBuf}; use std::cmp::min;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::thread; use std::path::{Path, PathBuf};
use std::cmp::min;
use std::sync::Arc; use std::sync::Arc;
use std::thread;
use anyhow::Result; use anyhow::Result;
extern crate tempfile; extern crate tempfile;
extern crate sequoia_openpgp as openpgp; extern crate sequoia_openpgp as openpgp;
use openpgp::Packet;
use openpgp::parse::{PacketParser, PacketParserResult, Parse}; use openpgp::parse::{PacketParser, PacketParserResult, Parse};
use openpgp::Packet;
extern crate hagrid_database as database; extern crate hagrid_database as database;
use database::{Database, KeyDatabase, ImportResult}; use database::{Database, ImportResult, KeyDatabase};
use indicatif::{MultiProgress,ProgressBar,ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use HagridConfig; use HagridConfig;
@ -38,8 +38,7 @@ pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>
let config = config.clone(); let config = config.clone();
let multi_progress = multi_progress.clone(); let multi_progress = multi_progress.clone();
thread::spawn(move || { thread::spawn(move || {
import_from_files( import_from_files(&config, dry_run, input_file_chunk, multi_progress).unwrap();
&config, dry_run, input_file_chunk, multi_progress).unwrap();
}) })
}) })
.collect(); .collect();
@ -53,15 +52,12 @@ pub fn do_import(config: &HagridConfig, dry_run: bool, input_files: Vec<PathBuf>
Ok(()) Ok(())
} }
fn setup_chunks( fn setup_chunks(mut input_files: Vec<PathBuf>, num_threads: usize) -> Vec<Vec<PathBuf>> {
mut input_files: Vec<PathBuf>,
num_threads: usize,
) -> Vec<Vec<PathBuf>> {
let chunk_size = (input_files.len() + (num_threads - 1)) / num_threads; let chunk_size = (input_files.len() + (num_threads - 1)) / num_threads;
(0..num_threads) (0..num_threads)
.map(|_| { .map(|_| {
let len = input_files.len(); let len = input_files.len();
input_files.drain(0..min(chunk_size,len)).collect() input_files.drain(0..min(chunk_size, len)).collect()
}) })
.collect() .collect()
} }
@ -76,7 +72,7 @@ struct ImportStats<'a> {
count_unchanged: u64, count_unchanged: u64,
} }
impl <'a> ImportStats<'a> { impl<'a> ImportStats<'a> {
fn new(progress: &'a ProgressBar, filename: String) -> Self { fn new(progress: &'a ProgressBar, filename: String) -> Self {
ImportStats { ImportStats {
progress, progress,
@ -107,8 +103,13 @@ impl <'a> ImportStats<'a> {
} }
self.progress.set_message(&format!( self.progress.set_message(&format!(
"{}, imported {:5} keys, {:5} New {:5} Updated {:5} Unchanged {:5} Errors", "{}, imported {:5} keys, {:5} New {:5} Updated {:5} Unchanged {:5} Errors",
&self.filename, self.count_total, self.count_new, self.count_updated, self.count_unchanged, self.count_err)); &self.filename,
self.count_total,
self.count_new,
self.count_updated,
self.count_unchanged,
self.count_err
));
} }
} }
@ -137,10 +138,11 @@ fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgre
let bytes_total = input_file.metadata()?.len(); let bytes_total = input_file.metadata()?.len();
let progress_bar = multi_progress.add(ProgressBar::new(bytes_total)); let progress_bar = multi_progress.add(ProgressBar::new(bytes_total));
progress_bar progress_bar.set_style(
.set_style(ProgressStyle::default_bar() ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}") .template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}")
.progress_chars("##-")); .progress_chars("##-"),
);
progress_bar.set_message("Starting…"); progress_bar.set_message("Starting…");
let input_reader = &mut progress_bar.wrap_read(input_file); let input_reader = &mut progress_bar.wrap_read(input_file);
@ -156,8 +158,13 @@ fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgre
Packet::SecretKey(key) => key.fingerprint().to_hex(), Packet::SecretKey(key) => key.fingerprint().to_hex(),
_ => "Unknown".to_owned(), _ => "Unknown".to_owned(),
}; };
let error = format!("{}:{:05}:{}: {}", filename, stats.count_total, let error = format!(
key_fpr, e.to_string()); "{}:{:05}:{}: {}",
filename,
stats.count_total,
key_fpr,
e.to_string()
);
progress_bar.println(error); progress_bar.println(error);
} }
stats.update(result); stats.update(result);
@ -169,7 +176,7 @@ fn import_from_file(db: &KeyDatabase, input: &Path, multi_progress: &MultiProgre
fn read_file_to_tpks( fn read_file_to_tpks(
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
callback: &mut impl FnMut(Vec<Packet>) -> () callback: &mut impl FnMut(Vec<Packet>) -> (),
) -> Result<()> { ) -> Result<()> {
let mut ppr = PacketParser::from_reader(reader)?; let mut ppr = PacketParser::from_reader(reader)?;
let mut acc = Vec::new(); let mut acc = Vec::new();
@ -183,7 +190,7 @@ fn read_file_to_tpks(
if !acc.is_empty() { if !acc.is_empty() {
if let Packet::PublicKey(_) | Packet::SecretKey(_) = packet { if let Packet::PublicKey(_) | Packet::SecretKey(_) = packet {
callback(acc); callback(acc);
acc = vec!(); acc = vec![];
} }
} }
@ -194,10 +201,7 @@ fn read_file_to_tpks(
} }
fn import_key(db: &KeyDatabase, packets: Vec<Packet>) -> Result<ImportResult> { fn import_key(db: &KeyDatabase, packets: Vec<Packet>) -> Result<ImportResult> {
openpgp::Cert::from_packets(packets.into_iter()) openpgp::Cert::from_packets(packets.into_iter()).and_then(|tpk| db.merge(tpk))
.and_then(|tpk| {
db.merge(tpk)
})
} }
/* /*

View File

@ -1,12 +1,12 @@
extern crate anyhow; extern crate anyhow;
extern crate clap; extern crate clap;
extern crate tempfile;
extern crate sequoia_openpgp as openpgp;
extern crate hagrid_database as database; extern crate hagrid_database as database;
extern crate sequoia_openpgp as openpgp;
extern crate tempfile;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
extern crate toml;
extern crate indicatif; extern crate indicatif;
extern crate toml;
extern crate walkdir; extern crate walkdir;
use std::fs; use std::fs;
@ -15,7 +15,7 @@ use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use clap::{Arg, App, SubCommand}; use clap::{App, Arg, SubCommand};
mod import; mod import;
mod regenerate; mod regenerate;
@ -30,7 +30,7 @@ pub struct HagridConfigs {
// this is not an exact match - Rocket config has more complicated semantics // this is not an exact match - Rocket config has more complicated semantics
// than a plain toml file. // than a plain toml file.
// see also https://github.com/SergioBenitez/Rocket/issues/228 // see also https://github.com/SergioBenitez/Rocket/issues/228
#[derive(Deserialize,Clone)] #[derive(Deserialize, Clone)]
pub struct HagridConfig { pub struct HagridConfig {
_template_dir: Option<PathBuf>, _template_dir: Option<PathBuf>,
keys_internal_dir: Option<PathBuf>, keys_internal_dir: Option<PathBuf>,
@ -45,31 +45,39 @@ fn main() -> Result<()> {
let matches = App::new("Hagrid Control") let matches = App::new("Hagrid Control")
.version("0.1") .version("0.1")
.about("Control hagrid database externally") .about("Control hagrid database externally")
.arg(Arg::with_name("config") .arg(
Arg::with_name("config")
.short("c") .short("c")
.long("config") .long("config")
.value_name("FILE") .value_name("FILE")
.help("Sets a custom config file") .help("Sets a custom config file")
.takes_value(true)) .takes_value(true),
.arg(Arg::with_name("env") )
.arg(
Arg::with_name("env")
.short("e") .short("e")
.long("env") .long("env")
.value_name("ENVIRONMENT") .value_name("ENVIRONMENT")
.takes_value(true) .takes_value(true)
.default_value("prod") .default_value("prod")
.possible_values(&["dev","stage","prod"])) .possible_values(&["dev", "stage", "prod"]),
.subcommand(SubCommand::with_name("regenerate") )
.about("Regenerate symlink directory")) .subcommand(SubCommand::with_name("regenerate").about("Regenerate symlink directory"))
.subcommand(SubCommand::with_name("import") .subcommand(
SubCommand::with_name("import")
.about("Import keys into Hagrid") .about("Import keys into Hagrid")
.arg(Arg::with_name("dry run") .arg(
Arg::with_name("dry run")
.short("n") .short("n")
.long("dry-run") .long("dry-run")
.help("don't actually keep imported keys") .help("don't actually keep imported keys"),
) )
.arg(Arg::with_name("keyring files") .arg(
Arg::with_name("keyring files")
.required(true) .required(true)
.multiple(true))) .multiple(true),
),
)
.get_matches(); .get_matches();
let config_file = matches.value_of("config").unwrap_or("Rocket.toml"); let config_file = matches.value_of("config").unwrap_or("Rocket.toml");

View File

@ -3,12 +3,12 @@ use anyhow::Result;
use std::path::Path; use std::path::Path;
use std::time::Instant; use std::time::Instant;
use indicatif::{ProgressBar, ProgressStyle};
use walkdir::WalkDir; use walkdir::WalkDir;
use indicatif::{ProgressBar,ProgressStyle};
use HagridConfig;
use database::{Database,KeyDatabase,RegenerateResult};
use database::types::Fingerprint; use database::types::Fingerprint;
use database::{Database, KeyDatabase, RegenerateResult};
use HagridConfig;
struct RegenerateStats<'a> { struct RegenerateStats<'a> {
progress: &'a ProgressBar, progress: &'a ProgressBar,
@ -22,7 +22,7 @@ struct RegenerateStats<'a> {
kps_partial: u64, kps_partial: u64,
} }
impl <'a> RegenerateStats<'a> { impl<'a> RegenerateStats<'a> {
fn new(progress: &'a ProgressBar) -> Self { fn new(progress: &'a ProgressBar) -> Self {
Self { Self {
progress, progress,
@ -48,7 +48,7 @@ impl <'a> RegenerateStats<'a> {
Err(e) => { Err(e) => {
self.progress.println(format!("{}: {}", fpr, e.to_string())); self.progress.println(format!("{}: {}", fpr, e.to_string()));
self.count_err += 1; self.count_err += 1;
}, }
Ok(RegenerateResult::Updated) => self.count_updated += 1, Ok(RegenerateResult::Updated) => self.count_updated += 1,
Ok(RegenerateResult::Unchanged) => self.count_unchanged += 1, Ok(RegenerateResult::Unchanged) => self.count_unchanged += 1,
} }
@ -79,21 +79,27 @@ pub fn do_regenerate(config: &HagridConfig) -> Result<()> {
false, false,
)?; )?;
let published_dir = config.keys_external_dir.as_ref().unwrap().join("links").join("by-email"); let published_dir = config
.keys_external_dir
.as_ref()
.unwrap()
.join("links")
.join("by-email");
let dirs: Vec<_> = WalkDir::new(published_dir) let dirs: Vec<_> = WalkDir::new(published_dir)
.min_depth(1) .min_depth(1)
.max_depth(1) .max_depth(1)
.sort_by(|a,b| a.file_name().cmp(b.file_name())) .sort_by(|a, b| a.file_name().cmp(b.file_name()))
.into_iter() .into_iter()
.flatten() .flatten()
.map(|entry| entry.into_path()) .map(|entry| entry.into_path())
.collect(); .collect();
let progress_bar = ProgressBar::new(dirs.len() as u64); let progress_bar = ProgressBar::new(dirs.len() as u64);
progress_bar progress_bar.set_style(
.set_style(ProgressStyle::default_bar() ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}") .template("[{elapsed_precise}] {bar:40.cyan/blue} {msg}")
.progress_chars("##-")); .progress_chars("##-"),
);
let mut stats = RegenerateStats::new(&progress_bar); let mut stats = RegenerateStats::new(&progress_bar);
@ -106,14 +112,18 @@ pub fn do_regenerate(config: &HagridConfig) -> Result<()> {
Ok(()) Ok(())
} }
fn regenerate_dir_recursively(db: &KeyDatabase, stats: &mut RegenerateStats, dir: &Path) -> Result<()> { fn regenerate_dir_recursively(
db: &KeyDatabase,
stats: &mut RegenerateStats,
dir: &Path,
) -> Result<()> {
for path in WalkDir::new(dir) for path in WalkDir::new(dir)
.follow_links(true) .follow_links(true)
.into_iter() .into_iter()
.flatten() .flatten()
.filter(|e| e.file_type().is_file()) .filter(|e| e.file_type().is_file())
.map(|entry| entry.into_path()) { .map(|entry| entry.into_path())
{
let fpr = KeyDatabase::path_to_primary(&path).unwrap(); let fpr = KeyDatabase::path_to_primary(&path).unwrap();
let result = db.regenerate_links(&fpr); let result = db.regenerate_links(&fpr);
stats.update(result, fpr); stats.update(result, fpr);

View File

@ -55,7 +55,8 @@ lazy_static! {
} }
pub fn anonymize_address(email: &Email) -> Option<String> { pub fn anonymize_address(email: &Email) -> Option<String> {
email.as_str() email
.as_str()
.rsplit('@') .rsplit('@')
.next() .next()
.map(|domain| domain.to_lowercase()) .map(|domain| domain.to_lowercase())

View File

@ -8,14 +8,21 @@ use crate::database::types::Email;
lazy_static! { lazy_static! {
static ref KEY_UPLOAD: LabelCounter = static ref KEY_UPLOAD: LabelCounter =
LabelCounter::new("hagrid_key_upload", "Uploaded keys", &["result"]); LabelCounter::new("hagrid_key_upload", "Uploaded keys", &["result"]);
static ref MAIL_SENT: LabelCounter = LabelCounter::new(
static ref MAIL_SENT: LabelCounter = "hagrid_mail_sent",
LabelCounter::new("hagrid_mail_sent", "Sent verification mails", &["type", "domain"]); "Sent verification mails",
&["type", "domain"]
static ref KEY_ADDRESS_PUBLISHED: LabelCounter = );
LabelCounter::new("hagrid_key_address_published", "Verified email addresses", &["domain"]); static ref KEY_ADDRESS_PUBLISHED: LabelCounter = LabelCounter::new(
static ref KEY_ADDRESS_UNPUBLISHED: LabelCounter = "hagrid_key_address_published",
LabelCounter::new("hagrid_key_address_unpublished", "Unpublished email addresses", &["domain"]); "Verified email addresses",
&["domain"]
);
static ref KEY_ADDRESS_UNPUBLISHED: LabelCounter = LabelCounter::new(
"hagrid_key_address_unpublished",
"Unpublished email addresses",
&["domain"]
);
} }
pub fn register_counters(registry: &prometheus::Registry) { pub fn register_counters(registry: &prometheus::Registry) {
@ -58,7 +65,9 @@ impl LabelCounter {
} }
fn register(&self, registry: &prometheus::Registry) { fn register(&self, registry: &prometheus::Registry) {
registry.register(Box::new(self.prometheus_counter.clone())).unwrap(); registry
.register(Box::new(self.prometheus_counter.clone()))
.unwrap();
} }
fn inc(&self, values: &[&str]) { fn inc(&self, values: &[&str]) {

View File

@ -4,13 +4,13 @@ use std::convert::TryInto;
use std::path::PathBuf; use std::path::PathBuf;
extern crate anyhow; extern crate anyhow;
use anyhow::Result as Result; use anyhow::Result;
extern crate structopt; extern crate structopt;
use structopt::StructOpt; use structopt::StructOpt;
extern crate hagrid_database as database; extern crate hagrid_database as database;
use crate::database::{Query, Database, KeyDatabase}; use crate::database::{Database, KeyDatabase, Query};
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
#[structopt( #[structopt(
@ -54,32 +54,30 @@ fn real_main() -> Result<()> {
delete(&db, &opt.query.parse()?, opt.all_bindings, opt.all) delete(&db, &opt.query.parse()?, opt.all_bindings, opt.all)
} }
fn delete(db: &KeyDatabase, query: &Query, all_bindings: bool, mut all: bool) fn delete(db: &KeyDatabase, query: &Query, all_bindings: bool, mut all: bool) -> Result<()> {
-> Result<()> {
match query { match query {
Query::ByFingerprint(_) | Query::ByKeyID(_) => { Query::ByFingerprint(_) | Query::ByKeyID(_) => {
eprintln!("Fingerprint or KeyID given, deleting key and all \ eprintln!(
bindings."); "Fingerprint or KeyID given, deleting key and all \
bindings."
);
all = true; all = true;
}, }
_ => (), _ => (),
} }
let tpk = db.lookup(query)?.ok_or_else( let tpk = db
|| anyhow::format_err!("No TPK matching {:?}", query))?; .lookup(query)?
.ok_or_else(|| anyhow::format_err!("No TPK matching {:?}", query))?;
let fp: database::types::Fingerprint = tpk.fingerprint().try_into()?; let fp: database::types::Fingerprint = tpk.fingerprint().try_into()?;
let mut results = Vec::new(); let mut results = Vec::new();
// First, delete the bindings. // First, delete the bindings.
if all_bindings || all { if all_bindings || all {
results.push( results.push(("all bindings".into(), db.set_email_unpublished_all(&fp)));
("all bindings".into(),
db.set_email_unpublished_all(&fp)));
} else if let Query::ByEmail(ref email) = query { } else if let Query::ByEmail(ref email) = query {
results.push( results.push((email.to_string(), db.set_email_unpublished(&fp, email)));
(email.to_string(),
db.set_email_unpublished(&fp, email)));
} else { } else {
unreachable!() unreachable!()
} }
@ -110,12 +108,15 @@ fn delete(db: &KeyDatabase, query: &Query, all_bindings: bool, mut all: bool)
let mut err = Ok(()); let mut err = Ok(());
for (slug, result) in results { for (slug, result) in results {
eprintln!("{}: {}", slug, eprintln!(
"{}: {}",
slug,
if let Err(ref e) = result { if let Err(ref e) = result {
e.to_string() e.to_string()
} else { } else {
"Deleted".into() "Deleted".into()
}); }
);
if err.is_ok() { if err.is_ok() {
if let Err(e) = result { if let Err(e) = result {
err = Err(e); err = Err(e);

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,9 @@
use handlebars::{ use handlebars::{
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError,
}; };
use std::io; use std::io;
pub struct I18NHelper { pub struct I18NHelper {
catalogs: Vec<(&'static str, gettext::Catalog)>, catalogs: Vec<(&'static str, gettext::Catalog)>,
} }
@ -14,11 +13,9 @@ impl I18NHelper {
Self { catalogs } Self { catalogs }
} }
pub fn get_catalog( pub fn get_catalog(&self, lang: &str) -> &gettext::Catalog {
&self, let (_, ref catalog) = self
lang: &str, .catalogs
) -> &gettext::Catalog {
let (_, ref catalog) = self.catalogs
.iter() .iter()
.find(|(candidate, _)| *candidate == lang) .find(|(candidate, _)| *candidate == lang)
.unwrap_or_else(|| self.catalogs.get(0).unwrap()); .unwrap_or_else(|| self.catalogs.get(0).unwrap());
@ -75,10 +72,8 @@ impl HelperDef for I18NHelper {
let rerender = h let rerender = h
.param(1) .param(1)
.and_then(|p| p .and_then(|p| p.relative_path().map(|v| v == "rerender"))
.relative_path() .unwrap_or(false);
.map(|v| v == "rerender")
).unwrap_or(false);
let lang = context let lang = context
.data() .data()
@ -89,14 +84,15 @@ impl HelperDef for I18NHelper {
fn render_error_with<E>(e: E) -> RenderError fn render_error_with<E>(e: E) -> RenderError
where where
E: std::error::Error + Send + Sync + 'static E: std::error::Error + Send + Sync + 'static,
{ {
RenderError::from_error("Failed to render", e) RenderError::from_error("Failed to render", e)
} }
let response = self.lookup(lang, id); let response = self.lookup(lang, id);
if rerender { if rerender {
let data = rcx.evaluate(context, "this").unwrap(); let data = rcx.evaluate(context, "this").unwrap();
let response = reg.render_template(response, data.as_json()) let response = reg
.render_template(response, data.as_json())
.map_err(render_error_with)?; .map_err(render_error_with)?;
out.write(&response).map_err(render_error_with)?; out.write(&response).map_err(render_error_with)?;
} else { } else {

View File

@ -1,16 +1,21 @@
use rocket_i18n::I18n;
use crate::database::Query; use crate::database::Query;
use gettext_macros::i18n; use gettext_macros::i18n;
use rocket_i18n::I18n;
pub fn describe_query_error(i18n: &I18n, q: &Query) -> String { pub fn describe_query_error(i18n: &I18n, q: &Query) -> String {
match q { match q {
Query::ByFingerprint(fpr) => Query::ByFingerprint(fpr) => {
i18n!(i18n.catalog, "No key found for fingerprint {}"; fpr), i18n!(i18n.catalog, "No key found for fingerprint {}"; fpr)
Query::ByKeyID(key_id) => }
i18n!(i18n.catalog, "No key found for key id {}"; key_id), Query::ByKeyID(key_id) => {
Query::ByEmail(email) => i18n!(i18n.catalog, "No key found for key id {}"; key_id)
i18n!(i18n.catalog, "No key found for email address {}"; email), }
Query::InvalidShort() => i18n!(i18n.catalog, "Search by Short Key ID is not supported."), Query::ByEmail(email) => {
i18n!(i18n.catalog, "No key found for email address {}"; email)
}
Query::InvalidShort() => {
i18n!(i18n.catalog, "Search by Short Key ID is not supported.")
}
Query::Invalid() => i18n!(i18n.catalog, "Invalid search query."), Query::Invalid() => i18n!(i18n.catalog, "Invalid search query."),
} }
} }

View File

@ -1,14 +1,14 @@
use std::path::{PathBuf, Path}; use std::path::{Path, PathBuf};
use crate::counters;
use handlebars::Handlebars; use handlebars::Handlebars;
use lettre::{Transport as LettreTransport, SendmailTransport, file::FileTransport}; use lettre::builder::{EmailBuilder, Mailbox, MimeMultipartType, PartBuilder};
use lettre::builder::{EmailBuilder, PartBuilder, Mailbox, MimeMultipartType}; use lettre::{file::FileTransport, SendmailTransport, Transport as LettreTransport};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::counters;
use rocket_i18n::I18n;
use gettext_macros::i18n; use gettext_macros::i18n;
use rocket_i18n::I18n;
use rfc2047::rfc2047_encode; use rfc2047::rfc2047_encode;
@ -67,17 +67,26 @@ impl Service {
/// Sends mail by storing it in the given directory. /// Sends mail by storing it in the given directory.
pub fn filemail(from: &str, base_uri: &str, template_dir: &Path, path: &Path) -> Result<Self> { pub fn filemail(from: &str, base_uri: &str, template_dir: &Path, path: &Path) -> Result<Self> {
Self::new(from, base_uri, template_dir, Transport::Filemail(path.to_owned())) Self::new(
from,
base_uri,
template_dir,
Transport::Filemail(path.to_owned()),
)
} }
fn new(from: &str, base_uri: &str, template_dir: &Path, transport: Transport) fn new(from: &str, base_uri: &str, template_dir: &Path, transport: Transport) -> Result<Self> {
-> Result<Self> {
let templates = template_helpers::load_handlebars(template_dir)?; let templates = template_helpers::load_handlebars(template_dir)?;
let domain = let domain = url::Url::parse(base_uri)?
url::Url::parse(base_uri) .host_str()
?.host_str().ok_or_else(|| anyhow!("No host in base-URI")) .ok_or_else(|| anyhow!("No host in base-URI"))?
?.to_string(); .to_string();
Ok(Self { from: from.into(), domain, templates, transport }) Ok(Self {
from: from.into(),
domain,
templates,
transport,
})
} }
pub fn send_verification( pub fn send_verification(
@ -86,7 +95,7 @@ impl Service {
base_uri: &str, base_uri: &str,
tpk_name: String, tpk_name: String,
userid: &Email, userid: &Email,
token: &str token: &str,
) -> Result<()> { ) -> Result<()> {
let ctx = context::Verification { let ctx = context::Verification {
lang: i18n.lang.to_string(), lang: i18n.lang.to_string(),
@ -151,7 +160,7 @@ impl Service {
base_uri: &str, base_uri: &str,
tpk_name: String, tpk_name: String,
userid: &Email, userid: &Email,
token: &str token: &str,
) -> Result<()> { ) -> Result<()> {
let ctx = context::Welcome { let ctx = context::Welcome {
lang: "en".to_owned(), lang: "en".to_owned(),
@ -176,12 +185,16 @@ impl Service {
&self, &self,
template: &str, template: &str,
locale: &str, locale: &str,
ctx: impl Serialize ctx: impl Serialize,
) -> Result<(String, String)> { ) -> Result<(String, String)> {
let html = self.templates.render(&format!("{}/{}.htm", locale, template), &ctx) let html = self
.templates
.render(&format!("{}/{}.htm", locale, template), &ctx)
.or_else(|_| self.templates.render(&format!("{}.htm", template), &ctx)) .or_else(|_| self.templates.render(&format!("{}.htm", template), &ctx))
.map_err(|_| anyhow!("Email template failed to render"))?; .map_err(|_| anyhow!("Email template failed to render"))?;
let txt = self.templates.render(&format!("{}/{}.txt", locale, template), &ctx) let txt = self
.templates
.render(&format!("{}/{}.txt", locale, template), &ctx)
.or_else(|_| self.templates.render(&format!("{}.txt", template), &ctx)) .or_else(|_| self.templates.render(&format!("{}.txt", template), &ctx))
.map_err(|_| anyhow!("Email template failed to render"))?; .map_err(|_| anyhow!("Email template failed to render"))?;
@ -194,7 +207,7 @@ impl Service {
subject: &str, subject: &str,
template: &str, template: &str,
locale: &str, locale: &str,
ctx: impl Serialize ctx: impl Serialize,
) -> Result<()> { ) -> Result<()> {
let (html, txt) = self.render_template(template, locale, ctx)?; let (html, txt) = self.render_template(template, locale, ctx)?;
@ -235,18 +248,17 @@ impl Service {
Transport::Sendmail => { Transport::Sendmail => {
let mut transport = SendmailTransport::new(); let mut transport = SendmailTransport::new();
transport.send(email)?; transport.send(email)?;
}, }
Transport::Filemail(ref path) => { Transport::Filemail(ref path) => {
let mut transport = FileTransport::new(path); let mut transport = FileTransport::new(path);
transport.send(email)?; transport.send(email)?;
}, }
} }
Ok(()) Ok(())
} }
} }
// for some reason, this is no longer public in lettre itself // for some reason, this is no longer public in lettre itself
// FIXME replace with builtin struct on lettre update // FIXME replace with builtin struct on lettre update
// see https://github.com/lettre/lettre/blob/master/lettre/src/file/mod.rs#L41 // see https://github.com/lettre/lettre/blob/master/lettre/src/file/mod.rs#L41
@ -281,9 +293,9 @@ pub fn pop_mail(dir: &Path) -> Result<Option<String>> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use tempfile::{tempdir, TempDir}; use gettext_macros::include_i18n;
use gettext_macros::{include_i18n};
use std::str::FromStr; use std::str::FromStr;
use tempfile::{tempdir, TempDir};
const BASEDIR: &str = "http://localhost/"; const BASEDIR: &str = "http://localhost/";
const FROM: &str = "test@localhost"; const FROM: &str = "test@localhost";
@ -291,12 +303,22 @@ mod test {
fn configure_i18n(lang: &'static str) -> I18n { fn configure_i18n(lang: &'static str) -> I18n {
let langs = include_i18n!(); let langs = include_i18n!();
let catalog = langs.clone().into_iter().find(|(l, _)| *l == lang).unwrap().1; let catalog = langs
.clone()
.into_iter()
.find(|(l, _)| *l == lang)
.unwrap()
.1;
rocket_i18n::I18n { catalog, lang } rocket_i18n::I18n { catalog, lang }
} }
fn configure_mail() -> (Service, TempDir) { fn configure_mail() -> (Service, TempDir) {
let template_dir: PathBuf = ::std::env::current_dir().unwrap().join("dist/email-templates").to_str().unwrap().into(); let template_dir: PathBuf = ::std::env::current_dir()
.unwrap()
.join("dist/email-templates")
.to_str()
.unwrap()
.into();
let tempdir = tempdir().unwrap(); let tempdir = tempdir().unwrap();
let service = Service::filemail(FROM, BASEDIR, &template_dir, tempdir.path()).unwrap(); let service = Service::filemail(FROM, BASEDIR, &template_dir, tempdir.path()).unwrap();
(service, tempdir) (service, tempdir)
@ -328,7 +350,9 @@ mod test {
assert!(headers.contains(&("Content-Type", "text/html; charset=utf-8"))); assert!(headers.contains(&("Content-Type", "text/html; charset=utf-8")));
assert!(headers.contains(&("From", "<test@localhost>"))); assert!(headers.contains(&("From", "<test@localhost>")));
assert!(headers.contains(&("To", "<recipient@example.org>"))); assert!(headers.contains(&("To", "<recipient@example.org>")));
assert_header(&headers, "Content-Type", |v| v.starts_with("multipart/alternative")); assert_header(&headers, "Content-Type", |v| {
v.starts_with("multipart/alternative")
});
assert_header(&headers, "Date", |v| v.contains("+0000")); assert_header(&headers, "Date", |v| v.contains("+0000"));
assert_header(&headers, "Message-ID", |v| v.contains("@localhost>")); assert_header(&headers, "Message-ID", |v| v.contains("@localhost>"));
} }
@ -345,7 +369,14 @@ mod test {
let i18n = configure_i18n("en"); let i18n = configure_i18n("en");
let recipient = Email::from_str(TO).unwrap(); let recipient = Email::from_str(TO).unwrap();
mail.send_verification(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap(); mail.send_verification(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap(); let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content); check_headers(&mail_content);
@ -363,7 +394,14 @@ mod test {
let i18n = configure_i18n("ja"); let i18n = configure_i18n("ja");
let recipient = Email::from_str(TO).unwrap(); let recipient = Email::from_str(TO).unwrap();
mail.send_verification(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap(); mail.send_verification(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap(); let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content); check_headers(&mail_content);
@ -373,8 +411,9 @@ mod test {
assert!(mail_content.contains("test/verify/token")); assert!(mail_content.contains("test/verify/token"));
assert!(mail_content.contains("test/about")); assert!(mail_content.contains("test/about"));
assert!(mail_content.contains("あなたのメールアド")); assert!(mail_content.contains("あなたのメールアド"));
assert!(mail_content.contains("Subject: =?utf-8?q?localhost=E3=81=AE=E3=81=82=E3=81=AA=E3=81=9F=E3=81=AE?=")); assert!(mail_content.contains(
"Subject: =?utf-8?q?localhost=E3=81=AE=E3=81=82=E3=81=AA=E3=81=9F=E3=81=AE?="
));
} }
#[test] #[test]
@ -383,7 +422,14 @@ mod test {
let i18n = configure_i18n("en"); let i18n = configure_i18n("en");
let recipient = Email::from_str(TO).unwrap(); let recipient = Email::from_str(TO).unwrap();
mail.send_manage_token(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap(); mail.send_manage_token(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap(); let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content); check_headers(&mail_content);
@ -401,7 +447,14 @@ mod test {
let i18n = configure_i18n("ja"); let i18n = configure_i18n("ja");
let recipient = Email::from_str(TO).unwrap(); let recipient = Email::from_str(TO).unwrap();
mail.send_manage_token(&i18n, "test", "fingerprintoo".to_owned(), &recipient, "token").unwrap(); mail.send_manage_token(
&i18n,
"test",
"fingerprintoo".to_owned(),
&recipient,
"token",
)
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap(); let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content); check_headers(&mail_content);
@ -412,7 +465,9 @@ mod test {
assert!(mail_content.contains("testtoken")); assert!(mail_content.contains("testtoken"));
assert!(mail_content.contains("test/about")); assert!(mail_content.contains("test/about"));
assert!(mail_content.contains("この鍵の掲示されたア")); assert!(mail_content.contains("この鍵の掲示されたア"));
assert!(mail_content.contains("Subject: =?utf-8?q?localhost=E3=81=AE=E9=8D=B5=E3=82=92=E7=AE=A1=E7=90=86?=")); assert!(mail_content.contains(
"Subject: =?utf-8?q?localhost=E3=81=AE=E9=8D=B5=E3=82=92=E7=AE=A1=E7=90=86?="
));
} }
#[test] #[test]
@ -420,7 +475,8 @@ mod test {
let (mail, tempdir) = configure_mail(); let (mail, tempdir) = configure_mail();
let recipient = Email::from_str(TO).unwrap(); let recipient = Email::from_str(TO).unwrap();
mail.send_welcome("test", "fingerprintoo".to_owned(), &recipient, "token").unwrap(); mail.send_welcome("test", "fingerprintoo".to_owned(), &recipient, "token")
.unwrap();
let mail_content = pop_mail(tempdir.path()).unwrap().unwrap(); let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();
check_headers(&mail_content); check_headers(&mail_content);
@ -432,4 +488,3 @@ mod test {
assert!(mail_content.contains("first time")); assert!(mail_content.contains("first time"));
} }
} }

View File

@ -2,7 +2,7 @@
#[macro_use] #[macro_use]
extern crate anyhow; extern crate anyhow;
use anyhow::Result as Result; use anyhow::Result;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
@ -23,18 +23,18 @@ init_i18n!("hagrid", en, de, ja);
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
init_i18n!("hagrid", en, de, fr, it, ja, nb, pl, tr, zh_Hans, ko, nl, ru, ar, sv, es, ro); init_i18n!("hagrid", en, de, fr, it, ja, nb, pl, tr, zh_Hans, ko, nl, ru, ar, sv, es, ro);
mod mail;
mod anonymize_utils; mod anonymize_utils;
mod tokens;
mod sealed_state;
mod rate_limiter;
mod dump;
mod counters; mod counters;
mod dump;
mod gettext_strings;
mod i18n; mod i18n;
mod i18n_helpers; mod i18n_helpers;
mod gettext_strings; mod mail;
mod web; mod rate_limiter;
mod sealed_state;
mod template_helpers; mod template_helpers;
mod tokens;
mod web;
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {

View File

@ -1,6 +1,6 @@
use std::sync::Mutex;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{Instant,Duration}; use std::sync::Mutex;
use std::time::{Duration, Instant};
pub struct RateLimiter { pub struct RateLimiter {
locked_map: Mutex<HashMap<String, Instant>>, locked_map: Mutex<HashMap<String, Instant>>,
@ -23,7 +23,8 @@ impl RateLimiter {
self.maybe_cleanup(); self.maybe_cleanup();
let mut locked_map = self.locked_map.lock().unwrap(); let mut locked_map = self.locked_map.lock().unwrap();
let action_ok = locked_map.get(&identifier) let action_ok = locked_map
.get(&identifier)
.map(|instant| instant.elapsed()) .map(|instant| instant.elapsed())
.map(|duration| duration >= self.timeout) .map(|duration| duration >= self.timeout)
.unwrap_or(true); .unwrap_or(true);
@ -35,7 +36,8 @@ impl RateLimiter {
pub fn action_check(&self, identifier: String) -> bool { pub fn action_check(&self, identifier: String) -> bool {
let locked_map = self.locked_map.lock().unwrap(); let locked_map = self.locked_map.lock().unwrap();
locked_map.get(&identifier) locked_map
.get(&identifier)
.map(|instant| instant.elapsed()) .map(|instant| instant.elapsed())
.map(|duration| duration >= self.timeout) .map(|duration| duration >= self.timeout)
.unwrap_or(true) .unwrap_or(true)

View File

@ -1,8 +1,8 @@
use ring::aead::{seal_in_place, open_in_place, Algorithm, AES_256_GCM}; use ring::aead::{open_in_place, seal_in_place, Algorithm, AES_256_GCM};
use ring::aead::{OpeningKey, SealingKey}; use ring::aead::{OpeningKey, SealingKey};
use ring::rand::{SecureRandom, SystemRandom};
use ring::hmac;
use ring::digest; use ring::digest;
use ring::hmac;
use ring::rand::{SecureRandom, SystemRandom};
// Keep these in sync, and keep the key len synced with the `private` docs as // Keep these in sync, and keep the key len synced with the `private` docs as
// well as the `KEYS_INFO` const in secure::Key. // well as the `KEYS_INFO` const in secure::Key.
@ -23,7 +23,10 @@ impl SealedState {
let sealing_key = SealingKey::new(ALGO, key.as_ref()).expect("sealing key creation"); let sealing_key = SealingKey::new(ALGO, key.as_ref()).expect("sealing key creation");
let opening_key = OpeningKey::new(ALGO, key.as_ref()).expect("sealing key creation"); let opening_key = OpeningKey::new(ALGO, key.as_ref()).expect("sealing key creation");
SealedState { sealing_key, opening_key } SealedState {
sealing_key,
opening_key,
}
} }
pub fn unseal(&self, mut data: Vec<u8>) -> Result<String, &'static str> { pub fn unseal(&self, mut data: Vec<u8>) -> Result<String, &'static str> {
@ -43,7 +46,9 @@ impl SealedState {
data = vec![0; NONCE_LEN + input.len() + overhead]; data = vec![0; NONCE_LEN + input.len() + overhead];
let (nonce, in_out) = data.split_at_mut(NONCE_LEN); let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
SystemRandom::new().fill(nonce).expect("couldn't random fill nonce"); SystemRandom::new()
.fill(nonce)
.expect("couldn't random fill nonce");
in_out[..input.len()].copy_from_slice(input.as_bytes()); in_out[..input.len()].copy_from_slice(input.as_bytes());
seal_in_place(&self.sealing_key, nonce, &[], in_out, overhead).expect("in-place seal") seal_in_place(&self.sealing_key, nonce, &[], in_out, overhead).expect("in-place seal")

View File

@ -1,12 +1,12 @@
use std::path::{Path, PathBuf};
use std::collections::HashSet; use std::collections::HashSet;
use std::path::{Path, PathBuf};
use handlebars::Handlebars; use handlebars::Handlebars;
use gettext_macros::include_i18n; use gettext_macros::include_i18n;
use crate::Result;
use crate::i18n::I18NHelper; use crate::i18n::I18NHelper;
use crate::Result;
#[derive(Debug)] #[derive(Debug)]
pub struct TemplateOverrides(String, HashSet<String>); pub struct TemplateOverrides(String, HashSet<String>);
@ -28,7 +28,10 @@ impl TemplateOverrides {
} }
} }
fn load_localized_template_names(template_path: &Path, localized_dir: &str) -> Result<HashSet<String>> { fn load_localized_template_names(
template_path: &Path,
localized_dir: &str,
) -> Result<HashSet<String>> {
let language_glob = template_path.join(localized_dir).join("*"); let language_glob = template_path.join(localized_dir).join("*");
glob::glob(language_glob.to_str().expect("valid glob path string")) glob::glob(language_glob.to_str().expect("valid glob path string"))
.unwrap() .unwrap()
@ -41,7 +44,8 @@ fn load_localized_template_names(template_path: &Path, localized_dir: &str) -> R
.flatten() .flatten()
.map(move |path| { .map(move |path| {
// TODO this is a hack // TODO this is a hack
let template_name = remove_extension(remove_extension(path.strip_prefix(&template_path)?)); let template_name =
remove_extension(remove_extension(path.strip_prefix(&template_path)?));
Ok(template_name.to_string_lossy().into_owned()) Ok(template_name.to_string_lossy().into_owned())
}) })
}) })
@ -71,12 +75,11 @@ fn remove_extension<P: AsRef<Path>>(path: P) -> PathBuf {
let path = path.as_ref(); let path = path.as_ref();
let stem = match path.file_stem() { let stem = match path.file_stem() {
Some(stem) => stem, Some(stem) => stem,
None => return path.to_path_buf() None => return path.to_path_buf(),
}; };
match path.parent() { match path.parent() {
Some(parent) => parent.join(stem), Some(parent) => parent.join(stem),
None => PathBuf::from(stem) None => PathBuf::from(stem),
} }
} }

View File

@ -1,17 +1,16 @@
use crate::sealed_state::SealedState; use crate::sealed_state::SealedState;
use serde::{Serialize,de::DeserializeOwned};
use crate::Result; use crate::Result;
use serde::{de::DeserializeOwned, Serialize};
pub trait StatelessSerializable : Serialize + DeserializeOwned { pub trait StatelessSerializable: Serialize + DeserializeOwned {}
}
pub struct Service { pub struct Service {
sealed_state: SealedState, sealed_state: SealedState,
validity: u64, validity: u64,
} }
#[derive(Serialize,Deserialize)] #[derive(Serialize, Deserialize)]
struct Token { struct Token {
#[serde(rename = "c")] #[serde(rename = "c")]
creation: u64, creation: u64,
@ -22,7 +21,10 @@ struct Token {
impl Service { impl Service {
pub fn init(secret: &str, validity: u64) -> Self { pub fn init(secret: &str, validity: u64) -> Self {
let sealed_state = SealedState::new(secret); let sealed_state = SealedState::new(secret);
Service { sealed_state, validity } Service {
sealed_state,
validity,
}
} }
pub fn create(&self, payload_content: &impl StatelessSerializable) -> String { pub fn create(&self, payload_content: &impl StatelessSerializable) -> String {
@ -37,13 +39,17 @@ impl Service {
} }
pub fn check<T>(&self, token_encoded: &str) -> Result<T> pub fn check<T>(&self, token_encoded: &str) -> Result<T>
where T: StatelessSerializable { where
T: StatelessSerializable,
{
let token_sealed = base64::decode_config(&token_encoded, base64::URL_SAFE_NO_PAD) let token_sealed = base64::decode_config(&token_encoded, base64::URL_SAFE_NO_PAD)
.map_err(|_| anyhow!("invalid b64"))?; .map_err(|_| anyhow!("invalid b64"))?;
let token_str = self.sealed_state.unseal(token_sealed) let token_str = self
.sealed_state
.unseal(token_sealed)
.map_err(|_| anyhow!("failed to validate"))?; .map_err(|_| anyhow!("failed to validate"))?;
let token: Token = serde_json::from_str(&token_str) let token: Token =
.map_err(|_| anyhow!("failed to deserialize"))?; serde_json::from_str(&token_str).map_err(|_| anyhow!("failed to deserialize"))?;
let elapsed = current_time() - token.creation; let elapsed = current_time() - token.creation;
if elapsed > self.validity { if elapsed > self.validity {
@ -55,13 +61,15 @@ impl Service {
Ok(payload) Ok(payload)
} }
} }
#[cfg(not(test))] #[cfg(not(test))]
fn current_time() -> u64 { fn current_time() -> u64 {
use std::time::SystemTime; use std::time::SystemTime;
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
} }
#[cfg(test)] #[cfg(test)]
@ -73,23 +81,23 @@ fn current_time() -> u64 {
mod tests { mod tests {
use super::*; use super::*;
#[derive(Debug,Serialize,Deserialize,Clone,PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct TestStruct1 { struct TestStruct1 {
payload: String, payload: String,
} }
impl StatelessSerializable for TestStruct1 { impl StatelessSerializable for TestStruct1 {}
}
#[derive(Debug,Serialize,Deserialize,Clone,PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct TestStruct2 { struct TestStruct2 {
something: String, something: String,
} }
impl StatelessSerializable for TestStruct2 { impl StatelessSerializable for TestStruct2 {}
}
#[test] #[test]
fn test_create_check() { fn test_create_check() {
let payload = TestStruct1 { payload: "hello".to_owned() }; let payload = TestStruct1 {
payload: "hello".to_owned(),
};
let mt = Service::init("secret", 60); let mt = Service::init("secret", 60);
let token = mt.create(&payload); let token = mt.create(&payload);
// println!("{}", &token); // println!("{}", &token);
@ -102,7 +110,9 @@ mod tests {
#[test] #[test]
fn test_ok() { fn test_ok() {
let payload = TestStruct1 { payload: "hello".to_owned() }; let payload = TestStruct1 {
payload: "hello".to_owned(),
};
let token = "rwM_S9gZaRQaf6DLvmWtZSipQhH_G5ronSIJv2FrMdwGBPSYYQ-1jaP58dTHU5WuC14vb8jxmz2Xf_b3pqzpCGTEJj9drm4t"; let token = "rwM_S9gZaRQaf6DLvmWtZSipQhH_G5ronSIJv2FrMdwGBPSYYQ-1jaP58dTHU5WuC14vb8jxmz2Xf_b3pqzpCGTEJj9drm4t";
let mt = Service::init("secret", 60); let mt = Service::init("secret", 60);
@ -113,7 +123,9 @@ mod tests {
#[test] #[test]
fn test_bad_type() { fn test_bad_type() {
let payload = TestStruct1 { payload: "hello".to_owned() }; let payload = TestStruct1 {
payload: "hello".to_owned(),
};
let mt = Service::init("secret", 60); let mt = Service::init("secret", 60);
let token = mt.create(&payload); let token = mt.create(&payload);

View File

@ -3,17 +3,13 @@ use std::io;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use crate::dump::{self, Kind}; use crate::dump::{self, Kind};
use crate::web::MyResponse;
use crate::i18n_helpers::describe_query_error; use crate::i18n_helpers::describe_query_error;
use crate::web::MyResponse;
use crate::database::{Database, KeyDatabase, Query}; use crate::database::{Database, KeyDatabase, Query};
#[get("/debug?<q>")] #[get("/debug?<q>")]
pub fn debug_info( pub fn debug_info(db: &rocket::State<KeyDatabase>, i18n: I18n, q: String) -> MyResponse {
db: &rocket::State<KeyDatabase>,
i18n: I18n,
q: String,
) -> MyResponse {
let query = match q.parse::<Query>() { let query = match q.parse::<Query>() {
Ok(query) => query, Ok(query) => query,
Err(_) => return MyResponse::bad_request_plain("bad request"), Err(_) => return MyResponse::bad_request_plain("bad request"),
@ -38,11 +34,9 @@ pub fn debug_info(
32 * 4 + 80, 32 * 4 + 80,
); );
match dump_result { match dump_result {
Ok(Kind::Cert) => { Ok(Kind::Cert) => match String::from_utf8(result) {
match String::from_utf8(result) {
Ok(dump_text) => MyResponse::plain(dump_text), Ok(dump_text) => MyResponse::plain(dump_text),
Err(e) => MyResponse::ise(e.into()), Err(e) => MyResponse::ise(e.into()),
}
}, },
Ok(_) => MyResponse::ise(anyhow!("Internal parsing error!")), Ok(_) => MyResponse::ise(anyhow!("Internal parsing error!")),
Err(e) => MyResponse::ise(e), Err(e) => MyResponse::ise(e),

View File

@ -1,29 +1,29 @@
use std::fmt; use std::fmt;
use std::time::SystemTime;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::SystemTime;
use rocket::Data;
use rocket::form::{Form, ValueField}; use rocket::form::{Form, ValueField};
use rocket::outcome::Outcome;
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::request::{self, Request, FromRequest}; use rocket::outcome::Outcome;
use rocket::request::{self, FromRequest, Request};
use rocket::Data;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use url::percent_encoding::{DEFAULT_ENCODE_SET, utf8_percent_encode}; use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use crate::database::{Database, Query, KeyDatabase};
use crate::database::types::{Email, Fingerprint, KeyID}; use crate::database::types::{Email, Fingerprint, KeyID};
use crate::database::{Database, KeyDatabase, Query};
use crate::rate_limiter::RateLimiter;
use crate::i18n_helpers::describe_query_error; use crate::i18n_helpers::describe_query_error;
use crate::rate_limiter::RateLimiter;
use crate::tokens; use crate::tokens;
use crate::web;
use crate::mail; use crate::mail;
use crate::web::{RequestOrigin, MyResponse, vks_web}; use crate::web;
use crate::web::vks::response::UploadResponse;
use crate::web::vks::response::EmailStatus; use crate::web::vks::response::EmailStatus;
use crate::web::vks::response::UploadResponse;
use crate::web::{vks_web, MyResponse, RequestOrigin};
#[derive(Debug)] #[derive(Debug)]
pub enum Hkp { pub enum Hkp {
@ -31,17 +31,17 @@ pub enum Hkp {
KeyID { keyid: KeyID, index: bool }, KeyID { keyid: KeyID, index: bool },
ShortKeyID { query: String, index: bool }, ShortKeyID { query: String, index: bool },
Email { email: Email, index: bool }, Email { email: Email, index: bool },
Invalid { query: String, }, Invalid { query: String },
} }
impl fmt::Display for Hkp { impl fmt::Display for Hkp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Hkp::Fingerprint{ ref fpr,.. } => write!(f, "{}", fpr), Hkp::Fingerprint { ref fpr, .. } => write!(f, "{}", fpr),
Hkp::KeyID{ ref keyid,.. } => write!(f, "{}", keyid), Hkp::KeyID { ref keyid, .. } => write!(f, "{}", keyid),
Hkp::Email{ ref email,.. } => write!(f, "{}", email), Hkp::Email { ref email, .. } => write!(f, "{}", email),
Hkp::ShortKeyID{ ref query,.. } => write!(f, "{}", query), Hkp::ShortKeyID { ref query, .. } => write!(f, "{}", query),
Hkp::Invalid{ ref query } => write!(f, "{}", query), Hkp::Invalid { ref query } => write!(f, "{}", query),
} }
} }
} }
@ -53,11 +53,13 @@ impl<'r> FromRequest<'r> for Hkp {
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Hkp, ()> { async fn from_request(request: &'r Request<'_>) -> request::Outcome<Hkp, ()> {
use std::str::FromStr; use std::str::FromStr;
let query = request.uri().query().map(|q| q.as_str()).unwrap_or_default(); let query = request
.uri()
.query()
.map(|q| q.as_str())
.unwrap_or_default();
let fields = Form::values(query) let fields = Form::values(query)
.map(|ValueField { name, value }| { .map(|ValueField { name, value }| (name.to_string(), value.to_string()))
(name.to_string(), value.to_string())
})
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
if fields.contains_key("search") if fields.contains_key("search")
@ -71,40 +73,28 @@ impl<'r> FromRequest<'r> for Hkp {
let maybe_fpr = Fingerprint::from_str(&search); let maybe_fpr = Fingerprint::from_str(&search);
let maybe_keyid = KeyID::from_str(&search); let maybe_keyid = KeyID::from_str(&search);
let looks_like_short_key_id = !search.contains('@') && let looks_like_short_key_id = !search.contains('@')
(search.starts_with("0x") && search.len() < 16 || search.len() == 8); && (search.starts_with("0x") && search.len() < 16 || search.len() == 8);
if looks_like_short_key_id { if looks_like_short_key_id {
Outcome::Success(Hkp::ShortKeyID { Outcome::Success(Hkp::ShortKeyID {
query: search, query: search,
index, index,
}) })
} else if let Ok(fpr) = maybe_fpr { } else if let Ok(fpr) = maybe_fpr {
Outcome::Success(Hkp::Fingerprint { Outcome::Success(Hkp::Fingerprint { fpr, index })
fpr,
index,
})
} else if let Ok(keyid) = maybe_keyid { } else if let Ok(keyid) = maybe_keyid {
Outcome::Success(Hkp::KeyID { Outcome::Success(Hkp::KeyID { keyid, index })
keyid,
index,
})
} else { } else {
match Email::from_str(&search) { match Email::from_str(&search) {
Ok(email) => { Ok(email) => Outcome::Success(Hkp::Email { email, index }),
Outcome::Success(Hkp::Email { Err(_) => Outcome::Success(Hkp::Invalid {
email,
index,
})
}
Err(_) => {
Outcome::Success(Hkp::Invalid{
query: search.to_string(), query: search.to_string(),
}) }),
} }
} }
} } else if fields
} else if fields.get("op").map(|x| x == "vindex" .get("op")
|| x.starts_with("x-")) .map(|x| x == "vindex" || x.starts_with("x-"))
.unwrap_or(false) .unwrap_or(false)
{ {
Outcome::Failure((Status::NotImplemented, ())) Outcome::Failure((Status::NotImplemented, ()))
@ -131,7 +121,11 @@ pub async fn pks_add_form_data(
} }
} }
#[post("/pks/add", format = "application/x-www-form-urlencoded", data = "<data>")] #[post(
"/pks/add",
format = "application/x-www-form-urlencoded",
data = "<data>"
)]
pub async fn pks_add_form( pub async fn pks_add_form(
origin: RequestOrigin, origin: RequestOrigin,
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
@ -142,8 +136,24 @@ pub async fn pks_add_form(
data: Data<'_>, data: Data<'_>,
) -> MyResponse { ) -> MyResponse {
match vks_web::process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await { match vks_web::process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await {
Ok(UploadResponse::Ok { is_new_key, key_fpr, primary_uid, token, status, .. }) => { Ok(UploadResponse::Ok {
let msg = pks_add_ok(&origin, mail_service, rate_limiter, token, status, is_new_key, key_fpr, primary_uid); is_new_key,
key_fpr,
primary_uid,
token,
status,
..
}) => {
let msg = pks_add_ok(
&origin,
mail_service,
rate_limiter,
token,
status,
is_new_key,
key_fpr,
primary_uid,
);
MyResponse::plain(msg) MyResponse::plain(msg)
} }
Ok(_) => { Ok(_) => {
@ -165,16 +175,17 @@ fn pks_add_ok(
primary_uid: Option<Email>, primary_uid: Option<Email>,
) -> String { ) -> String {
if primary_uid.is_none() { if primary_uid.is_none() {
return format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = origin.get_base_uri()) return format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = origin.get_base_uri());
} }
let primary_uid = primary_uid.unwrap(); let primary_uid = primary_uid.unwrap();
if is_new_key { if is_new_key {
if send_welcome_mail(origin, mail_service, key_fpr, &primary_uid, token) { if send_welcome_mail(origin, mail_service, key_fpr, &primary_uid, token) {
rate_limiter.action_perform(format!("hkp-sent-{}", &primary_uid)); rate_limiter.action_perform(format!("hkp-sent-{}", &primary_uid));
return "Upload successful. This is a new key, a welcome email has been sent.".to_string(); return "Upload successful. This is a new key, a welcome email has been sent."
.to_string();
} }
return format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = origin.get_base_uri()) return format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = origin.get_base_uri());
} }
let has_unverified = status.iter().any(|(_, v)| *v == EmailStatus::Unpublished); let has_unverified = status.iter().any(|(_, v)| *v == EmailStatus::Unpublished);
@ -182,7 +193,7 @@ fn pks_add_ok(
return "Upload successful.".to_string(); return "Upload successful.".to_string();
} }
return format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = origin.get_base_uri()) return format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = origin.get_base_uri());
} }
fn send_welcome_mail( fn send_welcome_mail(
@ -192,25 +203,21 @@ fn send_welcome_mail(
primary_uid: &Email, primary_uid: &Email,
token: String, token: String,
) -> bool { ) -> bool {
mail_service.send_welcome(origin.get_base_uri(), fpr, primary_uid, &token).is_ok() mail_service
.send_welcome(origin.get_base_uri(), fpr, primary_uid, &token)
.is_ok()
} }
#[get("/pks/lookup")] #[get("/pks/lookup")]
pub fn pks_lookup( pub fn pks_lookup(db: &rocket::State<KeyDatabase>, i18n: I18n, key: Hkp) -> MyResponse {
db: &rocket::State<KeyDatabase>,
i18n: I18n,
key: Hkp
) -> MyResponse {
let (query, index) = match key { let (query, index) = match key {
Hkp::Fingerprint { fpr, index } => Hkp::Fingerprint { fpr, index } => (Query::ByFingerprint(fpr), index),
(Query::ByFingerprint(fpr), index), Hkp::KeyID { keyid, index } => (Query::ByKeyID(keyid), index),
Hkp::KeyID { keyid, index } => Hkp::Email { email, index } => (Query::ByEmail(email), index),
(Query::ByKeyID(keyid), index),
Hkp::Email { email, index } => {
(Query::ByEmail(email), index)
}
Hkp::ShortKeyID { query: _, .. } => { Hkp::ShortKeyID { query: _, .. } => {
return MyResponse::bad_request_plain("Search by short key ids is not supported, sorry!"); return MyResponse::bad_request_plain(
"Search by short key ids is not supported, sorry!",
);
} }
Hkp::Invalid { query: _ } => { Hkp::Invalid { query: _ } => {
return MyResponse::bad_request_plain("Invalid search query!"); return MyResponse::bad_request_plain("Invalid search query!");
@ -232,31 +239,34 @@ pub fn pks_internal_index(
) -> MyResponse { ) -> MyResponse {
match query_string.parse() { match query_string.parse() {
Ok(query) => key_to_hkp_index(db, i18n, query), Ok(query) => key_to_hkp_index(db, i18n, query),
Err(_) => MyResponse::bad_request_plain("Invalid search query!") Err(_) => MyResponse::bad_request_plain("Invalid search query!"),
} }
} }
fn key_to_hkp_index( fn key_to_hkp_index(db: &rocket::State<KeyDatabase>, i18n: I18n, query: Query) -> MyResponse {
db: &rocket::State<KeyDatabase>,
i18n: I18n,
query: Query,
) -> MyResponse {
use sequoia_openpgp::types::RevocationStatus;
use sequoia_openpgp::policy::StandardPolicy; use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::types::RevocationStatus;
let tpk = match db.lookup(&query) { let tpk = match db.lookup(&query) {
Ok(Some(tpk)) => tpk, Ok(Some(tpk)) => tpk,
Ok(None) => return MyResponse::not_found_plain(describe_query_error(&i18n, &query)), Ok(None) => return MyResponse::not_found_plain(describe_query_error(&i18n, &query)),
Err(err) => { return MyResponse::ise(err); } Err(err) => {
return MyResponse::ise(err);
}
}; };
let mut out = String::default(); let mut out = String::default();
let p = tpk.primary_key(); let p = tpk.primary_key();
let policy = &StandardPolicy::new(); let policy = &StandardPolicy::new();
let ctime = format!("{}", p.creation_time().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()); let ctime = format!(
let is_rev = "{}",
if tpk.revocation_status(policy, None) != RevocationStatus::NotAsFarAsWeKnow { p.creation_time()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
);
let is_rev = if tpk.revocation_status(policy, None) != RevocationStatus::NotAsFarAsWeKnow {
"r" "r"
} else { } else {
"" ""
@ -285,18 +295,13 @@ fn key_to_hkp_index(
.and_then(|time| time.duration_since(SystemTime::UNIX_EPOCH).ok()) .and_then(|time| time.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|x| format!("{}", x.as_secs())) .map(|x| format!("{}", x.as_secs()))
.unwrap_or_default(); .unwrap_or_default();
let is_rev = if uid.revocation_status(policy, None) let is_rev = if uid.revocation_status(policy, None) != RevocationStatus::NotAsFarAsWeKnow {
!= RevocationStatus::NotAsFarAsWeKnow
{
"r" "r"
} else { } else {
"" ""
}; };
out.push_str(&format!( out.push_str(&format!("uid:{}:{}:{}:{}{}\r\n", u, ctime, "", "", is_rev));
"uid:{}:{}:{}:{}{}\r\n",
u, ctime, "", "", is_rev
));
} }
MyResponse::plain(out) MyResponse::plain(out)
@ -304,13 +309,13 @@ fn key_to_hkp_index(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rocket::http::Status;
use rocket::http::ContentType; use rocket::http::ContentType;
use rocket::http::Status;
use sequoia_openpgp::serialize::Serialize; use sequoia_openpgp::serialize::Serialize;
use crate::web::tests::*;
use crate::mail::pop_mail; use crate::mail::pop_mail;
use crate::web::tests::*;
#[test] #[test]
fn hkp() { fn hkp() {
@ -326,9 +331,8 @@ mod tests {
// Prepare to /pks/add // Prepare to /pks/add
let mut armored = Vec::new(); let mut armored = Vec::new();
{ {
use sequoia_openpgp::armor::{Writer, Kind}; use sequoia_openpgp::armor::{Kind, Writer};
let mut w = Writer::new(&mut armored, Kind::PublicKey) let mut w = Writer::new(&mut armored, Kind::PublicKey).unwrap();
.unwrap();
tpk.serialize(&mut w).unwrap(); tpk.serialize(&mut w).unwrap();
w.finalize().unwrap(); w.finalize().unwrap();
} }
@ -338,7 +342,8 @@ mod tests {
} }
// Add! // Add!
let response = client.post("/pks/add") let response = client
.post("/pks/add")
.body(post_data.as_bytes()) .body(post_data.as_bytes())
.header(ContentType::Form) .header(ContentType::Form)
.dispatch(); .dispatch();
@ -351,7 +356,8 @@ mod tests {
assert!(welcome_mail.is_some()); assert!(welcome_mail.is_some());
// Add! // Add!
let response = client.post("/pks/add") let response = client
.post("/pks/add")
.body(post_data.as_bytes()) .body(post_data.as_bytes())
.header(ContentType::Form) .header(ContentType::Form)
.dispatch(); .dispatch();
@ -374,7 +380,8 @@ mod tests {
check_hr_responses_by_fingerprint(&client, &tpk, 0); check_hr_responses_by_fingerprint(&client, &tpk, 0);
// Upload the same key again, make sure the welcome mail is not sent again // Upload the same key again, make sure the welcome mail is not sent again
let response = client.post("/pks/add") let response = client
.post("/pks/add")
.body(post_data.as_bytes()) .body(post_data.as_bytes())
.header(ContentType::Form) .header(ContentType::Form)
.dispatch(); .dispatch();
@ -399,14 +406,14 @@ mod tests {
let mut armored_first = Vec::new(); let mut armored_first = Vec::new();
let mut armored_both = Vec::new(); let mut armored_both = Vec::new();
{ {
use sequoia_openpgp::armor::{Writer, Kind}; use sequoia_openpgp::armor::{Kind, Writer};
let mut w = Writer::new(&mut armored_both, Kind::PublicKey).unwrap(); let mut w = Writer::new(&mut armored_both, Kind::PublicKey).unwrap();
tpk_0.serialize(&mut w).unwrap(); tpk_0.serialize(&mut w).unwrap();
tpk_1.serialize(&mut w).unwrap(); tpk_1.serialize(&mut w).unwrap();
w.finalize().unwrap(); w.finalize().unwrap();
} }
{ {
use sequoia_openpgp::armor::{Writer, Kind}; use sequoia_openpgp::armor::{Kind, Writer};
let mut w = Writer::new(&mut armored_first, Kind::PublicKey).unwrap(); let mut w = Writer::new(&mut armored_first, Kind::PublicKey).unwrap();
tpk_0.serialize(&mut w).unwrap(); tpk_0.serialize(&mut w).unwrap();
w.finalize().unwrap(); w.finalize().unwrap();
@ -421,7 +428,8 @@ mod tests {
} }
// Add! // Add!
let response = client.post("/pks/add") let response = client
.post("/pks/add")
.body(post_data_both.as_bytes()) .body(post_data_both.as_bytes())
.header(ContentType::Form) .header(ContentType::Form)
.dispatch(); .dispatch();
@ -432,7 +440,8 @@ mod tests {
assert!(welcome_mail.is_none()); assert!(welcome_mail.is_none());
// Add the first again // Add the first again
let response = client.post("/pks/add") let response = client
.post("/pks/add")
.body(post_data_first.as_bytes()) .body(post_data_first.as_bytes())
.header(ContentType::Form) .header(ContentType::Form)
.dispatch(); .dispatch();

View File

@ -1,6 +1,6 @@
use rocket::{Request, Data};
use rocket::fairing::{Fairing, Info, Kind}; use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Method; use rocket::http::Method;
use rocket::{Data, Request};
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use serde_json::json; use serde_json::json;
@ -29,7 +29,7 @@ impl Fairing for MaintenanceMode {
fn info(&self) -> Info { fn info(&self) -> Info {
Info { Info {
name: "Maintenance Mode", name: "Maintenance Mode",
kind: Kind::Request kind: Kind::Request,
} }
} }
@ -59,8 +59,7 @@ impl MaintenanceMode {
} }
fn is_request_json(&self, path: &str) -> bool { fn is_request_json(&self, path: &str) -> bool {
path.starts_with("/vks/v1/upload") || path.starts_with("/vks/v1/upload") || path.starts_with("/vks/v1/request-verify")
path.starts_with("/vks/v1/request-verify")
} }
fn is_request_plain(&self, path: &str, method: Method) -> bool { fn is_request_plain(&self, path: &str, method: Method) -> bool {
@ -68,9 +67,7 @@ impl MaintenanceMode {
} }
fn is_request_web(&self, path: &str) -> bool { fn is_request_web(&self, path: &str) -> bool {
path.starts_with("/upload") || path.starts_with("/upload") || path.starts_with("/manage") || path.starts_with("/verify")
path.starts_with("/manage") ||
path.starts_with("/verify")
} }
fn get_maintenance_message(&self) -> Option<String> { fn get_maintenance_message(&self) -> Option<String> {
@ -93,15 +90,12 @@ struct JsonErrorMessage {
#[get("/maintenance/json/<message>")] #[get("/maintenance/json/<message>")]
pub fn maintenance_error_json(message: String) -> MyResponse { pub fn maintenance_error_json(message: String) -> MyResponse {
MyResponse::MaintenanceJson(json!(JsonErrorMessage{ message })) MyResponse::MaintenanceJson(json!(JsonErrorMessage { message }))
} }
#[get("/maintenance/web/<message>")] #[get("/maintenance/web/<message>")]
pub fn maintenance_error_web( pub fn maintenance_error_web(message: String, i18n: I18n) -> MyResponse {
message: String, let ctx = templates::MaintenanceMode {
i18n: I18n,
) -> MyResponse {
let ctx = templates::MaintenanceMode{
message, message,
version: env!("VERGEN_SEMVER").to_string(), version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(), commit: env!("VERGEN_SHA_SHORT").to_string(),

View File

@ -5,20 +5,19 @@ use crate::Result;
use gettext_macros::i18n; use gettext_macros::i18n;
use crate::web::{RequestOrigin, MyResponse};
use crate::web::vks_web;
use crate::database::{Database, KeyDatabase, types::Email, types::Fingerprint};
use crate::mail;
use crate::counters; use crate::counters;
use crate::database::{types::Email, types::Fingerprint, Database, KeyDatabase};
use crate::mail;
use crate::rate_limiter::RateLimiter; use crate::rate_limiter::RateLimiter;
use crate::tokens::{self, StatelessSerializable}; use crate::tokens::{self, StatelessSerializable};
use crate::web::vks_web;
use crate::web::{MyResponse, RequestOrigin};
#[derive(Debug,Serialize,Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct StatelessVerifyToken { struct StatelessVerifyToken {
fpr: Fingerprint, fpr: Fingerprint,
} }
impl StatelessSerializable for StatelessVerifyToken { impl StatelessSerializable for StatelessVerifyToken {}
}
mod templates { mod templates {
#[derive(Serialize)] #[derive(Serialize)]
@ -74,18 +73,20 @@ pub fn vks_manage_key(
match db.lookup(&database::Query::ByFingerprint(fpr)) { match db.lookup(&database::Query::ByFingerprint(fpr)) {
Ok(Some(tpk)) => { Ok(Some(tpk)) => {
let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap(); let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap();
let mut emails: Vec<Email> = tpk.userids() let mut emails: Vec<Email> = tpk
.userids()
.map(|u| u.userid().to_string().parse::<Email>()) .map(|u| u.userid().to_string().parse::<Email>())
.flatten() .flatten()
.collect(); .collect();
emails.sort_unstable(); emails.sort_unstable();
emails.dedup(); emails.dedup();
let uid_status = emails.into_iter().map(|email| let uid_status = emails
templates::ManageKeyUidStatus { .into_iter()
.map(|email| templates::ManageKeyUidStatus {
address: email.to_string(), address: email.to_string(),
published: true, published: true,
} })
).collect(); .collect();
let key_link = uri!(vks_web::search(q = fp.to_string())).to_string(); let key_link = uri!(vks_web::search(q = fp.to_string())).to_string();
let context = templates::ManageKey { let context = templates::ManageKey {
key_fpr: fp.to_string(), key_fpr: fp.to_string(),
@ -95,11 +96,12 @@ pub fn vks_manage_key(
base_uri: origin.get_base_uri().to_owned(), base_uri: origin.get_base_uri().to_owned(),
}; };
MyResponse::ok("manage/manage_key", context, i18n, origin) MyResponse::ok("manage/manage_key", context, i18n, origin)
}, }
Ok(None) => MyResponse::not_found( Ok(None) => MyResponse::not_found(
Some("manage/manage"), Some("manage/manage"),
Some(i18n!(i18n.catalog, "This link is invalid or expired")), Some(i18n!(i18n.catalog, "This link is invalid or expired")),
i18n, origin, i18n,
origin,
), ),
Err(e) => MyResponse::ise(e), Err(e) => MyResponse::ise(e),
} }
@ -107,11 +109,13 @@ pub fn vks_manage_key(
MyResponse::not_found( MyResponse::not_found(
Some("manage/manage"), Some("manage/manage"),
Some(i18n!(i18n.catalog, "This link is invalid or expired")), Some(i18n!(i18n.catalog, "This link is invalid or expired")),
i18n, origin) i18n,
origin,
)
} }
} }
#[post("/manage", data="<request>")] #[post("/manage", data = "<request>")]
pub fn vks_manage_post( pub fn vks_manage_post(
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
origin: RequestOrigin, origin: RequestOrigin,
@ -125,35 +129,48 @@ pub fn vks_manage_post(
let email = match request.search_term.parse::<Email>() { let email = match request.search_term.parse::<Email>() {
Ok(email) => email, Ok(email) => email,
Err(_) => return MyResponse::not_found( Err(_) => {
return MyResponse::not_found(
Some("manage/manage"), Some("manage/manage"),
Some(i18n!(i18n.catalog, "Malformed address: {}"; &request.search_term)), Some(i18n!(i18n.catalog, "Malformed address: {}"; &request.search_term)),
i18n, origin) i18n,
origin,
)
}
}; };
let tpk = match db.lookup(&database::Query::ByEmail(email.clone())) { let tpk = match db.lookup(&database::Query::ByEmail(email.clone())) {
Ok(Some(tpk)) => tpk, Ok(Some(tpk)) => tpk,
Ok(None) => return MyResponse::not_found( Ok(None) => {
return MyResponse::not_found(
Some("manage/manage"), Some("manage/manage"),
Some(i18n!(i18n.catalog, "No key for address: {}"; &request.search_term)), Some(i18n!(i18n.catalog, "No key for address: {}"; &request.search_term)),
i18n, origin), i18n,
origin,
)
}
Err(e) => return MyResponse::ise(e), Err(e) => return MyResponse::ise(e),
}; };
let email_exists = tpk.userids() let email_exists = tpk
.userids()
.flat_map(|binding| binding.userid().to_string().parse::<Email>()) .flat_map(|binding| binding.userid().to_string().parse::<Email>())
.any(|candidate| candidate == email); .any(|candidate| candidate == email);
if !email_exists { if !email_exists {
return MyResponse::ise( return MyResponse::ise(anyhow!("Internal error: address check failed!"));
anyhow!("Internal error: address check failed!"));
} }
if !rate_limiter.action_perform(format!("manage-{}", &email)) { if !rate_limiter.action_perform(format!("manage-{}", &email)) {
return MyResponse::not_found( return MyResponse::not_found(
Some("manage/manage"), Some("manage/manage"),
Some(i18n!(i18n.catalog, "A request has already been sent for this address recently.")), Some(i18n!(
i18n, origin); i18n.catalog,
"A request has already been sent for this address recently."
)),
i18n,
origin,
);
} }
let fpr: Fingerprint = tpk.fingerprint().try_into().unwrap(); let fpr: Fingerprint = tpk.fingerprint().try_into().unwrap();
@ -172,7 +189,7 @@ pub fn vks_manage_post(
MyResponse::ok("manage/manage_link_sent", ctx, i18n, origin) MyResponse::ok("manage/manage_link_sent", ctx, i18n, origin)
} }
#[post("/manage/unpublish", data="<request>")] #[post("/manage/unpublish", data = "<request>")]
pub fn vks_manage_unpublish( pub fn vks_manage_unpublish(
origin: RequestOrigin, origin: RequestOrigin,
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
@ -199,5 +216,11 @@ pub fn vks_manage_unpublish_or_fail(
db.set_email_unpublished(&verify_token.fpr, &email)?; db.set_email_unpublished(&verify_token.fpr, &email)?;
counters::inc_address_unpublished(&email); counters::inc_address_unpublished(&email);
Ok(vks_manage_key(origin, db, i18n, request.token.to_owned(), token_service)) Ok(vks_manage_key(
origin,
db,
i18n,
request.token.to_owned(),
token_service,
))
} }

View File

@ -1,14 +1,14 @@
use hyperx::header::{Charset, ContentDisposition, DispositionParam, DispositionType};
use rocket::figment::Figment; use rocket::figment::Figment;
use rocket::fs::NamedFile; use rocket::fs::NamedFile;
use rocket::http::{Header, Status}; use rocket::http::{Header, Status};
use rocket::request;
use rocket::outcome::Outcome; use rocket::outcome::Outcome;
use rocket::response::{Responder, Response}; use rocket::request;
use rocket::response::status::Custom; use rocket::response::status::Custom;
use rocket::response::{Responder, Response};
use rocket_dyn_templates::{Engines, Template}; use rocket_dyn_templates::{Engines, Template};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use rocket_prometheus::PrometheusMetrics; use rocket_prometheus::PrometheusMetrics;
use hyperx::header::{ContentDisposition, DispositionType, DispositionParam, Charset};
use gettext_macros::{compile_i18n, include_i18n}; use gettext_macros::{compile_i18n, include_i18n};
@ -16,27 +16,27 @@ use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
use crate::mail;
use crate::tokens;
use crate::counters; use crate::counters;
use crate::i18n_helpers::describe_query_error;
use crate::template_helpers::TemplateOverrides;
use crate::i18n::I18NHelper; use crate::i18n::I18NHelper;
use crate::i18n_helpers::describe_query_error;
use crate::mail;
use crate::rate_limiter::RateLimiter; use crate::rate_limiter::RateLimiter;
use crate::template_helpers::TemplateOverrides;
use crate::tokens;
use crate::database::{Database, KeyDatabase, Query};
use crate::database::types::Fingerprint; use crate::database::types::Fingerprint;
use crate::database::{Database, KeyDatabase, Query};
use crate::Result; use crate::Result;
use std::convert::TryInto; use std::convert::TryInto;
mod hkp;
mod manage;
mod maintenance;
mod vks;
mod vks_web;
mod vks_api;
mod debug_web; mod debug_web;
mod hkp;
mod maintenance;
mod manage;
mod vks;
mod vks_api;
mod vks_web;
mod wkd; mod wkd;
use crate::web::maintenance::MaintenanceMode; use crate::web::maintenance::MaintenanceMode;
@ -44,10 +44,16 @@ use crate::web::maintenance::MaintenanceMode;
pub struct HagridTemplate(&'static str, serde_json::Value, I18n, RequestOrigin); pub struct HagridTemplate(&'static str, serde_json::Value, I18n, RequestOrigin);
impl<'r> Responder<'r, 'static> for HagridTemplate { impl<'r> Responder<'r, 'static> for HagridTemplate {
fn respond_to(self, req: &'r rocket::Request) -> std::result::Result<Response<'static>, Status> { fn respond_to(
self,
req: &'r rocket::Request,
) -> std::result::Result<Response<'static>, Status> {
let HagridTemplate(tmpl, ctx, i18n, origin) = self; let HagridTemplate(tmpl, ctx, i18n, origin) = self;
let template_overrides: &TemplateOverrides = req.rocket().state().expect("TemplateOverrides must be in managed state"); 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 template_override = template_overrides.get_template_override(i18n.lang, tmpl);
let layout_context = templates::HagridLayout::new(ctx, i18n, origin); let layout_context = templates::HagridLayout::new(ctx, i18n, origin);
@ -55,7 +61,8 @@ impl<'r> Responder<'r, 'static> for HagridTemplate {
Template::render(template_override, layout_context) Template::render(template_override, layout_context)
} else { } else {
Template::render(tmpl, layout_context) Template::render(tmpl, layout_context)
}.respond_to(req) }
.respond_to(req)
} }
} }
@ -114,12 +121,14 @@ impl MyResponse {
rocket::http::hyper::header::CONTENT_DISPOSITION.as_str(), rocket::http::hyper::header::CONTENT_DISPOSITION.as_str(),
ContentDisposition { ContentDisposition {
disposition: DispositionType::Attachment, disposition: DispositionType::Attachment,
parameters: vec![ parameters: vec![DispositionParam::Filename(
DispositionParam::Filename( Charset::Us_Ascii,
Charset::Us_Ascii, None, None,
(fp.to_string() + ".asc").into_bytes()), (fp.to_string() + ".asc").into_bytes(),
], )],
}.to_string()); }
.to_string(),
);
MyResponse::Key(armored_key, content_disposition) MyResponse::Key(armored_key, content_disposition)
} }
@ -128,12 +137,14 @@ impl MyResponse {
rocket::http::hyper::header::CONTENT_DISPOSITION.as_str(), rocket::http::hyper::header::CONTENT_DISPOSITION.as_str(),
ContentDisposition { ContentDisposition {
disposition: DispositionType::Attachment, disposition: DispositionType::Attachment,
parameters: vec![ parameters: vec![DispositionParam::Filename(
DispositionParam::Filename( Charset::Us_Ascii,
Charset::Us_Ascii, None, None,
(wkd_hash.to_string() + ".pgp").into_bytes()), (wkd_hash.to_string() + ".pgp").into_bytes(),
], )],
}.to_string()); }
.to_string(),
);
MyResponse::WkdKey(binary_key, content_disposition) MyResponse::WkdKey(binary_key, content_disposition)
} }
@ -148,8 +159,15 @@ impl MyResponse {
MyResponse::ServerError(Template::render("500", ctx)) MyResponse::ServerError(Template::render("500", ctx))
} }
pub fn bad_request(template: &'static str, e: anyhow::Error, i18n: I18n, origin: RequestOrigin) -> Self { pub fn bad_request(
let ctx = templates::Error { error: format!("{}", e) }; 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(); let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::BadRequest(HagridTemplate(template, context_json, i18n, origin)) MyResponse::BadRequest(HagridTemplate(template, context_json, i18n, origin))
} }
@ -168,10 +186,16 @@ impl MyResponse {
i18n: I18n, i18n: I18n,
origin: RequestOrigin, origin: RequestOrigin,
) -> Self { ) -> Self {
let ctx = templates::Error { error: message.into() let ctx = templates::Error {
.unwrap_or_else(|| "Key not found".to_owned()) }; error: message.into().unwrap_or_else(|| "Key not found".to_owned()),
};
let context_json = serde_json::to_value(ctx).unwrap(); let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::NotFound(HagridTemplate(tmpl.unwrap_or("index"), context_json, i18n, origin)) MyResponse::NotFound(HagridTemplate(
tmpl.unwrap_or("index"),
context_json,
i18n,
origin,
))
} }
} }
@ -219,8 +243,16 @@ mod templates {
base_uri: origin.get_base_uri().to_string(), base_uri: origin.get_base_uri().to_string(),
page, page,
lang: i18n.lang.to_string(), lang: i18n.lang.to_string(),
htmldir: if is_rtl { "rtl".to_owned() } else { "ltr".to_owned() }, htmldir: if is_rtl {
htmlclass: if is_rtl { "rtl".to_owned() } else { "".to_owned() }, "rtl".to_owned()
} else {
"ltr".to_owned()
},
htmlclass: if is_rtl {
"rtl".to_owned()
} else {
"".to_owned()
},
} }
} }
} }
@ -245,7 +277,9 @@ pub enum RequestOrigin {
impl<'r> request::FromRequest<'r> for RequestOrigin { impl<'r> request::FromRequest<'r> for RequestOrigin {
type Error = (); type Error = ();
async fn from_request(request: &'r request::Request<'_>) -> request::Outcome<Self, Self::Error> { async fn from_request(
request: &'r request::Request<'_>,
) -> request::Outcome<Self, Self::Error> {
let hagrid_state = request.rocket().state::<HagridState>().unwrap(); let hagrid_state = request.rocket().state::<HagridState>().unwrap();
let result = match request.headers().get("x-is-onion").next() { let result = match request.headers().get("x-is-onion").next() {
Some(_) => RequestOrigin::OnionService(hagrid_state.base_uri_onion.clone()), Some(_) => RequestOrigin::OnionService(hagrid_state.base_uri_onion.clone()),
@ -342,14 +376,16 @@ fn errors(
code: u16, code: u16,
template: String, template: String,
) -> std::result::Result<Custom<Template>, &'static str> { ) -> std::result::Result<Custom<Template>, &'static str> {
if !template.chars().all(|x| x == '-' || char::is_ascii_alphabetic(&x)) { if !template
.chars()
.all(|x| x == '-' || char::is_ascii_alphabetic(&x))
{
return Err("bad request"); return Err("bad request");
} }
let status_code = Status::from_code(code) let status_code = Status::from_code(code).ok_or("bad request")?;
.ok_or("bad request")?;
let response_body = Template::render( let response_body = Template::render(
format!("errors/{}-{}", code, template), format!("errors/{}-{}", code, template),
templates::HagridLayout::new(templates::Bare{dummy: ()}, i18n, origin) templates::HagridLayout::new(templates::Bare { dummy: () }, i18n, origin),
); );
Ok(Custom(status_code, response_body)) Ok(Custom(status_code, response_body))
} }
@ -360,7 +396,9 @@ pub fn serve() -> Result<rocket::Rocket<rocket::Build>> {
compile_i18n!(); compile_i18n!();
fn rocket_factory(mut rocket: rocket::Rocket<rocket::Build>) -> Result<rocket::Rocket<rocket::Build>> { fn rocket_factory(
mut rocket: rocket::Rocket<rocket::Build>,
) -> Result<rocket::Rocket<rocket::Build>> {
let routes = routes![ let routes = routes![
// infra // infra
root, root,
@ -431,7 +469,9 @@ fn rocket_factory(mut rocket: rocket::Rocket<rocket::Build>) -> Result<rocket::R
.attach(Template::custom(|engines: &mut Engines| { .attach(Template::custom(|engines: &mut Engines| {
let i18ns = include_i18n!(); let i18ns = include_i18n!();
let i18n_helper = I18NHelper::new(i18ns); let i18n_helper = I18NHelper::new(i18ns);
engines.handlebars.register_helper("text", Box::new(i18n_helper)); engines
.handlebars
.register_helper("text", Box::new(i18n_helper));
})) }))
.attach(maintenance_mode) .attach(maintenance_mode)
.manage(include_i18n!()) .manage(include_i18n!())
@ -476,7 +516,8 @@ fn configure_hagrid_state(config: &Figment) -> Result<HagridState> {
// State // State
let base_uri: String = config.extract_inner("base-URI")?; let base_uri: String = config.extract_inner("base-URI")?;
let base_uri_onion = config.extract_inner::<String>("base-URI-Onion") let base_uri_onion = config
.extract_inner::<String>("base-URI-Onion")
.unwrap_or_else(|_| base_uri.clone()); .unwrap_or_else(|_| base_uri.clone());
Ok(HagridState { Ok(HagridState {
assets_dir, assets_dir,
@ -524,7 +565,8 @@ fn configure_localized_template_list(config: &Figment) -> Result<TemplateOverrid
} }
fn configure_maintenance_mode(config: &Figment) -> Result<MaintenanceMode> { fn configure_maintenance_mode(config: &Figment) -> Result<MaintenanceMode> {
let maintenance_file: PathBuf = config.extract_inner("maintenance_file") let maintenance_file: PathBuf = config
.extract_inner("maintenance_file")
.unwrap_or_else(|_| PathBuf::from("maintenance")); .unwrap_or_else(|_| PathBuf::from("maintenance"));
Ok(MaintenanceMode::new(maintenance_file)) Ok(MaintenanceMode::new(maintenance_file))
} }
@ -532,27 +574,27 @@ fn configure_maintenance_mode(config: &Figment) -> Result<MaintenanceMode> {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use regex; use regex;
use rocket::http::ContentType;
use rocket::http::Header;
use rocket::http::Status;
use rocket::local::blocking::{Client, LocalResponse}; use rocket::local::blocking::{Client, LocalResponse};
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use rocket::http::Status;
use rocket::http::ContentType;
use rocket::http::Header;
use sequoia_openpgp::Cert;
use sequoia_openpgp::cert::CertBuilder; use sequoia_openpgp::cert::CertBuilder;
use sequoia_openpgp::parse::Parse; use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::serialize::Serialize; use sequoia_openpgp::serialize::Serialize;
use sequoia_openpgp::Cert;
use std::time::SystemTime; use std::time::SystemTime;
use mail::pop_mail; use mail::pop_mail;
use crate::database::*;
use super::*; use super::*;
use crate::database::*;
/// Fake base URI to use in tests. /// Fake base URI to use in tests.
const BASE_URI: &str = "http://local.connection"; const BASE_URI: &str = "http://local.connection";
@ -583,27 +625,56 @@ pub mod tests {
let config = rocket::Config::figment() let config = rocket::Config::figment()
.select("staging") .select("staging")
.merge(("root", root.path())) .merge(("root", root.path()))
.merge(("template_dir", .merge((
::std::env::current_dir().unwrap().join("dist/templates") "template_dir",
.to_str().unwrap())) ::std::env::current_dir()
.merge(("email_template_dir", .unwrap()
::std::env::current_dir().unwrap().join("dist/email-templates") .join("dist/templates")
.to_str().unwrap())) .to_str()
.merge(("assets_dir", .unwrap(),
::std::env::current_dir().unwrap().join("dist/assets") ))
.to_str().unwrap())) .merge((
.merge(("keys_internal_dir", base_dir.join("keys_internal").to_str().unwrap())) "email_template_dir",
.merge(("keys_external_dir", base_dir.join("keys_external").to_str().unwrap())) ::std::env::current_dir()
.unwrap()
.join("dist/email-templates")
.to_str()
.unwrap(),
))
.merge((
"assets_dir",
::std::env::current_dir()
.unwrap()
.join("dist/assets")
.to_str()
.unwrap(),
))
.merge((
"keys_internal_dir",
base_dir.join("keys_internal").to_str().unwrap(),
))
.merge((
"keys_external_dir",
base_dir.join("keys_external").to_str().unwrap(),
))
.merge(("tmp_dir", base_dir.join("tmp").to_str().unwrap())) .merge(("tmp_dir", base_dir.join("tmp").to_str().unwrap()))
.merge(("token_dir", base_dir.join("tokens").to_str().unwrap())) .merge(("token_dir", base_dir.join("tokens").to_str().unwrap()))
.merge(("maintenance_file", base_dir.join("maintenance").to_str().unwrap())) .merge((
"maintenance_file",
base_dir.join("maintenance").to_str().unwrap(),
))
.merge(("base-URI", BASE_URI)) .merge(("base-URI", BASE_URI))
.merge(("base-URI-Onion", BASE_URI_ONION)) .merge(("base-URI-Onion", BASE_URI_ONION))
.merge(("from", "from@example.com")) .merge(("from", "from@example.com"))
.merge(("token_secret", "hagrid")) .merge(("token_secret", "hagrid"))
.merge(("token_validity", 3600u64)) .merge(("token_validity", 3600u64))
.merge(("filemail_into", filemail.into_os_string().into_string() .merge((
.expect("path is valid UTF8"))); "filemail_into",
filemail
.into_os_string()
.into_string()
.expect("path is valid UTF8"),
));
Ok((root, config)) Ok((root, config))
} }
@ -625,7 +696,8 @@ pub mod tests {
let client = Client::untracked(rocket).expect("valid rocket instance"); let client = Client::untracked(rocket).expect("valid rocket instance");
// Check that we see the landing page. // Check that we see the landing page.
let response = client.get("/about") let response = client
.get("/about")
.header(Header::new("Accept-Language", "de")) .header(Header::new("Accept-Language", "de"))
.dispatch(); .dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
@ -650,7 +722,10 @@ pub mod tests {
let response = client.get("/about").dispatch(); let response = client.get("/about").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.into_string().unwrap().contains("distribution and discovery")); assert!(response
.into_string()
.unwrap()
.contains("distribution and discovery"));
// Check that we see the privacy policy. // Check that we see the privacy policy.
let response = client.get("/about/privacy").dispatch(); let response = client.get("/about/privacy").dispatch();
@ -674,7 +749,10 @@ pub mod tests {
let response = client.get("/manage").dispatch(); let response = client.get("/manage").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.into_string().unwrap().contains("any verified email address")); assert!(response
.into_string()
.unwrap()
.contains("any verified email address"));
assert_consistency(client.rocket()); assert_consistency(client.rocket());
} }
@ -699,21 +777,30 @@ pub mod tests {
let response = client.put("/").dispatch(); let response = client.put("/").dispatch();
assert_eq!(response.status(), Status::ServiceUnavailable); assert_eq!(response.status(), Status::ServiceUnavailable);
assert_eq!(response.content_type(), Some(ContentType::Plain)); assert_eq!(response.content_type(), Some(ContentType::Plain));
assert!(response.into_string().unwrap().contains("maintenance-message")); assert!(response
.into_string()
.unwrap()
.contains("maintenance-message"));
fs::remove_file(&maintenance_path).unwrap(); fs::remove_file(&maintenance_path).unwrap();
// Check that we see the upload form. // Check that we see the upload form.
let response = client.get("/upload").dispatch(); let response = client.get("/upload").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(!response.into_string().unwrap().contains("maintenance-message")); assert!(!response
.into_string()
.unwrap()
.contains("maintenance-message"));
} }
fn check_maintenance(client: &Client, uri: &str, content_type: ContentType) { fn check_maintenance(client: &Client, uri: &str, content_type: ContentType) {
let response = client.get(uri).dispatch(); let response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::ServiceUnavailable); assert_eq!(response.status(), Status::ServiceUnavailable);
assert_eq!(response.content_type(), Some(content_type)); assert_eq!(response.content_type(), Some(content_type));
assert!(response.into_string().unwrap().contains("maintenance-message")); assert!(response
.into_string()
.unwrap()
.contains("maintenance-message"));
} }
#[test] #[test]
@ -755,7 +842,11 @@ pub mod tests {
vks_manage(&client, "foo@invalid.example.com"); vks_manage(&client, "foo@invalid.example.com");
// Confirm deletion. // Confirm deletion.
check_mails_and_confirm_deletion(&client, filemail_into.as_path(), "foo@invalid.example.com"); 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 // Now, we should no longer be able to look it up by email
// address. // address.
@ -912,7 +1003,8 @@ pub mod tests {
.append_pair("address", "foo@invalid.example.com") .append_pair("address", "foo@invalid.example.com")
.finish(); .finish();
let response = client.post("/upload/request-verify") let response = client
.post("/upload/request-verify")
.header(ContentType::Form) .header(ContentType::Form)
.header(Header::new("X-Is-Onion", "true")) .header(Header::new("X-Is-Onion", "true"))
.body(encoded.as_bytes()) .body(encoded.as_bytes())
@ -929,7 +1021,6 @@ pub mod tests {
assert_consistency(client.rocket()); assert_consistency(client.rocket());
} }
#[test] #[test]
fn upload_curl_shortcut() { fn upload_curl_shortcut() {
let (_tmpdir, client) = client().unwrap(); let (_tmpdir, client) = client().unwrap();
@ -948,21 +1039,40 @@ pub mod tests {
#[test] #[test]
fn search_invalid() { fn search_invalid() {
let (_tmpdir, client) = client().unwrap(); let (_tmpdir, client) = client().unwrap();
check_response(&client, "/search?q=0x1234abcd", check_response(
Status::BadRequest, "not supported"); &client,
check_response(&client, "/search?q=1234abcd", "/search?q=0x1234abcd",
Status::BadRequest, "not supported"); Status::BadRequest,
check_response(&client, "/pks/lookup?op=get&search=0x1234abcd", "not supported",
Status::BadRequest, "not supported"); );
check_response(&client, "/pks/lookup?op=get&search=1234abcd", check_response(
Status::BadRequest, "not supported"); &client,
"/search?q=1234abcd",
Status::BadRequest,
"not supported",
);
check_response(
&client,
"/pks/lookup?op=get&search=0x1234abcd",
Status::BadRequest,
"not supported",
);
check_response(
&client,
"/pks/lookup?op=get&search=1234abcd",
Status::BadRequest,
"not supported",
);
} }
#[test] #[test]
fn wkd_policy() { fn wkd_policy() {
let (_tmpdir, client) = client().unwrap(); let (_tmpdir, client) = client().unwrap();
check_response(&client, "/.well-known/openpgpkey/example.org/policy", check_response(
Status::Ok, ""); &client,
"/.well-known/openpgpkey/example.org/policy",
Status::Ok,
"",
);
} }
/// Asserts that the given URI 404s. /// Asserts that the given URI 404s.
@ -973,67 +1083,68 @@ pub mod tests {
/// Asserts that lookups by the given email 404. /// Asserts that lookups by the given email 404.
pub fn check_null_responses_by_email(client: &Client, addr: &str) { 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( check_null_response(
client, &format!("/vks/v1/by-email/{}", addr)); client,
check_null_response( &format!("/pks/lookup?op=get&options=mr&search={}", addr),
client, &format!("/pks/lookup?op=get&search={}", addr)); );
check_null_response(
client, &format!("/pks/lookup?op=get&options=mr&search={}",
addr));
let (wkd_hash, domain) = crate::database::wkd::encode_wkd(addr).unwrap(); let (wkd_hash, domain) = crate::database::wkd::encode_wkd(addr).unwrap();
check_null_response( check_null_response(
&client, &client,
&format!("/.well-known/openpgpkey/{}/hu/{}", domain, wkd_hash)); &format!("/.well-known/openpgpkey/{}/hu/{}", domain, wkd_hash),
);
} }
/// Asserts that lookups by the given email are successful. /// Asserts that lookups by the given email are successful.
pub fn check_responses_by_email(client: &Client, addr: &str, tpk: &Cert, pub fn check_responses_by_email(client: &Client, addr: &str, tpk: &Cert, nr_uids: usize) {
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),
tpk, nr_uids);
check_mr_response( check_mr_response(
client, client,
&format!("/vks/v1/by-email/{}", addr.replace("@", "%40")), &format!("/vks/v1/by-email/{}", addr.replace("@", "%40")),
tpk, nr_uids); tpk,
nr_uids,
);
check_mr_response( check_mr_response(
client, client,
&format!("/pks/lookup?op=get&options=mr&search={}", addr), &format!("/pks/lookup?op=get&options=mr&search={}", addr),
tpk, nr_uids); tpk,
check_hr_response( nr_uids,
client, );
&format!("/search?q={}", addr), check_hr_response(client, &format!("/search?q={}", addr), tpk, nr_uids);
tpk, nr_uids); check_hr_response_onion(client, &format!("/search?q={}", addr), tpk, nr_uids);
check_hr_response_onion(
client,
&format!("/search?q={}", addr),
tpk, nr_uids);
let (wkd_hash, domain) = crate::database::wkd::encode_wkd(addr).unwrap(); let (wkd_hash, domain) = crate::database::wkd::encode_wkd(addr).unwrap();
check_wkd_response( check_wkd_response(
&client, &client,
&format!("/.well-known/openpgpkey/{}/hu/{}", domain, wkd_hash), &format!("/.well-known/openpgpkey/{}/hu/{}", domain, wkd_hash),
&tpk, nr_uids); &tpk,
nr_uids,
);
} }
/// Asserts that the given URI returns a Cert matching the given /// Asserts that the given URI returns a Cert matching the given
/// one, with the given number of userids. /// one, with the given number of userids.
pub fn check_mr_response(client: &Client, uri: &str, tpk: &Cert, pub fn check_mr_response(client: &Client, uri: &str, tpk: &Cert, nr_uids: usize) {
nr_uids: usize) {
let response = client.get(uri).dispatch(); let response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), assert_eq!(
Some(ContentType::new("application", "pgp-keys"))); response.content_type(),
Some(ContentType::new("application", "pgp-keys"))
);
let body = response.into_string().unwrap(); let body = response.into_string().unwrap();
assert!(body.contains("END PGP PUBLIC KEY BLOCK")); assert!(body.contains("END PGP PUBLIC KEY BLOCK"));
let tpk_ = Cert::from_bytes(body.as_bytes()).unwrap(); let tpk_ = Cert::from_bytes(body.as_bytes()).unwrap();
assert_eq!(tpk.fingerprint(), tpk_.fingerprint()); assert_eq!(tpk.fingerprint(), tpk_.fingerprint());
assert_eq!(tpk.keys().map(|skb| skb.key().fingerprint()) assert_eq!(
tpk.keys()
.map(|skb| skb.key().fingerprint())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
tpk_.keys().map(|skb| skb.key().fingerprint()) tpk_.keys()
.collect::<Vec<_>>()); .map(|skb| skb.key().fingerprint())
.collect::<Vec<_>>()
);
assert_eq!(tpk_.userids().count(), nr_uids); assert_eq!(tpk_.userids().count(), nr_uids);
} }
@ -1043,8 +1154,10 @@ pub mod tests {
pub fn check_index_response(client: &Client, uri: &str, tpk: &Cert) { pub fn check_index_response(client: &Client, uri: &str, tpk: &Cert) {
let response = client.get(uri).dispatch(); let response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), assert_eq!(
Some(ContentType::new("text", "plain"))); response.content_type(),
Some(ContentType::new("text", "plain"))
);
let body = response.into_string().unwrap(); let body = response.into_string().unwrap();
assert!(body.contains("info:1:1")); assert!(body.contains("info:1:1"));
@ -1052,46 +1165,60 @@ pub mod tests {
let algo: u8 = tpk.primary_key().pk_algo().into(); let algo: u8 = tpk.primary_key().pk_algo().into();
assert!(body.contains(&format!("pub:{}:{}:", primary_fpr, algo))); assert!(body.contains(&format!("pub:{}:{}:", primary_fpr, algo)));
let creation_time = tpk.primary_key().creation_time().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); let creation_time = tpk
.primary_key()
.creation_time()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
assert!(body.contains(&format!(":{}:", creation_time))); assert!(body.contains(&format!(":{}:", creation_time)));
} }
/// Asserts that we can get the given Cert back using the various /// Asserts that we can get the given Cert back using the various
/// by-fingerprint or by-keyid lookup mechanisms. /// by-fingerprint or by-keyid lookup mechanisms.
pub fn check_mr_responses_by_fingerprint(client: &Client, tpk: &Cert, pub fn check_mr_responses_by_fingerprint(client: &Client, tpk: &Cert, nr_uids: usize) {
nr_uids: usize) {
let fp = tpk.fingerprint().to_hex(); let fp = tpk.fingerprint().to_hex();
let keyid = sequoia_openpgp::KeyID::from(tpk.fingerprint()).to_hex(); let keyid = sequoia_openpgp::KeyID::from(tpk.fingerprint()).to_hex();
check_mr_response(client, &format!("/vks/v1/by-keyid/{}", keyid), tpk, nr_uids);
check_mr_response( check_mr_response(
client, &format!("/vks/v1/by-keyid/{}", keyid), tpk, nr_uids); client,
check_mr_response( &format!("/vks/v1/by-fingerprint/{}", fp),
client, &format!("/vks/v1/by-fingerprint/{}", fp), tpk, nr_uids); tpk,
nr_uids,
);
check_mr_response( check_mr_response(
client, client,
&format!("/pks/lookup?op=get&options=mr&search={}", fp), &format!("/pks/lookup?op=get&options=mr&search={}", fp),
tpk, nr_uids); tpk,
nr_uids,
);
check_mr_response( check_mr_response(
client, client,
&format!("/pks/lookup?op=get&options=mr&search=0x{}", fp), &format!("/pks/lookup?op=get&options=mr&search=0x{}", fp),
tpk, nr_uids); tpk,
nr_uids,
);
check_mr_response( check_mr_response(
client, client,
&format!("/pks/lookup?op=get&options=mr&search={}", keyid), &format!("/pks/lookup?op=get&options=mr&search={}", keyid),
tpk, nr_uids); tpk,
nr_uids,
);
check_mr_response( check_mr_response(
client, client,
&format!("/pks/lookup?op=get&options=mr&search=0x{}", keyid), &format!("/pks/lookup?op=get&options=mr&search=0x{}", keyid),
tpk, nr_uids); tpk,
nr_uids,
);
check_mr_response( check_mr_response(
client, client,
&format!("/pks/lookup?op=get&search=0x{}", keyid), &format!("/pks/lookup?op=get&search=0x{}", keyid),
tpk, nr_uids); tpk,
nr_uids,
);
check_index_response( check_index_response(client, &format!("/pks/lookup?op=index&search={}", fp), tpk);
client,
&format!("/pks/lookup?op=index&search={}", fp),
tpk);
} }
/// Asserts that the given URI contains the search string. /// Asserts that the given URI contains the search string.
@ -1105,8 +1232,7 @@ pub mod tests {
/// Asserts that the given URI returns human readable response /// Asserts that the given URI returns human readable response
/// page that contains a URI pointing to the Cert. /// page that contains a URI pointing to the Cert.
pub fn check_hr_response(client: &Client, uri: &str, tpk: &Cert, pub fn check_hr_response(client: &Client, uri: &str, tpk: &Cert, nr_uids: usize) {
nr_uids: usize) {
let response = client.get(uri).dispatch(); let response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -1115,12 +1241,10 @@ pub mod tests {
assert!(body.contains(&tpk.fingerprint().to_hex())); assert!(body.contains(&tpk.fingerprint().to_hex()));
// Extract the links. // Extract the links.
let link_re = regex::Regex::new( let link_re = regex::Regex::new(&format!("{}(/vks/[^ \t\n\"<]*)", BASE_URI)).unwrap();
&format!("{}(/vks/[^ \t\n\"<]*)", BASE_URI)).unwrap();
let mut n = 0; let mut n = 0;
for link in link_re.captures_iter(&body) { for link in link_re.captures_iter(&body) {
check_mr_response(client, link.get(1).unwrap().as_str(), tpk, check_mr_response(client, link.get(1).unwrap().as_str(), tpk, nr_uids);
nr_uids);
n += 1; n += 1;
} }
assert!(n > 0); assert!(n > 0);
@ -1128,8 +1252,7 @@ pub mod tests {
/// Asserts that the given URI returns human readable response /// Asserts that the given URI returns human readable response
/// page that contains an onion URI pointing to the Cert. /// page that contains an onion URI pointing to the Cert.
pub fn check_hr_response_onion(client: &Client, uri: &str, tpk: &Cert, pub fn check_hr_response_onion(client: &Client, uri: &str, tpk: &Cert, _nr_uids: usize) {
_nr_uids: usize) {
let response = client let response = client
.get(uri) .get(uri)
.header(Header::new("X-Is-Onion", "true")) .header(Header::new("X-Is-Onion", "true"))
@ -1140,63 +1263,53 @@ pub mod tests {
assert!(body.contains(&tpk.fingerprint().to_hex())); assert!(body.contains(&tpk.fingerprint().to_hex()));
// Extract the links. // Extract the links.
let link_re = regex::Regex::new( let link_re = regex::Regex::new(&format!("{}(/vks/[^ \t\n\"<]*)", BASE_URI_ONION)).unwrap();
&format!("{}(/vks/[^ \t\n\"<]*)", BASE_URI_ONION)).unwrap();
assert!(link_re.is_match(&body)); assert!(link_re.is_match(&body));
} }
/// Asserts that we can get the given Cert back using the various /// Asserts that we can get the given Cert back using the various
/// by-fingerprint or by-keyid lookup mechanisms. /// by-fingerprint or by-keyid lookup mechanisms.
pub fn check_hr_responses_by_fingerprint(client: &Client, tpk: &Cert, pub fn check_hr_responses_by_fingerprint(client: &Client, tpk: &Cert, nr_uids: usize) {
nr_uids: usize) {
let fp = tpk.fingerprint().to_hex(); let fp = tpk.fingerprint().to_hex();
let keyid = sequoia_openpgp::KeyID::from(tpk.fingerprint()).to_hex(); let keyid = sequoia_openpgp::KeyID::from(tpk.fingerprint()).to_hex();
check_hr_response( check_hr_response(client, &format!("/search?q={}", fp), tpk, nr_uids);
client, check_hr_response(client, &format!("/search?q=0x{}", fp), tpk, nr_uids);
&format!("/search?q={}", fp), check_hr_response(client, &format!("/search?q={}", keyid), tpk, nr_uids);
tpk, nr_uids); check_hr_response(client, &format!("/search?q=0x{}", keyid), tpk, nr_uids);
check_hr_response(
client,
&format!("/search?q=0x{}", fp),
tpk, nr_uids);
check_hr_response(
client,
&format!("/search?q={}", keyid),
tpk, nr_uids);
check_hr_response(
client,
&format!("/search?q=0x{}", keyid),
tpk, nr_uids);
} }
/// Asserts that the given URI returns correct WKD response with a Cert /// Asserts that the given URI returns correct WKD response with a Cert
/// matching the given one, with the given number of userids. /// matching the given one, with the given number of userids.
pub fn check_wkd_response(client: &Client, uri: &str, tpk: &Cert, pub fn check_wkd_response(client: &Client, uri: &str, tpk: &Cert, nr_uids: usize) {
nr_uids: usize) {
let response = client.get(uri).dispatch(); let response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), assert_eq!(
Some(ContentType::new("application", "octet-stream"))); response.content_type(),
Some(ContentType::new("application", "octet-stream"))
);
let body = response.into_bytes().unwrap(); let body = response.into_bytes().unwrap();
let tpk_ = Cert::from_bytes(&body).unwrap(); let tpk_ = Cert::from_bytes(&body).unwrap();
assert_eq!(tpk.fingerprint(), tpk_.fingerprint()); assert_eq!(tpk.fingerprint(), tpk_.fingerprint());
assert_eq!(tpk.keys().map(|skb| skb.key().fingerprint()) assert_eq!(
tpk.keys()
.map(|skb| skb.key().fingerprint())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
tpk_.keys().map(|skb| skb.key().fingerprint()) tpk_.keys()
.collect::<Vec<_>>()); .map(|skb| skb.key().fingerprint())
.collect::<Vec<_>>()
);
assert_eq!(tpk_.userids().count(), nr_uids); assert_eq!(tpk_.userids().count(), nr_uids);
} }
fn check_verify_link(client: &Client, token: &str, address: &str, lang: &'static str) { fn check_verify_link(client: &Client, token: &str, address: &str, lang: &'static str) {
let encoded = ::url::form_urlencoded::Serializer::new(String::new()) let encoded = ::url::form_urlencoded::Serializer::new(String::new())
.append_pair("token", token) .append_pair("token", token)
.append_pair("address", address) .append_pair("address", address)
.finish(); .finish();
let response = client.post("/upload/request-verify") let response = client
.post("/upload/request-verify")
.header(ContentType::Form) .header(ContentType::Form)
.header(Header::new("Accept-Language", lang)) .header(Header::new("Accept-Language", lang))
.body(encoded.as_bytes()) .body(encoded.as_bytes())
@ -1207,7 +1320,8 @@ pub mod tests {
fn check_verify_link_json(client: &Client, token: &str, address: &str) { fn check_verify_link_json(client: &Client, token: &str, address: &str) {
let json = format!(r#"{{"token":"{}","addresses":["{}"]}}"#, token, address); let json = format!(r#"{{"token":"{}","addresses":["{}"]}}"#, token, address);
let response = client.post("/vks/v1/request-verify") let response = client
.post("/vks/v1/request-verify")
.header(ContentType::JSON) .header(ContentType::JSON)
.body(json.as_bytes()) .body(json.as_bytes())
.dispatch(); .dispatch();
@ -1224,7 +1338,10 @@ pub mod tests {
let response_second = client.post(&confirm_uri).dispatch(); let response_second = client.post(&confirm_uri).dispatch();
assert_eq!(response_second.status(), Status::BadRequest); assert_eq!(response_second.status(), Status::BadRequest);
assert!(response_second.into_string().unwrap().contains("already been verified")); assert!(response_second
.into_string()
.unwrap()
.contains("already been verified"));
} }
fn check_mails_and_confirm_deletion(client: &Client, filemail_path: &Path, address: &str) { fn check_mails_and_confirm_deletion(client: &Client, filemail_path: &Path, address: &str) {
@ -1237,8 +1354,12 @@ pub mod tests {
let mail_content = pop_mail(filemail_path).unwrap().unwrap(); let mail_content = pop_mail(filemail_path).unwrap().unwrap();
let capture_re = regex::bytes::Regex::new(pattern).unwrap(); let capture_re = regex::bytes::Regex::new(pattern).unwrap();
let capture_content = capture_re.captures(mail_content.as_ref()).unwrap() let capture_content = capture_re
.get(1).unwrap().as_bytes(); .captures(mail_content.as_ref())
.unwrap()
.get(1)
.unwrap()
.as_bytes();
String::from_utf8_lossy(capture_content).to_string() String::from_utf8_lossy(capture_content).to_string()
} }
@ -1258,22 +1379,29 @@ pub mod tests {
let pattern = "name=\"token\" value=\"([^\"]*)\""; let pattern = "name=\"token\" value=\"([^\"]*)\"";
let capture_re = regex::bytes::Regex::new(pattern).unwrap(); let capture_re = regex::bytes::Regex::new(pattern).unwrap();
let capture_content = capture_re .captures(response_body.as_bytes()).unwrap() let capture_content = capture_re
.get(1).unwrap().as_bytes(); .captures(response_body.as_bytes())
.unwrap()
.get(1)
.unwrap()
.as_bytes();
let token = String::from_utf8_lossy(capture_content).to_string(); let token = String::from_utf8_lossy(capture_content).to_string();
assert_eq!(status, Status::Ok); assert_eq!(status, Status::Ok);
token token
} }
fn vks_publish_submit_response<'a>(client: &'a Client, data: &[u8]) -> fn vks_publish_submit_response<'a>(client: &'a Client, data: &[u8]) -> LocalResponse<'a> {
LocalResponse<'a> {
let ct = ContentType::with_params( let ct = ContentType::with_params(
"multipart", "form-data", "multipart",
("boundary", "---------------------------14733842173518794281682249499")); "form-data",
(
"boundary",
"---------------------------14733842173518794281682249499",
),
);
let header = let header = b"-----------------------------14733842173518794281682249499\r\n\
b"-----------------------------14733842173518794281682249499\r\n\
Content-Disposition: form-data; name=\"csrf\"\r\n\ Content-Disposition: form-data; name=\"csrf\"\r\n\
\r\n\ \r\n\
\r\n\ \r\n\
@ -1287,29 +1415,33 @@ pub mod tests {
body.extend_from_slice(header); body.extend_from_slice(header);
body.extend_from_slice(data); body.extend_from_slice(data);
body.extend_from_slice(footer); body.extend_from_slice(footer);
client.post("/upload/submit") client
.post("/upload/submit")
.header(ct) .header(ct)
.body(&body[..]) .body(&body[..])
.dispatch() .dispatch()
} }
fn vks_publish_shortcut_get_token(client: &Client, data: &[u8]) -> String { fn vks_publish_shortcut_get_token(client: &Client, data: &[u8]) -> String {
let response = client.put("/") let response = client.put("/").body(data).dispatch();
.body(data)
.dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
let response_body = response.into_string().unwrap(); let response_body = response.into_string().unwrap();
assert!(response_body.contains("Key successfully uploaded")); assert!(response_body.contains("Key successfully uploaded"));
let pattern = format!("{}/upload/([^ \t\n]*)", BASE_URI); let pattern = format!("{}/upload/([^ \t\n]*)", BASE_URI);
let capture_re = regex::bytes::Regex::new(&pattern).unwrap(); let capture_re = regex::bytes::Regex::new(&pattern).unwrap();
let capture_content = capture_re .captures(response_body.as_bytes()).unwrap() let capture_content = capture_re
.get(1).unwrap().as_bytes(); .captures(response_body.as_bytes())
.unwrap()
.get(1)
.unwrap()
.as_bytes();
String::from_utf8_lossy(capture_content).to_string() String::from_utf8_lossy(capture_content).to_string()
} }
fn vks_publish_json_get_token(client: &Client, data: &[u8]) -> String { fn vks_publish_json_get_token(client: &Client, data: &[u8]) -> String {
let response = client.post("/vks/v1/upload") let response = client
.post("/vks/v1/upload")
.header(ContentType::JSON) .header(ContentType::JSON)
.body(format!(r#"{{ "keytext": "{}" }}"#, base64::encode(data))) .body(format!(r#"{{ "keytext": "{}" }}"#, base64::encode(data)))
.dispatch(); .dispatch();
@ -1325,7 +1457,8 @@ pub mod tests {
let encoded = ::url::form_urlencoded::Serializer::new(String::new()) let encoded = ::url::form_urlencoded::Serializer::new(String::new())
.append_pair("search_term", search_term) .append_pair("search_term", search_term)
.finish(); .finish();
let response = client.post("/manage") let response = client
.post("/manage")
.header(ContentType::Form) .header(ContentType::Form)
.body(encoded.as_bytes()) .body(encoded.as_bytes())
.dispatch(); .dispatch();
@ -1337,7 +1470,8 @@ pub mod tests {
.append_pair("token", token) .append_pair("token", token)
.append_pair("address", address) .append_pair("address", address)
.finish(); .finish();
let response = client.post("/manage/unpublish") let response = client
.post("/manage/unpublish")
.header(ContentType::Form) .header(ContentType::Form)
.body(encoded.as_bytes()) .body(encoded.as_bytes())
.dispatch(); .dispatch();

View File

@ -1,24 +1,26 @@
use crate::Result; use crate::Result;
use crate::database::{Database, KeyDatabase, StatefulTokens, EmailAddressStatus, TpkStatus, ImportResult};
use crate::database::types::{Fingerprint,Email};
use crate::mail;
use crate::counters; use crate::counters;
use crate::tokens::{self, StatelessSerializable}; use crate::database::types::{Email, Fingerprint};
use crate::database::{
Database, EmailAddressStatus, ImportResult, KeyDatabase, StatefulTokens, TpkStatus,
};
use crate::mail;
use crate::rate_limiter::RateLimiter; use crate::rate_limiter::RateLimiter;
use crate::tokens::{self, StatelessSerializable};
use crate::web::RequestOrigin; use crate::web::RequestOrigin;
use rocket_i18n::I18n;
use gettext_macros::i18n; use gettext_macros::i18n;
use rocket_i18n::I18n;
use sequoia_openpgp::Cert;
use sequoia_openpgp::parse::{Parse, PacketParserBuilder, Dearmor};
use sequoia_openpgp::cert::CertParser;
use sequoia_openpgp::armor::ReaderMode; use sequoia_openpgp::armor::ReaderMode;
use sequoia_openpgp::cert::CertParser;
use sequoia_openpgp::parse::{Dearmor, PacketParserBuilder, Parse};
use sequoia_openpgp::Cert;
use std::io::Read;
use std::convert::TryFrom;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom;
use std::io::Read;
use self::response::*; use self::response::*;
@ -38,7 +40,7 @@ pub mod request {
pub mod response { pub mod response {
use crate::database::types::Email; use crate::database::types::Email;
#[derive(Debug,Serialize,Deserialize,PartialEq,Eq)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum EmailStatus { pub enum EmailStatus {
#[serde(rename = "unpublished")] #[serde(rename = "unpublished")]
Unpublished, Unpublished,
@ -57,12 +59,14 @@ pub mod response {
token: String, token: String,
key_fpr: String, key_fpr: String,
is_revoked: bool, is_revoked: bool,
status: HashMap<String,EmailStatus>, status: HashMap<String, EmailStatus>,
count_unparsed: usize, count_unparsed: usize,
is_new_key: bool, is_new_key: bool,
primary_uid: Option<Email>, primary_uid: Option<Email>,
}, },
OkMulti { key_fprs: Vec<String> }, OkMulti {
key_fprs: Vec<String>,
},
Error(String), Error(String),
} }
@ -84,15 +88,14 @@ pub mod response {
} }
} }
#[derive(Serialize,Deserialize)] #[derive(Serialize, Deserialize)]
struct VerifyTpkState { struct VerifyTpkState {
fpr: Fingerprint, fpr: Fingerprint,
addresses: Vec<Email>, addresses: Vec<Email>,
requested: Vec<Email>, requested: Vec<Email>,
} }
impl StatelessSerializable for VerifyTpkState { impl StatelessSerializable for VerifyTpkState {}
}
pub fn process_key( pub fn process_key(
db: &KeyDatabase, db: &KeyDatabase,
@ -103,9 +106,7 @@ pub fn process_key(
) -> response::UploadResponse { ) -> response::UploadResponse {
// First, parse all Certs and error out if one fails. // First, parse all Certs and error out if one fails.
let parser = match PacketParserBuilder::from_reader(reader) let parser = match PacketParserBuilder::from_reader(reader)
.and_then(|ppb| { .and_then(|ppb| ppb.dearmor(Dearmor::Auto(ReaderMode::VeryTolerant)).build())
ppb.dearmor(Dearmor::Auto(ReaderMode::VeryTolerant)).build()
})
{ {
Ok(ppr) => CertParser::from(ppr), Ok(ppr) => CertParser::from(ppr),
Err(_) => return UploadResponse::err(i18n!(i18n.catalog, "Parsing of key data failed.")), Err(_) => return UploadResponse::err(i18n!(i18n.catalog, "Parsing of key data failed.")),
@ -122,7 +123,7 @@ pub fn process_key(
)); ));
} }
t t
}, }
Err(_) => { Err(_) => {
return UploadResponse::err(i18n!(i18n.catalog, "Parsing of key data failed.")); return UploadResponse::err(i18n!(i18n.catalog, "Parsing of key data failed."));
} }
@ -131,7 +132,13 @@ pub fn process_key(
match tpks.len() { match tpks.len() {
0 => UploadResponse::err(i18n!(i18n.catalog, "No key uploaded.")), 0 => UploadResponse::err(i18n!(i18n.catalog, "No key uploaded.")),
1 => process_key_single(db, i18n, tokens_stateless, rate_limiter, tpks.into_iter().next().unwrap()), 1 => process_key_single(
db,
i18n,
tokens_stateless,
rate_limiter,
tpks.into_iter().next().unwrap(),
),
_ => process_key_multiple(db, tpks), _ => process_key_multiple(db, tpks),
} }
} }
@ -147,14 +154,10 @@ fn log_db_merge(import_result: Result<ImportResult>) -> Result<ImportResult> {
import_result import_result
} }
fn process_key_multiple( fn process_key_multiple(db: &KeyDatabase, tpks: Vec<Cert>) -> response::UploadResponse {
db: &KeyDatabase,
tpks: Vec<Cert>,
) -> response::UploadResponse {
let key_fprs: Vec<_> = tpks let key_fprs: Vec<_> = tpks
.into_iter() .into_iter()
.flat_map(|tpk| Fingerprint::try_from(tpk.fingerprint()) .flat_map(|tpk| Fingerprint::try_from(tpk.fingerprint()).map(|fpr| (fpr, tpk)))
.map(|fpr| (fpr, tpk)))
.flat_map(|(fpr, tpk)| log_db_merge(db.merge(tpk)).map(|_| fpr.to_string())) .flat_map(|(fpr, tpk)| log_db_merge(db.merge(tpk)).map(|_| fpr.to_string()))
.collect(); .collect();
@ -174,17 +177,21 @@ fn process_key_single(
Ok(ImportResult::New(tpk_status)) => (tpk_status, true), Ok(ImportResult::New(tpk_status)) => (tpk_status, true),
Ok(ImportResult::Updated(tpk_status)) => (tpk_status, false), Ok(ImportResult::Updated(tpk_status)) => (tpk_status, false),
Ok(ImportResult::Unchanged(tpk_status)) => (tpk_status, false), Ok(ImportResult::Unchanged(tpk_status)) => (tpk_status, false),
Err(_) => return UploadResponse::err(i18n!(i18n.catalog, "Error processing uploaded key.")), Err(_) => {
return UploadResponse::err(i18n!(i18n.catalog, "Error processing uploaded key."))
}
}; };
let verify_state = { let verify_state = {
let emails = tpk_status.email_status.iter() let emails = tpk_status
.map(|(email,_)| email.clone()) .email_status
.iter()
.map(|(email, _)| email.clone())
.collect(); .collect();
VerifyTpkState { VerifyTpkState {
fpr: fp, fpr: fp,
addresses: emails, addresses: emails,
requested: vec!(), requested: vec![],
} }
}; };
@ -210,18 +217,19 @@ pub fn request_verify(
}; };
if tpk_status.is_revoked { if tpk_status.is_revoked {
return show_upload_verify( return show_upload_verify(rate_limiter, token, tpk_status, verify_state, false);
rate_limiter, token, tpk_status, verify_state, false);
} }
let emails_requested: Vec<_> = addresses.into_iter() let emails_requested: Vec<_> = addresses
.into_iter()
.map(|address| address.parse::<Email>()) .map(|address| address.parse::<Email>())
.flatten() .flatten()
.filter(|email| verify_state.addresses.contains(email)) .filter(|email| verify_state.addresses.contains(email))
.filter(|email| tpk_status.email_status.iter() .filter(|email| {
.any(|(uid_email, status)| tpk_status.email_status.iter().any(|(uid_email, status)| {
uid_email == email && *status == EmailAddressStatus::NotPublished uid_email == email && *status == EmailAddressStatus::NotPublished
)) })
})
.collect(); .collect();
for email in emails_requested { for email in emails_requested {
@ -237,10 +245,7 @@ pub fn request_verify(
) )
.is_err() .is_err()
{ {
return UploadResponse::err(&format!( return UploadResponse::err(&format!("error sending email to {}", &email));
"error sending email to {}",
&email
));
} }
} }
@ -252,12 +257,15 @@ fn check_tpk_state(
token_stateless: &tokens::Service, token_stateless: &tokens::Service,
i18n: &I18n, i18n: &I18n,
token: &str, token: &str,
) -> Result<(VerifyTpkState,TpkStatus)> { ) -> Result<(VerifyTpkState, TpkStatus)> {
let verify_state = token_stateless.check::<VerifyTpkState>(token) let verify_state = token_stateless
.map_err(|_| anyhow!(i18n!( .check::<VerifyTpkState>(token)
.map_err(|_| {
anyhow!(i18n!(
i18n.catalog, i18n.catalog,
"Upload session expired. Please try again." "Upload session expired. Please try again."
)))?; ))
})?;
let tpk_status = db.get_tpk_status(&verify_state.fpr, &verify_state.addresses)?; let tpk_status = db.get_tpk_status(&verify_state.fpr, &verify_state.addresses)?;
Ok((verify_state, tpk_status)) Ok((verify_state, tpk_status))
} }
@ -291,13 +299,12 @@ pub fn verify_confirm(
) -> response::PublishResponse { ) -> response::PublishResponse {
let (fingerprint, email) = match check_publish_token(db, token_service, token) { let (fingerprint, email) = match check_publish_token(db, token_service, token) {
Ok(x) => x, Ok(x) => x,
Err(_) => return PublishResponse::err( Err(_) => return PublishResponse::err(i18n!(i18n.catalog, "Invalid verification link.")),
i18n!(i18n.catalog, "Invalid verification link.")),
}; };
response::PublishResponse::Ok { response::PublishResponse::Ok {
fingerprint: fingerprint.to_string(), fingerprint: fingerprint.to_string(),
email: email.to_string() email: email.to_string(),
} }
} }
@ -305,7 +312,7 @@ fn check_publish_token(
db: &KeyDatabase, db: &KeyDatabase,
token_service: &StatefulTokens, token_service: &StatefulTokens,
token: String, token: String,
) -> Result<(Fingerprint,Email)> { ) -> Result<(Fingerprint, Email)> {
let payload = token_service.pop_token("verify", &token)?; let payload = token_service.pop_token("verify", &token)?;
let (fingerprint, email) = serde_json::from_str(&payload)?; let (fingerprint, email) = serde_json::from_str(&payload)?;
@ -335,28 +342,41 @@ fn show_upload_verify(
}; };
} }
let status: HashMap<_,_> = tpk_status.email_status let status: HashMap<_, _> = tpk_status
.email_status
.iter() .iter()
.map(|(email,status)| { .map(|(email, status)| {
let is_pending = (*status == EmailAddressStatus::NotPublished) && let is_pending = (*status == EmailAddressStatus::NotPublished)
!rate_limiter.action_check(format!("verify-{}", &email)); && !rate_limiter.action_check(format!("verify-{}", &email));
if is_pending { if is_pending {
(email.to_string(), EmailStatus::Pending) (email.to_string(), EmailStatus::Pending)
} else { } else {
(email.to_string(), match status { (
email.to_string(),
match status {
EmailAddressStatus::NotPublished => EmailStatus::Unpublished, EmailAddressStatus::NotPublished => EmailStatus::Unpublished,
EmailAddressStatus::Published => EmailStatus::Published, EmailAddressStatus::Published => EmailStatus::Published,
EmailAddressStatus::Revoked => EmailStatus::Revoked, EmailAddressStatus::Revoked => EmailStatus::Revoked,
}) },
)
} }
}) })
.collect(); .collect();
let primary_uid = tpk_status.email_status let primary_uid = tpk_status
.email_status
.get(0) .get(0)
.map(|(email, _)| email) .map(|(email, _)| email)
.cloned(); .cloned();
let count_unparsed = tpk_status.unparsed_uids; let count_unparsed = tpk_status.unparsed_uids;
response::UploadResponse::Ok { token, key_fpr, count_unparsed, is_revoked: false, status, is_new_key, primary_uid } response::UploadResponse::Ok {
token,
key_fpr,
count_unparsed,
is_revoked: false,
status,
is_new_key,
primary_uid,
}
} }

View File

@ -1,20 +1,21 @@
use rocket::request::Request; use rocket::response::{self, Response, Responder}; use rocket::http::{ContentType, Status};
use rocket::http::{ContentType,Status}; use rocket::request::Request;
use rocket::response::{self, Responder, Response};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket_i18n::{I18n, Translations}; use rocket_i18n::{I18n, Translations};
use serde_json::json; use serde_json::json;
use std::io::Cursor; use std::io::Cursor;
use crate::database::{KeyDatabase, StatefulTokens, Query};
use crate::database::types::{Email, Fingerprint, KeyID}; use crate::database::types::{Email, Fingerprint, KeyID};
use crate::database::{KeyDatabase, Query, StatefulTokens};
use crate::mail; use crate::mail;
use crate::tokens;
use crate::rate_limiter::RateLimiter; use crate::rate_limiter::RateLimiter;
use crate::tokens;
use crate::web; use crate::web;
use crate::web::{RequestOrigin, MyResponse};
use crate::web::vks; use crate::web::vks;
use crate::web::vks::response::*; use crate::web::vks::response::*;
use crate::web::{MyResponse, RequestOrigin};
use rocket::serde::json::Error as JsonError; use rocket::serde::json::Error as JsonError;
@ -34,18 +35,18 @@ pub mod json {
pub keytext: String, pub keytext: String,
} }
#[derive(Serialize,Deserialize)] #[derive(Serialize, Deserialize)]
pub struct UploadResult { pub struct UploadResult {
pub token: String, pub token: String,
pub key_fpr: String, pub key_fpr: String,
pub status: HashMap<String,EmailStatus>, pub status: HashMap<String, EmailStatus>,
} }
} }
type JsonResult = Result<serde_json::Value, JsonErrorResponse>; type JsonResult = Result<serde_json::Value, JsonErrorResponse>;
#[derive(Debug)] #[derive(Debug)]
pub struct JsonErrorResponse(Status,String); pub struct JsonErrorResponse(Status, String);
impl<'r> Responder<'r, 'static> for JsonErrorResponse { impl<'r> Responder<'r, 'static> for JsonErrorResponse {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
@ -61,15 +62,26 @@ impl<'r> Responder<'r, 'static> for JsonErrorResponse {
fn json_or_error<T>(data: Result<Json<T>, JsonError>) -> Result<Json<T>, JsonErrorResponse> { fn json_or_error<T>(data: Result<Json<T>, JsonError>) -> Result<Json<T>, JsonErrorResponse> {
match data { match data {
Ok(data) => Ok(data), Ok(data) => Ok(data),
Err(JsonError::Io(_)) => Err(JsonErrorResponse(Status::InternalServerError, "i/o error!".to_owned())), Err(JsonError::Io(_)) => Err(JsonErrorResponse(
Status::InternalServerError,
"i/o error!".to_owned(),
)),
Err(JsonError::Parse(_, e)) => Err(JsonErrorResponse(Status::BadRequest, e.to_string())), Err(JsonError::Parse(_, e)) => Err(JsonErrorResponse(Status::BadRequest, e.to_string())),
} }
} }
fn upload_ok_json(response: UploadResponse) -> Result<serde_json::Value, JsonErrorResponse> { fn upload_ok_json(response: UploadResponse) -> Result<serde_json::Value, JsonErrorResponse> {
match response { match response {
UploadResponse::Ok { token, key_fpr, status, .. } => UploadResponse::Ok {
Ok(json!(json::UploadResult { token, key_fpr, status })), token,
key_fpr,
status,
..
} => Ok(json!(json::UploadResult {
token,
key_fpr,
status
})),
UploadResponse::OkMulti { key_fprs } => Ok(json!(key_fprs)), UploadResponse::OkMulti { key_fprs } => Ok(json!(key_fprs)),
UploadResponse::Error(error) => Err(JsonErrorResponse(Status::BadRequest, error)), UploadResponse::Error(error) => Err(JsonErrorResponse(Status::BadRequest, error)),
} }
@ -91,28 +103,29 @@ pub fn upload_json(
} }
#[post("/vks/v1/upload", rank = 2)] #[post("/vks/v1/upload", rank = 2)]
pub fn upload_fallback( pub fn upload_fallback(origin: RequestOrigin) -> JsonErrorResponse {
origin: RequestOrigin, let error_msg = format!(
) -> JsonErrorResponse { "expected application/json data. see {}/about/api for api docs.",
let error_msg = format!("expected application/json data. see {}/about/api for api docs.", origin.get_base_uri()); origin.get_base_uri()
);
JsonErrorResponse(Status::BadRequest, error_msg) JsonErrorResponse(Status::BadRequest, error_msg)
} }
fn get_locale( fn get_locale(langs: &rocket::State<Translations>, locales: Vec<String>) -> I18n {
langs: &rocket::State<Translations>,
locales: Vec<String>,
) -> I18n {
locales locales
.iter() .iter()
.flat_map(|lang| lang.split(|c| c == '-' || c == ';' || c == '_').next()) .flat_map(|lang| lang.split(|c| c == '-' || c == ';' || c == '_').next())
.flat_map(|lang| langs.iter().find(|(trans, _)| trans == &lang)) .flat_map(|lang| langs.iter().find(|(trans, _)| trans == &lang))
.next() .next()
.or_else(|| langs.iter().find(|(trans, _)| trans == &"en")) .or_else(|| langs.iter().find(|(trans, _)| trans == &"en"))
.map(|(lang, catalog)| I18n { catalog: catalog.clone(), lang }) .map(|(lang, catalog)| I18n {
catalog: catalog.clone(),
lang,
})
.expect("Expected to have an english translation!") .expect("Expected to have an english translation!")
} }
#[post("/vks/v1/request-verify", format = "json", data="<data>")] #[post("/vks/v1/request-verify", format = "json", data = "<data>")]
pub fn request_verify_json( pub fn request_verify_json(
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
langs: &rocket::State<Translations>, langs: &rocket::State<Translations>,
@ -124,19 +137,32 @@ pub fn request_verify_json(
data: Result<Json<json::VerifyRequest>, JsonError>, data: Result<Json<json::VerifyRequest>, JsonError>,
) -> JsonResult { ) -> JsonResult {
let data = json_or_error(data)?; let data = json_or_error(data)?;
let json::VerifyRequest { token, addresses, locale } = data.into_inner(); let json::VerifyRequest {
token,
addresses,
locale,
} = data.into_inner();
let i18n = get_locale(langs, locale.unwrap_or_default()); let i18n = get_locale(langs, locale.unwrap_or_default());
let result = vks::request_verify( let result = vks::request_verify(
db, &origin, token_stateful, token_stateless, mail_service, db,
rate_limiter, &i18n, token, addresses); &origin,
token_stateful,
token_stateless,
mail_service,
rate_limiter,
&i18n,
token,
addresses,
);
upload_ok_json(result) upload_ok_json(result)
} }
#[post("/vks/v1/request-verify", rank = 2)] #[post("/vks/v1/request-verify", rank = 2)]
pub fn request_verify_fallback( pub fn request_verify_fallback(origin: RequestOrigin) -> JsonErrorResponse {
origin: RequestOrigin, let error_msg = format!(
) -> JsonErrorResponse { "expected application/json data. see {}/about/api for api docs.",
let error_msg = format!("expected application/json data. see {}/about/api for api docs.", origin.get_base_uri()); origin.get_base_uri()
);
JsonErrorResponse(Status::BadRequest, error_msg) JsonErrorResponse(Status::BadRequest, error_msg)
} }
@ -155,11 +181,7 @@ pub fn vks_v1_by_fingerprint(
} }
#[get("/vks/v1/by-email/<email>")] #[get("/vks/v1/by-email/<email>")]
pub fn vks_v1_by_email( pub fn vks_v1_by_email(db: &rocket::State<KeyDatabase>, i18n: I18n, email: String) -> MyResponse {
db: &rocket::State<KeyDatabase>,
i18n: I18n,
email: String,
) -> MyResponse {
let email = email.replace("%40", "@"); let email = email.replace("%40", "@");
let query = match email.parse::<Email>() { let query = match email.parse::<Email>() {
Ok(email) => Query::ByEmail(email), Ok(email) => Query::ByEmail(email),
@ -170,11 +192,7 @@ pub fn vks_v1_by_email(
} }
#[get("/vks/v1/by-keyid/<kid>")] #[get("/vks/v1/by-keyid/<kid>")]
pub fn vks_v1_by_keyid( pub fn vks_v1_by_keyid(db: &rocket::State<KeyDatabase>, i18n: I18n, kid: String) -> MyResponse {
db: &rocket::State<KeyDatabase>,
i18n: I18n,
kid: String,
) -> MyResponse {
let query = match kid.parse::<KeyID>() { let query = match kid.parse::<KeyID>() {
Ok(keyid) => Query::ByKeyID(keyid), Ok(keyid) => Query::ByKeyID(keyid),
Err(_) => return MyResponse::bad_request_plain("malformed key id"), Err(_) => return MyResponse::bad_request_plain("malformed key id"),

View File

@ -4,21 +4,21 @@ use multipart::server::save::Entries;
use multipart::server::save::SaveResult::*; use multipart::server::save::SaveResult::*;
use multipart::server::Multipart; use multipart::server::Multipart;
use gettext_macros::i18n;
use rocket::data::ByteUnit; use rocket::data::ByteUnit;
use rocket::form::Form; use rocket::form::Form;
use rocket::form::ValueField; use rocket::form::ValueField;
use rocket::http::ContentType; use rocket::http::ContentType;
use rocket::Data; use rocket::Data;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use gettext_macros::i18n;
use url::percent_encoding::percent_decode; use url::percent_encoding::percent_decode;
use crate::database::{KeyDatabase, StatefulTokens, Query, Database}; use crate::database::{Database, KeyDatabase, Query, StatefulTokens};
use crate::mail;
use crate::tokens;
use crate::web::{RequestOrigin, MyResponse};
use crate::rate_limiter::RateLimiter;
use crate::i18n_helpers::describe_query_error; use crate::i18n_helpers::describe_query_error;
use crate::mail;
use crate::rate_limiter::RateLimiter;
use crate::tokens;
use crate::web::{MyResponse, RequestOrigin};
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Cursor; use std::io::Cursor;
@ -29,7 +29,7 @@ use crate::web::vks::response::*;
const UPLOAD_LIMIT: ByteUnit = ByteUnit::Mebibyte(1); const UPLOAD_LIMIT: ByteUnit = ByteUnit::Mebibyte(1);
mod forms { mod forms {
#[derive(FromForm,Deserialize)] #[derive(FromForm, Deserialize)]
pub struct VerifyRequest { pub struct VerifyRequest {
pub token: String, pub token: String,
pub address: String, pub address: String,
@ -90,12 +90,10 @@ mod template {
pub address: String, pub address: String,
pub requested: bool, pub requested: bool,
} }
} }
impl MyResponse { impl MyResponse {
fn upload_response_quick(response: UploadResponse, fn upload_response_quick(response: UploadResponse, i18n: I18n, origin: RequestOrigin) -> Self {
i18n: I18n, origin: RequestOrigin) -> Self {
match response { match response {
UploadResponse::Ok { token, .. } => { UploadResponse::Ok { token, .. } => {
let uri = uri!(quick_upload_proceed(token)); let uri = uri!(quick_upload_proceed(token));
@ -105,23 +103,39 @@ impl MyResponse {
uri uri
); );
MyResponse::plain(text) MyResponse::plain(text)
}, }
UploadResponse::OkMulti { key_fprs } => UploadResponse::OkMulti { key_fprs } => MyResponse::plain(format!(
MyResponse::plain(format!("Uploaded {} keys. For verification, please upload keys individually.\n", key_fprs.len())), "Uploaded {} keys. For verification, please upload keys individually.\n",
UploadResponse::Error(error) => MyResponse::bad_request( key_fprs.len()
"400-plain", anyhow!(error), i18n, origin), )),
UploadResponse::Error(error) => {
MyResponse::bad_request("400-plain", anyhow!(error), i18n, origin)
}
} }
} }
fn upload_response(response: UploadResponse, fn upload_response(response: UploadResponse, i18n: I18n, origin: RequestOrigin) -> Self {
i18n: I18n, origin: RequestOrigin) -> Self {
match response { match response {
UploadResponse::Ok { token, key_fpr, is_revoked, count_unparsed, status, .. } => UploadResponse::Ok {
Self::upload_ok(token, key_fpr, is_revoked, count_unparsed, status, i18n, origin), token,
UploadResponse::OkMulti { key_fprs } => key_fpr,
Self::upload_ok_multi(key_fprs, i18n, origin), is_revoked,
UploadResponse::Error(error) => MyResponse::bad_request( count_unparsed,
"upload/upload", anyhow!(error), i18n, origin), status,
..
} => Self::upload_ok(
token,
key_fpr,
is_revoked,
count_unparsed,
status,
i18n,
origin,
),
UploadResponse::OkMulti { key_fprs } => Self::upload_ok_multi(key_fprs, i18n, origin),
UploadResponse::Error(error) => {
MyResponse::bad_request("upload/upload", anyhow!(error), i18n, origin)
}
} }
} }
@ -130,33 +144,35 @@ impl MyResponse {
key_fpr: String, key_fpr: String,
is_revoked: bool, is_revoked: bool,
count_unparsed: usize, count_unparsed: usize,
uid_status: HashMap<String,EmailStatus>, uid_status: HashMap<String, EmailStatus>,
i18n: I18n, i18n: I18n,
origin: RequestOrigin, origin: RequestOrigin,
) -> Self { ) -> Self {
let key_link = uri!(search(q = &key_fpr)).to_string(); let key_link = uri!(search(q = &key_fpr)).to_string();
let count_revoked = uid_status.iter() let count_revoked = uid_status
.filter(|(_,status)| **status == EmailStatus::Revoked) .iter()
.filter(|(_, status)| **status == EmailStatus::Revoked)
.count(); .count();
let mut email_published: Vec<_> = uid_status.iter() let mut email_published: Vec<_> = uid_status
.filter(|(_,status)| **status == EmailStatus::Published) .iter()
.map(|(email,_)| email.to_string()) .filter(|(_, status)| **status == EmailStatus::Published)
.map(|(email, _)| email.to_string())
.collect(); .collect();
email_published.sort_unstable(); email_published.sort_unstable();
let mut email_unpublished: Vec<_> = uid_status.into_iter() let mut email_unpublished: Vec<_> = uid_status
.filter(|(_,status)| *status == EmailStatus::Unpublished || .into_iter()
*status == EmailStatus::Pending) .filter(|(_, status)| {
.map(|(email,status)| *status == EmailStatus::Unpublished || *status == EmailStatus::Pending
template::UploadUidStatus { })
.map(|(email, status)| template::UploadUidStatus {
address: email, address: email,
requested: status == EmailStatus::Pending, requested: status == EmailStatus::Pending,
}) })
.collect(); .collect();
email_unpublished email_unpublished.sort_unstable_by(|fst, snd| fst.address.cmp(&snd.address));
.sort_unstable_by(|fst,snd| fst.address.cmp(&snd.address));
let context = template::VerificationSent { let context = template::VerificationSent {
is_revoked, is_revoked,
@ -173,9 +189,9 @@ impl MyResponse {
MyResponse::ok("upload/upload-ok", context, i18n, origin) MyResponse::ok("upload/upload-ok", context, i18n, origin)
} }
fn upload_ok_multi(key_fprs: Vec<String>, fn upload_ok_multi(key_fprs: Vec<String>, i18n: I18n, origin: RequestOrigin) -> Self {
i18n: I18n, origin: RequestOrigin) -> Self { let keys = key_fprs
let keys = key_fprs.into_iter() .into_iter()
.map(|fpr| { .map(|fpr| {
let key_link = uri!(search(q = &fpr)).to_string(); let key_link = uri!(search(q = &fpr)).to_string();
template::UploadOkKey { template::UploadOkKey {
@ -185,9 +201,7 @@ impl MyResponse {
}) })
.collect(); .collect();
let context = template::UploadOkMultiple { let context = template::UploadOkMultiple { keys };
keys,
};
MyResponse::ok("upload/upload-ok-multiple", context, i18n, origin) MyResponse::ok("upload/upload-ok-multiple", context, i18n, origin)
} }
@ -208,9 +222,7 @@ pub async fn upload_post_form_data(
cont_type: &ContentType, cont_type: &ContentType,
data: Data<'_>, data: Data<'_>,
) -> MyResponse { ) -> MyResponse {
match process_upload(db, tokens_stateless, rate_limiter, &i18n, data, cont_type) match process_upload(db, tokens_stateless, rate_limiter, &i18n, data, cont_type).await {
.await
{
Ok(response) => MyResponse::upload_response(response, i18n, origin), Ok(response) => MyResponse::upload_response(response, i18n, origin),
Err(err) => MyResponse::bad_request("upload/upload", err, i18n, origin), Err(err) => MyResponse::bad_request("upload/upload", err, i18n, origin),
} }
@ -224,8 +236,7 @@ pub async fn process_post_form_data(
cont_type: &ContentType, cont_type: &ContentType,
data: Data<'_>, data: Data<'_>,
) -> Result<UploadResponse> { ) -> Result<UploadResponse> {
process_upload(db, tokens_stateless, rate_limiter, &i18n, data, cont_type) process_upload(db, tokens_stateless, rate_limiter, &i18n, data, cont_type).await
.await
} }
#[get("/search?<q>")] #[get("/search?<q>")]
@ -251,14 +262,17 @@ fn key_to_response(
let fp = if let Some(fp) = db.lookup_primary_fingerprint(&query) { let fp = if let Some(fp) = db.lookup_primary_fingerprint(&query) {
fp fp
} else if query.is_invalid() { } else if query.is_invalid() {
return MyResponse::bad_request("index", anyhow!(describe_query_error(&i18n, &query)), return MyResponse::bad_request(
i18n, origin); "index",
anyhow!(describe_query_error(&i18n, &query)),
i18n,
origin,
);
} else { } else {
return MyResponse::not_found(None, describe_query_error(&i18n, &query), return MyResponse::not_found(None, describe_query_error(&i18n, &query), i18n, origin);
i18n, origin);
}; };
let context = template::Search{ let context = template::Search {
query: query_string, query: query_string,
fpr: fp.to_string(), fpr: fp.to_string(),
}; };
@ -266,7 +280,6 @@ fn key_to_response(
MyResponse::ok("found", context, i18n, origin) MyResponse::ok("found", context, i18n, origin)
} }
#[put("/", data = "<data>")] #[put("/", data = "<data>")]
pub async fn quick_upload( pub async fn quick_upload(
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
@ -278,20 +291,14 @@ pub async fn quick_upload(
) -> MyResponse { ) -> MyResponse {
let buf = match data.open(UPLOAD_LIMIT).into_bytes().await { let buf = match data.open(UPLOAD_LIMIT).into_bytes().await {
Ok(buf) => buf.into_inner(), Ok(buf) => buf.into_inner(),
Err(error) => Err(error) => return MyResponse::bad_request("400-plain", anyhow!(error), i18n, origin),
return MyResponse::bad_request("400-plain", anyhow!(error),
i18n, origin),
}; };
MyResponse::upload_response_quick( MyResponse::upload_response_quick(
vks::process_key( vks::process_key(db, &i18n, tokens_stateless, rate_limiter, Cursor::new(buf)),
db, i18n,
&i18n, origin,
tokens_stateless, )
rate_limiter,
Cursor::new(buf)
),
i18n, origin)
} }
#[get("/upload/<token>", rank = 2)] #[get("/upload/<token>", rank = 2)]
@ -306,13 +313,24 @@ pub fn quick_upload_proceed(
token: String, token: String,
) -> MyResponse { ) -> MyResponse {
let result = vks::request_verify( let result = vks::request_verify(
db, &origin, token_stateful, token_stateless, mail_service, db,
rate_limiter, &i18n, token, vec!()); &origin,
token_stateful,
token_stateless,
mail_service,
rate_limiter,
&i18n,
token,
vec![],
);
MyResponse::upload_response(result, i18n, origin) MyResponse::upload_response(result, i18n, origin)
} }
#[post(
#[post("/upload/submit", format = "application/x-www-form-urlencoded", data = "<data>")] "/upload/submit",
format = "application/x-www-form-urlencoded",
data = "<data>"
)]
pub async fn upload_post_form( pub async fn upload_post_form(
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
origin: RequestOrigin, origin: RequestOrigin,
@ -322,10 +340,8 @@ pub async fn upload_post_form(
data: Data<'_>, data: Data<'_>,
) -> MyResponse { ) -> MyResponse {
match process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await { match process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await {
Ok(response) => MyResponse::upload_response(response, Ok(response) => MyResponse::upload_response(response, i18n, origin),
i18n, origin), Err(err) => MyResponse::bad_request("upload/upload", err, i18n, origin),
Err(err) => MyResponse::bad_request("upload/upload", err,
i18n, origin),
} }
} }
@ -340,9 +356,9 @@ pub async fn process_post_form(
let buf = data.open(UPLOAD_LIMIT).into_bytes().await?; let buf = data.open(UPLOAD_LIMIT).into_bytes().await?;
for ValueField { name, value } in Form::values(&*String::from_utf8_lossy(&buf)) { for ValueField { name, value } in Form::values(&*String::from_utf8_lossy(&buf)) {
let decoded_value = percent_decode(value.as_bytes()).decode_utf8().map_err(|_| let decoded_value = percent_decode(value.as_bytes())
anyhow!("`Content-Type: application/x-www-form-urlencoded` not valid") .decode_utf8()
)?; .map_err(|_| anyhow!("`Content-Type: application/x-www-form-urlencoded` not valid"))?;
if name.to_string().as_str() == "keytext" { if name.to_string().as_str() == "keytext" {
return Ok(vks::process_key( return Ok(vks::process_key(
@ -350,7 +366,7 @@ pub async fn process_post_form(
i18n, i18n,
tokens_stateless, tokens_stateless,
rate_limiter, rate_limiter,
Cursor::new(decoded_value.as_bytes()) Cursor::new(decoded_value.as_bytes()),
)); ));
} }
} }
@ -358,7 +374,6 @@ pub async fn process_post_form(
Err(anyhow!("No keytext found")) Err(anyhow!("No keytext found"))
} }
async fn process_upload( async fn process_upload(
db: &KeyDatabase, db: &KeyDatabase,
tokens_stateless: &tokens::Service, tokens_stateless: &tokens::Service,
@ -371,21 +386,23 @@ async fn process_upload(
let (_, boundary) = cont_type let (_, boundary) = cont_type
.params() .params()
.find(|&(k, _)| k == "boundary") .find(|&(k, _)| k == "boundary")
.ok_or_else(|| anyhow!("`Content-Type: multipart/form-data` \ .ok_or_else(|| {
boundary param not provided"))?; anyhow!(
"`Content-Type: multipart/form-data` \
boundary param not provided"
)
})?;
// saves all fields, any field longer than 10kB goes to a temporary directory // saves all fields, any field longer than 10kB goes to a temporary directory
// Entries could implement FromData though that would give zero control over // Entries could implement FromData though that would give zero control over
// how the files are saved; Multipart would be a good impl candidate though // how the files are saved; Multipart would be a good impl candidate though
let data = Cursor::new(data.open(UPLOAD_LIMIT).into_bytes().await?.value); let data = Cursor::new(data.open(UPLOAD_LIMIT).into_bytes().await?.value);
match Multipart::with_body(data, boundary).save().temp() { match Multipart::with_body(data, boundary).save().temp() {
Full(entries) => { Full(entries) => process_multipart(db, tokens_stateless, rate_limiter, i18n, entries),
process_multipart(db, tokens_stateless, rate_limiter, i18n, entries)
}
Partial(partial, _) => { Partial(partial, _) => {
process_multipart(db, tokens_stateless, rate_limiter, i18n, partial.entries) process_multipart(db, tokens_stateless, rate_limiter, i18n, partial.entries)
} }
Error(err) => Err(err.into()) Error(err) => Err(err.into()),
} }
} }
@ -399,14 +416,24 @@ fn process_multipart(
match entries.fields.get("keytext") { match entries.fields.get("keytext") {
Some(ent) if ent.len() == 1 => { Some(ent) if ent.len() == 1 => {
let reader = ent[0].data.readable()?; let reader = ent[0].data.readable()?;
Ok(vks::process_key(db, i18n, tokens_stateless, rate_limiter, reader)) Ok(vks::process_key(
db,
i18n,
tokens_stateless,
rate_limiter,
reader,
))
} }
Some(_) => Err(anyhow!("Multiple keytexts found")), Some(_) => Err(anyhow!("Multiple keytexts found")),
None => Err(anyhow!("No keytext found")), None => Err(anyhow!("No keytext found")),
} }
} }
#[post("/upload/request-verify", format = "application/x-www-form-urlencoded", data="<request>")] #[post(
"/upload/request-verify",
format = "application/x-www-form-urlencoded",
data = "<request>"
)]
pub fn request_verify_form( pub fn request_verify_form(
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
origin: RequestOrigin, origin: RequestOrigin,
@ -419,12 +446,24 @@ pub fn request_verify_form(
) -> MyResponse { ) -> MyResponse {
let forms::VerifyRequest { token, address } = request.into_inner(); let forms::VerifyRequest { token, address } = request.into_inner();
let result = vks::request_verify( let result = vks::request_verify(
db, &origin, token_stateful, token_stateless, mail_service, db,
rate_limiter, &i18n, token, vec!(address)); &origin,
token_stateful,
token_stateless,
mail_service,
rate_limiter,
&i18n,
token,
vec![address],
);
MyResponse::upload_response(result, i18n, origin) MyResponse::upload_response(result, i18n, origin)
} }
#[post("/upload/request-verify", format = "multipart/form-data", data="<request>")] #[post(
"/upload/request-verify",
format = "multipart/form-data",
data = "<request>"
)]
pub fn request_verify_form_data( pub fn request_verify_form_data(
db: &rocket::State<KeyDatabase>, db: &rocket::State<KeyDatabase>,
origin: RequestOrigin, origin: RequestOrigin,
@ -437,8 +476,16 @@ pub fn request_verify_form_data(
) -> MyResponse { ) -> MyResponse {
let forms::VerifyRequest { token, address } = request.into_inner(); let forms::VerifyRequest { token, address } = request.into_inner();
let result = vks::request_verify( let result = vks::request_verify(
db, &origin, token_stateful, token_stateless, mail_service, db,
rate_limiter, &i18n, token, vec!(address)); &origin,
token_stateful,
token_stateless,
mail_service,
rate_limiter,
&i18n,
token,
vec![address],
);
MyResponse::upload_response(result, i18n, origin) MyResponse::upload_response(result, i18n, origin)
} }
@ -463,12 +510,15 @@ pub fn verify_confirm(
}; };
MyResponse::ok("upload/publish-result", context, i18n, origin) MyResponse::ok("upload/publish-result", context, i18n, origin)
}, }
PublishResponse::Error(error) => { PublishResponse::Error(error) => {
let error_msg = if rate_limiter.action_check(rate_limit_id) { let error_msg = if rate_limiter.action_check(rate_limit_id) {
anyhow!(error) anyhow!(error)
} else { } else {
anyhow!(i18n!(i18n.catalog, "This address has already been verified.")) anyhow!(i18n!(
i18n.catalog,
"This address has already been verified."
))
}; };
MyResponse::bad_request("400", error_msg, i18n, origin) MyResponse::bad_request("400", error_msg, i18n, origin)
} }
@ -476,12 +526,11 @@ pub fn verify_confirm(
} }
#[get("/verify/<token>")] #[get("/verify/<token>")]
pub fn verify_confirm_form( pub fn verify_confirm_form(origin: RequestOrigin, i18n: I18n, token: String) -> MyResponse {
origin: RequestOrigin, MyResponse::ok(
i18n: I18n, "upload/verification-form",
token: String, template::VerifyForm { token },
) -> MyResponse { i18n,
MyResponse::ok("upload/verification-form", template::VerifyForm { origin,
token )
}, i18n, origin)
} }

View File

@ -3,24 +3,16 @@ use crate::web::MyResponse;
// WKD queries // WKD queries
#[get("/.well-known/openpgpkey/<domain>/hu/<wkd_hash>")] #[get("/.well-known/openpgpkey/<domain>/hu/<wkd_hash>")]
pub fn wkd_query( pub fn wkd_query(db: &rocket::State<KeyDatabase>, domain: String, wkd_hash: String) -> MyResponse {
db: &rocket::State<KeyDatabase>,
domain: String,
wkd_hash: String,
) -> MyResponse {
match db.by_domain_and_hash_wkd(&domain, &wkd_hash) { match db.by_domain_and_hash_wkd(&domain, &wkd_hash) {
Some(key) => MyResponse::wkd(key, &wkd_hash), Some(key) => MyResponse::wkd(key, &wkd_hash),
None => MyResponse::not_found_plain( None => MyResponse::not_found_plain("No key found for this email address."),
"No key found for this email address.",
),
} }
} }
// Policy requests. // Policy requests.
// 200 response with an empty body. // 200 response with an empty body.
#[get("/.well-known/openpgpkey/<_domain>/policy")] #[get("/.well-known/openpgpkey/<_domain>/policy")]
pub fn wkd_policy( pub fn wkd_policy(_domain: String) -> MyResponse {
_domain: String,
) -> MyResponse {
MyResponse::plain("".to_string()) MyResponse::plain("".to_string())
} }