hagrid-keyserver--hagrid/src/web/mod.rs

855 lines
27 KiB
Rust
Raw Normal View History

2018-08-16 18:35:19 +00:00
use rocket;
2019-02-07 19:58:31 +00:00
use rocket::fairing::AdHoc;
2018-09-19 20:24:38 +00:00
use rocket::http::Status;
2019-02-07 19:58:31 +00:00
use rocket::request::{self, FromRequest, Request};
2018-09-19 20:24:38 +00:00
use rocket::response::status::Custom;
2019-02-22 15:25:06 +00:00
use rocket::response::NamedFile;
2019-02-07 19:58:31 +00:00
use rocket::{Outcome, State};
2018-12-25 19:06:28 +00:00
use rocket_contrib::templates::Template;
2019-02-22 22:29:54 +00:00
use rocket::response::Flash;
use rocket::request::FlashMessage;
use rocket::response::Redirect;
2019-02-22 15:25:06 +00:00
use serde::Serialize;
use handlebars::Handlebars;
2019-02-22 15:25:06 +00:00
2018-10-18 14:26:25 +00:00
use std::path::{Path, PathBuf};
2018-08-16 18:35:19 +00:00
mod upload;
2019-02-07 19:58:31 +00:00
use database::{Database, Polymorphic};
use Result;
2019-02-07 19:58:31 +00:00
use types::{Email, Fingerprint, KeyID};
2018-09-19 20:24:38 +00:00
use Opt;
use std::result;
2019-02-07 19:58:31 +00:00
use std::str::FromStr;
2018-09-19 20:24:38 +00:00
mod queries {
2019-02-22 15:25:06 +00:00
use std::fmt;
use types::{Email, Fingerprint, KeyID};
2018-09-19 20:24:38 +00:00
#[derive(Debug)]
pub enum Hkp {
Fingerprint { fpr: Fingerprint, index: bool, machine_readable: bool },
KeyID { keyid: KeyID, index: bool, machine_readable: bool },
2019-02-07 19:58:31 +00:00
Email { email: Email, index: bool },
2019-02-22 15:25:06 +00:00
Invalid{ query: String, },
}
impl fmt::Display for Hkp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Hkp::Fingerprint{ ref fpr,.. } => write!(f, "{}", fpr.to_string()),
Hkp::KeyID{ ref keyid,.. } => write!(f, "{}", keyid.to_string()),
2019-02-22 15:25:06 +00:00
Hkp::Email{ ref email,.. } => write!(f, "{}", email.to_string()),
Hkp::Invalid{ ref query } => write!(f, "{}", query),
}
}
}
}
#[derive(Responder)]
enum MyResponse {
#[response(status = 200, content_type = "html")]
Success(Template),
#[response(status = 200, content_type = "plain")]
Plain(String),
#[response(status = 500, content_type = "html")]
ServerError(Template),
2019-02-22 22:29:54 +00:00
NotFound(Flash<Redirect>),
2019-02-22 15:25:06 +00:00
}
impl MyResponse {
pub fn ok<S: Serialize>(tmpl: &'static str, ctx: S) -> Self {
MyResponse::Success(Template::render(tmpl, ctx))
}
pub fn plain(s: String) -> Self {
MyResponse::Plain(s)
}
pub fn ise(e: failure::Error) -> Self {
2019-02-22 15:25:06 +00:00
let ctx = templates::FiveHundred{
error: format!("{}", e),
2019-02-22 15:25:06 +00:00
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ServerError(Template::render("500", ctx))
}
2019-02-22 22:49:36 +00:00
pub fn not_found() -> Self {
2019-02-22 22:29:54 +00:00
MyResponse::NotFound(Flash::error(Redirect::to("/?"), "Key not found".to_owned()))
2018-09-19 20:24:38 +00:00
}
2018-08-16 18:35:19 +00:00
}
2018-09-19 20:24:38 +00:00
mod templates {
#[derive(Serialize)]
pub struct Verify {
pub verified: bool,
pub userid: String,
pub fpr: String,
2019-02-22 20:34:48 +00:00
pub domain: String,
pub commit: String,
pub version: String,
2018-09-19 20:24:38 +00:00
}
#[derive(Serialize)]
pub struct Delete {
pub token: String,
pub fpr: String,
pub commit: String,
pub version: String,
2018-09-19 20:24:38 +00:00
}
2019-02-22 15:25:06 +00:00
#[derive(Serialize)]
pub struct Search {
pub query: String,
pub fpr: Option<String>,
pub armored: Option<String>,
2019-02-22 19:52:40 +00:00
pub domain: Option<String>,
2019-02-22 15:25:06 +00:00
pub commit: String,
pub version: String,
}
2018-09-19 20:24:38 +00:00
#[derive(Serialize)]
pub struct Confirm {
pub deleted: bool,
pub commit: String,
pub version: String,
2018-09-19 20:24:38 +00:00
}
2019-02-08 19:09:53 +00:00
#[derive(Serialize)]
2019-02-22 15:25:06 +00:00
pub struct FiveHundred {
pub error: String,
pub commit: String,
pub version: String,
}
2019-02-22 22:29:54 +00:00
#[derive(Serialize)]
pub struct Index {
pub error: Option<String>,
pub commit: String,
pub version: String,
}
2019-02-22 15:25:06 +00:00
#[derive(Serialize)]
pub struct General {
2019-02-08 19:09:53 +00:00
pub commit: String,
pub version: String,
}
2018-08-16 18:35:19 +00:00
}
2018-10-18 14:26:25 +00:00
struct StaticDir(String);
pub struct Domain(String);
pub struct From(String);
pub struct MailTemplates(Handlebars);
2018-10-18 14:26:25 +00:00
2018-10-24 17:44:36 +00:00
impl<'a, 'r> FromRequest<'a, 'r> for queries::Hkp {
type Error = ();
2019-02-07 19:58:31 +00:00
fn from_request(
request: &'a Request<'r>,
) -> request::Outcome<queries::Hkp, ()> {
2018-10-24 17:44:36 +00:00
use rocket::request::FormItems;
use std::collections::HashMap;
let query = request.uri().query().unwrap_or("");
2019-02-07 19:58:31 +00:00
let fields = FormItems::from(query)
.map(|item| {
let (k, v) = item.key_value();
let key = k.url_decode().unwrap_or_default();
let value = v.url_decode().unwrap_or_default();
(key, value)
})
.collect::<HashMap<_, _>>();
if fields.len() >= 2
&& fields
.get("op")
.map(|x| x == "get" || x == "index")
.unwrap_or(false)
{
let index = fields.get("op").map(|x| x == "index").unwrap_or(false);
let machine_readable =
fields.get("options").map(|x| x.contains("mr"))
.unwrap_or(false);
2018-10-24 17:44:36 +00:00
let search = fields.get("search").cloned().unwrap_or_default();
2018-11-02 10:55:07 +00:00
let maybe_fpr = Fingerprint::from_str(&search);
let maybe_keyid = KeyID::from_str(&search);
2018-10-24 17:44:36 +00:00
2018-11-02 10:55:07 +00:00
if let Ok(fpr) = maybe_fpr {
2019-02-07 19:58:31 +00:00
Outcome::Success(queries::Hkp::Fingerprint {
fpr: fpr,
index: index,
machine_readable: machine_readable,
2019-01-08 18:01:45 +00:00
})
} else if let Ok(keyid) = maybe_keyid {
Outcome::Success(queries::Hkp::KeyID {
keyid: keyid,
index: index,
machine_readable: machine_readable,
})
2018-10-24 17:44:36 +00:00
} else {
match Email::from_str(&search) {
2019-02-07 19:58:31 +00:00
Ok(email) => {
Outcome::Success(queries::Hkp::Email {
email: email,
index: index,
})
}
2019-02-22 15:25:06 +00:00
Err(_) => {
Outcome::Success(queries::Hkp::Invalid{
query: search
})
}
2018-08-16 18:35:19 +00:00
}
2018-09-19 20:24:38 +00:00
}
} else {
Outcome::Failure((Status::BadRequest, ()))
}
}
}
fn key_to_response<'a>(query: String, domain: String, armored: String,
machine_readable: bool) -> MyResponse {
2019-02-22 15:25:06 +00:00
use sequoia_openpgp::TPK;
use sequoia_openpgp::parse::Parse;
2019-02-22 19:52:40 +00:00
use std::convert::TryFrom;
2018-09-19 20:24:38 +00:00
if machine_readable {
return MyResponse::plain(armored);
}
2019-02-22 20:29:51 +00:00
let key = match TPK::from_bytes(armored.as_bytes()) {
2019-02-22 15:25:06 +00:00
Ok(key) => key,
Err(err) => { return MyResponse::ise(err); }
2019-02-22 15:25:06 +00:00
};
let fpr = key.primary().fingerprint();
let context = templates::Search{
query: query,
2019-02-22 19:52:40 +00:00
domain: Some(domain),
fpr: Fingerprint::try_from(fpr).unwrap().to_string().into(),
2019-02-22 15:25:06 +00:00
armored: armored.into(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
2018-10-25 15:37:33 +00:00
2019-02-22 15:25:06 +00:00
MyResponse::ok("found", context)
}
2019-02-22 20:29:51 +00:00
fn key_to_hkp_index<'a>(armored: String) -> MyResponse {
2019-02-22 15:25:06 +00:00
use sequoia_openpgp::RevocationStatus;
use sequoia_openpgp::{parse::Parse, TPK};
2019-02-22 20:29:51 +00:00
let tpk = match TPK::from_bytes(armored.as_bytes()) {
2019-02-22 15:25:06 +00:00
Ok(tpk) => tpk,
Err(err) => { return MyResponse::ise(err); }
2019-02-22 15:25:06 +00:00
};
let mut out = String::default();
let p = tpk.primary();
let ctime = tpk
.primary_key_signature()
.and_then(|x| x.signature_creation_time())
.map(|x| format!("{}", x.to_timespec().sec))
.unwrap_or_default();
let extime = tpk
.primary_key_signature()
.and_then(|x| x.signature_expiration_time())
.map(|x| format!("{}", x))
.unwrap_or_default();
let is_exp = tpk
.primary_key_signature()
.and_then(|x| {
if x.signature_expired() { "e" } else { "" }.into()
})
.unwrap_or_default();
let is_rev =
if tpk.revoked(None) != RevocationStatus::NotAsFarAsWeKnow {
"r"
} else {
""
};
let algo: u8 = p.pk_algo().into();
out.push_str("info:1:1\r\n");
out.push_str(&format!(
"pub:{}:{}:{}:{}:{}:{}{}\r\n",
p.fingerprint().to_string().replace(" ", ""),
algo,
p.mpis().bits(),
ctime,
extime,
is_exp,
is_rev
));
for uid in tpk.userids() {
let u =
url::form_urlencoded::byte_serialize(uid.userid().userid())
.fold(String::default(), |acc, x| acc + x);
let ctime = uid
.binding_signature()
.and_then(|x| x.signature_creation_time())
.map(|x| format!("{}", x.to_timespec().sec))
.unwrap_or_default();
let extime = uid
.binding_signature()
.and_then(|x| x.signature_expiration_time())
.map(|x| format!("{}", x))
.unwrap_or_default();
let is_exp = uid
.binding_signature()
.and_then(|x| {
if x.signature_expired() { "e" } else { "" }.into()
})
.unwrap_or_default();
let is_rev = if uid.revoked(None)
!= RevocationStatus::NotAsFarAsWeKnow
{
"r"
} else {
""
};
out.push_str(&format!(
"uid:{}:{}:{}:{}{}\r\n",
u, ctime, extime, is_exp, is_rev
));
2018-10-25 15:37:33 +00:00
}
2019-02-22 15:25:06 +00:00
MyResponse::plain(out)
2019-02-22 21:37:01 +00:00
2018-10-25 15:37:33 +00:00
}
#[get("/by-fingerprint/<fpr>")]
2019-02-22 19:52:40 +00:00
fn by_fingerprint(db: rocket::State<Polymorphic>, domain: rocket::State<Domain>, fpr: String) -> MyResponse {
2018-10-25 15:37:33 +00:00
let maybe_key = match Fingerprint::from_str(&fpr) {
Ok(ref fpr) => db.by_fpr(fpr),
Err(_) => None,
2018-09-19 20:24:38 +00:00
};
match maybe_key {
Some(armored) => key_to_response(fpr, domain.0.clone(), armored,
true),
2019-02-22 22:49:36 +00:00
None => MyResponse::not_found(),
2018-10-25 15:37:33 +00:00
}
}
2018-09-19 20:24:38 +00:00
#[get("/by-email/<email>")]
2019-02-22 19:52:40 +00:00
fn by_email(db: rocket::State<Polymorphic>, domain: rocket::State<Domain>, email: String) -> MyResponse {
2018-10-25 15:37:33 +00:00
let maybe_key = match Email::from_str(&email) {
Ok(ref email) => db.by_email(email),
Err(_) => None,
};
2018-09-19 20:24:38 +00:00
2018-10-25 15:37:33 +00:00
match maybe_key {
Some(armored) => key_to_response(email, domain.0.clone(), armored,
true),
2019-02-22 22:49:36 +00:00
None => MyResponse::not_found(),
2019-02-22 20:29:51 +00:00
2018-09-19 20:24:38 +00:00
}
}
#[get("/by-keyid/<kid>")]
2019-02-22 19:52:40 +00:00
fn by_keyid(db: rocket::State<Polymorphic>, domain: rocket::State<Domain>, kid: String) -> MyResponse {
2019-01-04 13:07:14 +00:00
let maybe_key = match KeyID::from_str(&kid) {
Ok(ref key) => db.by_kid(key),
Err(_) => None,
};
match maybe_key {
Some(armored) => key_to_response(kid, domain.0.clone(), armored,
true),
2019-02-22 22:49:36 +00:00
None => MyResponse::not_found(),
2019-01-04 13:07:14 +00:00
}
}
#[get("/vks/verify/<token>")]
2019-02-07 19:58:31 +00:00
fn verify(
2019-02-22 20:34:48 +00:00
db: rocket::State<Polymorphic>, domain: rocket::State<Domain>, token: String,
2019-02-07 19:58:31 +00:00
) -> result::Result<Template, Custom<String>> {
2018-09-19 20:24:38 +00:00
match db.verify_token(&token) {
Ok(Some((userid, fpr))) => {
2019-02-07 19:58:31 +00:00
let context = templates::Verify {
2018-09-19 20:24:38 +00:00
verified: true,
2019-02-22 20:34:48 +00:00
domain: domain.0.clone(),
2018-09-19 20:24:38 +00:00
userid: userid.to_string(),
fpr: fpr.to_string(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
2018-09-19 20:24:38 +00:00
};
Ok(Template::render("verify", context))
}
Ok(None) | Err(_) => {
2019-02-07 19:58:31 +00:00
let context = templates::Verify {
2018-09-19 20:24:38 +00:00
verified: false,
2019-02-22 20:34:48 +00:00
domain: domain.0.clone(),
2018-09-19 20:24:38 +00:00
userid: "".into(),
fpr: "".into(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
2018-09-19 20:24:38 +00:00
};
Ok(Template::render("verify", context))
2018-08-16 18:35:19 +00:00
}
2018-09-19 20:24:38 +00:00
}
}
#[get("/vks/delete/<fpr>")]
2019-02-07 19:58:31 +00:00
fn delete(
db: rocket::State<Polymorphic>, fpr: String, tmpl: State<MailTemplates>,
domain: State<Domain>, from: State<From>,
) -> result::Result<Template, Custom<String>> {
use mail::send_confirmation_mail;
2018-09-19 20:24:38 +00:00
let fpr = match Fingerprint::from_str(&fpr) {
Ok(fpr) => fpr,
Err(_) => {
2019-02-07 19:58:31 +00:00
return Err(Custom(
Status::BadRequest,
"Invalid fingerprint".to_string(),
));
2018-09-19 20:24:38 +00:00
}
};
match db.request_deletion(fpr.clone()) {
2019-02-07 19:58:31 +00:00
Ok((token, uids)) => {
let context = templates::Delete {
2018-09-19 20:24:38 +00:00
fpr: fpr.to_string(),
2018-11-02 10:53:49 +00:00
token: token.clone(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
2018-09-19 20:24:38 +00:00
};
2018-11-02 10:53:49 +00:00
for uid in uids {
2019-02-07 19:58:31 +00:00
send_confirmation_mail(
&uid, &token, &tmpl.0, &domain.0, &from.0,
)
.map_err(|err| {
Custom(Status::InternalServerError, format!("{:?}", err))
})?;
2018-11-02 10:53:49 +00:00
}
2018-09-19 20:24:38 +00:00
Ok(Template::render("delete", context))
}
2019-02-07 19:58:31 +00:00
Err(e) => Err(Custom(Status::InternalServerError, format!("{}", e))),
2018-09-19 20:24:38 +00:00
}
}
#[get("/vks/confirm/<token>")]
2019-02-07 19:58:31 +00:00
fn confirm(
db: rocket::State<Polymorphic>, token: String,
) -> result::Result<Template, Custom<String>> {
2018-09-19 20:24:38 +00:00
match db.confirm_deletion(&token) {
Ok(true) => {
let context = templates::Confirm {
deleted: true,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
2018-09-19 20:24:38 +00:00
Ok(Template::render("confirm", context))
}
Ok(false) | Err(_) => {
let context = templates::Confirm {
deleted: false,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
2018-09-19 20:24:38 +00:00
Ok(Template::render("confirm", context))
}
}
2018-08-16 18:35:19 +00:00
}
#[get("/assets/<file..>")]
2018-10-18 14:26:25 +00:00
fn files(file: PathBuf, static_dir: State<StaticDir>) -> Option<NamedFile> {
NamedFile::open(Path::new(&static_dir.0).join("assets").join(file)).ok()
2018-10-18 14:26:25 +00:00
}
2018-10-24 17:44:36 +00:00
#[get("/pks/lookup")]
2019-02-07 19:58:31 +00:00
fn lookup(
2019-02-22 19:52:40 +00:00
db: rocket::State<Polymorphic>, domain: rocket::State<Domain>, key: Option<queries::Hkp>,
2019-02-22 15:25:06 +00:00
) -> MyResponse {
let (maybe_key, index, machine_readable) = match key {
Some(queries::Hkp::Fingerprint { ref fpr, index, machine_readable }) =>
(db.by_fpr(fpr), index, machine_readable),
Some(queries::Hkp::KeyID { ref keyid, index, machine_readable }) =>
(db.by_kid(keyid), index, machine_readable),
2019-02-07 19:58:31 +00:00
Some(queries::Hkp::Email { ref email, index }) => {
// XXX: Maybe return 501 Not Implemented if machine_readable
(db.by_email(email), index, false)
2019-01-08 18:01:45 +00:00
}
2019-02-22 22:49:36 +00:00
Some(queries::Hkp::Invalid { query: _ }) => {
return MyResponse::not_found();
2019-02-22 15:25:06 +00:00
}
2019-02-07 19:58:31 +00:00
None => {
2019-02-22 22:49:36 +00:00
return MyResponse::not_found();
2019-01-08 18:01:45 +00:00
}
2018-10-24 17:44:36 +00:00
};
2019-02-22 15:25:06 +00:00
let query = format!("{}", key.unwrap());
2018-10-24 17:44:36 +00:00
match maybe_key {
2019-02-22 20:29:51 +00:00
Some(armored) => {
if index {
key_to_hkp_index(armored)
} else {
key_to_response(query, domain.0.clone(), armored,
machine_readable)
2019-02-22 20:29:51 +00:00
}
}
None => {
if index {
MyResponse::plain("info:1:0\r\n".into())
} else {
2019-02-22 22:49:36 +00:00
MyResponse::not_found()
2019-02-22 20:29:51 +00:00
}
}
2018-10-24 17:44:36 +00:00
}
}
2019-02-22 15:25:28 +00:00
#[get("/vks/manage")]
fn manage() -> result::Result<Template, Custom<String>> {
let context = templates::General {
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
Ok(Template::render("manage", context))
}
2019-02-22 22:29:54 +00:00
fn error_from_flash(flash: &FlashMessage) -> Option<String> {
if flash.name() == "error" {
Some(flash.msg().to_owned())
} else {
None
}
}
2018-08-16 18:35:19 +00:00
#[get("/")]
2019-02-22 22:29:54 +00:00
fn root(
flash: Option<FlashMessage>
) -> Template {
let context = templates::Index {
error: flash.and_then(|flash| error_from_flash(&flash)),
2019-02-08 19:09:53 +00:00
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
2018-09-19 20:24:38 +00:00
2019-02-08 19:09:53 +00:00
Template::render("index", context)
2018-08-16 18:35:19 +00:00
}
2019-02-22 20:15:06 +00:00
#[get("/about")]
fn about() -> Template {
2019-02-22 20:16:14 +00:00
let context = templates::General {
2019-02-22 20:15:06 +00:00
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
Template::render("about", context)
}
2018-09-19 20:24:38 +00:00
pub fn serve(opt: &Opt, db: Polymorphic) -> Result<()> {
use rocket::config::{Config, Environment};
use std::str::FromStr;
let (addr, port) = match opt.listen.find(':') {
Some(p) => {
let addr = opt.listen[0..p].to_string();
let port = if p < opt.listen.len() - 1 {
2019-02-07 19:58:31 +00:00
u16::from_str(&opt.listen[p + 1..]).ok().unwrap_or(8080)
2018-09-19 20:24:38 +00:00
} else {
8080
};
(addr, port)
}
2019-02-07 19:58:31 +00:00
None => (opt.listen.to_string(), 8080),
2018-09-19 20:24:38 +00:00
};
let config = Config::build(Environment::Staging)
.address(addr)
.port(port)
.workers(2)
.root(opt.base.clone())
2019-02-07 19:58:31 +00:00
.extra(
"template_dir",
opt.base
.join("templates")
.to_str()
.ok_or(failure::err_msg("Template path invalid"))?,
2019-02-07 19:58:31 +00:00
)
.extra(
"static_dir",
opt.base.join("public").to_str()
.ok_or(failure::err_msg("Static path invalid"))?,
2019-02-07 19:58:31 +00:00
)
.extra("domain", opt.domain.clone())
.extra("from", opt.from.clone())
2018-09-19 20:24:38 +00:00
.finalize()?;
rocket_factory(rocket::custom(config), db).launch();
Ok(())
}
fn rocket_factory(rocket: rocket::Rocket, db: Polymorphic) -> rocket::Rocket {
2018-09-19 20:24:38 +00:00
let routes = routes![
// infra
root,
2019-02-22 15:25:06 +00:00
manage,
files,
// nginx-supported lookup
2018-10-25 15:37:33 +00:00
by_email,
by_fingerprint,
by_keyid,
// HKP
lookup,
2019-02-22 20:38:38 +00:00
upload::vks_publish,
upload::vks_publish_submit,
// verification & deletion
2018-09-19 20:24:38 +00:00
verify,
delete,
confirm,
2019-02-22 20:15:06 +00:00
// about
about,
2018-09-19 20:24:38 +00:00
];
rocket
2018-09-19 20:24:38 +00:00
.attach(Template::fairing())
2018-12-25 19:06:28 +00:00
.attach(AdHoc::on_attach("static_dir", |rocket| {
2019-02-07 19:58:31 +00:00
let static_dir =
rocket.config().get_str("static_dir").unwrap().to_string();
2018-10-18 14:26:25 +00:00
Ok(rocket.manage(StaticDir(static_dir)))
}))
2018-12-25 19:06:28 +00:00
.attach(AdHoc::on_attach("domain", |rocket| {
2019-02-07 19:58:31 +00:00
let domain = rocket.config().get_str("domain").unwrap().to_string();
Ok(rocket.manage(Domain(domain)))
}))
.attach(AdHoc::on_attach("from", |rocket| {
2019-02-07 19:58:31 +00:00
let from = rocket.config().get_str("from").unwrap().to_string();
Ok(rocket.manage(From(from)))
}))
.attach(AdHoc::on_attach("mail_templates", |rocket| {
2019-02-07 19:58:31 +00:00
let dir: PathBuf = rocket
.config()
.get_str("template_dir")
.unwrap()
.to_string()
.into();
let confirm_html = dir.join("confirm-email-html.hbs");
let confirm_txt = dir.join("confirm-email-txt.hbs");
let verify_html = dir.join("verify-email-html.hbs");
let verify_txt = dir.join("verify-email-txt.hbs");
2019-02-07 19:58:31 +00:00
let mut handlebars = Handlebars::new();
handlebars
.register_template_file("confirm-html", confirm_html)
.unwrap();
handlebars
.register_template_file("confirm-txt", confirm_txt)
.unwrap();
handlebars
.register_template_file("verify-html", verify_html)
.unwrap();
handlebars
.register_template_file("verify-txt", verify_txt)
.unwrap();
Ok(rocket.manage(MailTemplates(handlebars)))
}))
2018-09-19 20:24:38 +00:00
.mount("/", routes)
.manage(db)
2018-08-16 18:35:19 +00:00
}
#[cfg(test)]
mod tests {
use fs_extra;
use tempfile::{tempdir, TempDir};
use super::rocket;
use rocket::local::Client;
use rocket::http::Status;
use rocket::http::ContentType;
2019-02-26 10:34:53 +00:00
use sequoia_openpgp::TPK;
use sequoia_openpgp::tpk::TPKBuilder;
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::serialize::Serialize;
use database::*;
use super::*;
/// Creates a configuration and empty state dir for testing purposes.
///
/// Note that you need to keep the returned TempDir alive for the
/// duration of your test. To debug the test, mem::forget it to
/// prevent cleanup.
fn configuration() -> Result<(TempDir, rocket::Config)> {
use rocket::config::{Config, Environment};
let root = tempdir()?;
fs_extra::copy_items(&vec!["dist/templates"], &root,
&fs_extra::dir::CopyOptions::new())?;
let config = Config::build(Environment::Staging)
.root(root.path().to_path_buf())
.extra(
"template_dir",
root.path().join("templates").to_str()
.ok_or(failure::err_msg("Template path invalid"))?,
)
.extra(
"static_dir",
root.path().join("public").to_str()
.ok_or(failure::err_msg("Static path invalid"))?,
)
.extra("domain", "domain")
.extra("from", "from")
.finalize()?;
Ok((root, config))
}
#[test]
fn basics() {
let (_tmpdir, config) = configuration().unwrap();
let db = Polymorphic::Filesystem(
Filesystem::new(config.root().unwrap().to_path_buf()).unwrap());
let rocket = rocket_factory(rocket::custom(config), db);
let client = Client::new(rocket).expect("valid rocket instance");
// Check that we see the landing page.
let mut response = client.get("/").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("Hagrid"));
// Check that we see the privacy policy.
let mut response = client.get("/about").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("Public Key Data"));
}
2019-02-26 10:34:53 +00:00
#[test]
fn upload() {
let (_tmpdir, config) = configuration().unwrap();
// eprintln!("LEAKING: {:?}", _tmpdir);
// ::std::mem::forget(_tmpdir);
let db = Polymorphic::Filesystem(
Filesystem::new(config.root().unwrap().to_path_buf()).unwrap());
let rocket = rocket_factory(rocket::custom(config), db);
let client = Client::new(rocket).expect("valid rocket instance");
// Generate a key and upload it.
let (tpk, _) = TPKBuilder::autocrypt(
None, Some("foo@invalid.example.com".into()))
.generate().unwrap();
let fp = tpk.fingerprint().to_hex();
let keyid = tpk.fingerprint().to_keyid().to_hex();
let mut tpk_serialized = Vec::new();
tpk.serialize(&mut tpk_serialized).unwrap();
let response = vks_publish_submit(&client, &tpk_serialized);
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get_one("Location"),
Some("/vks/publish?ok"));
// And check that we can get it back, modulo user ids.
fn check_mr_response(client: &Client, uri: &str, tpk: &TPK) {
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain));
let body = response.body_string().unwrap();
assert!(body.contains("END PGP PUBLIC KEY BLOCK"));
let tpk_ = TPK::from_bytes(body.as_bytes()).unwrap();
assert_eq!(tpk.fingerprint(), tpk_.fingerprint());
assert_eq!(tpk.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>(),
tpk_.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>());
assert_eq!(tpk_.userids().count(), 0);
}
check_mr_response(&client, &format!("/by-keyid/{}", keyid), &tpk);
check_mr_response(&client, &format!("/by-fingerprint/{}", fp), &tpk);
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search={}", fp),
&tpk);
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search=0x{}", fp),
&tpk);
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search={}", keyid),
&tpk);
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search=0x{}", keyid),
&tpk);
// And check that we can see the human-readable result page.
fn check_hr_response(client: &Client, uri: &str, tpk: &TPK) {
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
let body = response.body_string().unwrap();
assert!(body.contains("found"));
assert!(body.contains(&tpk.fingerprint().to_hex()));
}
check_hr_response(
&client,
&format!("/pks/lookup?op=get&search={}", fp),
&tpk);
check_hr_response(
&client,
&format!("/pks/lookup?op=get&search=0x{}", fp),
&tpk);
check_hr_response(
&client,
&format!("/pks/lookup?op=get&search={}", keyid),
&tpk);
check_hr_response(
&client,
&format!("/pks/lookup?op=get&search=0x{}", keyid),
&tpk);
}
fn vks_publish_submit<'a>(client: &'a Client, data: &[u8])
-> rocket::local::LocalResponse<'a> {
let ct = ContentType::with_params(
"multipart", "form-data",
("boundary", "---------------------------14733842173518794281682249499"));
let header =
b"-----------------------------14733842173518794281682249499\r\n\
Content-Disposition: form-data; name=\"csrf\"\r\n\
\r\n\
\r\n\
-----------------------------14733842173518794281682249499\r\n\
Content-Disposition: form-data; name=\"keytext\"; filename=\".k\"\r\n\
Content-Type: application/octet-stream\r\n\
\r\n";
let footer = b"\r\n-----------------------------14733842173518794281682249499--";
let mut body = Vec::new();
body.extend_from_slice(header);
body.extend_from_slice(data);
body.extend_from_slice(footer);
client.post("/vks/publish/submit")
.header(ct)
.body(&body[..])
.dispatch()
}
}