Rework API
This commit is contained in:
parent
72dcfec4a3
commit
9a225410c9
|
@ -26,7 +26,7 @@
|
||||||
{{#if requested}}
|
{{#if requested}}
|
||||||
Verification Pending
|
Verification Pending
|
||||||
{{else}}
|
{{else}}
|
||||||
<form action="/vks/v1/request-verify" method="post">
|
<form action="/upload/request-verify" method="post">
|
||||||
<input type="hidden" name="token" value="{{../token}}" />
|
<input type="hidden" name="token" value="{{../token}}" />
|
||||||
<input type="hidden" name="address" value="{{address}}" />
|
<input type="hidden" name="address" value="{{address}}" />
|
||||||
<input type="submit" class="link" value="Send Verification Mail">
|
<input type="submit" class="link" value="Send Verification Mail">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
After upload, you can also make it searchable by e-mail address.
|
After upload, you can also make it searchable by e-mail address.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form action="/vks/v1/upload" method="POST" enctype="multipart/form-data">
|
<form action="/upload/submit" method="POST" enctype="multipart/form-data">
|
||||||
<div class="upload">
|
<div class="upload">
|
||||||
<input type="file" id="keytext" name="keytext" autofocus class="fileUpload" placeholder="Your public key"/>
|
<input type="file" id="keytext" name="keytext" autofocus class="fileUpload" placeholder="Your public key"/>
|
||||||
<button type="submit" class="uploadButton button smallButton">
|
<button type="submit" class="uploadButton button smallButton">
|
||||||
|
|
|
@ -85,7 +85,11 @@ location /manage {
|
||||||
proxy_pass http://127.0.0.1:8080;
|
proxy_pass http://127.0.0.1:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /publish {
|
location /verify {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
proxy_pass http://127.0.0.1:8080;
|
proxy_pass http://127.0.0.1:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ impl Service {
|
||||||
-> Result<()> {
|
-> Result<()> {
|
||||||
let ctx = context::Verification {
|
let ctx = context::Verification {
|
||||||
primary_fp: tpk_name,
|
primary_fp: tpk_name,
|
||||||
uri: format!("{}/publish/{}", self.base_uri, token),
|
uri: format!("{}/verify/{}", self.base_uri, token),
|
||||||
userid: userid.to_string(),
|
userid: userid.to_string(),
|
||||||
base_uri: self.base_uri.clone(),
|
base_uri: self.base_uri.clone(),
|
||||||
domain: self.domain.clone(),
|
domain: self.domain.clone(),
|
||||||
|
|
|
@ -16,6 +16,7 @@ extern crate url;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
extern crate multipart;
|
extern crate multipart;
|
||||||
|
#[macro_use]
|
||||||
extern crate rocket_contrib;
|
extern crate rocket_contrib;
|
||||||
|
|
||||||
extern crate sequoia_openpgp;
|
extern crate sequoia_openpgp;
|
||||||
|
|
|
@ -17,7 +17,7 @@ use web::{
|
||||||
HagridState,
|
HagridState,
|
||||||
MyResponse,
|
MyResponse,
|
||||||
key_to_response,
|
key_to_response,
|
||||||
upload,
|
vks_web,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -127,7 +127,7 @@ pub fn pks_add_form_data(
|
||||||
cont_type: &ContentType,
|
cont_type: &ContentType,
|
||||||
data: Data,
|
data: Data,
|
||||||
) -> MyResponse {
|
) -> MyResponse {
|
||||||
match upload::vks_v1_upload_post_form_data(db, tokens_stateless, rate_limiter, cont_type, data) {
|
match vks_web::upload_post_form_data(db, tokens_stateless, rate_limiter, cont_type, data) {
|
||||||
Ok(_) => MyResponse::plain("Ok".into()),
|
Ok(_) => MyResponse::plain("Ok".into()),
|
||||||
Err(err) => MyResponse::ise(err),
|
Err(err) => MyResponse::ise(err),
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ pub fn pks_add_form(
|
||||||
rate_limiter: rocket::State<RateLimiter>,
|
rate_limiter: rocket::State<RateLimiter>,
|
||||||
data: Data,
|
data: Data,
|
||||||
) -> MyResponse {
|
) -> MyResponse {
|
||||||
match upload::vks_v1_upload_post_form(db, tokens_stateless, rate_limiter, data) {
|
match vks_web::upload_post_form(db, tokens_stateless, rate_limiter, data) {
|
||||||
Ok(_) => MyResponse::plain("Ok".into()),
|
Ok(_) => MyResponse::plain("Ok".into()),
|
||||||
Err(err) => MyResponse::ise(err),
|
Err(err) => MyResponse::ise(err),
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ use handlebars::Handlebars;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub mod upload;
|
|
||||||
use mail;
|
use mail;
|
||||||
use tokens;
|
use tokens;
|
||||||
use rate_limiter::RateLimiter;
|
use rate_limiter::RateLimiter;
|
||||||
|
@ -25,6 +24,9 @@ use std::convert::TryInto;
|
||||||
mod hkp;
|
mod hkp;
|
||||||
mod manage;
|
mod manage;
|
||||||
mod maintenance;
|
mod maintenance;
|
||||||
|
mod vks;
|
||||||
|
mod vks_web;
|
||||||
|
mod vks_api;
|
||||||
|
|
||||||
use web::maintenance::MaintenanceMode;
|
use web::maintenance::MaintenanceMode;
|
||||||
|
|
||||||
|
@ -34,11 +36,9 @@ use rocket::http::hyper::header::ContentDisposition;
|
||||||
pub enum MyResponse {
|
pub enum MyResponse {
|
||||||
#[response(status = 200, content_type = "html")]
|
#[response(status = 200, content_type = "html")]
|
||||||
Success(Template),
|
Success(Template),
|
||||||
#[response(status = 200, content_type = "plain")]
|
#[response(status = 200, content_type = "plain")]
|
||||||
Plain(String),
|
Plain(String),
|
||||||
#[response(status = 200, content_type = "application/json")]
|
#[response(status = 200, content_type = "application/pgp-keys")]
|
||||||
Json(String),
|
|
||||||
#[response(status = 200, content_type = "application/pgp-keys")]
|
|
||||||
Key(String, ContentDisposition),
|
Key(String, ContentDisposition),
|
||||||
#[response(status = 200, content_type = "application/pgp-keys")]
|
#[response(status = 200, content_type = "application/pgp-keys")]
|
||||||
XAccelRedirect(&'static str, Header<'static>, ContentDisposition),
|
XAccelRedirect(&'static str, Header<'static>, ContentDisposition),
|
||||||
|
@ -330,15 +330,17 @@ fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
|
||||||
vks_v1_by_email,
|
vks_v1_by_email,
|
||||||
vks_v1_by_fingerprint,
|
vks_v1_by_fingerprint,
|
||||||
vks_v1_by_keyid,
|
vks_v1_by_keyid,
|
||||||
upload::vks_v1_upload_post_form,
|
vks_api::upload_json,
|
||||||
upload::vks_v1_upload_post_form_data,
|
vks_api::upload_fallback,
|
||||||
upload::vks_v1_upload_post_json,
|
vks_api::request_verify_json,
|
||||||
|
vks_api::request_verify_fallback,
|
||||||
// User interaction.
|
// User interaction.
|
||||||
upload::upload,
|
vks_web::upload,
|
||||||
upload::vks_upload_verify_json,
|
vks_web::upload_post_form,
|
||||||
upload::vks_upload_verify_form,
|
vks_web::upload_post_form_data,
|
||||||
upload::vks_upload_verify_form_data,
|
vks_web::request_verify_form,
|
||||||
upload::publish_verify,
|
vks_web::request_verify_form_data,
|
||||||
|
vks_web::verify_confirm,
|
||||||
// HKP
|
// HKP
|
||||||
hkp::pks_lookup,
|
hkp::pks_lookup,
|
||||||
hkp::pks_add_form,
|
hkp::pks_add_form,
|
||||||
|
@ -553,7 +555,7 @@ pub mod tests {
|
||||||
assert!(response.body_string().unwrap().contains("/vks/v1/by-keyid"));
|
assert!(response.body_string().unwrap().contains("/vks/v1/by-keyid"));
|
||||||
|
|
||||||
// Check that we see the upload form.
|
// Check that we see the upload form.
|
||||||
let mut response = client.get("/publish").dispatch();
|
let mut response = client.get("/upload").dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||||
assert!(response.body_string().unwrap().contains("upload"));
|
assert!(response.body_string().unwrap().contains("upload"));
|
||||||
|
@ -877,7 +879,7 @@ pub mod tests {
|
||||||
.append_pair("address", address)
|
.append_pair("address", address)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let response = client.post("/vks/v1/request-verify")
|
let response = client.post("/upload/request-verify")
|
||||||
.header(ContentType::Form)
|
.header(ContentType::Form)
|
||||||
.body(encoded.as_bytes())
|
.body(encoded.as_bytes())
|
||||||
.dispatch();
|
.dispatch();
|
||||||
|
@ -885,7 +887,7 @@ pub mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_verify_link_json(client: &Client, token: &str, address: &str) {
|
fn check_verify_link_json(client: &Client, token: &str, address: &str) {
|
||||||
let json = format!(r#"{{"token":"{}","address":"{}"}}"#, token, address);
|
let json = format!(r#"{{"token":"{}","addresses":["{}"]}}"#, token, address);
|
||||||
|
|
||||||
let mut response = client.post("/vks/v1/request-verify")
|
let mut response = client.post("/vks/v1/request-verify")
|
||||||
.header(ContentType::JSON)
|
.header(ContentType::JSON)
|
||||||
|
@ -897,7 +899,7 @@ pub mod tests {
|
||||||
|
|
||||||
|
|
||||||
fn check_mails_and_verify_email(client: &Client, filemail_path: &Path) {
|
fn check_mails_and_verify_email(client: &Client, filemail_path: &Path) {
|
||||||
let pattern = format!("{}(/publish/[^ \t\n]*)", BASE_URI);
|
let pattern = format!("{}(/verify/[^ \t\n]*)", BASE_URI);
|
||||||
let confirm_uri = pop_mail_capture_pattern(filemail_path, &pattern);
|
let confirm_uri = pop_mail_capture_pattern(filemail_path, &pattern);
|
||||||
|
|
||||||
let response = client.get(&confirm_uri).dispatch();
|
let response = client.get(&confirm_uri).dispatch();
|
||||||
|
@ -979,19 +981,19 @@ pub mod tests {
|
||||||
body.extend_from_slice(header);
|
body.extend_from_slice(header);
|
||||||
body.extend_from_slice(data);
|
body.extend_from_slice(data);
|
||||||
body.extend_from_slice(footer);
|
body.extend_from_slice(footer);
|
||||||
client.post("/vks/v1/publish")
|
client.post("/upload/submit")
|
||||||
.header(ct)
|
.header(ct)
|
||||||
.body(&body[..])
|
.body(&body[..])
|
||||||
.dispatch()
|
.dispatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vks_publish_json_get_token<'a>(client: &'a Client, data: &[u8]) -> String {
|
fn vks_publish_json_get_token<'a>(client: &'a Client, data: &[u8]) -> String {
|
||||||
let mut response = client.post("/vks/v1/publish")
|
let mut response = client.post("/vks/v1/upload")
|
||||||
.header(ContentType::JSON)
|
.header(ContentType::JSON)
|
||||||
.body(format!(r#"{{ "keytext": "{}" }}"#, base64::encode(data)))
|
.body(format!(r#"{{ "keytext": "{}" }}"#, base64::encode(data)))
|
||||||
.dispatch();
|
.dispatch();
|
||||||
let response_body = response.body_string().unwrap();
|
let response_body = response.body_string().unwrap();
|
||||||
let result: upload::json::PublishResult = serde_json::from_str(&response_body).unwrap();
|
let result: vks_api::json::UploadResult = serde_json::from_str(&response_body).unwrap();
|
||||||
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
result.token
|
result.token
|
||||||
|
|
|
@ -1,560 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
|
|
||||||
use database::{Database, KeyDatabase, StatefulTokens, EmailAddressStatus, TpkStatus};
|
|
||||||
use database::types::{Fingerprint,Email};
|
|
||||||
use mail;
|
|
||||||
use tokens::{self, StatelessSerializable};
|
|
||||||
use web::MyResponse;
|
|
||||||
use rate_limiter::RateLimiter;
|
|
||||||
|
|
||||||
use sequoia_openpgp::TPK;
|
|
||||||
|
|
||||||
use std::io::Read;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
const UPLOAD_LIMIT: u64 = 1024 * 1024; // 1 MiB.
|
|
||||||
|
|
||||||
mod template {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Verify {
|
|
||||||
pub verified: bool,
|
|
||||||
pub userid: String,
|
|
||||||
pub commit: String,
|
|
||||||
pub version: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Upload {
|
|
||||||
pub commit: String,
|
|
||||||
pub version: String,
|
|
||||||
pub show_help: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
pub uid_status: Vec<UploadUidStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
pub published: bool,
|
|
||||||
pub revoked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
mod forms {
|
|
||||||
#[derive(FromForm,Deserialize)]
|
|
||||||
pub struct VerifyRequest {
|
|
||||||
pub token: String,
|
|
||||||
pub address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct UploadRequest {
|
|
||||||
pub keytext: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug,Serialize,Deserialize,PartialEq,Eq)]
|
|
||||||
pub enum EmailStatus {
|
|
||||||
#[serde(rename = "unpublished")]
|
|
||||||
Unpublished,
|
|
||||||
#[serde(rename = "pending")]
|
|
||||||
Pending,
|
|
||||||
#[serde(rename = "published")]
|
|
||||||
Published,
|
|
||||||
#[serde(rename = "revoked")]
|
|
||||||
Revoked,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod json {
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use super::EmailStatus;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct VerifyRequest {
|
|
||||||
pub token: String,
|
|
||||||
pub addresses: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize,Deserialize)]
|
|
||||||
pub struct UploadResult {
|
|
||||||
pub token: String,
|
|
||||||
pub key_fpr: String,
|
|
||||||
pub status: HashMap<String,EmailStatus>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum OutputType {
|
|
||||||
HumanReadable,
|
|
||||||
Json,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MyResponse {
|
|
||||||
fn upload_ok(
|
|
||||||
token: String,
|
|
||||||
verify_state: VerifyTpkState,
|
|
||||||
uid_status: HashMap<String,EmailStatus>,
|
|
||||||
output_type: OutputType,
|
|
||||||
) -> Self {
|
|
||||||
let key_fpr = verify_state.fpr.to_string();
|
|
||||||
let key_link = format!("/pks/lookup?op=get&search={}", &verify_state.fpr);
|
|
||||||
|
|
||||||
match output_type {
|
|
||||||
OutputType::HumanReadable =>
|
|
||||||
Self::upload_ok_hr(token, key_fpr, key_link, uid_status),
|
|
||||||
OutputType::Json =>
|
|
||||||
Self::upload_ok_json(token, key_fpr, uid_status),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upload_ok_json(
|
|
||||||
token: String,
|
|
||||||
key_fpr: String,
|
|
||||||
uid_status: HashMap<String,EmailStatus>,
|
|
||||||
) -> MyResponse {
|
|
||||||
let result = json::UploadResult { token, key_fpr, status: uid_status };
|
|
||||||
MyResponse::Json(serde_json::to_string(&result).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upload_ok_hr(
|
|
||||||
token: String,
|
|
||||||
key_fpr: String,
|
|
||||||
key_link: String,
|
|
||||||
uid_status: HashMap<String,EmailStatus>,
|
|
||||||
) -> MyResponse {
|
|
||||||
let mut uid_status: Vec<_> = uid_status
|
|
||||||
.into_iter()
|
|
||||||
.map(|(email,status)|
|
|
||||||
template::UploadUidStatus {
|
|
||||||
address: email.to_string(),
|
|
||||||
requested: status == EmailStatus::Pending,
|
|
||||||
published: status == EmailStatus::Published,
|
|
||||||
revoked: status == EmailStatus::Revoked,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
uid_status.sort_by(|fst,snd| {
|
|
||||||
fst.revoked.cmp(&snd.revoked).then(fst.address.cmp(&snd.address))
|
|
||||||
});
|
|
||||||
|
|
||||||
let context = template::VerificationSent {
|
|
||||||
version: env!("VERGEN_SEMVER").to_string(),
|
|
||||||
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
||||||
is_revoked: false, key_fpr, key_link, token, uid_status,
|
|
||||||
};
|
|
||||||
MyResponse::ok("upload/upload-ok", context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize,Deserialize)]
|
|
||||||
struct VerifyTpkState {
|
|
||||||
fpr: Fingerprint,
|
|
||||||
addresses: Vec<Email>,
|
|
||||||
requested: Vec<Email>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatelessSerializable for VerifyTpkState {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/upload?<guide>")]
|
|
||||||
pub fn upload(guide: bool) -> MyResponse {
|
|
||||||
let context = template::Upload {
|
|
||||||
version: env!("VERGEN_SEMVER").to_string(),
|
|
||||||
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
||||||
show_help: guide,
|
|
||||||
};
|
|
||||||
|
|
||||||
MyResponse::ok("upload/upload", context)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/vks/v1/upload", format = "json", data = "<data>")]
|
|
||||||
pub fn vks_v1_upload_post_json(
|
|
||||||
db: rocket::State<KeyDatabase>,
|
|
||||||
tokens_stateless: rocket::State<tokens::Service>,
|
|
||||||
rate_limiter: rocket::State<RateLimiter>,
|
|
||||||
data: Json<forms::UploadRequest>,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
use std::io::Cursor;
|
|
||||||
let data_reader = Cursor::new(data.keytext.as_bytes());
|
|
||||||
process_key(&db, &tokens_stateless, &rate_limiter, data_reader, OutputType::Json)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/vks/v1/upload", format = "multipart/form-data", data = "<data>")]
|
|
||||||
pub fn vks_v1_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, OutputType::HumanReadable)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/vks/v1/upload", format = "application/x-www-form-urlencoded", data = "<data>")]
|
|
||||||
pub fn vks_v1_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 process_key(
|
|
||||||
&db,
|
|
||||||
&tokens_stateless,
|
|
||||||
&rate_limiter,
|
|
||||||
Cursor::new(decoded_value.as_bytes()),
|
|
||||||
OutputType::HumanReadable,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => { /* 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,
|
|
||||||
output_type: OutputType,
|
|
||||||
) -> 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, output_type)
|
|
||||||
}
|
|
||||||
Partial(partial, _) => {
|
|
||||||
process_multipart(db, tokens_stateless, rate_limiter, partial.entries, output_type)
|
|
||||||
}
|
|
||||||
Error(err) => Err(err.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_multipart(
|
|
||||||
db: &KeyDatabase,
|
|
||||||
tokens_stateless: &tokens::Service,
|
|
||||||
rate_limiter: &RateLimiter,
|
|
||||||
entries: Entries,
|
|
||||||
output_type: OutputType,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
match entries.fields.get("keytext") {
|
|
||||||
Some(ent) if ent.len() == 1 => {
|
|
||||||
let reader = ent[0].data.readable()?;
|
|
||||||
process_key(db, tokens_stateless, rate_limiter, reader, output_type)
|
|
||||||
}
|
|
||||||
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"))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_key(
|
|
||||||
db: &KeyDatabase,
|
|
||||||
tokens_stateless: &tokens::Service,
|
|
||||||
rate_limiter: &RateLimiter,
|
|
||||||
reader: impl Read,
|
|
||||||
output_type: OutputType,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
use sequoia_openpgp::parse::Parse;
|
|
||||||
use sequoia_openpgp::tpk::TPKParser;
|
|
||||||
|
|
||||||
// First, parse all TPKs and error out if one fails.
|
|
||||||
let parser = match TPKParser::from_reader(reader) {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => return Ok(MyResponse::bad_request("upload/upload", e)),
|
|
||||||
};
|
|
||||||
let mut tpks = Vec::new();
|
|
||||||
for tpk in parser {
|
|
||||||
tpks.push(match tpk {
|
|
||||||
Ok(t) => {
|
|
||||||
if t.is_tsk() {
|
|
||||||
return Ok(MyResponse::bad_request("upload/upload",
|
|
||||||
failure::err_msg("Whoops, please don't upload secret keys!")));
|
|
||||||
}
|
|
||||||
t
|
|
||||||
},
|
|
||||||
Err(e) => return Ok(MyResponse::bad_request("upload/upload", e)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
match tpks.len() {
|
|
||||||
0 => Ok(MyResponse::bad_request("upload/upload",
|
|
||||||
failure::err_msg("No key submitted"))),
|
|
||||||
1 => process_key_single(db, tokens_stateless, rate_limiter, tpks.into_iter().next().unwrap(), output_type),
|
|
||||||
_ => process_key_multiple(db, tpks),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_key_single(
|
|
||||||
db: &KeyDatabase,
|
|
||||||
tokens_stateless: &tokens::Service,
|
|
||||||
rate_limiter: &RateLimiter,
|
|
||||||
tpk: TPK,
|
|
||||||
output_type: OutputType,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap();
|
|
||||||
|
|
||||||
let tpk_status = db.merge(tpk)?;
|
|
||||||
|
|
||||||
let verify_state = {
|
|
||||||
let emails = tpk_status.email_status.iter()
|
|
||||||
.map(|(email,_)| email.clone())
|
|
||||||
.collect();
|
|
||||||
VerifyTpkState {
|
|
||||||
fpr: fp.clone(),
|
|
||||||
addresses: emails,
|
|
||||||
requested: vec!(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let token = tokens_stateless.create(&verify_state);
|
|
||||||
|
|
||||||
Ok(show_upload_verify(rate_limiter, token, tpk_status, verify_state, output_type))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_key_multiple(
|
|
||||||
db: &KeyDatabase,
|
|
||||||
tpks: Vec<TPK>,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
let merged_keys: Vec<_> = tpks
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|tpk| Fingerprint::try_from(tpk.fingerprint())
|
|
||||||
.map(|fpr| (fpr, tpk)))
|
|
||||||
.flat_map(|(fpr, tpk)| db.merge(tpk).map(|_| fpr))
|
|
||||||
.map(|fpr| template::UploadOkKey {
|
|
||||||
key_fpr: fpr.to_string(),
|
|
||||||
key_link: format!("/pks/lookup?op=get&search={}", fpr),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let context = template::UploadOkMultiple {
|
|
||||||
version: env!("VERGEN_SEMVER").to_string(),
|
|
||||||
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
||||||
keys: merged_keys,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(MyResponse::ok("upload/upload-ok-multiple", context))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/vks/v1/request-verify", format = "json", data="<request>")]
|
|
||||||
pub fn vks_upload_verify_json(
|
|
||||||
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: Json<json::VerifyRequest>,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
let json::VerifyRequest { token, addresses } = request.into_inner();
|
|
||||||
vks_upload_verify(db, token_stateful, token_stateless, mail_service,
|
|
||||||
rate_limiter, token, addresses, OutputType::Json)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/vks/v1/request-verify", format = "application/x-www-form-urlencoded", data="<request>")]
|
|
||||||
pub fn vks_upload_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>,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
let forms::VerifyRequest { token, address } = request.into_inner();
|
|
||||||
vks_upload_verify(db, token_stateful, token_stateless, mail_service,
|
|
||||||
rate_limiter, token, vec!(address),
|
|
||||||
OutputType::HumanReadable)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/vks/v1/request-verify", format = "multipart/form-data", data="<request>")]
|
|
||||||
pub fn vks_upload_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>,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
let forms::VerifyRequest { token, address } = request.into_inner();
|
|
||||||
vks_upload_verify(db, token_stateful, token_stateless, mail_service,
|
|
||||||
rate_limiter, token, vec!(address),
|
|
||||||
OutputType::HumanReadable)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vks_upload_verify(
|
|
||||||
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,
|
|
||||||
addresses: Vec<String>,
|
|
||||||
output_type: OutputType,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
let verify_state = token_stateless.check::<VerifyTpkState>(&token)?;
|
|
||||||
let tpk_status = db.get_tpk_status(&verify_state.fpr, &verify_state.addresses)?;
|
|
||||||
|
|
||||||
if tpk_status.is_revoked {
|
|
||||||
return Ok(show_upload_verify(
|
|
||||||
&rate_limiter, token, tpk_status, verify_state, output_type))
|
|
||||||
}
|
|
||||||
|
|
||||||
let emails_requested: Vec<_> = addresses.into_iter()
|
|
||||||
.map(|address| address.parse::<Email>())
|
|
||||||
.flatten()
|
|
||||||
.filter(|email| verify_state.addresses.contains(email))
|
|
||||||
.filter(|email| tpk_status.email_status.iter()
|
|
||||||
.any(|(uid_email, status)|
|
|
||||||
uid_email == email && *status == EmailAddressStatus::NotPublished
|
|
||||||
))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for email in emails_requested {
|
|
||||||
let rate_limit_ok = rate_limiter.action_perform(format!("verify-{}", &email));
|
|
||||||
if rate_limit_ok {
|
|
||||||
let token_content = (verify_state.fpr.clone(), email.clone());
|
|
||||||
let token_str = serde_json::to_string(&token_content)?;
|
|
||||||
let token_verify = token_stateful.new_token("verify", token_str.as_bytes())?;
|
|
||||||
|
|
||||||
mail_service.send_verification(
|
|
||||||
verify_state.fpr.to_string(),
|
|
||||||
&email,
|
|
||||||
&token_verify,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(show_upload_verify(&rate_limiter, token, tpk_status, verify_state, output_type))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_upload_verify(
|
|
||||||
rate_limiter: &RateLimiter,
|
|
||||||
token: String,
|
|
||||||
tpk_status: TpkStatus,
|
|
||||||
verify_state: VerifyTpkState,
|
|
||||||
output_type: OutputType,
|
|
||||||
) -> MyResponse {
|
|
||||||
if tpk_status.is_revoked {
|
|
||||||
return MyResponse::upload_ok(token, verify_state, HashMap::new(), output_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
let uid_status: HashMap<_,_> = tpk_status.email_status
|
|
||||||
.iter()
|
|
||||||
.map(|(email,status)| {
|
|
||||||
let is_pending = (*status == EmailAddressStatus::NotPublished) &&
|
|
||||||
!rate_limiter.action_check(format!("verify-{}", &email));
|
|
||||||
if is_pending {
|
|
||||||
(email.to_string(), EmailStatus::Pending)
|
|
||||||
} else {
|
|
||||||
(email.to_string(), match status {
|
|
||||||
EmailAddressStatus::NotPublished => EmailStatus::Unpublished,
|
|
||||||
EmailAddressStatus::Published => EmailStatus::Published,
|
|
||||||
EmailAddressStatus::Revoked => EmailStatus::Revoked,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
MyResponse::upload_ok(token, verify_state, uid_status, output_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/publish/<token>")]
|
|
||||||
pub fn publish_verify(
|
|
||||||
db: rocket::State<KeyDatabase>,
|
|
||||||
token_service: rocket::State<StatefulTokens>,
|
|
||||||
token: String,
|
|
||||||
) -> MyResponse {
|
|
||||||
match publish_verify_or_fail(db, token_service, token) {
|
|
||||||
Ok(response) => response,
|
|
||||||
Err(e) => MyResponse::ise(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn publish_verify_or_fail(
|
|
||||||
db: rocket::State<KeyDatabase>,
|
|
||||||
token_service: rocket::State<StatefulTokens>,
|
|
||||||
token: String,
|
|
||||||
) -> Result<MyResponse> {
|
|
||||||
let payload = token_service.pop_token("verify", &token)?;
|
|
||||||
let (fingerprint, email) = serde_json::from_str(&payload)?;
|
|
||||||
|
|
||||||
db.set_email_published(&fingerprint, &email)?;
|
|
||||||
|
|
||||||
let context = template::Verify {
|
|
||||||
verified: true,
|
|
||||||
userid: email.to_string(),
|
|
||||||
version: env!("VERGEN_SEMVER").to_string(),
|
|
||||||
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(MyResponse::ok("upload/publish-result", context))
|
|
||||||
}
|
|
|
@ -0,0 +1,282 @@
|
||||||
|
use failure::Fallible as Result;
|
||||||
|
|
||||||
|
use database::{Database, KeyDatabase, StatefulTokens, EmailAddressStatus, TpkStatus};
|
||||||
|
use database::types::{Fingerprint,Email};
|
||||||
|
use mail;
|
||||||
|
use tokens::{self, StatelessSerializable};
|
||||||
|
use rate_limiter::RateLimiter;
|
||||||
|
|
||||||
|
use sequoia_openpgp::TPK;
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use self::response::*;
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UploadRequest {
|
||||||
|
pub keytext: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct VerifyRequest {
|
||||||
|
pub token: String,
|
||||||
|
pub addresses: Vec<String>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
#[derive(Debug,Serialize,Deserialize,PartialEq,Eq)]
|
||||||
|
pub enum EmailStatus {
|
||||||
|
#[serde(rename = "unpublished")]
|
||||||
|
Unpublished,
|
||||||
|
#[serde(rename = "pending")]
|
||||||
|
Pending,
|
||||||
|
#[serde(rename = "published")]
|
||||||
|
Published,
|
||||||
|
#[serde(rename = "revoked")]
|
||||||
|
Revoked,
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub enum UploadResponse {
|
||||||
|
Ok {
|
||||||
|
token: String,
|
||||||
|
key_fpr: String,
|
||||||
|
status: HashMap<String,EmailStatus>,
|
||||||
|
},
|
||||||
|
OkMulti { key_fprs: Vec<String> },
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UploadResponse {
|
||||||
|
pub fn err(err: &str) -> Self {
|
||||||
|
UploadResponse::Error(err.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PublishResponse {
|
||||||
|
Ok { email: String },
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PublishResponse {
|
||||||
|
pub fn err(err: &str) -> Self {
|
||||||
|
PublishResponse::Error(err.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize,Deserialize)]
|
||||||
|
struct VerifyTpkState {
|
||||||
|
fpr: Fingerprint,
|
||||||
|
addresses: Vec<Email>,
|
||||||
|
requested: Vec<Email>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatelessSerializable for VerifyTpkState {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_key(
|
||||||
|
db: &KeyDatabase,
|
||||||
|
tokens_stateless: &tokens::Service,
|
||||||
|
rate_limiter: &RateLimiter,
|
||||||
|
reader: impl Read,
|
||||||
|
) -> response::UploadResponse {
|
||||||
|
use sequoia_openpgp::parse::Parse;
|
||||||
|
use sequoia_openpgp::tpk::TPKParser;
|
||||||
|
|
||||||
|
// First, parse all TPKs and error out if one fails.
|
||||||
|
let parser = match TPKParser::from_reader(reader) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return UploadResponse::err("Failed parsing key"),
|
||||||
|
};
|
||||||
|
let mut tpks = Vec::new();
|
||||||
|
for tpk in parser {
|
||||||
|
tpks.push(match tpk {
|
||||||
|
Ok(t) => {
|
||||||
|
if t.is_tsk() {
|
||||||
|
return UploadResponse::err("Whoops, please don't upload secret keys!");
|
||||||
|
}
|
||||||
|
t
|
||||||
|
},
|
||||||
|
Err(_) => return UploadResponse::err("No keys uploaded"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match tpks.len() {
|
||||||
|
0 => UploadResponse::err("No key submitted"),
|
||||||
|
1 => process_key_single(db, tokens_stateless, rate_limiter, tpks.into_iter().next().unwrap()),
|
||||||
|
_ => process_key_multiple(db, tpks),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_key_multiple(
|
||||||
|
db: &KeyDatabase,
|
||||||
|
tpks: Vec<TPK>,
|
||||||
|
) -> response::UploadResponse {
|
||||||
|
let key_fprs: Vec<_> = tpks
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|tpk| Fingerprint::try_from(tpk.fingerprint())
|
||||||
|
.map(|fpr| (fpr, tpk)))
|
||||||
|
.flat_map(|(fpr, tpk)| db.merge(tpk).map(|_| fpr.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
response::UploadResponse::OkMulti { key_fprs }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_key_single(
|
||||||
|
db: &KeyDatabase,
|
||||||
|
tokens_stateless: &tokens::Service,
|
||||||
|
rate_limiter: &RateLimiter,
|
||||||
|
tpk: TPK,
|
||||||
|
) -> response::UploadResponse {
|
||||||
|
let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap();
|
||||||
|
|
||||||
|
let tpk_status = match db.merge(tpk) {
|
||||||
|
Ok(tpk_status) => tpk_status,
|
||||||
|
Err(_) => return UploadResponse::err("internal error"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let verify_state = {
|
||||||
|
let emails = tpk_status.email_status.iter()
|
||||||
|
.map(|(email,_)| email.clone())
|
||||||
|
.collect();
|
||||||
|
VerifyTpkState {
|
||||||
|
fpr: fp.clone(),
|
||||||
|
addresses: emails,
|
||||||
|
requested: vec!(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = tokens_stateless.create(&verify_state);
|
||||||
|
|
||||||
|
show_upload_verify(rate_limiter, token, tpk_status, verify_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_verify(
|
||||||
|
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,
|
||||||
|
addresses: Vec<String>,
|
||||||
|
) -> response::UploadResponse {
|
||||||
|
let (verify_state, tpk_status) = match check_tpk_state(&db, &token_stateless, &token) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(e) => return UploadResponse::err(&e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if tpk_status.is_revoked {
|
||||||
|
return show_upload_verify(
|
||||||
|
&rate_limiter, token, tpk_status, verify_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
let emails_requested: Vec<_> = addresses.into_iter()
|
||||||
|
.map(|address| address.parse::<Email>())
|
||||||
|
.flatten()
|
||||||
|
.filter(|email| verify_state.addresses.contains(email))
|
||||||
|
.filter(|email| tpk_status.email_status.iter()
|
||||||
|
.any(|(uid_email, status)|
|
||||||
|
uid_email == email && *status == EmailAddressStatus::NotPublished
|
||||||
|
))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for email in emails_requested {
|
||||||
|
let rate_limit_ok = rate_limiter.action_perform(format!("verify-{}", &email));
|
||||||
|
if rate_limit_ok {
|
||||||
|
if send_verify_email(&mail_service, &token_stateful, &verify_state.fpr, &email).is_err() {
|
||||||
|
return UploadResponse::err(&format!("error sending email to {}", &email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show_upload_verify(&rate_limiter, token, tpk_status, verify_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_tpk_state(
|
||||||
|
db: &KeyDatabase,
|
||||||
|
token_stateless: &tokens::Service,
|
||||||
|
token: &str,
|
||||||
|
) -> Result<(VerifyTpkState,TpkStatus)> {
|
||||||
|
let verify_state = token_stateless.check::<VerifyTpkState>(token)?;
|
||||||
|
let tpk_status = db.get_tpk_status(&verify_state.fpr, &verify_state.addresses)?;
|
||||||
|
Ok((verify_state, tpk_status))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_verify_email(
|
||||||
|
mail_service: &mail::Service,
|
||||||
|
token_stateful: &StatefulTokens,
|
||||||
|
fpr: &Fingerprint,
|
||||||
|
email: &Email,
|
||||||
|
) -> Result<()> {
|
||||||
|
let token_content = (fpr.clone(), email.clone());
|
||||||
|
let token_str = serde_json::to_string(&token_content)?;
|
||||||
|
let token_verify = token_stateful.new_token("verify", token_str.as_bytes())?;
|
||||||
|
|
||||||
|
mail_service.send_verification(
|
||||||
|
fpr.to_string(),
|
||||||
|
&email,
|
||||||
|
&token_verify,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_confirm(
|
||||||
|
db: rocket::State<KeyDatabase>,
|
||||||
|
token_service: rocket::State<StatefulTokens>,
|
||||||
|
token: String,
|
||||||
|
) -> response::PublishResponse {
|
||||||
|
let email = match check_publish_token(&db, &token_service, token) {
|
||||||
|
Ok(email) => email,
|
||||||
|
Err(_) => return PublishResponse::err("token verification failed"),
|
||||||
|
};
|
||||||
|
|
||||||
|
response::PublishResponse::Ok { email: email.to_string() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_publish_token(
|
||||||
|
db: &KeyDatabase,
|
||||||
|
token_service: &StatefulTokens,
|
||||||
|
token: String,
|
||||||
|
) -> Result<Email> {
|
||||||
|
let payload = token_service.pop_token("verify", &token)?;
|
||||||
|
let (fingerprint, email) = serde_json::from_str(&payload)?;
|
||||||
|
db.set_email_published(&fingerprint, &email)?;
|
||||||
|
|
||||||
|
Ok(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_upload_verify(
|
||||||
|
rate_limiter: &RateLimiter,
|
||||||
|
token: String,
|
||||||
|
tpk_status: TpkStatus,
|
||||||
|
verify_state: VerifyTpkState,
|
||||||
|
) -> response::UploadResponse {
|
||||||
|
let key_fpr = verify_state.fpr.to_string();
|
||||||
|
if tpk_status.is_revoked {
|
||||||
|
return response::UploadResponse::Ok { token, key_fpr, status: HashMap::new() };
|
||||||
|
}
|
||||||
|
|
||||||
|
let status: HashMap<_,_> = tpk_status.email_status
|
||||||
|
.iter()
|
||||||
|
.map(|(email,status)| {
|
||||||
|
let is_pending = (*status == EmailAddressStatus::NotPublished) &&
|
||||||
|
!rate_limiter.action_check(format!("verify-{}", &email));
|
||||||
|
if is_pending {
|
||||||
|
(email.to_string(), EmailStatus::Pending)
|
||||||
|
} else {
|
||||||
|
(email.to_string(), match status {
|
||||||
|
EmailAddressStatus::NotPublished => EmailStatus::Unpublished,
|
||||||
|
EmailAddressStatus::Published => EmailStatus::Published,
|
||||||
|
EmailAddressStatus::Revoked => EmailStatus::Revoked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
response::UploadResponse::Ok { token, key_fpr, status }
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
use rocket_contrib::json::{Json,JsonValue,JsonError};
|
||||||
|
use rocket::request::Request;
|
||||||
|
use rocket::response::{self, Response, Responder};
|
||||||
|
use rocket::http::{ContentType,Status};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use database::{KeyDatabase, StatefulTokens};
|
||||||
|
use mail;
|
||||||
|
use tokens;
|
||||||
|
use rate_limiter::RateLimiter;
|
||||||
|
|
||||||
|
use web::vks;
|
||||||
|
use web::vks::response::*;
|
||||||
|
|
||||||
|
pub mod json {
|
||||||
|
use web::vks::response::EmailStatus;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct VerifyRequest {
|
||||||
|
pub token: String,
|
||||||
|
pub addresses: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UploadRequest {
|
||||||
|
pub keytext: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize,Deserialize)]
|
||||||
|
pub struct UploadResult {
|
||||||
|
pub token: String,
|
||||||
|
pub key_fpr: String,
|
||||||
|
pub status: HashMap<String,EmailStatus>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonResult = Result<JsonValue, JsonErrorResponse>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct JsonErrorResponse(Status,String);
|
||||||
|
|
||||||
|
impl<'r> Responder<'r> for JsonErrorResponse {
|
||||||
|
fn respond_to(self, _: &Request) -> response::Result<'r> {
|
||||||
|
let error_json = json!({"error": self.1});
|
||||||
|
Response::build()
|
||||||
|
.status(self.0)
|
||||||
|
.sized_body(Cursor::new(error_json.to_string()))
|
||||||
|
.header(ContentType::JSON)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_or_error<T>(data: Result<Json<T>, JsonError>) -> Result<Json<T>, JsonErrorResponse> {
|
||||||
|
match data {
|
||||||
|
Ok(data) => Ok(data),
|
||||||
|
Err(JsonError::Io(_)) => Err(JsonErrorResponse(Status::InternalServerError, "i/o error!".to_owned())),
|
||||||
|
Err(JsonError::Parse(_, e)) => Err(JsonErrorResponse(Status::BadRequest, e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_ok_json(response: UploadResponse) -> Result<JsonValue,JsonErrorResponse> {
|
||||||
|
match response {
|
||||||
|
UploadResponse::Ok { token, key_fpr, status } =>
|
||||||
|
Ok(json!(json::UploadResult { token, key_fpr, status })),
|
||||||
|
UploadResponse::OkMulti { key_fprs } => Ok(json!(key_fprs)),
|
||||||
|
UploadResponse::Error(error) => Err(JsonErrorResponse(Status::BadRequest, error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/vks/v1/upload", format = "json", data = "<data>")]
|
||||||
|
pub fn upload_json(
|
||||||
|
db: rocket::State<KeyDatabase>,
|
||||||
|
tokens_stateless: rocket::State<tokens::Service>,
|
||||||
|
rate_limiter: rocket::State<RateLimiter>,
|
||||||
|
data: Result<Json<json::UploadRequest>, JsonError>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data = json_or_error(data)?;
|
||||||
|
use std::io::Cursor;
|
||||||
|
let data_reader = Cursor::new(data.keytext.as_bytes());
|
||||||
|
let result = vks::process_key(&db, &tokens_stateless, &rate_limiter, data_reader);
|
||||||
|
upload_ok_json(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/vks/v1/upload", rank = 2)]
|
||||||
|
pub fn upload_fallback(
|
||||||
|
) -> JsonErrorResponse {
|
||||||
|
JsonErrorResponse(Status::BadRequest, "expected json data".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/vks/v1/request-verify", format = "json", data="<data>")]
|
||||||
|
pub fn request_verify_json(
|
||||||
|
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>,
|
||||||
|
data: Result<Json<json::VerifyRequest>, JsonError>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data = json_or_error(data)?;
|
||||||
|
let json::VerifyRequest { token, addresses } = data.into_inner();
|
||||||
|
let result = vks::request_verify(
|
||||||
|
db, token_stateful, token_stateless, mail_service,
|
||||||
|
rate_limiter, token, addresses);
|
||||||
|
upload_ok_json(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/vks/v1/request-verify", rank = 2)]
|
||||||
|
pub fn request_verify_fallback(
|
||||||
|
) -> JsonErrorResponse {
|
||||||
|
JsonErrorResponse(Status::BadRequest, "expected json data".to_owned())
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
use database::{KeyDatabase, StatefulTokens};
|
||||||
|
use mail;
|
||||||
|
use tokens;
|
||||||
|
use web::MyResponse;
|
||||||
|
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,
|
||||||
|
pub userid: String,
|
||||||
|
pub commit: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
pub uid_status: Vec<UploadUidStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
pub published: bool,
|
||||||
|
pub revoked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MyResponse {
|
||||||
|
fn upload_response(response: UploadResponse) -> Self {
|
||||||
|
match response {
|
||||||
|
UploadResponse::Ok { token, key_fpr, status } => Self::upload_ok(token, key_fpr, status),
|
||||||
|
UploadResponse::OkMulti { key_fprs } => Self::upload_ok_multi(key_fprs),
|
||||||
|
UploadResponse::Error(error) => MyResponse::bad_request(
|
||||||
|
"upload/upload", failure::err_msg(error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_ok(
|
||||||
|
token: String,
|
||||||
|
key_fpr: String,
|
||||||
|
uid_status: HashMap<String,EmailStatus>,
|
||||||
|
) -> Self {
|
||||||
|
let key_link = format!("/pks/lookup?op=get&search={}", &key_fpr);
|
||||||
|
|
||||||
|
let mut uid_status: Vec<_> = uid_status
|
||||||
|
.into_iter()
|
||||||
|
.map(|(email,status)|
|
||||||
|
template::UploadUidStatus {
|
||||||
|
address: email.to_string(),
|
||||||
|
requested: status == EmailStatus::Pending,
|
||||||
|
published: status == EmailStatus::Published,
|
||||||
|
revoked: status == EmailStatus::Revoked,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
uid_status.sort_by(|fst,snd| {
|
||||||
|
fst.revoked.cmp(&snd.revoked).then(fst.address.cmp(&snd.address))
|
||||||
|
});
|
||||||
|
|
||||||
|
let context = template::VerificationSent {
|
||||||
|
version: env!("VERGEN_SEMVER").to_string(),
|
||||||
|
commit: env!("VERGEN_SHA_SHORT").to_string(),
|
||||||
|
is_revoked: false, key_fpr, key_link, token, uid_status,
|
||||||
|
};
|
||||||
|
MyResponse::ok("upload/upload-ok", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_ok_multi(key_fprs: Vec<String>) -> Self {
|
||||||
|
let keys = key_fprs.into_iter()
|
||||||
|
.map(|fpr| template::UploadOkKey {
|
||||||
|
key_fpr: fpr.to_owned(),
|
||||||
|
key_link: format!("/pks/lookup?op=get&search={}", &fpr),
|
||||||
|
})
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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) {
|
||||||
|
PublishResponse::Ok { email } => {
|
||||||
|
let context = template::Verify {
|
||||||
|
verified: true,
|
||||||
|
userid: email,
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue