mirror of
https://gitlab.com/hagrid-keyserver/hagrid.git
synced 2023-02-13 20:55:02 -05:00
initial commit
This commit is contained in:
commit
66fef4a275
10 changed files with 1195 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
/target
|
||||
**/*.rs.bk
|
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "garbage-pile"
|
||||
version = "0.1.0"
|
||||
authors = ["seu <seu@panopticon.re>"]
|
||||
|
||||
[dependencies]
|
||||
rocket = "0"
|
||||
rocket_codegen = "0"
|
||||
openpgp = { path = "../sequoia/openpgp" }
|
||||
multipart = "0"
|
||||
error-chain = "0"
|
||||
log = "0"
|
||||
rand = "0.5"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
base64 = "0.9"
|
||||
time = "0.1"
|
||||
tempfile = "3.0"
|
||||
parking_lot = "0.6"
|
||||
structopt = "0.2"
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
Garbage Pile - The verifying OpenPGP key server
|
||||
===============================================
|
||||
|
||||
`cargo run -- --help`
|
249
src/database/fs.rs
Normal file
249
src/database/fs.rs
Normal file
|
@ -0,0 +1,249 @@
|
|||
use std::str;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::{File, remove_file, create_dir_all, read_link};
|
||||
use std::io::{Write, Read};
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use tempfile;
|
||||
use serde_json;
|
||||
use openpgp::UserID;
|
||||
use base64;
|
||||
|
||||
use database::{Verify, Delete, Fingerprint, Database};
|
||||
use Result;
|
||||
|
||||
struct Filesystem {
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
impl Filesystem {
|
||||
pub fn new<P: Into<PathBuf>>(base: P) -> Result<Self> {
|
||||
use std::fs;
|
||||
|
||||
let base: PathBuf = base.into();
|
||||
|
||||
if fs::create_dir(&base).is_err() {
|
||||
let meta = fs::metadata(&base);
|
||||
|
||||
match meta {
|
||||
Ok(meta) => {
|
||||
if !meta.file_type().is_dir() {
|
||||
return Err(format!("'{}' exists already and is not a directory",
|
||||
base.display()).into());
|
||||
}
|
||||
|
||||
if meta.permissions().readonly() {
|
||||
return Err(format!("Cannot write '{}'", base.display()).into());
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
return Err(format!("Cannot read '{}': {}", base.display(),e).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create directories
|
||||
create_dir_all(base.join("verification_tokens"))?;
|
||||
create_dir_all(base.join("deletion_tokens"))?;
|
||||
create_dir_all(base.join("scratch_pad"))?;
|
||||
create_dir_all(base.join("public").join("by-fpr"))?;
|
||||
create_dir_all(base.join("public").join("by-uid"))?;
|
||||
|
||||
info!("Opened base dir '{}'", base.display());
|
||||
Ok(Filesystem{
|
||||
base: base,
|
||||
})
|
||||
}
|
||||
|
||||
fn new_token<'a>(&self, base: &'a str) -> Result<(File, String)> {
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand::distributions::Alphanumeric;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
// samples from [a-zA-Z0-9]
|
||||
// 43 chars ~ 256 bit
|
||||
let name: String = rng.sample_iter(&Alphanumeric).take(43).collect();
|
||||
let dir = self.base.join(base);
|
||||
|
||||
let fd = File::create(dir.join(name.clone()))?;
|
||||
|
||||
Ok((fd, name))
|
||||
}
|
||||
|
||||
fn pop_token<'a>(&self, base: &'a str, token: &'a str) -> Result<Box<[u8]>> {
|
||||
let path = self.base.join(base).join(token);
|
||||
let buf = {
|
||||
let mut fd = File::open(path.clone())?;
|
||||
let mut buf = Vec::default();
|
||||
|
||||
fd.read_to_end(&mut buf)?;
|
||||
buf.into_boxed_slice()
|
||||
};
|
||||
|
||||
remove_file(path)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Database for Filesystem {
|
||||
fn new_verify_token(&mut self, payload: Verify) -> Result<String> {
|
||||
let (mut fd, name) = self.new_token("verification_tokens")?;
|
||||
fd.write_all(serde_json::to_string(&payload)?.as_bytes())?;
|
||||
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
fn new_delete_token(&mut self, payload: Delete) -> Result<String> {
|
||||
let (mut fd, name) = self.new_token("deletion_tokens")?;
|
||||
fd.write_all(serde_json::to_string(&payload)?.as_bytes())?;
|
||||
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
fn compare_and_swap(&mut self, fpr: &Fingerprint, old: Option<&[u8]>, new: Option<&[u8]>) -> Result<bool> {
|
||||
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
||||
let dir = self.base.join("scratch_pad");
|
||||
|
||||
match new {
|
||||
Some(new) => {
|
||||
let mut tmp = tempfile::Builder::new()
|
||||
.prefix("key")
|
||||
.rand_bytes(16)
|
||||
.tempfile_in(dir)?;
|
||||
tmp.write_all(new)?;
|
||||
|
||||
if target.is_file() {
|
||||
if old.is_some() { remove_file(target.clone())?; }
|
||||
else { return Err(format!("stray file {}", target.display()).into()); }
|
||||
}
|
||||
let _ = tmp.persist(target)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
None => {
|
||||
remove_file(target)?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn link_userid(&mut self, uid: &UserID, fpr: &Fingerprint) {
|
||||
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
|
||||
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
||||
let link = self.base.join("public").join("by-uid").join(uid);
|
||||
|
||||
if link.exists() {
|
||||
let _ = remove_file(link.clone());
|
||||
}
|
||||
|
||||
let _ = symlink(target, link);
|
||||
}
|
||||
|
||||
fn unlink_userid(&mut self, uid: &UserID, fpr: &Fingerprint) {
|
||||
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
|
||||
let link = self.base.join("public").join("by-uid").join(uid);
|
||||
|
||||
match read_link(link.clone()) {
|
||||
Ok(target) => {
|
||||
let expected = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
||||
|
||||
if target == expected {
|
||||
let _ = remove_file(link);
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_verify_token(&mut self, token: &str) -> Option<Verify> {
|
||||
self.pop_token("verification_tokens", token).ok().and_then(|raw| {
|
||||
str::from_utf8(&raw).ok().map(|s| s.to_string())
|
||||
}).and_then(|s| {
|
||||
let s = serde_json::from_str(&s);
|
||||
s.ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn pop_delete_token(&mut self, token: &str) -> Option<Delete> {
|
||||
self.pop_token("deletion_tokens", token).ok().and_then(|raw| {
|
||||
str::from_utf8(&raw).ok().map(|s| s.to_string())
|
||||
}).and_then(|s| {
|
||||
serde_json::from_str(&s).ok()
|
||||
})
|
||||
}
|
||||
|
||||
// XXX: slow
|
||||
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>> {
|
||||
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
||||
|
||||
File::open(target).ok().and_then(|mut fd| {
|
||||
let mut buf = Vec::default();
|
||||
if fd.read_to_end(&mut buf).is_ok() {
|
||||
Some(buf.into_boxed_slice())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// XXX: slow
|
||||
fn by_uid(&self, uid: &str) -> Option<Box<[u8]>> {
|
||||
let target = self.base.join("public").join("by-uid").join(uid);
|
||||
|
||||
File::open(target).ok().and_then(|mut fd| {
|
||||
let mut buf = Vec::default();
|
||||
if fd.read_to_end(&mut buf).is_ok() {
|
||||
Some(buf.into_boxed_slice())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
use openpgp::tpk::TPKBuilder;
|
||||
use database::test;
|
||||
|
||||
#[test]
|
||||
fn init() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let _ = Filesystem::new(tmpdir.path()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
||||
let k1 = TPKBuilder::default().add_userid("a").generate().unwrap();
|
||||
let k2 = TPKBuilder::default().add_userid("b").generate().unwrap();
|
||||
let k3 = TPKBuilder::default().add_userid("c").generate().unwrap();
|
||||
|
||||
assert!(db.merge_or_publish(k1).unwrap().len() > 0);
|
||||
assert!(db.merge_or_publish(k2.clone()).unwrap().len() > 0);
|
||||
assert!(!db.merge_or_publish(k2).unwrap().len() > 0);
|
||||
assert!(db.merge_or_publish(k3.clone()).unwrap().len() > 0);
|
||||
assert!(!db.merge_or_publish(k3.clone()).unwrap().len() > 0);
|
||||
assert!(!db.merge_or_publish(k3).unwrap().len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uid_verification() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
||||
|
||||
test::test_uid_verification(&mut db);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uid_deletion() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
||||
|
||||
test::test_uid_deletion(&mut db);
|
||||
}
|
||||
}
|
130
src/database/memory.rs
Normal file
130
src/database/memory.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use std::collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use openpgp::UserID;
|
||||
use base64;
|
||||
|
||||
use database::{Verify, Delete, Fingerprint, Database};
|
||||
use Result;
|
||||
|
||||
struct InMemory {
|
||||
fpr: Mutex<HashMap<Fingerprint, Box<[u8]>>>,
|
||||
userid: Mutex<HashMap<String, Fingerprint>>,
|
||||
verify_token: Mutex<HashMap<String, Verify>>,
|
||||
delete_token: Mutex<HashMap<String, Delete>>,
|
||||
}
|
||||
|
||||
impl Default for InMemory {
|
||||
fn default() -> Self {
|
||||
InMemory{
|
||||
fpr: Mutex::new(HashMap::default()),
|
||||
userid: Mutex::new(HashMap::default()),
|
||||
verify_token: Mutex::new(HashMap::default()),
|
||||
delete_token: Mutex::new(HashMap::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database for InMemory {
|
||||
fn new_verify_token(&mut self, payload: Verify) -> Result<String> {
|
||||
let token = Self::new_token();
|
||||
|
||||
self.verify_token.lock().insert(token.clone(), payload);
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn new_delete_token(&mut self, payload: Delete) -> Result<String> {
|
||||
let token = Self::new_token();
|
||||
|
||||
self.delete_token.lock().insert(token.clone(), payload);
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn compare_and_swap(&mut self, fpr: &Fingerprint, present: Option<&[u8]>, new: Option<&[u8]>) -> Result<bool> {
|
||||
let mut fprs = self.fpr.lock();
|
||||
|
||||
if fprs.get(fpr).map(|x| &x[..]) == present {
|
||||
if let Some(new) = new {
|
||||
fprs.insert(fpr.clone(), new.into());
|
||||
} else {
|
||||
fprs.remove(fpr);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn link_userid(&mut self, uid: &UserID, fpr: &Fingerprint) {
|
||||
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
|
||||
self.userid.lock().insert(uid.to_string(), fpr.clone());
|
||||
}
|
||||
|
||||
fn unlink_userid(&mut self, uid: &UserID, _: &Fingerprint) {
|
||||
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
|
||||
self.userid.lock().remove(&uid.to_string());
|
||||
}
|
||||
|
||||
// (verified uid, fpr)
|
||||
fn pop_verify_token(&mut self, token: &str) -> Option<Verify> {
|
||||
self.verify_token.lock().remove(token)
|
||||
}
|
||||
|
||||
// fpr
|
||||
fn pop_delete_token(&mut self, token: &str) -> Option<Delete> {
|
||||
self.delete_token.lock().remove(token)
|
||||
}
|
||||
|
||||
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>> {
|
||||
self.fpr.lock().get(fpr).map(|x| x.clone())
|
||||
}
|
||||
|
||||
fn by_uid(&self, uid: &str) -> Option<Box<[u8]>> {
|
||||
let userid = self.userid.lock();
|
||||
let fprs = self.fpr.lock();
|
||||
|
||||
userid.get(uid).and_then(|fpr| fprs.get(fpr).map(|x| x.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl InMemory {
|
||||
pub fn new_token() -> String {
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand::distributions::Alphanumeric;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
// samples from [a-zA-Z0-9]
|
||||
// 43 chars ~ 256 bit
|
||||
rng.sample_iter(&Alphanumeric).take(43).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use openpgp::tpk::TPKBuilder;
|
||||
use database::test;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let mut db = InMemory::default();
|
||||
let k1 = TPKBuilder::default().add_userid("a").generate().unwrap();
|
||||
let k2 = TPKBuilder::default().add_userid("b").generate().unwrap();
|
||||
let k3 = TPKBuilder::default().add_userid("c").generate().unwrap();
|
||||
|
||||
assert!(db.merge_or_publish(k1).unwrap().len() > 0);
|
||||
assert!(db.merge_or_publish(k2.clone()).unwrap().len() > 0);
|
||||
assert!(!db.merge_or_publish(k2).unwrap().len() > 0);
|
||||
assert!(db.merge_or_publish(k3.clone()).unwrap().len() > 0);
|
||||
assert!(!db.merge_or_publish(k3.clone()).unwrap().len() > 0);
|
||||
assert!(!db.merge_or_publish(k3).unwrap().len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uid_verification() {
|
||||
let mut db = InMemory::default();
|
||||
|
||||
test::test_uid_verification(&mut db);
|
||||
}
|
||||
}
|
305
src/database/mod.rs
Normal file
305
src/database/mod.rs
Normal file
|
@ -0,0 +1,305 @@
|
|||
use std::result;
|
||||
use std::io::Cursor;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Serializer, Deserializer, de};
|
||||
use time;
|
||||
use openpgp::{self, TPK, UserID, Packet, PacketPile, Signature, constants::SignatureType};
|
||||
use base64;
|
||||
use {Error, Result};
|
||||
|
||||
mod fs;
|
||||
mod memory;
|
||||
mod test;
|
||||
|
||||
#[derive(Serialize,Deserialize,Clone,Debug,Hash,PartialEq,Eq)]
|
||||
pub struct Fingerprint([u8; 20]);
|
||||
|
||||
impl TryFrom<openpgp::Fingerprint> for Fingerprint {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(fpr: openpgp::Fingerprint) -> Result<Self> {
|
||||
match fpr {
|
||||
openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
|
||||
openpgp::Fingerprint::Invalid(_) => Err("invalid fingerprint".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Fingerprint {
|
||||
fn to_string(&self) -> String {
|
||||
base64::encode_config(&self.0[..], base64::URL_SAFE)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Fingerprint {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Fingerprint> {
|
||||
let vec = base64::decode(s)?;
|
||||
if vec.len() == 20 {
|
||||
let mut arr = [0u8; 20];
|
||||
|
||||
arr.copy_from_slice(&vec[..]);
|
||||
Ok(Fingerprint(arr))
|
||||
} else {
|
||||
Err(format!("'{}' is not a valid fingerprint", s).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize,Deserialize,Clone,Debug)]
|
||||
pub struct Verify {
|
||||
created: i64,
|
||||
packets: Box<[u8]>,
|
||||
fpr: Fingerprint,
|
||||
#[serde(deserialize_with = "Verify::deserialize_userid", serialize_with = "Verify::serialize_userid")]
|
||||
uid: UserID,
|
||||
}
|
||||
|
||||
impl Verify {
|
||||
pub fn new(uid: &UserID, sig: &[&Signature], fpr: Fingerprint) -> Result<Self> {
|
||||
use openpgp::serialize::Serialize;
|
||||
|
||||
let mut cur = Cursor::new(Vec::default());
|
||||
|
||||
let res: Result<()> = uid.serialize(&mut cur)
|
||||
.map_err(|e| format!("openpgp: {}", e).into());
|
||||
res?;
|
||||
|
||||
for s in sig {
|
||||
let res: Result<()> = s.serialize(&mut cur)
|
||||
.map_err(|e| format!("openpgp: {}", e).into());
|
||||
res?;
|
||||
}
|
||||
|
||||
Ok(Verify{
|
||||
created: time::now().to_timespec().sec,
|
||||
packets: cur.into_inner().into(),
|
||||
fpr: fpr,
|
||||
uid: uid.clone(),
|
||||
})
|
||||
}
|
||||
fn deserialize_userid<'de, D>(de: D) -> result::Result<UserID, D::Error> where D: Deserializer<'de> {
|
||||
de.deserialize_bytes(UserIDVisitor)
|
||||
}
|
||||
|
||||
fn serialize_userid<S>(uid: &UserID, ser: S) -> result::Result<S::Ok, S::Error> where S: Serializer {
|
||||
ser.serialize_bytes(&uid.value)
|
||||
}
|
||||
}
|
||||
|
||||
struct UserIDVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for UserIDVisitor {
|
||||
type Value = UserID;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "a OpenPGP User ID")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, s: &[u8]) -> result::Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(UserID::new().userid_from_bytes(s))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> result::Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>
|
||||
{
|
||||
let mut buf = Vec::default();
|
||||
|
||||
while let Some(x) = seq.next_element()? {
|
||||
buf.push(x);
|
||||
}
|
||||
|
||||
Ok(UserID::new().userid_from_bytes(&buf))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize,Deserialize,Clone,Debug)]
|
||||
pub struct Delete {
|
||||
created: i64,
|
||||
fpr: Fingerprint
|
||||
}
|
||||
|
||||
impl Delete {
|
||||
pub fn new(fpr: Fingerprint) -> Self {
|
||||
Delete{
|
||||
created: time::now().to_timespec().sec,
|
||||
fpr: fpr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// uid -> uidsig+
|
||||
// subkey -> subkeysig+
|
||||
|
||||
pub trait Database: Sync + Send{
|
||||
fn new_verify_token(&mut self, payload: Verify) -> Result<String>;
|
||||
fn new_delete_token(&mut self, payload: Delete) -> Result<String>;
|
||||
|
||||
fn compare_and_swap(&mut self, fpr: &Fingerprint, present: Option<&[u8]>, new: Option<&[u8]>) -> Result<bool>;
|
||||
|
||||
fn link_userid(&mut self, uid: &UserID, fpr: &Fingerprint);
|
||||
fn unlink_userid(&mut self, uid: &UserID, fpr: &Fingerprint);
|
||||
|
||||
// (verified uid, fpr)
|
||||
fn pop_verify_token(&mut self, token: &str) -> Option<Verify>;
|
||||
// fpr
|
||||
fn pop_delete_token(&mut self, token: &str) -> Option<Delete>;
|
||||
|
||||
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>>;
|
||||
fn by_uid(&self, uid: &str) -> Option<Box<[u8]>>;
|
||||
// fn by_kid<'a>(&self, fpr: &str) -> Option<&[u8]>;
|
||||
|
||||
fn strip_userids(tpk: TPK) -> Result<TPK> {
|
||||
let pile = tpk.to_packet_pile().into_children().filter(|pkt| {
|
||||
match pkt {
|
||||
&Packet::PublicKey(_) | &Packet::PublicSubkey(_) => true,
|
||||
&Packet::Signature(ref sig) => sig.sigtype == SignatureType::DirectKey,
|
||||
_ => false,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
TPK::from_packet_pile(PacketPile::from_packets(pile))
|
||||
.map_err(|e| format!("openpgp: {}", e).into())
|
||||
}
|
||||
|
||||
fn tpk_into_bytes(tpk: &TPK) -> Result<Vec<u8>> {
|
||||
use std::io::Cursor;
|
||||
|
||||
let mut cur = Cursor::new(Vec::default());
|
||||
tpk.serialize(&mut cur).map(|_| cur.into_inner()).map_err(|e| format!("{}", e).into())
|
||||
}
|
||||
|
||||
fn merge_or_publish(&mut self, mut tpk: TPK) -> Result<Vec<String>> {
|
||||
let fpr = Fingerprint::try_from(tpk.primary().fingerprint())?;
|
||||
let mut ret = Vec::default();
|
||||
|
||||
// update verify tokens
|
||||
for uid in tpk.userids() {
|
||||
let enc = base64::encode_config(&format!("{}", uid.userid()), base64::URL_SAFE);
|
||||
if self.by_uid(&enc).is_none() {
|
||||
let payload = Verify::new(uid.userid(), &uid.selfsigs().collect::<Vec<_>>(), fpr.clone())?;
|
||||
|
||||
// XXX: send mail
|
||||
ret.push(self.new_verify_token(payload)?);
|
||||
}
|
||||
}
|
||||
|
||||
tpk = Self::strip_userids(tpk)?;
|
||||
|
||||
for _ in 0..100 /* while cas failed */ {
|
||||
// merge or update key db
|
||||
match self.by_fpr(&fpr).map(|x| x.to_vec()) {
|
||||
Some(old) => {
|
||||
let new = TPK::from_bytes(&old).unwrap();
|
||||
let new = new.merge(tpk.clone()).unwrap();
|
||||
let new = Self::tpk_into_bytes(&new)?;
|
||||
|
||||
if self.compare_and_swap(&fpr, Some(&old), Some(&new))? {
|
||||
return Ok(ret);
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
let fresh = Self::tpk_into_bytes(&tpk)?;
|
||||
|
||||
if self.compare_and_swap(&fpr, None, Some(&fresh))? {
|
||||
return Ok(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error!("Compare-and-swap of {} failed {} times in a row. Aborting.", fpr.to_string(), 100);
|
||||
Err("Database update failed".into())
|
||||
}
|
||||
|
||||
// if (uid, fpr) = pop-token(tok) {
|
||||
// while cas-failed() {
|
||||
// tpk = by_fpr(fpr)
|
||||
// merged = add-uid(tpk, uid)
|
||||
// cas(tpk, merged)
|
||||
// }
|
||||
// }
|
||||
fn verify_token(&mut self, token: &str) -> Result<bool> {
|
||||
match self.pop_verify_token(token) {
|
||||
Some(Verify{ created, packets, fpr, uid }) => {
|
||||
let now = time::now().to_timespec().sec;
|
||||
if created > now || now - created > 3 * 3600 { return Ok(false); }
|
||||
|
||||
loop /* while cas falied */ {
|
||||
match self.by_fpr(&fpr).map(|x| x.to_vec()) {
|
||||
Some(old) => {
|
||||
let mut new = old.clone();
|
||||
new.extend(packets.into_iter());
|
||||
|
||||
if self.compare_and_swap(&fpr, Some(&old), Some(&new))? {
|
||||
self.link_userid(&uid, &fpr);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err("No such token".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn request_deletion(&mut self, fpr: Fingerprint) -> Result<String> {
|
||||
if self.by_fpr(&fpr).is_none() { return Err("Unknown key".into()); }
|
||||
|
||||
let payload = Delete::new(fpr);
|
||||
self.new_delete_token(payload)
|
||||
}
|
||||
|
||||
// if fpr = pop-token(tok) {
|
||||
// tpk = by_fpr(fpr)
|
||||
// for uid in tpk.userids {
|
||||
// del-uid(uid)
|
||||
// }
|
||||
// del-fpr(fpr)
|
||||
// }
|
||||
fn confirm_deletion(&mut self, token: &str) -> Result<bool> {
|
||||
match self.pop_delete_token(token) {
|
||||
Some(Delete{ created, fpr }) => {
|
||||
let now = time::now().to_timespec().sec;
|
||||
if created > now || now - created > 3 * 3600 { return Ok(false); }
|
||||
|
||||
loop {
|
||||
match self.by_fpr(&fpr).map(|x| x.to_vec()) {
|
||||
Some(old) => {
|
||||
let tpk = match TPK::from_bytes(&old) {
|
||||
Ok(tpk) => tpk,
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to parse old TPK: {:?}", e).into());
|
||||
}
|
||||
};
|
||||
|
||||
for uid in tpk.userids() {
|
||||
self.unlink_userid(uid.userid(), &fpr);
|
||||
}
|
||||
|
||||
while !self.compare_and_swap(&fpr, Some(&old), None)? {}
|
||||
return Ok(true);
|
||||
}
|
||||
None => {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
268
src/database/test.rs
Normal file
268
src/database/test.rs
Normal file
|
@ -0,0 +1,268 @@
|
|||
// pub, fetch by fpr, verify no uid
|
||||
// verify uid fetch by fpr fetch by uid
|
||||
// verify again
|
||||
// verify other uid fetch by ui1 uid2 fpr
|
||||
// pub again
|
||||
// pub with less uid
|
||||
// pub with new uid
|
||||
//
|
||||
// pub & verify
|
||||
// req del one
|
||||
// fetch by uid & fpr
|
||||
// confirm
|
||||
// fetch by uid & fpr
|
||||
// confirm again
|
||||
// fetch by uid & fpr
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::thread;
|
||||
use std::sync::atomic::{Ordering, AtomicBool};
|
||||
|
||||
use database::{Fingerprint, Database};
|
||||
use openpgp::tpk::{TPKBuilder, UserIDBinding};
|
||||
use openpgp::{UserID, TPK, PacketPile, Packet};
|
||||
use base64;
|
||||
|
||||
pub fn test_uid_verification<D: Database>(db: &mut D) {
|
||||
let str_uid1 = "Test A <test_a@example.com>";
|
||||
let str_uid2 = "Test B <test_b@example.com>";
|
||||
let tpk = TPKBuilder::default()
|
||||
.add_userid(str_uid1)
|
||||
.add_userid(str_uid2)
|
||||
.generate().unwrap();
|
||||
let uid1 = UserID::new().userid_from_bytes(str_uid1.as_bytes());
|
||||
let uid2 = UserID::new().userid_from_bytes(str_uid2.as_bytes());
|
||||
let b64_uid1 = base64::encode_config(str_uid1, base64::URL_SAFE);
|
||||
let b64_uid2 = base64::encode_config(str_uid2, base64::URL_SAFE);
|
||||
|
||||
// upload key
|
||||
let tokens = db.merge_or_publish(tpk.clone()).unwrap();
|
||||
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
|
||||
{
|
||||
// fetch by fpr
|
||||
let raw = db.by_fpr(&fpr).unwrap();
|
||||
let key = TPK::from_bytes(&raw[..]).unwrap();
|
||||
|
||||
assert!(key.userids().next().is_none());
|
||||
assert!(key.user_attributes().next().is_none());
|
||||
assert!(key.subkeys().next().is_none());
|
||||
}
|
||||
|
||||
// fail to fetch by uid
|
||||
assert!(db.by_uid(&b64_uid1).is_none());
|
||||
assert!(db.by_uid(&b64_uid2).is_none());
|
||||
|
||||
// verify 1st uid
|
||||
assert!(db.verify_token(&tokens[0]).unwrap());
|
||||
|
||||
{
|
||||
// fetch by fpr
|
||||
let raw = db.by_fpr(&fpr).unwrap();
|
||||
let key = TPK::from_bytes(&raw[..]).unwrap();
|
||||
|
||||
assert!(key.userids().skip(1).next().is_none());
|
||||
assert!(key.user_attributes().next().is_none());
|
||||
assert!(key.subkeys().next().is_none());
|
||||
|
||||
let uid = key.userids().next().unwrap().userid().clone();
|
||||
|
||||
assert!((uid == uid1) ^ (uid == uid2));
|
||||
let b64_uid = base64::encode_config(&String::from_utf8(uid.value.clone()).unwrap(), base64::URL_SAFE);
|
||||
assert_eq!(db.by_uid(&b64_uid).unwrap(), raw);
|
||||
|
||||
if b64_uid1 == b64_uid {
|
||||
assert!(db.by_uid(&b64_uid2).is_none());
|
||||
} else if b64_uid2 == b64_uid {
|
||||
assert!(db.by_uid(&b64_uid1).is_none());
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
// verify 1st uid again
|
||||
assert!(db.verify_token(&tokens[0]).is_err());
|
||||
|
||||
{
|
||||
// fetch by fpr
|
||||
let raw = db.by_fpr(&fpr).unwrap();
|
||||
let key = TPK::from_bytes(&raw[..]).unwrap();
|
||||
|
||||
assert!(key.userids().skip(1).next().is_none());
|
||||
assert!(key.user_attributes().next().is_none());
|
||||
assert!(key.subkeys().next().is_none());
|
||||
|
||||
let uid = key.userids().next().unwrap().userid().clone();
|
||||
|
||||
assert!((uid == uid1) ^ (uid == uid2));
|
||||
let b64_uid = base64::encode_config(&String::from_utf8(uid.value.clone()).unwrap(), base64::URL_SAFE);
|
||||
assert_eq!(db.by_uid(&b64_uid).unwrap(), raw);
|
||||
|
||||
if b64_uid1 == b64_uid {
|
||||
assert!(db.by_uid(&b64_uid2).is_none());
|
||||
} else if b64_uid2 == b64_uid {
|
||||
assert!(db.by_uid(&b64_uid1).is_none());
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
// verify 2nd uid
|
||||
assert!(db.verify_token(&tokens[1]).unwrap());
|
||||
|
||||
{
|
||||
// fetch by fpr
|
||||
let raw = db.by_fpr(&fpr).unwrap();
|
||||
let key = TPK::from_bytes(&raw[..]).unwrap();
|
||||
|
||||
assert_eq!(key.userids().len(), 2);
|
||||
assert!(key.user_attributes().next().is_none());
|
||||
assert!(key.subkeys().next().is_none());
|
||||
|
||||
let myuid1 = key.userids().next().unwrap().userid().clone();
|
||||
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
|
||||
|
||||
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
|
||||
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
|
||||
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
|
||||
}
|
||||
|
||||
// upload again
|
||||
assert_eq!(db.merge_or_publish(tpk.clone()).unwrap(), Vec::<String>::default());
|
||||
|
||||
// publish w/ one uid less
|
||||
{
|
||||
let packets = tpk.clone()
|
||||
.to_packet_pile()
|
||||
.into_children()
|
||||
.filter(|pkt| {
|
||||
match pkt {
|
||||
Packet::UserID(ref uid) => *uid != uid1,
|
||||
_ => true
|
||||
}
|
||||
});
|
||||
let pile = PacketPile::from_packets(packets.collect());
|
||||
let short_tpk = TPK::from_packet_pile(pile).unwrap();
|
||||
|
||||
assert_eq!(db.merge_or_publish(short_tpk.clone()).unwrap(), Vec::<String>::default());
|
||||
|
||||
// fetch by fpr
|
||||
let raw = db.by_fpr(&fpr).unwrap();
|
||||
let key = TPK::from_bytes(&raw[..]).unwrap();
|
||||
|
||||
assert_eq!(key.userids().len(), 2);
|
||||
assert!(key.user_attributes().next().is_none());
|
||||
assert!(key.subkeys().next().is_none());
|
||||
|
||||
let myuid1 = key.userids().next().unwrap().userid().clone();
|
||||
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
|
||||
|
||||
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
|
||||
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
|
||||
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
|
||||
}
|
||||
|
||||
// publish w/one uid more
|
||||
{
|
||||
let mut packets = tpk.clone()
|
||||
.to_packet_pile()
|
||||
.into_children()
|
||||
.filter(|pkt| {
|
||||
match pkt {
|
||||
Packet::UserID(ref uid) => *uid != uid1,
|
||||
_ => true
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
let str_uid3 = "Test C <test_c@example.com>";
|
||||
let uid3 = UserID::new().userid_from_bytes(str_uid3.as_bytes());
|
||||
let b64_uid3 = base64::encode_config(str_uid3, base64::URL_SAFE);
|
||||
let key = tpk.primary();
|
||||
let bind = UserIDBinding::new(key, uid3.clone(), key).unwrap();
|
||||
|
||||
packets.push(Packet::UserID(uid3.clone()));
|
||||
packets.push(Packet::Signature(bind.selfsigs().next().unwrap().clone()));
|
||||
|
||||
let pile = PacketPile::from_packets(packets);
|
||||
let ext_tpk = TPK::from_packet_pile(pile).unwrap();
|
||||
let tokens = db.merge_or_publish(ext_tpk.clone()).unwrap();
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
// fetch by fpr
|
||||
let raw = db.by_fpr(&fpr).unwrap();
|
||||
let key = TPK::from_bytes(&raw[..]).unwrap();
|
||||
|
||||
assert_eq!(key.userids().len(), 2);
|
||||
assert!(key.user_attributes().next().is_none());
|
||||
assert!(key.subkeys().next().is_none());
|
||||
|
||||
let myuid1 = key.userids().next().unwrap().userid().clone();
|
||||
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
|
||||
|
||||
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
|
||||
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
|
||||
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
|
||||
assert!(db.by_uid(&b64_uid3).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_uid_deletion<D: Database>(db: &mut D) {
|
||||
let str_uid1 = "Test A <test_a@example.com>";
|
||||
let str_uid2 = "Test B <test_b@example.com>";
|
||||
let tpk = TPKBuilder::default()
|
||||
.add_userid(str_uid1)
|
||||
.add_userid(str_uid2)
|
||||
.generate().unwrap();
|
||||
let uid1 = UserID::new().userid_from_bytes(str_uid1.as_bytes());
|
||||
let uid2 = UserID::new().userid_from_bytes(str_uid2.as_bytes());
|
||||
let b64_uid1 = base64::encode_config(str_uid1, base64::URL_SAFE);
|
||||
let b64_uid2 = base64::encode_config(str_uid2, base64::URL_SAFE);
|
||||
|
||||
// upload key and verify uids
|
||||
let tokens = db.merge_or_publish(tpk.clone()).unwrap();
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
assert!(db.verify_token(&tokens[0]).unwrap());
|
||||
assert!(db.verify_token(&tokens[1]).unwrap());
|
||||
|
||||
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
|
||||
|
||||
// req. deletion
|
||||
let del = db.request_deletion(fpr.clone()).unwrap();
|
||||
|
||||
// check it's still there
|
||||
{
|
||||
// fetch by fpr
|
||||
let raw = db.by_fpr(&fpr).unwrap();
|
||||
let key = TPK::from_bytes(&raw[..]).unwrap();
|
||||
|
||||
assert_eq!(key.userids().len(), 2);
|
||||
assert!(key.user_attributes().next().is_none());
|
||||
assert!(key.subkeys().next().is_none());
|
||||
|
||||
let myuid1 = key.userids().next().unwrap().userid().clone();
|
||||
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
|
||||
|
||||
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
|
||||
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
|
||||
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
|
||||
}
|
||||
|
||||
// confirm deletion
|
||||
assert!(db.confirm_deletion(&del).unwrap());
|
||||
|
||||
// check it's gone
|
||||
assert!(db.by_fpr(&fpr).is_none());
|
||||
assert!(db.by_uid(&b64_uid1).is_none());
|
||||
assert!(db.by_uid(&b64_uid2).is_none());
|
||||
|
||||
// confirm deletion again
|
||||
assert!(!db.confirm_deletion(&del).unwrap());
|
||||
|
||||
// check it's still gone
|
||||
assert!(db.by_fpr(&fpr).is_none());
|
||||
assert!(db.by_uid(&b64_uid1).is_none());
|
||||
assert!(db.by_uid(&b64_uid2).is_none());
|
||||
}
|
63
src/main.rs
Normal file
63
src/main.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
#![feature(plugin, decl_macro, custom_derive)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![recursion_limit = "1024"]
|
||||
#![feature(try_from)]
|
||||
|
||||
extern crate serde;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
|
||||
extern crate time;
|
||||
extern crate base64;
|
||||
#[cfg(not(test))] #[macro_use] extern crate rocket;
|
||||
#[cfg(test)] extern crate rocket;
|
||||
extern crate openpgp;
|
||||
extern crate multipart;
|
||||
#[macro_use] extern crate error_chain;
|
||||
#[macro_use] extern crate log;
|
||||
extern crate rand;
|
||||
extern crate tempfile;
|
||||
extern crate parking_lot;
|
||||
#[macro_use] extern crate structopt;
|
||||
|
||||
mod web;
|
||||
mod database;
|
||||
|
||||
mod errors {
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Fmt(::std::fmt::Error);
|
||||
Io(::std::io::Error);
|
||||
Json(::serde_json::Error);
|
||||
Persist(::tempfile::PersistError);
|
||||
Base64(::base64::DecodeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
use errors::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "garbage", about = "Garbage Pile - The verifying OpenPGP key server.")]
|
||||
struct Opt {
|
||||
/// Debug mode
|
||||
#[structopt(short = "v", long = "verbose")]
|
||||
debug: bool,
|
||||
/// Daemon
|
||||
#[structopt(short = "d", long = "daemon")]
|
||||
daemon: bool,
|
||||
/// Base directory
|
||||
#[structopt(parse(from_os_str))]
|
||||
base: PathBuf,
|
||||
/// Listen
|
||||
#[structopt(short = "l", long = "listen", default_value = "0.0.0.0:80")]
|
||||
listen: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = Opt::from_args();
|
||||
println!("{:?}", opt);
|
||||
}
|
||||
|
75
src/web/mod.rs
Normal file
75
src/web/mod.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use rocket;
|
||||
use rocket::response::content;
|
||||
|
||||
mod upload;
|
||||
|
||||
#[get("/key/<fpr>")]
|
||||
fn key_by_fingerprint(fpr: String) -> String {
|
||||
format!("{}", fpr)
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct KeySubmissionForm {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[post("/keys", data = "<data>")]
|
||||
fn submit_key(data: String) -> String {
|
||||
//use multipart::server::Multipart;
|
||||
//use openpgp::TPK;
|
||||
format!("{:?}", data)/*
|
||||
let strm = form.open();
|
||||
|
||||
match TPK::from_reader(strm) {
|
||||
Ok(tpk) => {
|
||||
match tpk.userids().next() {
|
||||
Some(uid) => {
|
||||
format!("Hello, {:?}", uid.userid())
|
||||
}
|
||||
None => {
|
||||
format!("Hello, {:?}", tpk.primary().fingerprint())
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
format!("Error: {:?}", e)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn root() -> content::Html<&'static str> {
|
||||
content::Html("
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garbage Pile Public Key Server</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Garbage Pile Public Key Server</h1>
|
||||
<p>The verifying PGP key server. Powered by p≡pnology!
|
||||
<h2>Search for keys</h2>
|
||||
<form action=\"/search\" method=POST>
|
||||
<input type=\"search\" id=\"query\" name=\"query\" placeholder=\"Email\">
|
||||
<input type=\"submit\" value=\"Search\">
|
||||
</form>
|
||||
<h2>Upload your key</h2>
|
||||
<form action=\"/keys\" method=POST enctype=multipart/form-data>
|
||||
<input type=\"file\" id=\"key\" name=\"key\" placeholder=\"Your public key\">
|
||||
<input type=\"submit\" value=\"Upload\">
|
||||
</form>
|
||||
</body>
|
||||
</html>")
|
||||
}
|
||||
|
||||
fn main() {
|
||||
rocket::ignite().mount("/", routes![
|
||||
upload::multipart_upload,
|
||||
key_by_fingerprint,
|
||||
root]).launch();
|
||||
}
|
||||
|
||||
//POST /keys
|
||||
//GET /keys/<fpr>
|
||||
|
77
src/web/upload.rs
Normal file
77
src/web/upload.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use multipart::server::Multipart;
|
||||
use multipart::server::save::Entries;
|
||||
use multipart::server::save::SaveResult::*;
|
||||
|
||||
use rocket::Data;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::response::status::Custom;
|
||||
|
||||
#[post("/keys", data = "<data>")]
|
||||
// signature requires the request to have a `Content-Type`
|
||||
pub fn multipart_upload(cont_type: &ContentType, data: Data) -> Result<String, Custom<String>> {
|
||||
// this and the next check can be implemented as a request guard but it seems like just
|
||||
// more boilerplate than necessary
|
||||
if !cont_type.is_form_data() {
|
||||
return Err(Custom(
|
||||
Status::BadRequest,
|
||||
"Content-Type not multipart/form-data".into()
|
||||
));
|
||||
}
|
||||
|
||||
let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else(
|
||||
|| Custom(
|
||||
Status::BadRequest,
|
||||
"`Content-Type: multipart/form-data` boundary param not provided".into()
|
||||
)
|
||||
)?;
|
||||
|
||||
process_upload(boundary, data)
|
||||
}
|
||||
|
||||
fn process_upload(boundary: &str, data: Data) -> Result<String, Custom<String>> {
|
||||
// saves all fields, any field longer than 10kB goes to a temporary directory
|
||||
// Entries could implement FromData though that would give zero control over
|
||||
// how the files are saved; Multipart would be a good impl candidate though
|
||||
match Multipart::with_body(data.open(), boundary).save().temp() {
|
||||
Full(entries) => process_entries(entries),
|
||||
Partial(partial, _) => {
|
||||
process_entries(partial.entries)
|
||||
},
|
||||
Error(err) => Err(Custom(Status::InternalServerError, err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// having a streaming output would be nice; there's one for returning a `Read` impl
|
||||
// but not one that you can `write()` to
|
||||
fn process_entries(entries: Entries) -> Result<String, Custom<String>> {
|
||||
use openpgp::TPK;
|
||||
|
||||
match entries.fields.get(&"key".to_string()) {
|
||||
Some(ent) if ent.len() == 1 => {
|
||||
let reader = ent[0].data.readable().map_err(|err| {
|
||||
Custom(Status::InternalServerError, err.to_string())
|
||||
})?;
|
||||
|
||||
match TPK::from_reader(reader) {
|
||||
Ok(tpk) => {
|
||||
match tpk.userids().next() {
|
||||
Some(uid) => {
|
||||
Ok(format!("Hello, {:?}", uid.userid()))
|
||||
}
|
||||
None => {
|
||||
Ok(format!("Hello, {:?}", tpk.primary().fingerprint()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
Ok(format!("Error: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) | None => Err(Custom(
|
||||
Status::BadRequest,
|
||||
"Not a PGP public key".into()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue