expose db via web

This commit is contained in:
seu 2018-09-19 22:24:38 +02:00
parent 2013eb21bf
commit fe8ad784a9
10 changed files with 361 additions and 79 deletions

View File

@ -19,3 +19,8 @@ time = "0.1"
tempfile = "3.0"
parking_lot = "0.6"
structopt = "0.2"
[dependencies.rocket_contrib]
version = "0"
default-features = false
features = ["handlebars_templates"]

View File

@ -9,10 +9,13 @@ 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 rocket_contrib;
extern crate multipart;
extern crate openpgp;
#[macro_use] extern crate error_chain;
#[macro_use] extern crate log;
extern crate rand;
@ -31,6 +34,9 @@ mod errors {
Json(::serde_json::Error);
Persist(::tempfile::PersistError);
Base64(::base64::DecodeError);
RktConfig(::rocket::config::ConfigError);
StringUtf8Error(::std::string::FromUtf8Error);
StrUtf8Error(::std::str::Utf8Error);
}
}
}
@ -41,23 +47,38 @@ use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(name = "garbage", about = "Garbage Pile - The verifying OpenPGP key server.")]
struct Opt {
/// Debug mode
pub struct Opt {
/// More verbose output. Disabled when running as daemon.
#[structopt(short = "v", long = "verbose")]
debug: bool,
/// Daemon
verbose: bool,
/// Daemonize after startup.
#[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")]
/// Template directory
#[structopt(parse(from_os_str))]
templates: PathBuf,
/// Port and address to listen on.
#[structopt(short = "l", long = "listen", default_value = "0.0.0.0:8080")]
listen: String,
}
fn main() {
let opt = Opt::from_args();
println!("{:?}", opt);
}
use database::{Filesystem, Polymorphic};
let opt = Opt::from_args();
println!("{:#?}", opt);
if !opt.base.is_absolute() {
panic!("Base directory must be absolute");
}
if !opt.templates.is_absolute() {
panic!("Template directory must be absolute");
}
let db = Filesystem::new(opt.base.clone()).unwrap();
web::serve(&opt, Polymorphic::Filesystem(db)).unwrap();
}

View File

