use crate::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_i18n::I18n; use gettext_macros::i18n; use crate::database::{KeyDatabase, StatefulTokens, Query, Database}; use crate::mail; use crate::tokens; use crate::web::{RequestOrigin, MyResponse}; use crate::rate_limiter::RateLimiter; use std::io::Read; use std::collections::HashMap; use crate::web::vks; use crate::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 VerifyForm { pub token: String, } #[derive(Serialize)] pub struct Verify { pub key_fpr: String, pub userid: String, pub userid_link: String, } #[derive(Serialize)] pub struct Search { pub query: String, pub fpr: String, } #[derive(Serialize)] pub struct VerificationSent { pub key_fpr: String, pub key_link: String, pub is_revoked: bool, pub token: String, pub email_published: Vec, pub email_unpublished: Vec, pub count_revoked_one: bool, pub count_revoked: usize, pub count_unparsed_one: bool, pub count_unparsed: usize, } #[derive(Serialize)] pub struct UploadOkKey { pub key_fpr: String, pub key_link: String, } #[derive(Serialize)] pub struct UploadOkMultiple { pub keys: Vec, } #[derive(Serialize)] pub struct UploadUidStatus { pub address: String, pub requested: bool, } } impl MyResponse { fn upload_response_quick(response: UploadResponse, base_uri: &str) -> Self { match response { UploadResponse::Ok { token, .. } => { let uri = uri!(quick_upload_proceed: token); let text = format!( "Key successfully uploaded. Proceed with verification here:\n{}{}\n", base_uri, uri ); MyResponse::plain(text) }, UploadResponse::OkMulti { key_fprs } => MyResponse::plain(format!("Uploaded {} keys. For verification, please upload keys individually.\n", key_fprs.len())), UploadResponse::Error(error) => MyResponse::bad_request( "400-plain", anyhow!(error)), } } fn upload_response(response: UploadResponse) -> Self { match response { UploadResponse::Ok { token, key_fpr, is_revoked, count_unparsed, status, .. } => Self::upload_ok(token, key_fpr, is_revoked, count_unparsed, status), UploadResponse::OkMulti { key_fprs } => Self::upload_ok_multi(key_fprs), UploadResponse::Error(error) => MyResponse::bad_request( "upload/upload", anyhow!(error)), } } fn upload_ok( token: String, key_fpr: String, is_revoked: bool, count_unparsed: usize, uid_status: HashMap, ) -> Self { let key_link = uri!(search: &key_fpr).to_string(); 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, }) .collect(); email_unpublished .sort_unstable_by(|fst,snd| fst.address.cmp(&snd.address)); let context = template::VerificationSent { is_revoked, key_fpr, key_link, token, email_published, email_unpublished, count_revoked_one: count_revoked == 1, count_revoked, count_unparsed_one: count_unparsed == 1, count_unparsed, }; MyResponse::ok("upload/upload-ok", context) } fn upload_ok_multi(key_fprs: Vec) -> Self { let keys = key_fprs.into_iter() .map(|fpr| { let key_link = uri!(search: &fpr).to_string(); template::UploadOkKey { key_fpr: fpr.to_owned(), key_link, } }) .collect(); let context = template::UploadOkMultiple { keys, }; MyResponse::ok("upload/upload-ok-multiple", context) } } #[get("/upload")] pub fn upload() -> MyResponse { MyResponse::ok_bare("upload/upload") } #[post("/upload/submit", format = "multipart/form-data", data = "")] pub fn upload_post_form_data( db: rocket::State, tokens_stateless: rocket::State, rate_limiter: rocket::State, i18n: I18n, cont_type: &ContentType, data: Data, ) -> MyResponse { match process_post_form_data(db, tokens_stateless, rate_limiter, i18n, cont_type, data) { Ok(response) => MyResponse::upload_response(response), Err(err) => MyResponse::bad_request("upload/upload", err), } } pub fn process_post_form_data( db: rocket::State, tokens_stateless: rocket::State, rate_limiter: rocket::State, i18n: I18n, cont_type: &ContentType, data: Data, ) -> Result { // multipart/form-data let (_, boundary) = cont_type .params() .find(|&(k, _)| k == "boundary") .ok_or_else(|| anyhow!("`Content-Type: multipart/form-data` \ boundary param not provided"))?; process_upload(&db, &tokens_stateless, &rate_limiter, &i18n, data, boundary) } #[get("/search?")] pub fn search( db: rocket::State, q: String, ) -> MyResponse { match q.parse::() { Ok(query) => key_to_response(db, q, query), Err(e) => MyResponse::bad_request("index", e), } } fn key_to_response( db: rocket::State, 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, fpr: fp.to_string(), }; MyResponse::ok("found", context) } #[put("/", data = "")] pub fn quick_upload( db: rocket::State, tokens_stateless: rocket::State, rate_limiter: rocket::State, i18n: I18n, request_origin: RequestOrigin, 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) { return MyResponse::bad_request("400-plain", anyhow!(error)); } MyResponse::upload_response_quick( vks::process_key( &db, &i18n, &tokens_stateless, &rate_limiter, Cursor::new(buf) ), request_origin.get_base_uri()) } #[get("/upload/", rank = 2)] pub fn quick_upload_proceed( db: rocket::State, request_origin: RequestOrigin, token_stateful: rocket::State, token_stateless: rocket::State, mail_service: rocket::State, rate_limiter: rocket::State, i18n: I18n, token: String, ) -> MyResponse { let result = vks::request_verify( db, request_origin, token_stateful, token_stateless, mail_service, rate_limiter, i18n, token, vec!()); MyResponse::upload_response(result) } #[post("/upload/submit", format = "application/x-www-form-urlencoded", data = "")] pub fn upload_post_form( db: rocket::State, tokens_stateless: rocket::State, rate_limiter: rocket::State, i18n: I18n, data: Data, ) -> MyResponse { match process_post_form(&db, &tokens_stateless, &rate_limiter, &i18n, data) { Ok(response) => MyResponse::upload_response(response), Err(err) => MyResponse::bad_request("upload/upload", err), } } pub fn process_post_form( db: &KeyDatabase, tokens_stateless: &tokens::Service, rate_limiter: &RateLimiter, i18n: &I18n, data: Data, ) -> Result { 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(anyhow!( "`Content-Type: application/x-www-form-urlencoded` \ not valid")) })?; match key.as_str() { "keytext" => { return Ok(vks::process_key( &db, &i18n, &tokens_stateless, &rate_limiter, Cursor::new(decoded_value.as_bytes()) )); } _ => { /* skip */ } } } Err(anyhow!("No keytext found")) } fn process_upload( db: &KeyDatabase, tokens_stateless: &tokens::Service, rate_limiter: &RateLimiter, i18n: &I18n, data: Data, boundary: &str, ) -> Result { // 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, i18n, entries) } Partial(partial, _) => { process_multipart(db, tokens_stateless, rate_limiter, i18n, partial.entries) } Error(err) => Err(err.into()) } } fn process_multipart( db: &KeyDatabase, tokens_stateless: &tokens::Service, rate_limiter: &RateLimiter, i18n: &I18n, entries: Entries, ) -> Result { match entries.fields.get("keytext") { Some(ent) if ent.len() == 1 => { let reader = ent[0].data.readable()?; Ok(vks::process_key(db, i18n, tokens_stateless, rate_limiter, reader)) } Some(_) => Err(anyhow!("Multiple keytexts found")), None => Err(anyhow!("No keytext found")), } } #[post("/upload/request-verify", format = "application/x-www-form-urlencoded", data="")] pub fn request_verify_form( db: rocket::State, request_origin: RequestOrigin, token_stateful: rocket::State, token_stateless: rocket::State, mail_service: rocket::State, rate_limiter: rocket::State, i18n: I18n, request: Form, ) -> MyResponse { let forms::VerifyRequest { token, address } = request.into_inner(); let result = vks::request_verify( db, request_origin, token_stateful, token_stateless, mail_service, rate_limiter, i18n, token, vec!(address)); MyResponse::upload_response(result) } #[post("/upload/request-verify", format = "multipart/form-data", data="")] pub fn request_verify_form_data( db: rocket::State, request_origin: RequestOrigin, token_stateful: rocket::State, token_stateless: rocket::State, mail_service: rocket::State, rate_limiter: rocket::State, i18n: I18n, request: Form, ) -> MyResponse { let forms::VerifyRequest { token, address } = request.into_inner(); let result = vks::request_verify( db, request_origin, token_stateful, token_stateless, mail_service, rate_limiter, i18n, token, vec!(address)); MyResponse::upload_response(result) } #[post("/verify/")] pub fn verify_confirm( db: rocket::State, token_service: rocket::State, rate_limiter: rocket::State, i18n: I18n, token: String, ) -> MyResponse { let rate_limit_id = format!("verify-token-{}", &token); match vks::verify_confirm(db, &i18n, token_service, token) { PublishResponse::Ok { fingerprint, email } => { rate_limiter.action_perform(rate_limit_id); let userid_link = uri!(search: &email).to_string(); let context = template::Verify { userid: email, key_fpr: fingerprint, userid_link, }; MyResponse::ok("upload/publish-result", context) }, PublishResponse::Error(error) => { let error_msg = if rate_limiter.action_check(rate_limit_id) { anyhow!(error) } else { anyhow!(i18n!(i18n.catalog, "This address has already been verified.")) }; MyResponse::bad_request("400", error_msg) } } } #[get("/verify/")] pub fn verify_confirm_form( token: String, ) -> MyResponse { MyResponse::ok("upload/verification-form", template::VerifyForm { token }) }