cargo: apply cargo fmt --all
This commit is contained in:
parent
961559e154
commit
b29845b893
3
build.rs
3
build.rs
|
@ -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!");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(¤t_fpr,
|
self.nolock_set_email_unpublished_filter(¤t_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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
864
src/dump.rs
864
src/dump.rs
File diff suppressed because it is too large
Load Diff
22
src/i18n.rs
22
src/i18n.rs
|
@ -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 {
|
||||||
|
|
|
@ -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."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
127
src/mail.rs
127
src/mail.rs
|
@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/main.rs
16
src/main.rs
|
@ -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() -> _ {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
195
src/web/hkp.rs
195
src/web/hkp.rs
|
@ -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();
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
516
src/web/mod.rs
516
src/web/mod.rs
|
@ -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();
|
||||||
|
|
134
src/web/vks.rs
134
src/web/vks.rs
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue