2019-05-23 23:01:24 +00:00
|
|
|
use failure;
|
|
|
|
use failure::Fallible as Result;
|
|
|
|
|
|
|
|
use multipart::server::save::Entries;
|
|
|
|
use multipart::server::save::SaveResult::*;
|
|
|
|
use multipart::server::Multipart;
|
|
|
|
|
|
|
|
use rocket::http::ContentType;
|
|
|
|
use rocket::request::Form;
|
|
|
|
use rocket::Data;
|
|
|
|
|
2019-06-11 14:59:27 +00:00
|
|
|
use database::{KeyDatabase, StatefulTokens, Query, Database};
|
2019-05-23 23:01:24 +00:00
|
|
|
use mail;
|
|
|
|
use tokens;
|
2019-06-22 21:25:49 +00:00
|
|
|
use web::{RequestOrigin, MyResponse};
|
2019-05-23 23:01:24 +00:00
|
|
|
use rate_limiter::RateLimiter;
|
|
|
|
|
|
|
|
use std::io::Read;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
|
|
use web::vks;
|
|
|
|
use web::vks::response::*;
|
|
|
|
|
|
|
|
const UPLOAD_LIMIT: u64 = 1024 * 1024; // 1 MiB.
|
|
|
|
|
|
|
|
mod forms {
|
|
|
|
#[derive(FromForm,Deserialize)]
|
|
|
|
pub struct VerifyRequest {
|
|
|
|
pub token: String,
|
|
|
|
pub address: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct UploadRequest {
|
|
|
|
pub keytext: String,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mod template {
|
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct Verify {
|
|
|
|
pub verified: bool,
|
2019-06-12 14:56:21 +00:00
|
|
|
pub key_fpr: String,
|
2019-05-23 23:01:24 +00:00
|
|
|
pub userid: String,
|
2019-06-12 14:56:21 +00:00
|
|
|
pub userid_link: String,
|
2019-05-23 23:01:24 +00:00
|
|
|
pub commit: String,
|
|
|
|
pub version: String,
|
|
|
|
}
|
|
|
|
|
2019-06-11 14:59:27 +00:00
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct Search {
|
|
|
|
pub query: String,
|
|
|
|
pub fpr: String,
|
|
|
|
pub base_uri: String,
|
|
|
|
pub commit: String,
|
|
|
|
pub version: String,
|
|
|
|
}
|
|
|
|
|
2019-05-23 23:01:24 +00:00
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct Upload {
|
|
|
|
pub commit: String,
|
|
|
|
pub version: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct VerificationSent {
|
|
|
|
pub commit: String,
|
|
|
|
pub version: String,
|
|
|
|
pub key_fpr: String,
|
|
|
|
pub key_link: String,
|
|
|
|
pub is_revoked: bool,
|
|
|
|
pub token: String,
|
2019-06-05 11:21:37 +00:00
|
|
|
pub email_published: Vec<String>,
|
|
|
|
pub email_unpublished: Vec<UploadUidStatus>,
|
|
|
|
pub count_revoked_one: bool,
|
|
|
|
pub count_revoked: usize,
|
2019-06-08 15:15:46 +00:00
|
|
|
pub count_unparsed_one: bool,
|
|
|
|
pub count_unparsed: usize,
|
2019-05-23 23:01:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct UploadOkKey {
|
|
|
|
pub key_fpr: String,
|
|
|
|
pub key_link: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct UploadOkMultiple {
|
|
|
|
pub commit: String,
|
|
|
|
pub version: String,
|
|
|
|
pub keys: Vec<UploadOkKey>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct UploadUidStatus {
|
|
|
|
pub address: String,
|
|
|
|
pub requested: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MyResponse {
|
2019-06-05 12:33:02 +00:00
|
|
|
fn upload_response_quick(response: UploadResponse, base_uri: &str) -> Self {
|
|
|
|
match response {
|
2019-06-08 15:15:46 +00:00
|
|
|
UploadResponse::Ok { token, .. } => {
|
2019-06-05 12:33:02 +00:00
|
|
|
let uri = uri!(quick_upload_proceed: token);
|
|
|
|
let text = format!(
|
|
|
|
"Key successfully uploaded. Proceed here:\n{}{}\n",
|
|
|
|
base_uri,
|
|
|
|
uri
|
|
|
|
);
|
|
|
|
MyResponse::plain(text)
|
|
|
|
},
|
|
|
|
UploadResponse::OkMulti { key_fprs } =>
|
2019-06-12 14:00:40 +00:00
|
|
|
MyResponse::plain(format!("Uploaded {} keys. For verification, please upload keys individually.\n", key_fprs.len())),
|
2019-06-05 12:33:02 +00:00
|
|
|
UploadResponse::Error(error) => MyResponse::bad_request(
|
2019-06-07 20:31:24 +00:00
|
|
|
"400-plain", failure::err_msg(error)),
|
2019-06-05 12:33:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-23 23:01:24 +00:00
|
|
|
fn upload_response(response: UploadResponse) -> Self {
|
|
|
|
match response {
|
2019-06-08 15:15:46 +00:00
|
|
|
UploadResponse::Ok { token, key_fpr, is_revoked, count_unparsed, status } =>
|
|
|
|
Self::upload_ok(token, key_fpr, is_revoked, count_unparsed, status),
|
2019-06-05 12:33:02 +00:00
|
|
|
UploadResponse::OkMulti { key_fprs } =>
|
|
|
|
Self::upload_ok_multi(key_fprs),
|
2019-05-23 23:01:24 +00:00
|
|
|
UploadResponse::Error(error) => MyResponse::bad_request(
|
|
|
|
"upload/upload", failure::err_msg(error)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn upload_ok(
|
|
|
|
token: String,
|
|
|
|
key_fpr: String,
|
2019-06-05 11:21:37 +00:00
|
|
|
is_revoked: bool,
|
2019-06-08 15:15:46 +00:00
|
|
|
count_unparsed: usize,
|
2019-05-23 23:01:24 +00:00
|
|
|
uid_status: HashMap<String,EmailStatus>,
|
|
|
|
) -> Self {
|
2019-06-12 14:56:21 +00:00
|
|
|
let key_link = uri!(search: &key_fpr).to_string();
|
2019-05-23 23:01:24 +00:00
|
|
|
|
2019-06-05 11:21:37 +00:00
|
|
|
let count_revoked = uid_status.iter()
|
|
|
|
.filter(|(_,status)| **status == EmailStatus::Revoked)
|
|
|
|
.count();
|
|
|
|
|
|
|
|
let mut email_published: Vec<_> = uid_status.iter()
|
|
|
|
.filter(|(_,status)| **status == EmailStatus::Published)
|
|
|
|
.map(|(email,_)| email.to_string())
|
|
|
|
.collect();
|
|
|
|
email_published.sort_unstable();
|
|
|
|
|
|
|
|
let mut email_unpublished: Vec<_> = uid_status.into_iter()
|
|
|
|
.filter(|(_,status)| *status == EmailStatus::Unpublished ||
|
|
|
|
*status == EmailStatus::Pending)
|
|
|
|
.map(|(email,status)|
|
|
|
|
template::UploadUidStatus {
|
|
|
|
address: email.to_string(),
|
|
|
|
requested: status == EmailStatus::Pending,
|
|
|
|
})
|
2019-05-23 23:01:24 +00:00
|
|
|
.collect();
|
2019-06-05 11:21:37 +00:00
|
|
|
email_unpublished
|
|
|
|
.sort_unstable_by(|fst,snd| fst.address.cmp(&snd.address));
|
2019-05-23 23:01:24 +00:00
|
|
|
|
|
|
|
let context = template::VerificationSent {
|
|
|
|
version: env!("VERGEN_SEMVER").to_string(),
|
|
|
|
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
2019-06-05 11:21:37 +00:00
|
|
|
is_revoked,
|
|
|
|
key_fpr,
|
|
|
|
key_link,
|
|
|
|
token,
|
|
|
|
email_published,
|
|
|
|
email_unpublished,
|
|
|
|
count_revoked_one: count_revoked == 1,
|
|
|
|
count_revoked,
|
2019-06-08 15:15:46 +00:00
|
|
|
count_unparsed_one: count_unparsed == 1,
|
|
|
|
count_unparsed,
|
2019-05-23 23:01:24 +00:00
|
|
|
};
|
|
|
|
MyResponse::ok("upload/upload-ok", context)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn upload_ok_multi(key_fprs: Vec<String>) -> Self {
|
|
|
|
let keys = key_fprs.into_iter()
|
2019-06-12 14:56:21 +00:00
|
|
|
.map(|fpr| {
|
|
|
|
let key_link = uri!(search: &fpr).to_string();
|
|
|
|
template::UploadOkKey {
|
|
|
|
key_fpr: fpr.to_owned(),
|
|
|
|
key_link,
|
|
|
|
}
|
2019-05-23 23:01:24 +00:00
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let context = template::UploadOkMultiple {
|
|
|
|
version: env!("VERGEN_SEMVER").to_string(),
|
|
|
|
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
|
|
keys,
|
|
|
|
};
|
|
|
|
|
|
|
|
MyResponse::ok("upload/upload-ok-multiple", context)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/upload")]
|
|
|
|
pub fn upload() -> MyResponse {
|
|
|
|
let context = template::Upload {
|
|
|
|
version: env!("VERGEN_SEMVER").to_string(),
|
|
|
|
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
|
|
};
|
|
|
|
|
|
|
|
MyResponse::ok("upload/upload", context)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/upload/submit", format = "multipart/form-data", data = "<data>")]
|
|
|
|
pub fn upload_post_form_data(
|
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
tokens_stateless: rocket::State<tokens::Service>,
|
|
|
|
rate_limiter: rocket::State<RateLimiter>,
|
|
|
|
cont_type: &ContentType,
|
|
|
|
data: Data,
|
|
|
|
) -> Result<MyResponse> {
|
|
|
|
// multipart/form-data
|
|
|
|
let (_, boundary) =
|
|
|
|
match cont_type.params().find(|&(k, _)| k == "boundary") {
|
|
|
|
Some(v) => v,
|
|
|
|
None => return Ok(MyResponse::bad_request(
|
|
|
|
"upload/upload",
|
|
|
|
failure::err_msg("`Content-Type: multipart/form-data` \
|
|
|
|
boundary param not provided"))),
|
|
|
|
};
|
|
|
|
|
|
|
|
process_upload(&db, &tokens_stateless, &rate_limiter, data, boundary)
|
|
|
|
}
|
|
|
|
|
2019-06-11 14:59:27 +00:00
|
|
|
#[get("/search?<q>")]
|
|
|
|
pub fn search(
|
2019-06-22 21:25:49 +00:00
|
|
|
request_origin: RequestOrigin,
|
2019-06-11 14:59:27 +00:00
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
q: String,
|
|
|
|
) -> MyResponse {
|
|
|
|
match q.parse::<Query>() {
|
2019-06-22 21:25:49 +00:00
|
|
|
Ok(query) => key_to_response(request_origin, db, q, query),
|
2019-06-11 14:59:27 +00:00
|
|
|
Err(e) => MyResponse::bad_request("index", e),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn key_to_response(
|
2019-06-22 21:25:49 +00:00
|
|
|
request_origin: RequestOrigin,
|
2019-06-11 14:59:27 +00:00
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
query_string: String,
|
|
|
|
query: Query,
|
|
|
|
) -> MyResponse {
|
|
|
|
let fp = if let Some(fp) = db.lookup_primary_fingerprint(&query) {
|
|
|
|
fp
|
|
|
|
} else {
|
|
|
|
return MyResponse::not_found(None, query.describe_error());
|
|
|
|
};
|
|
|
|
|
|
|
|
let context = template::Search{
|
|
|
|
query: query_string,
|
2019-06-22 21:25:49 +00:00
|
|
|
base_uri: request_origin.get_base_uri().to_owned(),
|
2019-06-11 14:59:27 +00:00
|
|
|
fpr: fp.to_string(),
|
|
|
|
version: env!("VERGEN_SEMVER").to_string(),
|
|
|
|
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
|
|
};
|
|
|
|
|
|
|
|
MyResponse::ok("found", context)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-06-05 12:33:02 +00:00
|
|
|
#[put("/", data = "<data>")]
|
|
|
|
pub fn quick_upload(
|
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
tokens_stateless: rocket::State<tokens::Service>,
|
|
|
|
rate_limiter: rocket::State<RateLimiter>,
|
2019-06-22 21:25:49 +00:00
|
|
|
request_origin: RequestOrigin,
|
2019-06-05 12:33:02 +00:00
|
|
|
data: Data,
|
|
|
|
) -> MyResponse {
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
|
|
|
let mut buf = Vec::default();
|
|
|
|
if let Err(error) = std::io::copy(&mut data.open().take(UPLOAD_LIMIT), &mut buf) {
|
2019-06-06 14:33:16 +00:00
|
|
|
return MyResponse::bad_request("400-plain", failure::err_msg(error));
|
2019-06-05 12:33:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
MyResponse::upload_response_quick(vks::process_key(
|
|
|
|
&db,
|
|
|
|
&tokens_stateless,
|
|
|
|
&rate_limiter,
|
2019-06-22 21:25:49 +00:00
|
|
|
Cursor::new(buf)), request_origin.get_base_uri())
|
2019-06-05 12:33:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/upload/<token>", rank = 2)]
|
|
|
|
pub fn quick_upload_proceed(
|
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
token_stateful: rocket::State<StatefulTokens>,
|
|
|
|
token_stateless: rocket::State<tokens::Service>,
|
|
|
|
mail_service: rocket::State<mail::Service>,
|
|
|
|
rate_limiter: rocket::State<RateLimiter>,
|
|
|
|
token: String,
|
|
|
|
) -> MyResponse {
|
|
|
|
let result = vks::request_verify(
|
|
|
|
db, token_stateful, token_stateless, mail_service,
|
|
|
|
rate_limiter, token, vec!());
|
|
|
|
MyResponse::upload_response(result)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-05-23 23:01:24 +00:00
|
|
|
#[post("/upload/submit", format = "application/x-www-form-urlencoded", data = "<data>")]
|
|
|
|
pub fn upload_post_form(
|
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
tokens_stateless: rocket::State<tokens::Service>,
|
|
|
|
rate_limiter: rocket::State<RateLimiter>,
|
|
|
|
data: Data,
|
|
|
|
) -> Result<MyResponse> {
|
|
|
|
use rocket::request::FormItems;
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
|
|
|
// application/x-www-form-urlencoded
|
|
|
|
let mut buf = Vec::default();
|
|
|
|
|
|
|
|
std::io::copy(&mut data.open().take(UPLOAD_LIMIT), &mut buf)?;
|
|
|
|
|
|
|
|
for item in FormItems::from(&*String::from_utf8_lossy(&buf)) {
|
|
|
|
let (key, value) = item.key_value();
|
|
|
|
let decoded_value = value.url_decode().or_else(|_| {
|
|
|
|
Err(failure::err_msg(
|
|
|
|
"`Content-Type: application/x-www-form-urlencoded` \
|
|
|
|
not valid"))
|
|
|
|
})?;
|
|
|
|
|
|
|
|
match key.as_str() {
|
|
|
|
"keytext" => {
|
|
|
|
return Ok(MyResponse::upload_response(vks::process_key(
|
|
|
|
&db,
|
|
|
|
&tokens_stateless,
|
|
|
|
&rate_limiter,
|
|
|
|
Cursor::new(decoded_value.as_bytes())
|
|
|
|
)));
|
|
|
|
}
|
|
|
|
_ => { /* skip */ }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(MyResponse::bad_request("upload/upload",
|
|
|
|
failure::err_msg("No keytext found")))
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn process_upload(
|
|
|
|
db: &KeyDatabase,
|
|
|
|
tokens_stateless: &tokens::Service,
|
|
|
|
rate_limiter: &RateLimiter,
|
|
|
|
data: Data,
|
|
|
|
boundary: &str,
|
|
|
|
) -> Result<MyResponse> {
|
|
|
|
// 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().take(UPLOAD_LIMIT), boundary).save().temp() {
|
|
|
|
Full(entries) => {
|
|
|
|
process_multipart(db, tokens_stateless, rate_limiter, entries)
|
|
|
|
}
|
|
|
|
Partial(partial, _) => {
|
|
|
|
process_multipart(db, tokens_stateless, rate_limiter, partial.entries)
|
|
|
|
}
|
|
|
|
Error(err) => Err(err.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process_multipart(
|
|
|
|
db: &KeyDatabase,
|
|
|
|
tokens_stateless: &tokens::Service,
|
|
|
|
rate_limiter: &RateLimiter,
|
|
|
|
entries: Entries,
|
|
|
|
) -> Result<MyResponse> {
|
|
|
|
match entries.fields.get("keytext") {
|
|
|
|
Some(ent) if ent.len() == 1 => {
|
|
|
|
let reader = ent[0].data.readable()?;
|
|
|
|
Ok(MyResponse::upload_response(vks::process_key(db, tokens_stateless, rate_limiter, reader)))
|
|
|
|
}
|
|
|
|
Some(_) =>
|
|
|
|
Ok(MyResponse::bad_request(
|
|
|
|
"upload/upload", failure::err_msg("Multiple keytexts found"))),
|
|
|
|
None =>
|
|
|
|
Ok(MyResponse::bad_request(
|
|
|
|
"upload/upload", failure::err_msg("No keytext found"))),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/upload/request-verify", format = "application/x-www-form-urlencoded", data="<request>")]
|
|
|
|
pub fn request_verify_form(
|
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
token_stateful: rocket::State<StatefulTokens>,
|
|
|
|
token_stateless: rocket::State<tokens::Service>,
|
|
|
|
mail_service: rocket::State<mail::Service>,
|
|
|
|
rate_limiter: rocket::State<RateLimiter>,
|
|
|
|
request: Form<forms::VerifyRequest>,
|
|
|
|
) -> MyResponse {
|
|
|
|
let forms::VerifyRequest { token, address } = request.into_inner();
|
|
|
|
let result = vks::request_verify(
|
|
|
|
db, token_stateful, token_stateless, mail_service,
|
|
|
|
rate_limiter, token, vec!(address));
|
|
|
|
MyResponse::upload_response(result)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/upload/request-verify", format = "multipart/form-data", data="<request>")]
|
|
|
|
pub fn request_verify_form_data(
|
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
token_stateful: rocket::State<StatefulTokens>,
|
|
|
|
token_stateless: rocket::State<tokens::Service>,
|
|
|
|
mail_service: rocket::State<mail::Service>,
|
|
|
|
rate_limiter: rocket::State<RateLimiter>,
|
|
|
|
request: Form<forms::VerifyRequest>,
|
|
|
|
) -> MyResponse {
|
|
|
|
let forms::VerifyRequest { token, address } = request.into_inner();
|
|
|
|
let result = vks::request_verify(
|
|
|
|
db, token_stateful, token_stateless, mail_service,
|
|
|
|
rate_limiter, token, vec!(address));
|
|
|
|
MyResponse::upload_response(result)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/verify/<token>")]
|
|
|
|
pub fn verify_confirm(
|
|
|
|
db: rocket::State<KeyDatabase>,
|
|
|
|
token_service: rocket::State<StatefulTokens>,
|
|
|
|
token: String,
|
|
|
|
) -> MyResponse {
|
|
|
|
match vks::verify_confirm(db, token_service, token) {
|
2019-06-12 14:56:21 +00:00
|
|
|
PublishResponse::Ok { fingerprint, email } => {
|
|
|
|
let userid_link = uri!(search: &email).to_string();
|
2019-05-23 23:01:24 +00:00
|
|
|
let context = template::Verify {
|
|
|
|
verified: true,
|
|
|
|
userid: email,
|
2019-06-12 14:56:21 +00:00
|
|
|
key_fpr: fingerprint,
|
|
|
|
userid_link,
|
2019-05-23 23:01:24 +00:00
|
|
|
version: env!("VERGEN_SEMVER").to_string(),
|
|
|
|
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
|
|
};
|
|
|
|
|
|
|
|
MyResponse::ok("upload/publish-result", context)
|
|
|
|
},
|
|
|
|
PublishResponse::Error(error) => MyResponse::plain(error),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|