@ -1,73 +1,242 @@
use rocket;
use rocket::Outcome;
use rocket::http::Status;
use rocket::request::{self, Request, FromRequest};
use rocket::response::content;
use rocket::response::status::Custom;
use rocket::http::uri::URI;
use rocket_contrib::Template;
mod upload;
#[get("/key/<fpr>")]
fn key_by_fingerprint(fpr: String) -> String {
format!("{}", fpr)
use database::{Polymorphic, Fingerprint, Database};
use errors::Result;
use errors;
use Opt;
use std::str::FromStr;
use std::result;
mod queries {
use database::Fingerprint;
pub enum Key {
Fingerprint(Fingerprint),
UserID(String),
}
}
#[derive(FromForm)]
struct KeySubmissionForm {
key: String,
mod templates {
#[derive(Serialize)]
pub struct Verify {
pub verified: bool,
pub userid: String,
pub fpr: String,
}
#[derive(Serialize)]
pub struct Delete {
pub token: String,
pub fpr: String,
}
#[derive(Serialize)]
pub struct Confirm {
pub deleted: bool,
}
}
#[post("/keys", data = "<data>")]
fn submit_key(data: String) -> String {
//use multipart::server::Multipart;
//use openpgp::TPK;
format!("{:?}", data)/*
let strm = form.open();
impl<'a, 'r> FromRequest<'a, 'r> for queries::Key {
type Error = ();
match TPK::from_reader(strm) {
Ok(tpk) => {
match tpk.userids().next() {
Some(uid) => {
format!("Hello, {:?}", uid.userid())
fn from_request(request: &'a Request<'r>) -> request::Outcome<queries::Key, ()> {
let query = request.uri().query().unwrap_or("");
if query.starts_with("fpr=") {
let maybe_fpr = URI::percent_decode(&query[4..].as_bytes())
.map_err(|e| errors::ErrorKind::StrUtf8Error(e).into())
.and_then(|x| Fingerprint::from_str(&*x));
match maybe_fpr {
Ok(fpr) => Outcome::Success(queries::Key::Fingerprint(fpr)),
Err(e) => Outcome::Failure((Status::BadRequest, ())),
}
} else if query.starts_with("uid=") {
let maybe_uid: Result<_> = URI::percent_decode(&query[4..].as_bytes())
.map_err(|e| errors::ErrorKind::StrUtf8Error(e).into());
match maybe_uid {
Ok(uid) => {
eprintln!("get {}", uid);
Outcome::Success(queries::Key::UserID(uid.into()))
}
None => {
format!("Hello, {:?}", tpk.primary().fingerprint())
Err(e) => Outcome::Failure((Status::BadRequest, ())),
}
} else {
Outcome::Failure((Status::BadRequest, ()))
}
}
}
#[get("/keys")]
fn get_key(db: rocket::State<Polymorphic>, key: Option<queries::Key>)
-> result::Result<String, Custom<String>>
{
use std::io::Write;
use openpgp::armor::{Writer, Kind};
use std::str::FromStr;
let maybe_key = match key {
Some(queries::Key::Fingerprint(ref fpr)) => db.by_fpr(fpr),
Some(queries::Key::UserID(ref uid)) => db.by_uid(uid),
None => { return Ok("nothing to do".to_string()); }
};
match maybe_key {
Some(bytes) => {
let key = || -> Result<String> {
let mut buffer = Vec::default();
{
let mut writer = Writer::new(&mut buffer, Kind::PublicKey, &[])?;
writer.write_all(&bytes)?;
}
Ok(String::from_utf8(buffer)?)
}();
match key {
Ok(s) => Ok(s),
Err(_) =>
Err(Custom(Status::InternalServerError,
"Failed to ASCII armor key".to_string())),
}
}
Err(e) => {
format!("Error: {:?}", e)
None => Ok("No such key :-(".to_string()),
}
}
#[get("/verify/<token>")]
fn verify(db: rocket::State<Polymorphic>, token: String)
-> result::Result<Template, Custom<String>>
{
match db.verify_token(&token) {
Ok(Some((userid, fpr))) => {
let context = templates::Verify{
verified: true,
userid: userid.to_string(),
fpr: fpr.to_string(),
};
Ok(Template::render("verify", context))
}
}*/
Ok(None) | Err(_) => {
let context = templates::Verify{
verified: false,
userid: "".into(),
fpr: "".into(),
};
Ok(Template::render("verify", context))
}
}
}
#[get("/delete/<fpr>")]
fn delete(db: rocket::State<Polymorphic>, fpr: String)
-> result::Result<Template, Custom<String>>
{
let fpr = match Fingerprint::from_str(&fpr) {
Ok(fpr) => fpr,
Err(_) => {
return Err(Custom(Status::BadRequest,
"Invalid fingerprint".to_string()));
}
};
match db.request_deletion(fpr.clone()) {
Ok(token) => {
let context = templates::Delete{
fpr: fpr.to_string(),
token: token,
};
Ok(Template::render("delete", context))
}
Err(e) => Err(Custom(Status::InternalServerError,
format!("{}", e))),
}
}
#[get("/confirm/<token>")]
fn confirm(db: rocket::State<Polymorphic>, token: String)
-> result::Result<Template, Custom<String>>
{
match db.confirm_deletion(&token) {
Ok(true) => {
let context = templates::Confirm{
deleted: true,
};
Ok(Template::render("confirm", context))
}
Ok(false) | Err(_) => {
let context = templates::Confirm{
deleted: false,
};
Ok(Template::render("confirm", context))
}
}
}
#[get("/")]
fn root() -> content::Html<&'static str> {
content::Html("
<!doctype html>
<html>
<head>
<title>Garbage Pile Public Key Server</title>
</head>
fn root() -> Template {
use std::collections::HashMap;
<body>
<h1>Garbage Pile Public Key Server</h1>
<p>The verifying PGP key server. Powered by p&equiv;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>")
Template::render("index", HashMap::<String, String>::default())
}
fn main() {
rocket::ignite().mount("/", routes![
upload::multipart_upload,
key_by_fingerprint,
root]).launch();
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 {
u16::from_str(&opt.listen[p+1..]).ok().unwrap_or(8080)
} else {
8080
};
(addr, port)
}
None => (opt.listen.to_string(), 8080)
};
let config = Config::build(Environment::Staging)
.address(addr)
.port(port)
.workers(2)
.root(opt.base.join("public"))
.extra("template_dir", format!("{}", opt.templates.display()))
.finalize()?;
let routes = routes![
upload::multipart_upload,
get_key,
verify,
delete,
confirm,
root
];
rocket::custom(config, opt.verbose)
.attach(Template::fairing())
.mount("/", routes)
.manage(db)
.launch();
Ok(())
}
//POST /keys

View File

