expose db via web
This commit is contained in:
parent
2013eb21bf
commit
fe8ad784a9
|
@ -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"]
|
||||
|
|
41
src/main.rs
41
src/main.rs
|
@ -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();
|
||||
}
|
||||
|
|
267
src/web/mod.rs
267
src/web/mod.rs
|
@ -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≡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
|
||||
|
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -0,0 +1,16 @@
|
|||
{{#> layout }}
|
||||
<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>
|
||||
{{/layout}}
|
|
@ -0,0 +1,10 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Garbage Pile Public Key Server</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{> @partial-block }}
|
||||
</body>
|
||||
</html>
|
|
@ -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}}
|
|
@ -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}}
|
||||
|
Loading…
Reference in New Issue