mirror of
https://gitlab.com/keys.openpgp.org/hagrid.git
synced 2025-10-06 00:23:08 +02:00
Extract route handlers from hagrid::web to newly added hagrid::routes module.
hagrid::web module was and is highly overloaded with functionality and contains all kind of code from business logic to http handling. With this commit I tried to offload complexity of handler to a dedicated module to make reasoning about available endpoints a bit easier. Changes: - Move the following endoints to hangrid::routes module and its newly added submodules: - hagrid::web::routes() -> hagrid::routes::routes() - hagrid::web::root() -> hagrid::routes::index::root() - hagrid::web::about() -> hagrid::routes::about::about() - hagrid::web::news() -> hagrid::routes::about::news() - hagrid::web::news_atom() -> hagrid::routes::atom::news_atom() - hagrid::web::privacy() -> hagrid::routes::about::privacy() - hagrid::web::apidoc() -> hagrid::routes::about::apidoc() - hagrid::web::faq() -> hagrid::routes::about::faq() - hagrid::web::usage() -> hagrid::routes::about::usage() - hagrid::web::files() -> hagrid::routes::assets::files() - hagrid::web::stats() -> hagrid::routes::about::stats() - hagrid::web::errors() -> hagrid::routes::errors::errors() - hagrid::web::vks_api::vks_v1_by_email() -> hagrid::routes::api::rest::vks::vks_v1_by_email() - hagrid::web::vks_api::vks_v1_by_fingerprint() -> hagrid::routes::api::rest::vks::vks_v1_by_fingerprint() - hagrid::web::vks_api::vks_v1_by_keyid() -> hagrid::routes::api::rest::vks::vks_v1_by_keyid() - hagrid::web::vks_api::upload_json() -> hagrid::routes::api::rest::vks::upload_json() - hagrid::web::vks_api::upload_fallback() -> hagrid::routes::api::rest::vks::upload_fallback() - hagrid::web::vks_api::request_verify_json() -> hagrid::routes::api::rest::vks::request_verify_json() - hagrid::web::vks_api::request_verify_fallback() -> hagrid::routes::api::rest::vks::request_verify_fallback() - hagrid::web::vks_web::search() -> hagrid::routes::vks::search() - hagrid::web::vks_web::upload() -> hagrid::routes::vks::upload() - hagrid::web::vks_web::upload_post_form() -> hagrid::routes::vks::upload_post_form() - hagrid::web::vks_web::upload_post_form_data() -> hagrid::routes::vks::upload_post_form_data() - hagrid::web::vks_web::request_verify_form() -> hagrid::routes::vks::request_verify_form() - hagrid::web::vks_web::request_verify_form_data() -> hagrid::routes::vks::request_verify_form_data() - hagrid::web::vks_web::verify_confirm() -> hagrid::routes::vks::verify_confirm() - hagrid::web::vks_web::verify_confirm_form() -> hagrid::routes::vks::verify_confirm_form() - hagrid::web::vks_web::quick_upload() -> hagrid::routes::vks::quick_upload() - hagrid::web::vks_web::quick_upload_proceed() -> hagrid::routes::vks::quick_upload_proceed() - hagrid::web::debug_web::debug_info() -> hagrid::routes::debug::debug_info() - hagrid::web::hkp::pks_lookup() -> hagrid::routes::pks::pks_lookup() - hagrid::web::hkp::pks_add_form() -> hagrid::routes::pks::pks_add_form() - hagrid::web::hkp::pks_add_form_data() -> hagrid::routes::pks::pks_add_form_data() - hagrid::web::hkp::pks_internal_index() -> hagrid::routes::pks::pks_internal_index() - hagrid::web::wkd::wkd_policy() -> hagrid::routes::wkd::wkd_policy() - hagrid::web::wkd::wkd_query() -> hagrid::routes::wkd::wkd_query() - hagrid::web::manage::vks_manage() -> hagrid::routes::manage::vks_manage() - hagrid::web::manage::vks_manage_key() -> hagrid::routes::manage::vks_manage_key() - hagrid::web::manage::vks_manage_post() -> hagrid::routes::manage::vks_manage_post() - hagrid::web::manage::vks_manage_unpublish() -> hagrid::routes::manage::vks_manage_unpublish() - hagrid::web::maintenance::maintenance_error_web() -> hagrid::routes::maintenance::maintenance_error_web() - hagrid::web::maintenance::maintenance_error_json() -> hagrid::routes::maintenance::maintenance_error_json() - hagrid::web::maintenance::maintenance_error_plain() -> hagrid::routes::maintenance::maintenance_error_plain()
This commit is contained in:
@@ -11,6 +11,7 @@ mod i18n;
|
||||
mod i18n_helpers;
|
||||
mod mail;
|
||||
mod rate_limiter;
|
||||
mod routes;
|
||||
mod sealed_state;
|
||||
mod template_helpers;
|
||||
mod tokens;
|
||||
|
38
src/routes/about.rs
Normal file
38
src/routes/about.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::web::{MyResponse, RequestOrigin};
|
||||
use rocket_codegen::get;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
#[get("/about")]
|
||||
pub fn about(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/about", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/news")]
|
||||
pub fn news(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/news", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/faq")]
|
||||
pub fn faq(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/faq", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/usage")]
|
||||
pub fn usage(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/usage", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/privacy")]
|
||||
pub fn privacy(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/privacy", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/api")]
|
||||
pub fn apidoc(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/api", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/stats")]
|
||||
pub fn stats(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/stats", i18n, origin)
|
||||
}
|
1
src/routes/api.rs
Normal file
1
src/routes/api.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod rest;
|
1
src/routes/api/rest/mod.rs
Normal file
1
src/routes/api/rest/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod vks;
|
110
src/routes/api/rest/vks.rs
Normal file
110
src/routes/api/rest/vks.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::web::vks_api::{JsonErrorResponse, JsonResult, json};
|
||||
use crate::web::{MyResponse, RequestOrigin, vks, vks_api};
|
||||
use crate::{mail, tokens, web};
|
||||
use hagrid_database::types::{Email, Fingerprint, KeyID};
|
||||
use hagrid_database::{Query, Sqlite, StatefulTokens};
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::{Error as JsonError, Json};
|
||||
use rocket_codegen::{get, post};
|
||||
use rocket_i18n::{I18n, Translations};
|
||||
|
||||
#[post("/vks/v1/upload", format = "json", data = "<data>")]
|
||||
pub fn upload_json(
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
data: Result<Json<json::UploadRequest>, JsonError>,
|
||||
) -> JsonResult {
|
||||
let data = vks_api::json_or_error(data)?;
|
||||
|
||||
use crate::web::{vks, vks_api};
|
||||
use std::io::Cursor;
|
||||
|
||||
let data_reader = Cursor::new(data.keytext.as_bytes());
|
||||
let result = vks::process_key(db, &i18n, tokens_stateless, rate_limiter, data_reader);
|
||||
|
||||
vks_api::upload_ok_json(result)
|
||||
}
|
||||
|
||||
#[post("/vks/v1/upload", rank = 2)]
|
||||
pub fn upload_fallback(origin: RequestOrigin) -> JsonErrorResponse {
|
||||
let error_msg = format!(
|
||||
"expected application/json data. see {}/about/api for api docs.",
|
||||
origin.get_base_uri()
|
||||
);
|
||||
JsonErrorResponse(Status::BadRequest, error_msg)
|
||||
}
|
||||
|
||||
#[post("/vks/v1/request-verify", format = "json", data = "<data>")]
|
||||
pub fn request_verify_json(
|
||||
db: &rocket::State<Sqlite>,
|
||||
langs: &rocket::State<Translations>,
|
||||
origin: RequestOrigin,
|
||||
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 = vks_api::json_or_error(data)?;
|
||||
let json::VerifyRequest {
|
||||
token,
|
||||
addresses,
|
||||
locale,
|
||||
} = data.into_inner();
|
||||
let i18n = vks_api::get_locale(langs, locale.unwrap_or_default());
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
addresses,
|
||||
);
|
||||
vks_api::upload_ok_json(result)
|
||||
}
|
||||
|
||||
#[get("/vks/v1/by-fingerprint/<fpr>")]
|
||||
pub fn vks_v1_by_fingerprint(db: &rocket::State<Sqlite>, i18n: I18n, fpr: String) -> MyResponse {
|
||||
let query = match fpr.parse::<Fingerprint>() {
|
||||
Ok(fpr) => Query::ByFingerprint(fpr),
|
||||
Err(_) => return MyResponse::bad_request_plain("malformed fingerprint"),
|
||||
};
|
||||
|
||||
web::key_to_response_plain(db, i18n, query)
|
||||
}
|
||||
|
||||
#[post("/vks/v1/request-verify", rank = 2)]
|
||||
pub fn request_verify_fallback(origin: RequestOrigin) -> JsonErrorResponse {
|
||||
let error_msg = format!(
|
||||
"expected application/json data. see {}/about/api for api docs.",
|
||||
origin.get_base_uri()
|
||||
);
|
||||
JsonErrorResponse(Status::BadRequest, error_msg)
|
||||
}
|
||||
|
||||
#[get("/vks/v1/by-email/<email>")]
|
||||
pub fn vks_v1_by_email(db: &rocket::State<Sqlite>, i18n: I18n, email: String) -> MyResponse {
|
||||
let email = email.replace("%40", "@");
|
||||
let query = match email.parse::<Email>() {
|
||||
Ok(email) => Query::ByEmail(email),
|
||||
Err(_) => return MyResponse::bad_request_plain("malformed e-mail address"),
|
||||
};
|
||||
|
||||
web::key_to_response_plain(db, i18n, query)
|
||||
}
|
||||
|
||||
#[get("/vks/v1/by-keyid/<kid>")]
|
||||
pub fn vks_v1_by_keyid(db: &rocket::State<Sqlite>, i18n: I18n, kid: String) -> MyResponse {
|
||||
let query = match kid.parse::<KeyID>() {
|
||||
Ok(keyid) => Query::ByKeyID(keyid),
|
||||
Err(_) => return MyResponse::bad_request_plain("malformed key id"),
|
||||
};
|
||||
|
||||
web::key_to_response_plain(db, i18n, query)
|
||||
}
|
9
src/routes/assets.rs
Normal file
9
src/routes/assets.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use crate::web::HagridState;
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket_codegen::get;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[get("/assets/<file..>")]
|
||||
pub async fn files(file: PathBuf, state: &rocket::State<HagridState>) -> Option<NamedFile> {
|
||||
NamedFile::open(state.assets_dir.join(file)).await.ok()
|
||||
}
|
8
src/routes/atom.rs
Normal file
8
src/routes/atom.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::web::{MyResponse, RequestOrigin};
|
||||
use rocket_codegen::get;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
#[get("/atom.xml")]
|
||||
pub fn news_atom(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::xml("atom", i18n, origin)
|
||||
}
|
29
src/routes/errors.rs
Normal file
29
src/routes/errors.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::web::RequestOrigin;
|
||||
use crate::web::templates::Bare;
|
||||
use crate::web::templates::HagridLayout;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::status::Custom;
|
||||
use rocket_codegen::get;
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
#[get("/errors/<code>/<template>")]
|
||||
pub fn errors(
|
||||
i18n: I18n,
|
||||
origin: RequestOrigin,
|
||||
code: u16,
|
||||
template: String,
|
||||
) -> Result<Custom<Template>, &'static str> {
|
||||
if !template
|
||||
.chars()
|
||||
.all(|x| x == '-' || char::is_ascii_alphabetic(&x))
|
||||
{
|
||||
return Err("bad request");
|
||||
}
|
||||
let status_code = Status::from_code(code).ok_or("bad request")?;
|
||||
let response_body = Template::render(
|
||||
format!("errors/{}-{}", code, template),
|
||||
HagridLayout::new(Bare { dummy: () }, i18n, origin),
|
||||
);
|
||||
Ok(Custom(status_code, response_body))
|
||||
}
|
8
src/routes/index.rs
Normal file
8
src/routes/index.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use crate::web::{MyResponse, RequestOrigin};
|
||||
use rocket_codegen::get;
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
#[get("/")]
|
||||
pub fn root(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("index", i18n, origin)
|
||||
}
|
29
src/routes/maintenance.rs
Normal file
29
src/routes/maintenance.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::web::MyResponse;
|
||||
use crate::web::maintenance::JsonErrorMessage;
|
||||
use crate::web::maintenance::templates::MaintenanceMode;
|
||||
use crate::web::util::get_commit_sha;
|
||||
use rocket_codegen::get;
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket_i18n::I18n;
|
||||
use serde_json::json;
|
||||
|
||||
#[get("/maintenance/plain/<message>")]
|
||||
pub fn maintenance_error_plain(message: String) -> MyResponse {
|
||||
MyResponse::MaintenancePlain(message)
|
||||
}
|
||||
|
||||
#[get("/maintenance/json/<message>")]
|
||||
pub fn maintenance_error_json(message: String) -> MyResponse {
|
||||
MyResponse::MaintenanceJson(json!(JsonErrorMessage { message }))
|
||||
}
|
||||
|
||||
#[get("/maintenance/web/<message>")]
|
||||
pub fn maintenance_error_web(message: String, i18n: I18n) -> MyResponse {
|
||||
let ctx = MaintenanceMode {
|
||||
message,
|
||||
version: env!("VERGEN_SEMVER").to_string(),
|
||||
commit: get_commit_sha(),
|
||||
lang: i18n.lang.to_owned(),
|
||||
};
|
||||
MyResponse::Maintenance(Template::render("maintenance", ctx))
|
||||
}
|
154
src/routes/manage.rs
Normal file
154
src/routes/manage.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::web::manage::{StatelessVerifyToken, forms, templates};
|
||||
use crate::web::{MyResponse, RequestOrigin, manage};
|
||||
use crate::{mail, tokens};
|
||||
use anyhow::anyhow;
|
||||
use gettext_macros::i18n;
|
||||
use hagrid_database::types::{Email, Fingerprint};
|
||||
use hagrid_database::{Database, Query, Sqlite};
|
||||
use rocket::form::Form;
|
||||
use rocket::uri;
|
||||
use rocket_codegen::{get, post};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
#[get("/manage")]
|
||||
pub fn vks_manage(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("manage/manage", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/manage/<token>")]
|
||||
pub fn vks_manage_key(
|
||||
origin: RequestOrigin,
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
token: String,
|
||||
token_service: &rocket::State<tokens::Service>,
|
||||
) -> MyResponse {
|
||||
if let Ok(StatelessVerifyToken { fpr }) = token_service.check(&token) {
|
||||
match db.lookup(&Query::ByFingerprint(fpr)) {
|
||||
Ok(Some(tpk)) => {
|
||||
let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap();
|
||||
let mut emails: Vec<Email> = tpk
|
||||
.userids()
|
||||
.flat_map(|u| u.userid().to_string().parse::<Email>())
|
||||
.collect();
|
||||
emails.sort_unstable();
|
||||
emails.dedup();
|
||||
let uid_status = emails
|
||||
.into_iter()
|
||||
.map(|email| templates::ManageKeyUidStatus {
|
||||
address: email.to_string(),
|
||||
published: true,
|
||||
})
|
||||
.collect();
|
||||
let key_link = uri!(crate::routes::vks::search(q = fp.to_string())).to_string();
|
||||
let context = templates::ManageKey {
|
||||
key_fpr: fp.to_string(),
|
||||
key_link,
|
||||
uid_status,
|
||||
token,
|
||||
base_uri: origin.get_base_uri().to_owned(),
|
||||
};
|
||||
MyResponse::ok("manage/manage_key", context, i18n, origin)
|
||||
}
|
||||
Ok(None) => MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "This link is invalid or expired")),
|
||||
i18n,
|
||||
origin,
|
||||
),
|
||||
Err(e) => MyResponse::ise(e),
|
||||
}
|
||||
} else {
|
||||
MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "This link is invalid or expired")),
|
||||
i18n,
|
||||
origin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/manage", data = "<request>")]
|
||||
pub fn vks_manage_post(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
request: Form<forms::ManageRequest>,
|
||||
token_service: &rocket::State<tokens::Service>,
|
||||
) -> MyResponse {
|
||||
let email = match request.search_term.parse::<Email>() {
|
||||
Ok(email) => email,
|
||||
Err(_) => {
|
||||
return MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "Malformed address: {}"; request.search_term.as_str())),
|
||||
i18n,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let tpk = match db.lookup(&Query::ByEmail(email.clone())) {
|
||||
Ok(Some(tpk)) => tpk,
|
||||
Ok(None) => {
|
||||
return MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "No key for address: {}"; request.search_term.as_str())),
|
||||
i18n,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
Err(e) => return MyResponse::ise(e),
|
||||
};
|
||||
|
||||
let email_exists = tpk
|
||||
.userids()
|
||||
.flat_map(|binding| binding.userid().to_string().parse::<Email>())
|
||||
.any(|candidate| candidate == email);
|
||||
|
||||
if !email_exists {
|
||||
return MyResponse::ise(anyhow!("Internal error: address check failed!"));
|
||||
}
|
||||
|
||||
if !rate_limiter.action_perform(format!("manage-{}", &email)) {
|
||||
return MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(
|
||||
i18n.catalog,
|
||||
"A request has already been sent for this address recently."
|
||||
)),
|
||||
i18n,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
let fpr: Fingerprint = tpk.fingerprint().try_into().unwrap();
|
||||
let fpr_text = fpr.to_string();
|
||||
let token = token_service.create(&StatelessVerifyToken { fpr });
|
||||
let link_path = uri!(vks_manage_key(token)).to_string();
|
||||
|
||||
let base_uri = origin.get_base_uri();
|
||||
if let Err(e) = mail_service.send_manage_token(&i18n, base_uri, fpr_text, &email, &link_path) {
|
||||
return MyResponse::ise(e);
|
||||
}
|
||||
|
||||
let ctx = templates::ManageLinkSent {
|
||||
address: email.to_string(),
|
||||
};
|
||||
MyResponse::ok("manage/manage_link_sent", ctx, i18n, origin)
|
||||
}
|
||||
|
||||
#[post("/manage/unpublish", data = "<request>")]
|
||||
pub fn vks_manage_unpublish(
|
||||
origin: RequestOrigin,
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
token_service: &rocket::State<tokens::Service>,
|
||||
request: Form<forms::ManageDelete>,
|
||||
) -> MyResponse {
|
||||
manage::vks_manage_unpublish_or_fail(origin, db, token_service, i18n, request)
|
||||
.unwrap_or_else(MyResponse::ise)
|
||||
}
|
69
src/routes/mod.rs
Normal file
69
src/routes/mod.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
mod about;
|
||||
mod api;
|
||||
mod assets;
|
||||
mod atom;
|
||||
mod debug;
|
||||
mod errors;
|
||||
mod index;
|
||||
pub mod maintenance;
|
||||
pub mod manage;
|
||||
mod pks;
|
||||
pub mod vks;
|
||||
mod wkd;
|
||||
|
||||
use rocket::{Route, routes};
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
// infra
|
||||
index::root,
|
||||
about::about,
|
||||
about::news,
|
||||
atom::news_atom,
|
||||
about::privacy,
|
||||
about::apidoc,
|
||||
about::faq,
|
||||
about::usage,
|
||||
assets::files,
|
||||
about::stats,
|
||||
errors::errors,
|
||||
// VKSv1
|
||||
api::rest::vks::vks_v1_by_email,
|
||||
api::rest::vks::vks_v1_by_fingerprint,
|
||||
api::rest::vks::vks_v1_by_keyid,
|
||||
api::rest::vks::upload_json,
|
||||
api::rest::vks::upload_fallback,
|
||||
api::rest::vks::request_verify_json,
|
||||
api::rest::vks::request_verify_fallback,
|
||||
// User interaction.
|
||||
vks::search,
|
||||
vks::upload,
|
||||
vks::upload_post_form,
|
||||
vks::upload_post_form_data,
|
||||
vks::request_verify_form,
|
||||
vks::request_verify_form_data,
|
||||
vks::verify_confirm,
|
||||
vks::verify_confirm_form,
|
||||
vks::quick_upload,
|
||||
vks::quick_upload_proceed,
|
||||
// Debug
|
||||
debug::debug_info,
|
||||
// HKP
|
||||
pks::pks_lookup,
|
||||
pks::pks_add_form,
|
||||
pks::pks_add_form_data,
|
||||
pks::pks_internal_index,
|
||||
// WKD
|
||||
wkd::wkd_policy,
|
||||
wkd::wkd_query,
|
||||
// Manage
|
||||
manage::vks_manage,
|
||||
manage::vks_manage_key,
|
||||
manage::vks_manage_post,
|
||||
manage::vks_manage_unpublish,
|
||||
// Maintenance error page
|
||||
maintenance::maintenance_error_web,
|
||||
maintenance::maintenance_error_json,
|
||||
maintenance::maintenance_error_plain,
|
||||
]
|
||||
}
|
124
src/routes/pks.rs
Normal file
124
src/routes/pks.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::web::hkp::Hkp;
|
||||
use crate::web::vks::response::UploadResponse;
|
||||
use crate::web::{MyResponse, RequestOrigin, hkp, vks_web};
|
||||
use crate::{mail, tokens, web};
|
||||
use hagrid_database::{Query, Sqlite};
|
||||
use rocket::Data;
|
||||
use rocket::http::ContentType;
|
||||
use rocket_codegen::{get, post};
|
||||
use rocket_i18n::I18n;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[post("/pks/add", format = "multipart/form-data", data = "<data>")]
|
||||
pub async fn pks_add_form_data(
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
cont_type: &ContentType,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match vks_web::process_post_form_data(db, tokens_stateless, rate_limiter, i18n, cont_type, data)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MyResponse::plain("Ok".into()),
|
||||
Err(err) => MyResponse::ise(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/pks/add",
|
||||
format = "application/x-www-form-urlencoded",
|
||||
data = "<data>"
|
||||
)]
|
||||
pub async fn pks_add_form(
|
||||
origin: RequestOrigin,
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
i18n: I18n,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match vks_web::process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await {
|
||||
Ok(UploadResponse::Ok {
|
||||
is_new_key,
|
||||
key_fpr,
|
||||
primary_uid,
|
||||
token,
|
||||
status,
|
||||
..
|
||||
}) => {
|
||||
let msg = hkp::pks_add_ok(
|
||||
&origin,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
token,
|
||||
status,
|
||||
is_new_key,
|
||||
key_fpr,
|
||||
primary_uid,
|
||||
);
|
||||
MyResponse::plain(msg)
|
||||
}
|
||||
Ok(_) => {
|
||||
let msg = format!(
|
||||
"Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload",
|
||||
baseuri = origin.get_base_uri()
|
||||
);
|
||||
MyResponse::plain(msg)
|
||||
}
|
||||
Err(err) => MyResponse::ise(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/pks/lookup?<op>&<search>")]
|
||||
pub fn pks_lookup(
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
op: Option<String>,
|
||||
search: Option<String>,
|
||||
) -> MyResponse {
|
||||
let search = search.unwrap_or_default();
|
||||
let key = match Hkp::from_str(&search) {
|
||||
Ok(key) => key,
|
||||
Err(_) => return MyResponse::bad_request_plain("Invalid search query!"),
|
||||
};
|
||||
let query = match key {
|
||||
Hkp::Fingerprint { fpr } => Query::ByFingerprint(fpr),
|
||||
Hkp::KeyID { keyid } => Query::ByKeyID(keyid),
|
||||
Hkp::Email { email } => Query::ByEmail(email),
|
||||
Hkp::ShortKeyID { query: _, .. } => {
|
||||
return MyResponse::bad_request_plain(
|
||||
"Search by short key ids is not supported, sorry!",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(op) = op {
|
||||
match op.as_str() {
|
||||
"index" => hkp::key_to_hkp_index(db, i18n, query),
|
||||
"get" => web::key_to_response_plain(db, i18n, query),
|
||||
"vindex" => MyResponse::not_implemented_plain("vindex not implemented"),
|
||||
s if s.starts_with("x-") => {
|
||||
MyResponse::not_implemented_plain("x-* operations not implemented")
|
||||
}
|
||||
&_ => MyResponse::bad_request_plain("Invalid op parameter!"),
|
||||
}
|
||||
} else {
|
||||
MyResponse::bad_request_plain("op parameter required!")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/pks/internal/index/<query_string>")]
|
||||
pub fn pks_internal_index(
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
query_string: String,
|
||||
) -> MyResponse {
|
||||
match query_string.parse() {
|
||||
Ok(query) => hkp::key_to_hkp_index(db, i18n, query),
|
||||
Err(_) => MyResponse::bad_request_plain("Invalid search query!"),
|
||||
}
|
||||
}
|
227
src/routes/vks.rs
Normal file
227
src/routes/vks.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
// GOTCHA:
|
||||
// This module is kinda special as it contains a number of various handlers
|
||||
// which related to WebUI of Verifying Keys Service (hence VKS),
|
||||
// but their URLs DO NOT start with /vks.
|
||||
// Otherwise, these handlers would be scattered all over the other modules.
|
||||
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::web::vks::response::PublishResponse;
|
||||
use crate::web::vks_web::{UPLOAD_LIMIT, forms, template};
|
||||
use crate::web::{MyResponse, RequestOrigin, vks, vks_web};
|
||||
use crate::{mail, tokens};
|
||||
use anyhow::anyhow;
|
||||
use gettext_macros::i18n;
|
||||
use hagrid_database::{Query, Sqlite, StatefulTokens};
|
||||
use rocket::Data;
|
||||
use rocket::form::Form;
|
||||
use rocket::http::ContentType;
|
||||
use rocket::uri;
|
||||
use rocket_codegen::{get, post, put};
|
||||
use rocket_i18n::I18n;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[get("/upload")]
|
||||
pub fn upload(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("upload/upload", i18n, origin)
|
||||
}
|
||||
|
||||
#[post("/upload/submit", format = "multipart/form-data", data = "<data>")]
|
||||
pub async fn upload_post_form_data(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
cont_type: &ContentType,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match vks_web::process_upload(db, tokens_stateless, rate_limiter, &i18n, data, cont_type).await
|
||||
{
|
||||
Ok(response) => MyResponse::upload_response(response, i18n, origin),
|
||||
Err(err) => MyResponse::bad_request("upload/upload", err, i18n, origin),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/search?<q>")]
|
||||
pub fn search(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
i18n: I18n,
|
||||
q: String,
|
||||
) -> MyResponse {
|
||||
match q.parse::<Query>() {
|
||||
Ok(query) => vks_web::key_to_response(db, origin, i18n, q, query),
|
||||
Err(e) => MyResponse::bad_request("index", e, i18n, origin),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/", data = "<data>")]
|
||||
pub async fn quick_upload(
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
origin: RequestOrigin,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
let buf = match data.open(UPLOAD_LIMIT).into_bytes().await {
|
||||
Ok(buf) => buf.into_inner(),
|
||||
Err(error) => return MyResponse::bad_request("400-plain", anyhow!(error), i18n, origin),
|
||||
};
|
||||
|
||||
MyResponse::upload_response_quick(
|
||||
vks::process_key(db, &i18n, tokens_stateless, rate_limiter, Cursor::new(buf)),
|
||||
i18n,
|
||||
origin,
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/upload/<token>", rank = 2)]
|
||||
pub fn quick_upload_proceed(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_stateful: &rocket::State<StatefulTokens>,
|
||||
token_stateless: &rocket::State<tokens::Service>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
token: String,
|
||||
) -> MyResponse {
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
vec![],
|
||||
);
|
||||
MyResponse::upload_response(result, i18n, origin)
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/upload/submit",
|
||||
format = "application/x-www-form-urlencoded",
|
||||
data = "<data>"
|
||||
)]
|
||||
pub async fn upload_post_form(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match vks_web::process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await {
|
||||
Ok(response) => MyResponse::upload_response(response, i18n, origin),
|
||||
Err(err) => MyResponse::bad_request("upload/upload", err, i18n, origin),
|
||||
}
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/upload/request-verify",
|
||||
format = "application/x-www-form-urlencoded",
|
||||
data = "<request>"
|
||||
)]
|
||||
pub fn request_verify_form(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_stateful: &rocket::State<StatefulTokens>,
|
||||
token_stateless: &rocket::State<tokens::Service>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
request: Form<forms::VerifyRequest>,
|
||||
) -> MyResponse {
|
||||
let forms::VerifyRequest { token, address } = request.into_inner();
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
vec![address],
|
||||
);
|
||||
MyResponse::upload_response(result, i18n, origin)
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/upload/request-verify",
|
||||
format = "multipart/form-data",
|
||||
data = "<request>"
|
||||
)]
|
||||
pub fn request_verify_form_data(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_stateful: &rocket::State<StatefulTokens>,
|
||||
token_stateless: &rocket::State<tokens::Service>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
request: Form<forms::VerifyRequest>,
|
||||
) -> MyResponse {
|
||||
let forms::VerifyRequest { token, address } = request.into_inner();
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
vec![address],
|
||||
);
|
||||
MyResponse::upload_response(result, i18n, origin)
|
||||
}
|
||||
|
||||
#[post("/verify/<token>")]
|
||||
pub fn verify_confirm(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_service: &rocket::State<StatefulTokens>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
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(q = &email)).to_string();
|
||||
let context = template::Verify {
|
||||
userid: email,
|
||||
key_fpr: fingerprint,
|
||||
userid_link,
|
||||
};
|
||||
|
||||
MyResponse::ok("upload/publish-result", context, i18n, origin)
|
||||
}
|
||||
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, i18n, origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/verify/<token>")]
|
||||
pub fn verify_confirm_form(origin: RequestOrigin, i18n: I18n, token: String) -> MyResponse {
|
||||
MyResponse::ok(
|
||||
"upload/verification-form",
|
||||
template::VerifyForm { token },
|
||||
i18n,
|
||||
origin,
|
||||
)
|
||||
}
|
127
src/web/hkp.rs
127
src/web/hkp.rs
@@ -1,12 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use rocket::Data;
|
||||
use rocket::http::ContentType;
|
||||
use rocket::{get, post};
|
||||
use rocket_i18n::I18n;
|
||||
use url::percent_encoding::{DEFAULT_ENCODE_SET, utf8_percent_encode};
|
||||
|
||||
@@ -18,13 +14,9 @@ use hagrid_database::{
|
||||
use crate::i18n_helpers::describe_query_error;
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
|
||||
use crate::tokens;
|
||||
|
||||
use crate::mail;
|
||||
use crate::web;
|
||||
use crate::web::vks::response::EmailStatus;
|
||||
use crate::web::vks::response::UploadResponse;
|
||||
use crate::web::{MyResponse, RequestOrigin, vks_web};
|
||||
use crate::web::{MyResponse, RequestOrigin};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Hkp {
|
||||
@@ -71,70 +63,7 @@ impl std::str::FromStr for Hkp {
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/pks/add", format = "multipart/form-data", data = "<data>")]
|
||||
pub async fn pks_add_form_data(
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
cont_type: &ContentType,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match vks_web::process_post_form_data(db, tokens_stateless, rate_limiter, i18n, cont_type, data)
|
||||
.await
|
||||
{
|
||||
Ok(_) => MyResponse::plain("Ok".into()),
|
||||
Err(err) => MyResponse::ise(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/pks/add",
|
||||
format = "application/x-www-form-urlencoded",
|
||||
data = "<data>"
|
||||
)]
|
||||
pub async fn pks_add_form(
|
||||
origin: RequestOrigin,
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
i18n: I18n,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match vks_web::process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await {
|
||||
Ok(UploadResponse::Ok {
|
||||
is_new_key,
|
||||
key_fpr,
|
||||
primary_uid,
|
||||
token,
|
||||
status,
|
||||
..
|
||||
}) => {
|
||||
let msg = pks_add_ok(
|
||||
&origin,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
token,
|
||||
status,
|
||||
is_new_key,
|
||||
key_fpr,
|
||||
primary_uid,
|
||||
);
|
||||
MyResponse::plain(msg)
|
||||
}
|
||||
Ok(_) => {
|
||||
let msg = format!(
|
||||
"Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload",
|
||||
baseuri = origin.get_base_uri()
|
||||
);
|
||||
MyResponse::plain(msg)
|
||||
}
|
||||
Err(err) => MyResponse::ise(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn pks_add_ok(
|
||||
pub fn pks_add_ok(
|
||||
origin: &RequestOrigin,
|
||||
mail_service: &mail::Service,
|
||||
rate_limiter: &RateLimiter,
|
||||
@@ -187,57 +116,7 @@ fn send_welcome_mail(
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[get("/pks/lookup?<op>&<search>")]
|
||||
pub fn pks_lookup(
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
op: Option<String>,
|
||||
search: Option<String>,
|
||||
) -> MyResponse {
|
||||
let search = search.unwrap_or_default();
|
||||
let key = match Hkp::from_str(&search) {
|
||||
Ok(key) => key,
|
||||
Err(_) => return MyResponse::bad_request_plain("Invalid search query!"),
|
||||
};
|
||||
let query = match key {
|
||||
Hkp::Fingerprint { fpr } => Query::ByFingerprint(fpr),
|
||||
Hkp::KeyID { keyid } => Query::ByKeyID(keyid),
|
||||
Hkp::Email { email } => Query::ByEmail(email),
|
||||
Hkp::ShortKeyID { query: _, .. } => {
|
||||
return MyResponse::bad_request_plain(
|
||||
"Search by short key ids is not supported, sorry!",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(op) = op {
|
||||
match op.as_str() {
|
||||
"index" => key_to_hkp_index(db, i18n, query),
|
||||
"get" => web::key_to_response_plain(db, i18n, query),
|
||||
"vindex" => MyResponse::not_implemented_plain("vindex not implemented"),
|
||||
s if s.starts_with("x-") => {
|
||||
MyResponse::not_implemented_plain("x-* operations not implemented")
|
||||
}
|
||||
&_ => MyResponse::bad_request_plain("Invalid op parameter!"),
|
||||
}
|
||||
} else {
|
||||
MyResponse::bad_request_plain("op parameter required!")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/pks/internal/index/<query_string>")]
|
||||
pub fn pks_internal_index(
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
query_string: String,
|
||||
) -> MyResponse {
|
||||
match query_string.parse() {
|
||||
Ok(query) => key_to_hkp_index(db, i18n, query),
|
||||
Err(_) => MyResponse::bad_request_plain("Invalid search query!"),
|
||||
}
|
||||
}
|
||||
|
||||
fn key_to_hkp_index(db: &rocket::State<Sqlite>, i18n: I18n, query: Query) -> MyResponse {
|
||||
pub fn key_to_hkp_index(db: &rocket::State<Sqlite>, i18n: I18n, query: Query) -> MyResponse {
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use sequoia_openpgp::types::RevocationStatus;
|
||||
|
||||
|
@@ -1,23 +1,16 @@
|
||||
use rocket::fairing::{Fairing, Info, Kind};
|
||||
use rocket::http::Method;
|
||||
use rocket::{Data, Request};
|
||||
use rocket::{async_trait, get, uri};
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket_i18n::I18n;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::web::MyResponse;
|
||||
use rocket::{async_trait, uri};
|
||||
use serde_derive::Serialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::util::get_commit_sha;
|
||||
|
||||
pub struct MaintenanceMode {
|
||||
maintenance_file: PathBuf,
|
||||
}
|
||||
|
||||
mod templates {
|
||||
pub mod templates {
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -46,13 +39,19 @@ impl Fairing for MaintenanceMode {
|
||||
|
||||
let path = request.uri().path().as_str();
|
||||
if self.is_request_json(path) {
|
||||
request.set_uri(uri!(maintenance_error_json(message)));
|
||||
request.set_uri(uri!(crate::routes::maintenance::maintenance_error_json(
|
||||
message
|
||||
)));
|
||||
request.set_method(Method::Get);
|
||||
} else if self.is_request_plain(path, request.method()) {
|
||||
request.set_uri(uri!(maintenance_error_plain(message)));
|
||||
request.set_uri(uri!(crate::routes::maintenance::maintenance_error_plain(
|
||||
message
|
||||
)));
|
||||
request.set_method(Method::Get);
|
||||
} else if self.is_request_web(path) {
|
||||
request.set_uri(uri!(maintenance_error_web(message)));
|
||||
request.set_uri(uri!(crate::routes::maintenance::maintenance_error_web(
|
||||
message
|
||||
)));
|
||||
request.set_method(Method::Get);
|
||||
}
|
||||
}
|
||||
@@ -83,28 +82,7 @@ impl MaintenanceMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/maintenance/plain/<message>")]
|
||||
pub fn maintenance_error_plain(message: String) -> MyResponse {
|
||||
MyResponse::MaintenancePlain(message)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonErrorMessage {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/maintenance/json/<message>")]
|
||||
pub fn maintenance_error_json(message: String) -> MyResponse {
|
||||
MyResponse::MaintenanceJson(json!(JsonErrorMessage { message }))
|
||||
}
|
||||
|
||||
#[get("/maintenance/web/<message>")]
|
||||
pub fn maintenance_error_web(message: String, i18n: I18n) -> MyResponse {
|
||||
let ctx = templates::MaintenanceMode {
|
||||
message,
|
||||
version: env!("VERGEN_SEMVER").to_string(),
|
||||
commit: get_commit_sha(),
|
||||
lang: i18n.lang.to_owned(),
|
||||
};
|
||||
MyResponse::Maintenance(Template::render("maintenance", ctx))
|
||||
pub struct JsonErrorMessage {
|
||||
pub message: String,
|
||||
}
|
||||
|
@@ -1,28 +1,19 @@
|
||||
use anyhow::anyhow;
|
||||
use rocket::form::Form;
|
||||
use rocket::{get, post, uri};
|
||||
use rocket_i18n::I18n;
|
||||
|
||||
use crate::counters;
|
||||
use crate::mail;
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::tokens::{self, StatelessSerializable};
|
||||
use crate::web::vks_web;
|
||||
use crate::web::{MyResponse, RequestOrigin};
|
||||
use gettext_macros::i18n;
|
||||
use hagrid_database::{Database, Query, Sqlite, types::Email, types::Fingerprint};
|
||||
use hagrid_database::{Database, Sqlite, types::Email, types::Fingerprint};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StatelessVerifyToken {
|
||||
fpr: Fingerprint,
|
||||
pub struct StatelessVerifyToken {
|
||||
pub fpr: Fingerprint,
|
||||
}
|
||||
impl StatelessSerializable for StatelessVerifyToken {}
|
||||
|
||||
mod templates {
|
||||
pub mod templates {
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -61,148 +52,6 @@ pub mod forms {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/manage")]
|
||||
pub fn vks_manage(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("manage/manage", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/manage/<token>")]
|
||||
pub fn vks_manage_key(
|
||||
origin: RequestOrigin,
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
token: String,
|
||||
token_service: &rocket::State<tokens::Service>,
|
||||
) -> MyResponse {
|
||||
if let Ok(StatelessVerifyToken { fpr }) = token_service.check(&token) {
|
||||
match db.lookup(&Query::ByFingerprint(fpr)) {
|
||||
Ok(Some(tpk)) => {
|
||||
let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap();
|
||||
let mut emails: Vec<Email> = tpk
|
||||
.userids()
|
||||
.flat_map(|u| u.userid().to_string().parse::<Email>())
|
||||
.collect();
|
||||
emails.sort_unstable();
|
||||
emails.dedup();
|
||||
let uid_status = emails
|
||||
.into_iter()
|
||||
.map(|email| templates::ManageKeyUidStatus {
|
||||
address: email.to_string(),
|
||||
published: true,
|
||||
})
|
||||
.collect();
|
||||
let key_link = uri!(vks_web::search(q = fp.to_string())).to_string();
|
||||
let context = templates::ManageKey {
|
||||
key_fpr: fp.to_string(),
|
||||
key_link,
|
||||
uid_status,
|
||||
token,
|
||||
base_uri: origin.get_base_uri().to_owned(),
|
||||
};
|
||||
MyResponse::ok("manage/manage_key", context, i18n, origin)
|
||||
}
|
||||
Ok(None) => MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "This link is invalid or expired")),
|
||||
i18n,
|
||||
origin,
|
||||
),
|
||||
Err(e) => MyResponse::ise(e),
|
||||
}
|
||||
} else {
|
||||
MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "This link is invalid or expired")),
|
||||
i18n,
|
||||
origin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/manage", data = "<request>")]
|
||||
pub fn vks_manage_post(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
request: Form<forms::ManageRequest>,
|
||||
token_service: &rocket::State<tokens::Service>,
|
||||
) -> MyResponse {
|
||||
let email = match request.search_term.parse::<Email>() {
|
||||
Ok(email) => email,
|
||||
Err(_) => {
|
||||
return MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "Malformed address: {}"; request.search_term.as_str())),
|
||||
i18n,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let tpk = match db.lookup(&Query::ByEmail(email.clone())) {
|
||||
Ok(Some(tpk)) => tpk,
|
||||
Ok(None) => {
|
||||
return MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(i18n.catalog, "No key for address: {}"; request.search_term.as_str())),
|
||||
i18n,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
Err(e) => return MyResponse::ise(e),
|
||||
};
|
||||
|
||||
let email_exists = tpk
|
||||
.userids()
|
||||
.flat_map(|binding| binding.userid().to_string().parse::<Email>())
|
||||
.any(|candidate| candidate == email);
|
||||
|
||||
if !email_exists {
|
||||
return MyResponse::ise(anyhow!("Internal error: address check failed!"));
|
||||
}
|
||||
|
||||
if !rate_limiter.action_perform(format!("manage-{}", &email)) {
|
||||
return MyResponse::not_found(
|
||||
Some("manage/manage"),
|
||||
Some(i18n!(
|
||||
i18n.catalog,
|
||||
"A request has already been sent for this address recently."
|
||||
)),
|
||||
i18n,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
let fpr: Fingerprint = tpk.fingerprint().try_into().unwrap();
|
||||
let fpr_text = fpr.to_string();
|
||||
let token = token_service.create(&StatelessVerifyToken { fpr });
|
||||
let link_path = uri!(vks_manage_key(token)).to_string();
|
||||
|
||||
let base_uri = origin.get_base_uri();
|
||||
if let Err(e) = mail_service.send_manage_token(&i18n, base_uri, fpr_text, &email, &link_path) {
|
||||
return MyResponse::ise(e);
|
||||
}
|
||||
|
||||
let ctx = templates::ManageLinkSent {
|
||||
address: email.to_string(),
|
||||
};
|
||||
MyResponse::ok("manage/manage_link_sent", ctx, i18n, origin)
|
||||
}
|
||||
|
||||
#[post("/manage/unpublish", data = "<request>")]
|
||||
pub fn vks_manage_unpublish(
|
||||
origin: RequestOrigin,
|
||||
db: &rocket::State<Sqlite>,
|
||||
i18n: I18n,
|
||||
token_service: &rocket::State<tokens::Service>,
|
||||
request: Form<forms::ManageDelete>,
|
||||
) -> MyResponse {
|
||||
vks_manage_unpublish_or_fail(origin, db, token_service, i18n, request)
|
||||
.unwrap_or_else(MyResponse::ise)
|
||||
}
|
||||
|
||||
pub fn vks_manage_unpublish_or_fail(
|
||||
origin: RequestOrigin,
|
||||
db: &rocket::State<Sqlite>,
|
||||
@@ -216,7 +65,7 @@ pub fn vks_manage_unpublish_or_fail(
|
||||
db.set_email_unpublished(&verify_token.fpr, &email)?;
|
||||
counters::inc_address_unpublished(&email);
|
||||
|
||||
Ok(vks_manage_key(
|
||||
Ok(crate::routes::manage::vks_manage_key(
|
||||
origin,
|
||||
db,
|
||||
i18n,
|
||||
|
156
src/web/mod.rs
156
src/web/mod.rs
@@ -1,10 +1,8 @@
|
||||
use hyperx::header::{Charset, ContentDisposition, DispositionParam, DispositionType};
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::http::{Header, Status};
|
||||
use rocket::outcome::Outcome;
|
||||
use rocket::response::status::Custom;
|
||||
use rocket::response::{Responder, Response};
|
||||
use rocket::{async_trait, get, request, routes};
|
||||
use rocket::{async_trait, request};
|
||||
use rocket_dyn_templates::{Engines, Template};
|
||||
use rocket_i18n::I18n;
|
||||
use rocket_prometheus::PrometheusMetrics;
|
||||
@@ -17,25 +15,23 @@ use util::get_commit_sha;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::counters;
|
||||
use crate::i18n::I18NHelper;
|
||||
use crate::i18n_helpers::describe_query_error;
|
||||
use crate::mail;
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::template_helpers::TemplateOverrides;
|
||||
use crate::tokens;
|
||||
use crate::{counters, routes};
|
||||
|
||||
use hagrid_database::{Database, Query, Sqlite, StatefulTokens, types::Fingerprint};
|
||||
|
||||
mod debug_web;
|
||||
mod hkp;
|
||||
mod maintenance;
|
||||
mod manage;
|
||||
mod util;
|
||||
mod vks;
|
||||
mod vks_api;
|
||||
mod vks_web;
|
||||
mod wkd;
|
||||
pub mod hkp;
|
||||
pub mod maintenance;
|
||||
pub mod manage;
|
||||
pub mod util;
|
||||
pub mod vks;
|
||||
pub mod vks_api;
|
||||
pub mod vks_web;
|
||||
|
||||
use crate::web::maintenance::MaintenanceMode;
|
||||
|
||||
@@ -200,7 +196,7 @@ impl MyResponse {
|
||||
}
|
||||
}
|
||||
|
||||
mod templates {
|
||||
pub mod templates {
|
||||
use super::{I18n, RequestOrigin, util::get_commit_sha};
|
||||
use serde_derive::Serialize;
|
||||
|
||||
@@ -262,7 +258,7 @@ mod templates {
|
||||
|
||||
pub struct HagridState {
|
||||
/// Assets directory, mounted to /assets, served by hagrid or nginx
|
||||
assets_dir: PathBuf,
|
||||
pub assets_dir: PathBuf,
|
||||
|
||||
/// XXX
|
||||
base_uri: String,
|
||||
@@ -292,7 +288,7 @@ impl<'r> request::FromRequest<'r> for RequestOrigin {
|
||||
}
|
||||
|
||||
impl RequestOrigin {
|
||||
fn get_base_uri(&self) -> &str {
|
||||
pub fn get_base_uri(&self) -> &str {
|
||||
match self {
|
||||
RequestOrigin::Direct(uri) => uri.as_str(),
|
||||
RequestOrigin::OnionService(uri) => uri.as_str(),
|
||||
@@ -317,77 +313,6 @@ pub fn key_to_response_plain(db: &rocket::State<Sqlite>, i18n: I18n, query: Quer
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/assets/<file..>")]
|
||||
async fn files(file: PathBuf, state: &rocket::State<HagridState>) -> Option<NamedFile> {
|
||||
NamedFile::open(state.assets_dir.join(file)).await.ok()
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn root(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("index", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about")]
|
||||
fn about(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/about", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/news")]
|
||||
fn news(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/news", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/atom.xml")]
|
||||
fn news_atom(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::xml("atom", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/faq")]
|
||||
fn faq(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/faq", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/usage")]
|
||||
fn usage(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/usage", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/privacy")]
|
||||
fn privacy(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/privacy", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/api")]
|
||||
fn apidoc(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/api", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/about/stats")]
|
||||
fn stats(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("about/stats", i18n, origin)
|
||||
}
|
||||
|
||||
#[get("/errors/<code>/<template>")]
|
||||
fn errors(
|
||||
i18n: I18n,
|
||||
origin: RequestOrigin,
|
||||
code: u16,
|
||||
template: String,
|
||||
) -> Result<Custom<Template>, &'static str> {
|
||||
if !template
|
||||
.chars()
|
||||
.all(|x| x == '-' || char::is_ascii_alphabetic(&x))
|
||||
{
|
||||
return Err("bad request");
|
||||
}
|
||||
let status_code = Status::from_code(code).ok_or("bad request")?;
|
||||
let response_body = Template::render(
|
||||
format!("errors/{}-{}", code, template),
|
||||
templates::HagridLayout::new(templates::Bare { dummy: () }, i18n, origin),
|
||||
);
|
||||
Ok(Custom(status_code, response_body))
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), rocket::Error> {
|
||||
// TODO: expect statement is actually misleading and there could be many other kind of errors,
|
||||
// apart from configuration ones. So, we have to rework this error handling.
|
||||
@@ -408,61 +333,6 @@ pub fn get_i18n() -> Vec<(&'static str, gettext::Catalog)> {
|
||||
include_i18n!()
|
||||
}
|
||||
|
||||
fn routes() -> Vec<rocket::Route> {
|
||||
routes![
|
||||
// infra
|
||||
root,
|
||||
about,
|
||||
news,
|
||||
news_atom,
|
||||
privacy,
|
||||
apidoc,
|
||||
faq,
|
||||
usage,
|
||||
files,
|
||||
stats,
|
||||
errors,
|
||||
// VKSv1
|
||||
vks_api::vks_v1_by_email,
|
||||
vks_api::vks_v1_by_fingerprint,
|
||||
vks_api::vks_v1_by_keyid,
|
||||
vks_api::upload_json,
|
||||
vks_api::upload_fallback,
|
||||
vks_api::request_verify_json,
|
||||
vks_api::request_verify_fallback,
|
||||
// User interaction.
|
||||
vks_web::search,
|
||||
vks_web::upload,
|
||||
vks_web::upload_post_form,
|
||||
vks_web::upload_post_form_data,
|
||||
vks_web::request_verify_form,
|
||||
vks_web::request_verify_form_data,
|
||||
vks_web::verify_confirm,
|
||||
vks_web::verify_confirm_form,
|
||||
vks_web::quick_upload,
|
||||
vks_web::quick_upload_proceed,
|
||||
// Debug
|
||||
debug_web::debug_info,
|
||||
// HKP
|
||||
hkp::pks_lookup,
|
||||
hkp::pks_add_form,
|
||||
hkp::pks_add_form_data,
|
||||
hkp::pks_internal_index,
|
||||
// WKD
|
||||
wkd::wkd_policy,
|
||||
wkd::wkd_query,
|
||||
// Manage
|
||||
manage::vks_manage,
|
||||
manage::vks_manage_key,
|
||||
manage::vks_manage_post,
|
||||
manage::vks_manage_unpublish,
|
||||
// Maintenance error page
|
||||
maintenance::maintenance_error_web,
|
||||
maintenance::maintenance_error_json,
|
||||
maintenance::maintenance_error_plain,
|
||||
]
|
||||
}
|
||||
|
||||
fn rocket_factory(
|
||||
mut rocket: rocket::Rocket<rocket::Build>,
|
||||
) -> anyhow::Result<rocket::Rocket<rocket::Build>> {
|
||||
@@ -509,7 +379,7 @@ fn rocket_factory(
|
||||
.manage(db_service)
|
||||
.manage(rate_limiter)
|
||||
.manage(localized_template_list)
|
||||
.mount("/", routes());
|
||||
.mount("/", routes::routes());
|
||||
|
||||
if let Some(prometheus) = prometheus {
|
||||
rocket = rocket
|
||||
|
@@ -2,24 +2,11 @@ use rocket::http::{ContentType, Status};
|
||||
use rocket::request::Request;
|
||||
use rocket::response::{self, Responder, Response};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{get, post};
|
||||
use rocket_i18n::{I18n, Translations};
|
||||
use serde_json::json;
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::mail;
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::tokens;
|
||||
use hagrid_database::{
|
||||
Query, Sqlite, StatefulTokens,
|
||||
types::{Email, Fingerprint, KeyID},
|
||||
};
|
||||
|
||||
use crate::web;
|
||||
use crate::web::vks;
|
||||
use crate::web::vks::response::*;
|
||||
use crate::web::{MyResponse, RequestOrigin};
|
||||
|
||||
use rocket::serde::json::Error as JsonError;
|
||||
|
||||
pub mod json {
|
||||
@@ -47,10 +34,10 @@ pub mod json {
|
||||
}
|
||||
}
|
||||
|
||||
type JsonResult = Result<serde_json::Value, JsonErrorResponse>;
|
||||
pub type JsonResult = Result<serde_json::Value, JsonErrorResponse>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JsonErrorResponse(Status, String);
|
||||
pub struct JsonErrorResponse(pub Status, pub String);
|
||||
|
||||
impl<'r> Responder<'r, 'static> for JsonErrorResponse {
|
||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
||||
@@ -63,7 +50,7 @@ impl<'r> Responder<'r, 'static> for JsonErrorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
fn json_or_error<T>(data: Result<Json<T>, JsonError>) -> Result<Json<T>, JsonErrorResponse> {
|
||||
pub 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(
|
||||
@@ -74,7 +61,7 @@ fn json_or_error<T>(data: Result<Json<T>, JsonError>) -> Result<Json<T>, JsonErr
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_ok_json(response: UploadResponse) -> Result<serde_json::Value, JsonErrorResponse> {
|
||||
pub fn upload_ok_json(response: UploadResponse) -> Result<serde_json::Value, JsonErrorResponse> {
|
||||
match response {
|
||||
UploadResponse::Ok {
|
||||
token,
|
||||
@@ -91,31 +78,7 @@ fn upload_ok_json(response: UploadResponse) -> Result<serde_json::Value, JsonErr
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/vks/v1/upload", format = "json", data = "<data>")]
|
||||
pub fn upload_json(
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
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, &i18n, tokens_stateless, rate_limiter, data_reader);
|
||||
upload_ok_json(result)
|
||||
}
|
||||
|
||||
#[post("/vks/v1/upload", rank = 2)]
|
||||
pub fn upload_fallback(origin: RequestOrigin) -> JsonErrorResponse {
|
||||
let error_msg = format!(
|
||||
"expected application/json data. see {}/about/api for api docs.",
|
||||
origin.get_base_uri()
|
||||
);
|
||||
JsonErrorResponse(Status::BadRequest, error_msg)
|
||||
}
|
||||
|
||||
fn get_locale(langs: &rocket::State<Translations>, locales: Vec<String>) -> I18n {
|
||||
pub fn get_locale(langs: &rocket::State<Translations>, locales: Vec<String>) -> I18n {
|
||||
locales
|
||||
.iter()
|
||||
.flat_map(|lang| lang.split(['-', ';', '_']).next())
|
||||
@@ -128,75 +91,3 @@ fn get_locale(langs: &rocket::State<Translations>, locales: Vec<String>) -> I18n
|
||||
})
|
||||
.expect("Expected to have an english translation!")
|
||||
}
|
||||
|
||||
#[post("/vks/v1/request-verify", format = "json", data = "<data>")]
|
||||
pub fn request_verify_json(
|
||||
db: &rocket::State<Sqlite>,
|
||||
langs: &rocket::State<Translations>,
|
||||
origin: RequestOrigin,
|
||||
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,
|
||||
locale,
|
||||
} = data.into_inner();
|
||||
let i18n = get_locale(langs, locale.unwrap_or_default());
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
addresses,
|
||||
);
|
||||
upload_ok_json(result)
|
||||
}
|
||||
|
||||
#[post("/vks/v1/request-verify", rank = 2)]
|
||||
pub fn request_verify_fallback(origin: RequestOrigin) -> JsonErrorResponse {
|
||||
let error_msg = format!(
|
||||
"expected application/json data. see {}/about/api for api docs.",
|
||||
origin.get_base_uri()
|
||||
);
|
||||
JsonErrorResponse(Status::BadRequest, error_msg)
|
||||
}
|
||||
|
||||
#[get("/vks/v1/by-fingerprint/<fpr>")]
|
||||
pub fn vks_v1_by_fingerprint(db: &rocket::State<Sqlite>, i18n: I18n, fpr: String) -> MyResponse {
|
||||
let query = match fpr.parse::<Fingerprint>() {
|
||||
Ok(fpr) => Query::ByFingerprint(fpr),
|
||||
Err(_) => return MyResponse::bad_request_plain("malformed fingerprint"),
|
||||
};
|
||||
|
||||
web::key_to_response_plain(db, i18n, query)
|
||||
}
|
||||
|
||||
#[get("/vks/v1/by-email/<email>")]
|
||||
pub fn vks_v1_by_email(db: &rocket::State<Sqlite>, i18n: I18n, email: String) -> MyResponse {
|
||||
let email = email.replace("%40", "@");
|
||||
let query = match email.parse::<Email>() {
|
||||
Ok(email) => Query::ByEmail(email),
|
||||
Err(_) => return MyResponse::bad_request_plain("malformed e-mail address"),
|
||||
};
|
||||
|
||||
web::key_to_response_plain(db, i18n, query)
|
||||
}
|
||||
|
||||
#[get("/vks/v1/by-keyid/<kid>")]
|
||||
pub fn vks_v1_by_keyid(db: &rocket::State<Sqlite>, i18n: I18n, kid: String) -> MyResponse {
|
||||
let query = match kid.parse::<KeyID>() {
|
||||
Ok(keyid) => Query::ByKeyID(keyid),
|
||||
Err(_) => return MyResponse::bad_request_plain("malformed key id"),
|
||||
};
|
||||
|
||||
web::key_to_response_plain(db, i18n, query)
|
||||
}
|
||||
|
@@ -2,23 +2,20 @@ use multipart::server::Multipart;
|
||||
use multipart::server::save::Entries;
|
||||
use multipart::server::save::SaveResult::*;
|
||||
|
||||
use gettext_macros::i18n;
|
||||
use rocket::Data;
|
||||
use rocket::data::ByteUnit;
|
||||
use rocket::form::Form;
|
||||
use rocket::form::ValueField;
|
||||
use rocket::http::ContentType;
|
||||
use rocket::uri;
|
||||
use rocket::{get, post, put};
|
||||
use rocket_i18n::I18n;
|
||||
use url::percent_encoding::percent_decode;
|
||||
|
||||
use crate::i18n_helpers::describe_query_error;
|
||||
use crate::mail;
|
||||
use crate::rate_limiter::RateLimiter;
|
||||
use crate::tokens;
|
||||
use crate::web::{MyResponse, RequestOrigin};
|
||||
use hagrid_database::{Database, Query, Sqlite, StatefulTokens};
|
||||
use hagrid_database::{Database, Query, Sqlite};
|
||||
|
||||
use crate::web::vks;
|
||||
use crate::web::vks::response::*;
|
||||
@@ -26,9 +23,9 @@ use anyhow::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
|
||||
const UPLOAD_LIMIT: ByteUnit = ByteUnit::Mebibyte(1);
|
||||
pub const UPLOAD_LIMIT: ByteUnit = ByteUnit::Mebibyte(1);
|
||||
|
||||
mod forms {
|
||||
pub mod forms {
|
||||
use rocket::FromForm;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
@@ -44,7 +41,7 @@ mod forms {
|
||||
}
|
||||
}
|
||||
|
||||
mod template {
|
||||
pub mod template {
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -98,10 +95,14 @@ mod template {
|
||||
}
|
||||
|
||||
impl MyResponse {
|
||||
fn upload_response_quick(response: UploadResponse, i18n: I18n, origin: RequestOrigin) -> Self {
|
||||
pub fn upload_response_quick(
|
||||
response: UploadResponse,
|
||||
i18n: I18n,
|
||||
origin: RequestOrigin,
|
||||
) -> Self {
|
||||
match response {
|
||||
UploadResponse::Ok { token, .. } => {
|
||||
let uri = uri!(quick_upload_proceed(token));
|
||||
let uri = uri!(crate::routes::vks::quick_upload_proceed(token));
|
||||
let text = format!(
|
||||
"Key successfully uploaded. Proceed with verification here:\n{}{}\n",
|
||||
origin.get_base_uri(),
|
||||
@@ -119,7 +120,7 @@ impl MyResponse {
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_response(response: UploadResponse, i18n: I18n, origin: RequestOrigin) -> Self {
|
||||
pub fn upload_response(response: UploadResponse, i18n: I18n, origin: RequestOrigin) -> Self {
|
||||
match response {
|
||||
UploadResponse::Ok {
|
||||
token,
|
||||
@@ -153,7 +154,7 @@ impl MyResponse {
|
||||
i18n: I18n,
|
||||
origin: RequestOrigin,
|
||||
) -> Self {
|
||||
let key_link = uri!(search(q = &key_fpr)).to_string();
|
||||
let key_link = uri!(crate::routes::vks::search(q = &key_fpr)).to_string();
|
||||
|
||||
let count_revoked = uid_status
|
||||
.iter()
|
||||
@@ -198,7 +199,7 @@ impl MyResponse {
|
||||
let keys = key_fprs
|
||||
.into_iter()
|
||||
.map(|fpr| {
|
||||
let key_link = uri!(search(q = &fpr)).to_string();
|
||||
let key_link = uri!(crate::routes::vks::search(q = &fpr)).to_string();
|
||||
template::UploadOkKey {
|
||||
key_fpr: fpr,
|
||||
key_link,
|
||||
@@ -212,27 +213,6 @@ impl MyResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/upload")]
|
||||
pub fn upload(origin: RequestOrigin, i18n: I18n) -> MyResponse {
|
||||
MyResponse::ok_bare("upload/upload", i18n, origin)
|
||||
}
|
||||
|
||||
#[post("/upload/submit", format = "multipart/form-data", data = "<data>")]
|
||||
pub async fn upload_post_form_data(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
cont_type: &ContentType,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match process_upload(db, tokens_stateless, rate_limiter, &i18n, data, cont_type).await {
|
||||
Ok(response) => MyResponse::upload_response(response, i18n, origin),
|
||||
Err(err) => MyResponse::bad_request("upload/upload", err, i18n, origin),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_post_form_data(
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
@@ -244,20 +224,7 @@ pub async fn process_post_form_data(
|
||||
process_upload(db, tokens_stateless, rate_limiter, &i18n, data, cont_type).await
|
||||
}
|
||||
|
||||
#[get("/search?<q>")]
|
||||
pub fn search(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
i18n: I18n,
|
||||
q: String,
|
||||
) -> MyResponse {
|
||||
match q.parse::<Query>() {
|
||||
Ok(query) => key_to_response(db, origin, i18n, q, query),
|
||||
Err(e) => MyResponse::bad_request("index", e, i18n, origin),
|
||||
}
|
||||
}
|
||||
|
||||
fn key_to_response(
|
||||
pub fn key_to_response(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
i18n: I18n,
|
||||
@@ -285,71 +252,6 @@ fn key_to_response(
|
||||
MyResponse::ok("found", context, i18n, origin)
|
||||
}
|
||||
|
||||
#[put("/", data = "<data>")]
|
||||
pub async fn quick_upload(
|
||||
db: &rocket::State<Sqlite>,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
origin: RequestOrigin,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
let buf = match data.open(UPLOAD_LIMIT).into_bytes().await {
|
||||
Ok(buf) => buf.into_inner(),
|
||||
Err(error) => return MyResponse::bad_request("400-plain", anyhow!(error), i18n, origin),
|
||||
};
|
||||
|
||||
MyResponse::upload_response_quick(
|
||||
vks::process_key(db, &i18n, tokens_stateless, rate_limiter, Cursor::new(buf)),
|
||||
i18n,
|
||||
origin,
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/upload/<token>", rank = 2)]
|
||||
pub fn quick_upload_proceed(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_stateful: &rocket::State<StatefulTokens>,
|
||||
token_stateless: &rocket::State<tokens::Service>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
token: String,
|
||||
) -> MyResponse {
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
vec![],
|
||||
);
|
||||
MyResponse::upload_response(result, i18n, origin)
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/upload/submit",
|
||||
format = "application/x-www-form-urlencoded",
|
||||
data = "<data>"
|
||||
)]
|
||||
pub async fn upload_post_form(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
tokens_stateless: &rocket::State<tokens::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
data: Data<'_>,
|
||||
) -> MyResponse {
|
||||
match process_post_form(db, tokens_stateless, rate_limiter, &i18n, data).await {
|
||||
Ok(response) => MyResponse::upload_response(response, i18n, origin),
|
||||
Err(err) => MyResponse::bad_request("upload/upload", err, i18n, origin),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn process_post_form(
|
||||
db: &Sqlite,
|
||||
tokens_stateless: &tokens::Service,
|
||||
@@ -379,7 +281,7 @@ pub async fn process_post_form(
|
||||
Err(anyhow!("No keytext found"))
|
||||
}
|
||||
|
||||
async fn process_upload(
|
||||
pub async fn process_upload(
|
||||
db: &Sqlite,
|
||||
tokens_stateless: &tokens::Service,
|
||||
rate_limiter: &RateLimiter,
|
||||
@@ -433,109 +335,3 @@ fn process_multipart(
|
||||
None => Err(anyhow!("No keytext found")),
|
||||
}
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/upload/request-verify",
|
||||
format = "application/x-www-form-urlencoded",
|
||||
data = "<request>"
|
||||
)]
|
||||
pub fn request_verify_form(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_stateful: &rocket::State<StatefulTokens>,
|
||||
token_stateless: &rocket::State<tokens::Service>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
request: Form<forms::VerifyRequest>,
|
||||
) -> MyResponse {
|
||||
let forms::VerifyRequest { token, address } = request.into_inner();
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
vec![address],
|
||||
);
|
||||
MyResponse::upload_response(result, i18n, origin)
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/upload/request-verify",
|
||||
format = "multipart/form-data",
|
||||
data = "<request>"
|
||||
)]
|
||||
pub fn request_verify_form_data(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_stateful: &rocket::State<StatefulTokens>,
|
||||
token_stateless: &rocket::State<tokens::Service>,
|
||||
mail_service: &rocket::State<mail::Service>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
i18n: I18n,
|
||||
request: Form<forms::VerifyRequest>,
|
||||
) -> MyResponse {
|
||||
let forms::VerifyRequest { token, address } = request.into_inner();
|
||||
let result = vks::request_verify(
|
||||
db,
|
||||
&origin,
|
||||
token_stateful,
|
||||
token_stateless,
|
||||
mail_service,
|
||||
rate_limiter,
|
||||
&i18n,
|
||||
token,
|
||||
vec![address],
|
||||
);
|
||||
MyResponse::upload_response(result, i18n, origin)
|
||||
}
|
||||
|
||||
#[post("/verify/<token>")]
|
||||
pub fn verify_confirm(
|
||||
db: &rocket::State<Sqlite>,
|
||||
origin: RequestOrigin,
|
||||
token_service: &rocket::State<StatefulTokens>,
|
||||
rate_limiter: &rocket::State<RateLimiter>,
|
||||
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(q = &email)).to_string();
|
||||
let context = template::Verify {
|
||||
userid: email,
|
||||
key_fpr: fingerprint,
|
||||
userid_link,
|
||||
};
|
||||
|
||||
MyResponse::ok("upload/publish-result", context, i18n, origin)
|
||||
}
|
||||
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, i18n, origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/verify/<token>")]
|
||||
pub fn verify_confirm_form(origin: RequestOrigin, i18n: I18n, token: String) -> MyResponse {
|
||||
MyResponse::ok(
|
||||
"upload/verification-form",
|
||||
template::VerifyForm { token },
|
||||
i18n,
|
||||
origin,
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user