@ -2,13 +2,29 @@ use multipart::server::Multipart;
use multipart::server::save::Entries;
use multipart::server::save::SaveResult::*;
use rocket::Data;
use rocket::{State, Data};
use rocket::http::{ContentType, Status};
use rocket::response::status::Custom;
use rocket_contrib::Template;
use database::{Database, Polymorphic};
mod template {
#[derive(Serialize)]
pub struct Token {
pub userid: String,
pub token: String,
}
#[derive(Serialize)]
pub struct Context {
pub tokens: Vec<Token>,
}
}
#[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>> {
pub fn multipart_upload(db: State<Polymorphic>, cont_type: &ContentType, data: Data) -> Result<Template, 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() {
@ -25,17 +41,17 @@ pub fn multipart_upload(cont_type: &ContentType, data: Data) -> Result<String, C
)
)?;
process_upload(boundary, data)
process_upload(boundary, data, db.inner())
}
fn process_upload(boundary: &str, data: Data) -> Result<String, Custom<String>> {
fn process_upload(boundary: &str, data: Data, db: &Polymorphic) -> Result<Template, 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),
Full(entries) => process_entries(entries, db),
Partial(partial, _) => {
process_entries(partial.entries)
process_entries(partial.entries, db)
},
Error(err) => Err(Custom(Status::InternalServerError, err.to_string())),
}
@ -43,7 +59,7 @@ fn process_upload(boundary: &str, data: Data) -> Result<String, Custom<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>> {
fn process_entries(entries: Entries, db: &Polymorphic) -> Result<Template, Custom<String>> {
use openpgp::TPK;
match entries.fields.get(&"key".to_string()) {
@ -54,24 +70,29 @@ fn process_entries(entries: Entries) -> Result<String, Custom<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()))
match db.merge_or_publish(tpk) {
Ok(tokens) => {
let tokens = tokens
.into_iter().map(|(uid,tok)| {
template::Token{ userid: uid.to_string(), token: tok }
}).collect::<Vec<_>>();
let context = template::Context{
tokens: tokens
};
Ok(Template::render("upload", context))
}
Err(err) =>
Err(Custom(Status::InternalServerError,
format!("{:?}", err))),
}
}
Err(e) => {
Ok(format!("Error: {:?}", e))
}
Err(_) => Err(Custom(Status::BadRequest,
"Not a PGP public key".into())),
}
}
Some(_) | None => Err(Custom(
Status::BadRequest,
"Not a PGP public key".into()
)),
Some(_) | None =>
Err(Custom(Status::BadRequest, "Not a PGP public key".into())),
}
}

View File

@ -0,0 +1,10 @@
{{#> layout}}
{{#if deleted }}
<h1>Key deleted</h1>
<p>We deleted your key from the server. You can re-upload any time.</p>
{{else}}
<h1>Key deletion failed</h1>
<p>We don't recognize the token you provided. It's either wrong or expired.
You can request the deletion of your key again to get another deletion token.</p>
{{/if}}
{{/layout}}

View File

@ -0,0 +1,5 @@
{{#> layout}}
<h1>Deletion requested</h1>
<p>We sent you an email with instuctions how to delete you key to all of your verified email addresses.</p>
<b>Token:</b> <a href="/confirm/{{ token }}"><tt>{{ token }}</tt></a>
{{/layout}}

16
templates/index.html.hbs Normal file
View File

@ -0,0 +1,16 @@
{{#> layout }}
<h1>Garbage Pile Public Key Server</h1>
<p>The verifying PGP key server. Powered by p&equiv;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>
{{/layout}}

10
templates/layout.html.hbs Normal file
View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Garbage Pile Public Key Server</title>
</head>
<body>
{{> @partial-block }}
</body>
</html>

12
templates/upload.html.hbs Normal file
View File

@ -0,0 +1,12 @@
{{#> layout }}
<h1>Tokens</h1>
{{#if tokens}}
<ul>
{{#each tokens}}
<li>{{userid}}: <a href="/verify/{{token}}"><tt>{{token}}</tt></a></li>
{{/each}}
</ul>
{{else}}
<em>No new user IDs</em>
{{/if}}
{{/layout}}

13
templates/verify.html.hbs Normal file
View File

@ -0,0 +1,13 @@
{{#> layout}}
{{#if verified }}
<h1>Email verified</h1>
<p>You've verified <em>{{ userid }}</em> successfully. Everybody who knows
your email address is no able to find your key. You can <a href="/delete/{{
fpr }}">delete</a> your key any time you want.</p>
{{else}}
<h1>Email verification failed</h1>
<p>We don't recognize the token you provided. It's either wrong or expired.
Upload your key again to get another verification token sent to you.</p>
{{/if}}
{{/layout}}