hagrid-keyserver--hagrid/src/web/mod.rs

1114 lines
41 KiB
Rust
Raw Normal View History

2018-08-16 18:35:19 +00:00
use rocket;
use rocket::http::Header;
2019-06-22 21:12:14 +00:00
use rocket::request;
use rocket::outcome::Outcome;
2019-02-22 15:25:06 +00:00
use rocket::response::NamedFile;
2019-04-26 16:33:26 +00:00
use rocket::config::Config;
2018-12-25 19:06:28 +00:00
use rocket_contrib::templates::Template;
use rocket::http::uri::Uri;
use rocket_contrib::json::JsonValue;
2019-02-22 15:25:06 +00:00
use serde::Serialize;
use handlebars::Handlebars;
2019-02-22 15:25:06 +00:00
2019-03-06 15:19:33 +00:00
use std::path::PathBuf;
2018-08-16 18:35:19 +00:00
use mail;
2019-04-02 12:54:40 +00:00
use tokens;
2019-05-05 12:58:05 +00:00
use rate_limiter::RateLimiter;
2018-08-16 18:35:19 +00:00
2019-05-01 20:00:06 +00:00
use database::{Database, KeyDatabase, Query};
2019-06-11 14:59:27 +00:00
use database::types::Fingerprint;
use Result;
2018-09-19 20:24:38 +00:00
2019-05-05 12:58:05 +00:00
use std::convert::TryInto;
2018-09-19 20:24:38 +00:00
2019-03-12 11:18:28 +00:00
mod hkp;
2019-04-05 15:07:40 +00:00
mod manage;
2019-05-05 18:17:54 +00:00
mod maintenance;
2019-05-23 23:01:24 +00:00
mod vks;
mod vks_web;
mod vks_api;
2019-05-05 18:17:54 +00:00
use web::maintenance::MaintenanceMode;
2019-02-22 15:25:06 +00:00
use rocket::http::hyper::header::ContentDisposition;
2019-02-22 15:25:06 +00:00
#[derive(Responder)]
2019-03-05 15:15:03 +00:00
pub enum MyResponse {
2019-02-22 15:25:06 +00:00
#[response(status = 200, content_type = "html")]
Success(Template),
2019-05-23 23:01:24 +00:00
#[response(status = 200, content_type = "plain")]
2019-02-22 15:25:06 +00:00
Plain(String),
2019-05-23 23:01:24 +00:00
#[response(status = 200, content_type = "application/pgp-keys")]
Key(String, ContentDisposition),
2019-03-01 11:58:17 +00:00
#[response(status = 200, content_type = "application/pgp-keys")]
XAccelRedirect(&'static str, Header<'static>, ContentDisposition),
2019-02-22 15:25:06 +00:00
#[response(status = 500, content_type = "html")]
ServerError(Template),
#[response(status = 404, content_type = "html")]
NotFound(Template),
2019-06-11 14:59:27 +00:00
#[response(status = 404, content_type = "html")]
NotFoundPlain(String),
#[response(status = 400, content_type = "html")]
BadRequest(Template),
2019-06-11 14:59:27 +00:00
#[response(status = 400, content_type = "html")]
BadRequestPlain(String),
#[response(status = 503, content_type = "html")]
Maintenance(Template),
#[response(status = 503, content_type = "json")]
MaintenanceJson(JsonValue),
#[response(status = 503, content_type = "plain")]
MaintenancePlain(String),
2019-02-22 15:25:06 +00:00
}
impl MyResponse {
pub fn ok<S: Serialize>(tmpl: &'static str, ctx: S) -> Self {
MyResponse::Success(Template::render(tmpl, ctx))
}
pub fn plain(s: String) -> Self {
MyResponse::Plain(s)
}
pub fn key(armored_key: String, fp: &Fingerprint) -> Self {
2019-06-22 21:08:40 +00:00
use rocket::http::hyper::header::{DispositionType, DispositionParam, Charset};
MyResponse::Key(
armored_key,
ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![
DispositionParam::Filename(
Charset::Us_Ascii, None,
(fp.to_string() + ".asc").into_bytes()),
],
})
}
pub fn x_accel_redirect(x_accel_path: String, fp: &Fingerprint) -> Self {
2019-06-22 21:08:40 +00:00
use rocket::http::hyper::header::{DispositionType, DispositionParam, Charset};
// nginx expects percent-encoded URIs
let x_accel_path = Uri::percent_encode(&x_accel_path).into_owned();
2019-03-01 11:58:17 +00:00
MyResponse::XAccelRedirect(
"",
Header::new("X-Accel-Redirect", x_accel_path),
2019-03-01 11:58:17 +00:00
ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![
DispositionParam::Filename(
Charset::Us_Ascii, None,
(fp.to_string() + ".asc").into_bytes()),
],
})
}
pub fn ise(e: failure::Error) -> Self {
2019-06-07 20:31:39 +00:00
eprintln!("Internal error: {:?}", e);
2019-02-22 15:25:06 +00:00
let ctx = templates::FiveHundred{
2019-05-05 21:31:40 +00:00
internal_error: e.to_string(),
2019-02-22 15:25:06 +00:00
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ServerError(Template::render("500", ctx))
}
pub fn bad_request(template: &'static str, e: failure::Error) -> Self {
let ctx = templates::General {
error: Some(format!("{}", e)),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::BadRequest(Template::render(template, ctx))
}
2019-06-11 14:59:27 +00:00
pub fn bad_request_plain(message: impl Into<String>) -> Self {
MyResponse::BadRequestPlain(message.into())
}
pub fn not_found_plain(message: impl Into<String>) -> Self {
MyResponse::NotFoundPlain(message.into())
}
pub fn not_found(
tmpl: Option<&'static str>,
message: impl Into<Option<String>>
) -> Self {
MyResponse::NotFound(
Template::render(
tmpl.unwrap_or("index"),
templates::General::new(
Some(message.into()
.unwrap_or_else(|| "Key not found".to_owned())))))
2018-09-19 20:24:38 +00:00
}
2018-08-16 18:35:19 +00:00
}
2018-09-19 20:24:38 +00:00
mod templates {
2019-02-08 19:09:53 +00:00
#[derive(Serialize)]
2019-02-22 15:25:06 +00:00
pub struct FiveHundred {
2019-05-05 21:31:40 +00:00
pub internal_error: String,
2019-02-22 15:25:06 +00:00
pub commit: String,
pub version: String,
}
2019-02-22 22:29:54 +00:00
#[derive(Serialize)]
pub struct General {
2019-02-22 22:29:54 +00:00
pub error: Option<String>,
pub commit: String,
pub version: String,
}
2019-06-05 12:33:02 +00:00
#[derive(Serialize)]
pub struct About {
pub base_uri: String,
pub commit: String,
pub version: String,
}
impl About {
pub fn new(base_uri: impl Into<String>) -> Self {
Self {
base_uri: base_uri.into(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
}
}
}
impl General {
pub fn new(error: Option<String>) -> Self {
Self {
error: error,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
}
}
}
2019-03-05 10:11:23 +00:00
impl Default for General {
fn default() -> Self {
Self::new(None)
2019-03-05 10:11:23 +00:00
}
}
2018-08-16 18:35:19 +00:00
}
pub struct HagridState {
2019-04-26 16:33:26 +00:00
/// Assets directory, mounted to /assets, served by hagrid or nginx
assets_dir: PathBuf,
/// The keys directory, where keys are located, served by hagrid or nginx
keys_external_dir: PathBuf,
/// XXX
base_uri: String,
2019-06-22 21:12:14 +00:00
base_uri_onion: String,
2019-05-03 22:06:05 +00:00
///
x_accel_redirect: bool,
2019-05-03 22:06:05 +00:00
x_accel_prefix: Option<PathBuf>,
}
2018-10-18 14:26:25 +00:00
2019-06-22 21:12:14 +00:00
#[derive(Debug)]
enum RequestOrigin {
Direct(String),
OnionService(String),
}
impl<'a, 'r> request::FromRequest<'a, 'r> for RequestOrigin {
type Error = ();
fn from_request(request: &'a request::Request<'r>) -> request::Outcome<Self, Self::Error> {
let hagrid_state = request.guard::<rocket::State<HagridState>>().unwrap();
let result = match request.headers().get("x-is-tor").next() {
Some(_) => RequestOrigin::OnionService(hagrid_state.base_uri_onion.clone()),
None => RequestOrigin::Direct(hagrid_state.base_uri.clone()),
};
Outcome::Success(result)
}
}
impl RequestOrigin {
fn get_base_uri(&self) -> &str {
match self {
RequestOrigin::Direct(uri) => uri.as_str(),
RequestOrigin::OnionService(uri) => uri.as_str(),
}
}
}
2019-06-11 14:59:27 +00:00
pub fn key_to_response_plain(
state: rocket::State<HagridState>,
db: rocket::State<KeyDatabase>,
query: Query,
) -> MyResponse {
let fp = if let Some(fp) = db.lookup_primary_fingerprint(&query) {
fp
} else {
2019-06-11 14:59:27 +00:00
return MyResponse::not_found_plain(query.describe_error());
2019-02-22 15:25:06 +00:00
};
2019-06-11 14:59:27 +00:00
if state.x_accel_redirect {
if let Some(key_path) = db.lookup_path(&query) {
let mut x_accel_path = state.keys_external_dir.join(&key_path);
if let Some(prefix) = state.x_accel_prefix.as_ref() {
x_accel_path = x_accel_path.strip_prefix(&prefix).unwrap().to_path_buf();
2019-03-01 11:58:17 +00:00
}
2019-06-11 14:59:27 +00:00
// prepend a / to make path relative to nginx root
let x_accel_path = format!("/{}", x_accel_path.to_string_lossy());
return MyResponse::x_accel_redirect(x_accel_path, &fp);
}
}
2019-06-11 14:59:27 +00:00
return match db.by_fpr(&fp) {
Some(armored) => MyResponse::key(armored, &fp.into()),
None => MyResponse::not_found_plain(query.describe_error()),
}
2019-01-04 13:07:14 +00:00
}
#[get("/assets/<file..>")]
fn files(file: PathBuf, state: rocket::State<HagridState>) -> Option<NamedFile> {
2019-04-26 16:33:26 +00:00
NamedFile::open(state.assets_dir.join(file)).ok()
2018-10-18 14:26:25 +00:00
}
2018-08-16 18:35:19 +00:00
#[get("/")]
fn root() -> Template {
Template::render("index", templates::General::default())
2018-08-16 18:35:19 +00:00
}
2019-02-22 20:15:06 +00:00
#[get("/about")]
fn about() -> Template {
2019-04-25 15:52:54 +00:00
Template::render("about/about", templates::General::default())
2019-02-22 20:15:06 +00:00
}
2019-06-12 12:02:45 +00:00
#[get("/about/news")]
fn news() -> Template {
Template::render("about/news", templates::General::default())
}
2019-06-04 22:11:38 +00:00
#[get("/about/faq")]
fn faq() -> Template {
Template::render("about/faq", templates::General::default())
}
2019-05-17 20:31:04 +00:00
#[get("/about/usage")]
2019-06-05 12:33:02 +00:00
fn usage(state: rocket::State<HagridState>) -> Template {
2019-06-06 12:12:40 +00:00
Template::render("about/usage", templates::About::new(state.base_uri.clone()))
2019-05-17 20:31:04 +00:00
}
2019-04-25 15:52:54 +00:00
#[get("/about/privacy")]
2019-04-25 15:44:46 +00:00
fn privacy() -> Template {
2019-04-25 15:52:54 +00:00
Template::render("about/privacy", templates::General::default())
2019-04-25 15:44:46 +00:00
}
2019-04-25 15:52:54 +00:00
#[get("/about/api")]
fn apidoc() -> Template {
2019-04-25 15:52:54 +00:00
Template::render("about/api", templates::General::default())
}
pub fn serve() -> Result<()> {
Err(rocket_factory(rocket::ignite())?.launch().into())
}
fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
2018-09-19 20:24:38 +00:00
let routes = routes![
// infra
root,
about,
2019-06-12 12:02:45 +00:00
news,
2019-04-25 15:44:46 +00:00
privacy,
apidoc,
2019-06-04 22:11:38 +00:00
faq,
2019-05-17 20:31:04 +00:00
usage,
files,
// VKSv1
2019-06-11 14:59:27 +00:00
vks_api::vks_v1_by_email,
vks_api::vks_v1_by_fingerprint,
vks_api::vks_v1_by_keyid,
2019-05-23 23:01:24 +00:00
vks_api::upload_json,
vks_api::upload_fallback,
vks_api::request_verify_json,
vks_api::request_verify_fallback,
// User interaction.
2019-06-11 14:59:27 +00:00
vks_web::search,
2019-05-23 23:01:24 +00:00
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,
2019-06-05 12:33:02 +00:00
vks_web::quick_upload,
vks_web::quick_upload_proceed,
2019-03-12 11:18:28 +00:00
// HKP
hkp::pks_lookup,
2019-05-20 21:17:50 +00:00
hkp::pks_add_form,
hkp::pks_add_form_data,
2019-04-05 15:07:40 +00:00
// EManage
manage::vks_manage,
manage::vks_manage_key,
manage::vks_manage_post,
manage::vks_manage_unpublish,
2019-05-05 18:17:54 +00:00
// Maintenance error page
maintenance::maintenance_error_web,
maintenance::maintenance_error_json,
maintenance::maintenance_error_plain,
2018-09-19 20:24:38 +00:00
];
2019-04-26 16:33:26 +00:00
let db_service = configure_db_service(rocket.config())?;
let hagrid_state = configure_hagrid_state(rocket.config())?;
let stateful_token_service = configure_stateful_token_service(rocket.config())?;
let stateless_token_service = configure_stateless_token_service(rocket.config())?;
2019-04-26 16:33:26 +00:00
let mail_service = configure_mail_service(rocket.config())?;
2019-05-05 12:58:05 +00:00
let rate_limiter = configure_rate_limiter(rocket.config())?;
2019-05-05 18:17:54 +00:00
let maintenance_mode = configure_maintenance_mode(rocket.config())?;
2019-04-26 16:33:26 +00:00
Ok(rocket
.attach(Template::fairing())
2019-05-05 18:17:54 +00:00
.attach(maintenance_mode)
2019-04-26 16:33:26 +00:00
.manage(hagrid_state)
.manage(stateless_token_service)
.manage(stateful_token_service)
2019-04-26 16:33:26 +00:00
.manage(mail_service)
.manage(db_service)
2019-05-05 12:58:05 +00:00
.manage(rate_limiter)
2019-04-26 16:33:26 +00:00
.mount("/", routes)
)
}
2019-04-26 22:21:30 +00:00
fn configure_db_service(config: &Config) -> Result<KeyDatabase> {
let keys_internal_dir: PathBuf = config.get_str("keys_internal_dir")?.into();
let keys_external_dir: PathBuf = config.get_str("keys_external_dir")?.into();
2019-04-26 16:33:26 +00:00
let tmp_dir: PathBuf = config.get_str("tmp_dir")?.into();
let fs_db = KeyDatabase::new(keys_internal_dir, keys_external_dir, tmp_dir)?;
2019-04-26 22:21:30 +00:00
Ok(fs_db)
2019-04-26 16:33:26 +00:00
}
fn configure_hagrid_state(config: &Config) -> Result<HagridState> {
let assets_dir: PathBuf = config.get_str("assets_dir")?.into();
let keys_external_dir: PathBuf = config.get_str("keys_external_dir")?.into();
2019-05-03 22:06:05 +00:00
let x_accel_prefix: Option<PathBuf> =
config.get_string("x_accel_prefix").map(|prefix| prefix.into()).ok();
// State
2019-04-26 16:33:26 +00:00
let base_uri = config.get_str("base-URI")?.to_string();
2019-06-22 21:12:14 +00:00
let base_uri_onion = config.get_str("base-URI-Onion")
.map(|c| c.to_string())
.unwrap_or(base_uri.clone());
2019-04-26 16:33:26 +00:00
Ok(HagridState {
assets_dir,
keys_external_dir: keys_external_dir,
2019-06-22 21:12:14 +00:00
base_uri,
base_uri_onion,
2019-04-26 16:33:26 +00:00
x_accel_redirect: config.get_bool("x-accel-redirect")?,
2019-05-03 22:06:05 +00:00
x_accel_prefix,
2019-04-26 16:33:26 +00:00
})
}
fn configure_stateful_token_service(config: &Config) -> Result<database::StatefulTokens> {
2019-04-26 21:18:01 +00:00
let token_dir: PathBuf = config.get_str("token_dir")?.into();
database::StatefulTokens::new(token_dir)
}
fn configure_stateless_token_service(config: &Config) -> Result<tokens::Service> {
2019-04-26 16:33:26 +00:00
use std::convert::TryFrom;
let secret = config.get_str("token_secret")?.to_string();
let validity = config.get_int("token_validity")?;
let validity = u64::try_from(validity)?;
Ok(tokens::Service::init(&secret, validity))
}
fn configure_mail_service(config: &Config) -> Result<mail::Service> {
// Mail service
2019-04-26 16:33:26 +00:00
let template_dir: PathBuf = config.get_str("template_dir")?.into();
let base_uri = config.get_str("base-URI")?.to_string();
let from = config.get_str("from")?.to_string();
2019-04-05 15:07:40 +00:00
let verify_html = template_dir.join("email/publish-html.hbs");
let verify_txt = template_dir.join("email/publish-txt.hbs");
let manage_html = template_dir.join("email/manage-html.hbs");
let manage_txt = template_dir.join("email/manage-txt.hbs");
let mut handlebars = Handlebars::new();
handlebars.register_template_file("verify-html", verify_html)?;
handlebars.register_template_file("verify-txt", verify_txt)?;
2019-04-05 15:07:40 +00:00
handlebars.register_template_file("manage-html", manage_html)?;
handlebars.register_template_file("manage-txt", manage_txt)?;
2019-04-26 16:33:26 +00:00
let filemail_into = config.get_str("filemail_into")
.ok().map(|p| PathBuf::from(p));
2019-04-02 12:54:40 +00:00
2019-04-26 16:33:26 +00:00
if let Some(path) = filemail_into {
mail::Service::filemail(from, base_uri, handlebars, path)
} else {
mail::Service::sendmail(from, base_uri, handlebars)
}
2018-08-16 18:35:19 +00:00
}
2019-05-05 12:58:05 +00:00
fn configure_rate_limiter(config: &Config) -> Result<RateLimiter> {
let timeout_secs = config.get_int("mail_rate_limit").unwrap_or(60);
let timeout_secs = timeout_secs.try_into()?;
Ok(RateLimiter::new(timeout_secs))
}
2019-05-05 18:17:54 +00:00
fn configure_maintenance_mode(config: &Config) -> Result<MaintenanceMode> {
let maintenance_file: PathBuf = config.get_str("maintenance_file")
.unwrap_or("maintenance").into();
Ok(MaintenanceMode::new(maintenance_file))
}
#[cfg(test)]
2019-03-12 11:18:28 +00:00
pub mod tests {
use regex;
use std::fs;
2019-06-22 22:09:35 +00:00
use std::fs::File;
use std::io::Write;
2019-03-06 15:19:33 +00:00
use std::path::Path;
use tempfile::{tempdir, TempDir};
use super::rocket;
use rocket::local::{Client, LocalResponse};
use rocket::http::Status;
use rocket::http::ContentType;
use lettre::Envelope;
2019-02-26 10:34:53 +00:00
use sequoia_openpgp::TPK;
use sequoia_openpgp::tpk::TPKBuilder;
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::serialize::Serialize;
use database::*;
use super::*;
// for some reason, this is no longer public in lettre itself
// FIXME replace with builtin struct on lettre update
// see https://github.com/lettre/lettre/blob/master/lettre/src/file/mod.rs#L41
#[derive(Deserialize)]
struct SerializableEmail {
#[serde(alias = "envelope")]
_envelope: Envelope,
#[serde(alias = "message_id")]
_message_id: String,
message: Vec<u8>,
}
/// Fake base URI to use in tests.
const BASE_URI: &'static str = "http://local.connection";
/// Creates a configuration and empty state dir for testing purposes.
///
/// Note that you need to keep the returned TempDir alive for the
/// duration of your test. To debug the test, mem::forget it to
/// prevent cleanup.
2019-03-12 11:18:28 +00:00
pub fn configuration() -> Result<(TempDir, rocket::Config)> {
use rocket::config::{Config, Environment};
let root = tempdir()?;
let filemail = root.path().join("filemail");
::std::fs::create_dir_all(&filemail)?;
2019-04-26 16:33:26 +00:00
let base_dir: PathBuf = root.path().into();
let config = Config::build(Environment::Staging)
.root(root.path().to_path_buf())
2019-03-13 11:22:06 +00:00
.extra("template_dir",
::std::env::current_dir().unwrap().join("dist/templates")
.to_str().unwrap())
2019-04-26 16:33:26 +00:00
.extra("assets_dir",
::std::env::current_dir().unwrap().join("dist/assets")
.to_str().unwrap())
.extra("keys_internal_dir", base_dir.join("keys_internal").to_str().unwrap())
.extra("keys_external_dir", base_dir.join("keys_external").to_str().unwrap())
2019-04-26 16:33:26 +00:00
.extra("tmp_dir", base_dir.join("tmp").to_str().unwrap())
2019-04-26 21:18:01 +00:00
.extra("token_dir", base_dir.join("tokens").to_str().unwrap())
2019-06-22 22:09:35 +00:00
.extra("maintenance_file", base_dir.join("maintenance").to_str().unwrap())
.extra("base-URI", BASE_URI)
.extra("from", "from@example.com")
2019-04-02 12:54:40 +00:00
.extra("token_secret", "hagrid")
.extra("token_validity", 3600)
.extra("filemail_into", filemail.into_os_string().into_string()
.expect("path is valid UTF8"))
2019-03-01 11:58:17 +00:00
.extra("x-accel-redirect", false)
.finalize()?;
Ok((root, config))
}
2019-03-12 11:18:28 +00:00
pub fn client() -> Result<(TempDir, Client)> {
let (tmpdir, config) = configuration()?;
let rocket = rocket_factory(rocket::custom(config))?;
Ok((tmpdir, Client::new(rocket)?))
}
#[cfg(test)]
2019-03-12 11:18:28 +00:00
pub fn assert_consistency(rocket: &rocket::Rocket) {
2019-04-26 22:21:30 +00:00
let db = rocket.state::<KeyDatabase>().unwrap();
db.check_consistency().unwrap();
}
#[test]
fn basics() {
let (_tmpdir, config) = configuration().unwrap();
let rocket = rocket_factory(rocket::custom(config)).unwrap();
let client = Client::new(rocket).expect("valid rocket instance");
// Check that we see the landing page.
let mut response = client.get("/").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("Hagrid"));
// Check that we see the privacy policy.
let mut response = client.get("/about").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
2019-04-25 17:44:16 +00:00
assert!(response.body_string().unwrap().contains("distribution and discovery"));
// Check that we see the privacy policy.
let mut response = client.get("/about/privacy").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("Public Key Data"));
2019-03-07 11:58:03 +00:00
// Check that we see the API docs.
2019-04-25 17:44:16 +00:00
let mut response = client.get("/about/api").dispatch();
2019-03-07 11:58:03 +00:00
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("/vks/v1/by-keyid"));
2019-03-12 14:47:16 +00:00
// Check that we see the upload form.
2019-05-23 23:01:24 +00:00
let mut response = client.get("/upload").dispatch();
2019-03-12 14:47:16 +00:00
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("upload"));
// Check that we see the deletion form.
2019-04-25 18:04:20 +00:00
let mut response = client.get("/manage").dispatch();
2019-03-12 14:47:16 +00:00
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
2019-06-06 09:10:37 +00:00
assert!(response.body_string().unwrap().contains("any verified e-mail address"));
2019-03-12 14:47:16 +00:00
assert_consistency(client.rocket());
}
2019-02-26 10:34:53 +00:00
2019-06-22 22:09:35 +00:00
#[test]
fn maintenance() {
let (tmpdir, client) = client().unwrap();
let maintenance_path = tmpdir.path().join("maintenance");
let mut file = File::create(&maintenance_path).unwrap();
file.write_all(b"maintenance-message").unwrap();
// Check that endpoints return a maintenance message
check_maintenance(&client, "/upload", ContentType::HTML);
check_maintenance(&client, "/manage", ContentType::HTML);
check_maintenance(&client, "/verify", ContentType::HTML);
check_maintenance(&client, "/pks/add", ContentType::Plain);
check_maintenance(&client, "/vks/v1/upload", ContentType::JSON);
check_maintenance(&client, "/vks/v1/request-verify", ContentType::JSON);
// Extra check for the shortcut "PUT" endpoint
let mut response = client.put("/").dispatch();
assert_eq!(response.status(), Status::ServiceUnavailable);
assert_eq!(response.content_type(), Some(ContentType::Plain));
assert!(response.body_string().unwrap().contains("maintenance-message"));
fs::remove_file(&maintenance_path).unwrap();
// Check that we see the upload form.
let mut response = client.get("/upload").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(!response.body_string().unwrap().contains("maintenance-message"));
}
fn check_maintenance(client: &Client, uri: &str, content_type: ContentType) {
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::ServiceUnavailable);
assert_eq!(response.content_type(), Some(content_type));
assert!(response.body_string().unwrap().contains("maintenance-message"));
}
2019-02-26 10:34:53 +00:00
#[test]
2019-05-03 13:34:34 +00:00
fn upload_verify_single() {
2019-03-12 12:16:10 +00:00
let (tmpdir, client) = client().unwrap();
let filemail_into = tmpdir.path().join("filemail");
2019-02-26 10:34:53 +00:00
// Generate a key and upload it.
let (tpk, _) = TPKBuilder::autocrypt(
2019-06-07 20:30:51 +00:00
None, Some("foo@invalid.example.com"))
2019-02-26 10:34:53 +00:00
.generate().unwrap();
let mut tpk_serialized = Vec::new();
tpk.serialize(&mut tpk_serialized).unwrap();
2019-05-03 13:34:34 +00:00
let token = vks_publish_submit_get_token(&client, &tpk_serialized);
2019-02-26 10:34:53 +00:00
2019-03-04 16:45:50 +00:00
// Prior to email confirmation, we should not be able to look
// it up by email address.
2019-03-05 15:14:26 +00:00
check_null_responses_by_email(&client, "foo@invalid.example.com");
// And check that we can get it back via the machine readable
// interface.
check_mr_responses_by_fingerprint(&client, &tpk, 0);
// And check that we can see the human-readable result page.
check_hr_responses_by_fingerprint(&client, &tpk, 0);
2019-03-05 15:14:26 +00:00
2019-05-03 13:34:34 +00:00
// Check the verification link
check_verify_link(&client, &token, "foo@invalid.example.com");
// Now check for the verification mail.
check_mails_and_verify_email(&client, filemail_into.as_path());
2019-03-05 15:14:26 +00:00
// Now lookups using the mail address should work.
check_responses_by_email(&client, "foo@invalid.example.com", &tpk, 1);
// And check that we can see the human-readable result page.
check_hr_responses_by_fingerprint(&client, &tpk, 1);
// Request deletion of the binding.
vks_manage(&client, "foo@invalid.example.com");
// Confirm deletion.
2019-04-17 10:13:45 +00:00
check_mails_and_confirm_deletion(&client, filemail_into.as_path(), "foo@invalid.example.com");
// Now, we should no longer be able to look it up by email
// address.
check_null_responses_by_email(&client, "foo@invalid.example.com");
// But lookup by fingerprint should still work.
check_mr_responses_by_fingerprint(&client, &tpk, 0);
// And check that we can see the human-readable result page.
check_hr_responses_by_fingerprint(&client, &tpk, 0);
assert_consistency(client.rocket());
2019-03-05 15:14:26 +00:00
}
2019-03-04 16:45:50 +00:00
#[test]
fn upload_two() {
2019-05-03 13:34:34 +00:00
let (_tmpdir, config) = configuration().unwrap();
let rocket = rocket_factory(rocket::custom(config)).unwrap();
let client = Client::new(rocket).expect("valid rocket instance");
// Generate two keys and upload them.
let tpk_0 = TPKBuilder::autocrypt(
2019-06-07 20:30:51 +00:00
None, Some("foo@invalid.example.com"))
.generate().unwrap().0;
let tpk_1 = TPKBuilder::autocrypt(
2019-06-07 20:30:51 +00:00
None, Some("bar@invalid.example.com"))
.generate().unwrap().0;
let mut tpk_serialized = Vec::new();
tpk_0.serialize(&mut tpk_serialized).unwrap();
tpk_1.serialize(&mut tpk_serialized).unwrap();
2019-05-03 13:34:34 +00:00
vks_publish_submit_multiple(&client, &tpk_serialized);
// Prior to email confirmation, we should not be able to look
// them up by email address.
check_null_responses_by_email(&client, "foo@invalid.example.com");
check_null_responses_by_email(&client, "bar@invalid.example.com");
// And check that we can get them back via the machine readable
// interface.
check_mr_responses_by_fingerprint(&client, &tpk_0, 0);
check_mr_responses_by_fingerprint(&client, &tpk_1, 0);
// And check that we can see the human-readable result page.
check_hr_responses_by_fingerprint(&client, &tpk_0, 0);
check_hr_responses_by_fingerprint(&client, &tpk_1, 0);
2019-05-03 13:34:34 +00:00
}
#[test]
fn upload_verify_two() {
let (tmpdir, config) = configuration().unwrap();
let filemail_into = tmpdir.path().join("filemail");
let rocket = rocket_factory(rocket::custom(config)).unwrap();
let client = Client::new(rocket).expect("valid rocket instance");
// Generate two keys and upload them.
let tpk_1 = TPKBuilder::autocrypt(
2019-06-07 20:30:51 +00:00
None, Some("foo@invalid.example.com"))
2019-05-03 13:34:34 +00:00
.generate().unwrap().0;
let tpk_2 = TPKBuilder::autocrypt(
2019-06-07 20:30:51 +00:00
None, Some("bar@invalid.example.com"))
2019-05-03 13:34:34 +00:00
.generate().unwrap().0;
let mut tpk_serialized_1 = Vec::new();
tpk_1.serialize(&mut tpk_serialized_1).unwrap();
let token_1 = vks_publish_submit_get_token(&client, &tpk_serialized_1);
let mut tpk_serialized_2 = Vec::new();
tpk_2.serialize(&mut tpk_serialized_2).unwrap();
2019-05-20 21:17:50 +00:00
let token_2 = vks_publish_json_get_token(&client, &tpk_serialized_2);
2019-05-03 13:34:34 +00:00
// Prior to email confirmation, we should not be able to look
// them up by email address.
check_null_responses_by_email(&client, "foo@invalid.example.com");
check_null_responses_by_email(&client, "bar@invalid.example.com");
// And check that we can get them back via the machine readable
// interface.
check_mr_responses_by_fingerprint(&client, &tpk_1, 0);
check_mr_responses_by_fingerprint(&client, &tpk_2, 0);
// And check that we can see the human-readable result page.
check_hr_responses_by_fingerprint(&client, &tpk_1, 0);
check_hr_responses_by_fingerprint(&client, &tpk_2, 0);
// Check the verification link
check_verify_link(&client, &token_1, "foo@invalid.example.com");
2019-05-20 21:17:50 +00:00
check_verify_link_json(&client, &token_2, "bar@invalid.example.com");
// Now check for the verification mails.
2019-04-17 10:13:45 +00:00
check_mails_and_verify_email(&client, &filemail_into);
check_mails_and_verify_email(&client, &filemail_into);
// Now lookups using the mail address should work.
2019-05-03 13:34:34 +00:00
check_responses_by_email(&client, "foo@invalid.example.com", &tpk_1, 1);
check_responses_by_email(&client, "bar@invalid.example.com", &tpk_2, 1);
// Request deletion of the bindings.
2019-04-17 10:13:45 +00:00
vks_manage(&client, "foo@invalid.example.com");
check_mails_and_confirm_deletion(&client, &filemail_into, "foo@invalid.example.com");
vks_manage(&client, "bar@invalid.example.com");
check_mails_and_confirm_deletion(&client, &filemail_into, "bar@invalid.example.com");
// Now, we should no longer be able to look it up by email
// address.
check_null_responses_by_email(&client, "foo@invalid.example.com");
check_null_responses_by_email(&client, "bar@invalid.example.com");
// But lookup by fingerprint should still work.
check_mr_responses_by_fingerprint(&client, &tpk_1, 0);
2019-05-03 13:34:34 +00:00
check_mr_responses_by_fingerprint(&client, &tpk_2, 0);
// And check that we can see the human-readable result page.
check_hr_responses_by_fingerprint(&client, &tpk_1, 0);
2019-05-03 13:34:34 +00:00
check_hr_responses_by_fingerprint(&client, &tpk_2, 0);
assert_consistency(client.rocket());
}
#[test]
fn upload_no_key() {
let (_tmpdir, client) = client().unwrap();
let response = vks_publish_submit_response(&client, b"");
assert_eq!(response.status(), Status::BadRequest);
}
2019-06-05 12:33:02 +00:00
#[test]
fn upload_curl_shortcut() {
let (_tmpdir, client) = client().unwrap();
let (tpk, _) = TPKBuilder::autocrypt(
2019-06-07 20:30:51 +00:00
None, Some("foo@invalid.example.com"))
2019-06-05 12:33:02 +00:00
.generate().unwrap();
let mut tpk_serialized = Vec::new();
tpk.serialize(&mut tpk_serialized).unwrap();
let _token = vks_publish_shortcut_get_token(&client, &tpk_serialized);
check_mr_responses_by_fingerprint(&client, &tpk, 0);
check_null_responses_by_email(&client, "foo@invalid.example.com");
}
2019-03-05 15:14:26 +00:00
/// Asserts that the given URI 404s.
2019-03-12 11:18:28 +00:00
pub fn check_null_response(client: &Client, uri: &str) {
2019-03-05 15:14:26 +00:00
let response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::NotFound);
}
2019-03-12 12:16:10 +00:00
/// Asserts that lookups by the given email 404.
2019-03-12 11:18:28 +00:00
pub fn check_null_responses_by_email(client: &Client, addr: &str) {
2019-03-04 16:45:50 +00:00
check_null_response(
2019-03-05 15:14:26 +00:00
&client, &format!("/vks/v1/by-email/{}", addr));
2019-03-04 16:45:50 +00:00
check_null_response(
2019-03-05 15:14:26 +00:00
&client, &format!("/pks/lookup?op=get&search={}", addr));
2019-03-04 16:45:50 +00:00
check_null_response(
2019-03-05 15:14:26 +00:00
&client, &format!("/pks/lookup?op=get&options=mr&search={}",
addr));
}
2019-03-12 12:16:10 +00:00
/// Asserts that lookups by the given email are successful.
pub fn check_responses_by_email(client: &Client, addr: &str, tpk: &TPK,
nr_uids: usize) {
2019-03-12 12:16:10 +00:00
check_mr_response(
&client,
&format!("/vks/v1/by-email/{}", addr),
&tpk, nr_uids);
2019-03-12 12:16:10 +00:00
check_mr_response(
&client,
&format!("/vks/v1/by-email/{}", addr.replace("@", "%40")),
&tpk, nr_uids);
2019-03-12 12:16:10 +00:00
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search={}", addr),
&tpk, nr_uids);
2019-03-12 12:16:10 +00:00
check_hr_response(
&client,
2019-06-11 14:59:27 +00:00
&format!("/search?q={}", addr),
&tpk, nr_uids);
2019-03-12 12:16:10 +00:00
}
2019-03-05 15:14:26 +00:00
/// Asserts that the given URI returns a TPK matching the given
/// one, with the given number of userids.
2019-03-12 11:18:28 +00:00
pub fn check_mr_response(client: &Client, uri: &str, tpk: &TPK,
nr_uids: usize) {
2019-03-05 15:14:26 +00:00
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(),
Some(ContentType::new("application", "pgp-keys")));
let body = response.body_string().unwrap();
assert!(body.contains("END PGP PUBLIC KEY BLOCK"));
let tpk_ = TPK::from_bytes(body.as_bytes()).unwrap();
assert_eq!(tpk.fingerprint(), tpk_.fingerprint());
assert_eq!(tpk.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>(),
tpk_.subkeys().map(|skb| skb.subkey().fingerprint())
.collect::<Vec<_>>());
assert_eq!(tpk_.userids().count(), nr_uids);
}
/// Asserts that we can get the given TPK back using the various
/// by-fingerprint or by-keyid lookup mechanisms.
2019-03-12 11:18:28 +00:00
pub fn check_mr_responses_by_fingerprint(client: &Client, tpk: &TPK,
nr_uids: usize) {
2019-03-05 15:14:26 +00:00
let fp = tpk.fingerprint().to_hex();
let keyid = tpk.fingerprint().to_keyid().to_hex();
2019-02-26 10:34:53 +00:00
2019-03-05 12:36:26 +00:00
check_mr_response(
2019-03-05 15:14:26 +00:00
&client, &format!("/vks/v1/by-keyid/{}", keyid), &tpk, nr_uids);
2019-03-05 12:36:26 +00:00
check_mr_response(
2019-03-05 15:14:26 +00:00
&client, &format!("/vks/v1/by-fingerprint/{}", fp), &tpk, nr_uids);
2019-02-26 10:34:53 +00:00
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search={}", fp),
2019-03-05 15:14:26 +00:00
&tpk, nr_uids);
2019-02-26 10:34:53 +00:00
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search=0x{}", fp),
2019-03-05 15:14:26 +00:00
&tpk, nr_uids);
2019-02-26 10:34:53 +00:00
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search={}", keyid),
2019-03-05 15:14:26 +00:00
&tpk, nr_uids);
2019-02-26 10:34:53 +00:00
check_mr_response(
&client,
&format!("/pks/lookup?op=get&options=mr&search=0x{}", keyid),
2019-03-05 15:14:26 +00:00
&tpk, nr_uids);
2019-06-11 14:59:27 +00:00
check_mr_response(
&client,
&format!("/pks/lookup?op=get&search=0x{}", keyid),
&tpk, nr_uids);
2019-03-05 15:14:26 +00:00
}
2019-02-26 10:34:53 +00:00
2019-03-05 15:14:26 +00:00
/// Asserts that the given URI returns human readable response
/// page that contains a URI pointing to the TPK.
pub fn check_hr_response(client: &Client, uri: &str, tpk: &TPK,
nr_uids: usize) {
2019-03-05 15:14:26 +00:00
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
let body = response.body_string().unwrap();
assert!(body.contains("found"));
assert!(body.contains(&tpk.fingerprint().to_hex()));
// Extract the links.
let link_re = regex::Regex::new(
&format!("{}(/vks/[^ \t\n\"<]*)", BASE_URI)).unwrap();
let mut n = 0;
for link in link_re.captures_iter(&body) {
check_mr_response(client, link.get(1).unwrap().as_str(), tpk,
nr_uids);
n += 1;
}
assert!(n > 0);
2019-03-05 15:14:26 +00:00
}
/// Asserts that we can get the given TPK back using the various
/// by-fingerprint or by-keyid lookup mechanisms.
pub fn check_hr_responses_by_fingerprint(client: &Client, tpk: &TPK,
nr_uids: usize) {
2019-03-05 15:14:26 +00:00
let fp = tpk.fingerprint().to_hex();
let keyid = tpk.fingerprint().to_keyid().to_hex();
2019-02-26 10:34:53 +00:00
check_hr_response(
&client,
2019-06-11 14:59:27 +00:00
&format!("/search?q={}", fp),
&tpk, nr_uids);
2019-02-26 10:34:53 +00:00
check_hr_response(
&client,
2019-06-11 14:59:27 +00:00
&format!("/search?q=0x{}", fp),
&tpk, nr_uids);
2019-02-26 10:34:53 +00:00
check_hr_response(
&client,
2019-06-11 14:59:27 +00:00
&format!("/search?q={}", keyid),
&tpk, nr_uids);
2019-02-26 10:34:53 +00:00
check_hr_response(
&client,
2019-06-11 14:59:27 +00:00
&format!("/search?q=0x{}", keyid),
&tpk, nr_uids);
}
2019-05-03 13:34:34 +00:00
fn check_verify_link(client: &Client, token: &str, address: &str) {
let encoded = ::url::form_urlencoded::Serializer::new(String::new())
.append_pair("token", token)
.append_pair("address", address)
.finish();
2019-05-23 23:01:24 +00:00
let response = client.post("/upload/request-verify")
2019-05-03 13:34:34 +00:00
.header(ContentType::Form)
.body(encoded.as_bytes())
.dispatch();
assert_eq!(response.status(), Status::Ok);
}
2019-05-20 21:17:50 +00:00
fn check_verify_link_json(client: &Client, token: &str, address: &str) {
2019-05-23 23:01:24 +00:00
let json = format!(r#"{{"token":"{}","addresses":["{}"]}}"#, token, address);
2019-05-20 21:17:50 +00:00
let mut response = client.post("/vks/v1/request-verify")
.header(ContentType::JSON)
.body(json.as_bytes())
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert!(response.body_string().unwrap().contains("pending"));
}
fn check_mails_and_verify_email(client: &Client, filemail_path: &Path) {
2019-05-23 23:01:24 +00:00
let pattern = format!("{}(/verify/[^ \t\n]*)", BASE_URI);
2019-04-17 10:13:45 +00:00
let confirm_uri = pop_mail_capture_pattern(filemail_path, &pattern);
2019-03-11 11:06:44 +00:00
let response = client.get(&confirm_uri).dispatch();
assert_eq!(response.status(), Status::Ok);
}
2019-04-17 10:13:45 +00:00
fn check_mails_and_confirm_deletion(client: &Client, filemail_path: &Path, address: &str) {
2019-04-25 18:04:20 +00:00
let pattern = format!("{}/manage/([^ \t\n]*)", BASE_URI);
2019-04-17 10:13:45 +00:00
let token = pop_mail_capture_pattern(filemail_path, &pattern);
vks_manage_delete(client, &token, address);
}
fn pop_mail_capture_pattern(filemail_path: &Path, pattern: &str) -> String {
let mail_content = pop_mail(filemail_path).unwrap().unwrap();
println!("{}", mail_content);
2019-04-17 10:13:45 +00:00
let capture_re = regex::bytes::Regex::new(pattern).unwrap();
let capture_content = capture_re.captures(mail_content.as_ref()).unwrap()
.get(1).unwrap().as_bytes();
2019-04-17 10:13:45 +00:00
String::from_utf8_lossy(capture_content).to_string()
}
/// Returns and removes the first mail it finds from the given
/// directory.
pub fn pop_mail(dir: &Path) -> Result<Option<String>> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
let fh = fs::File::open(entry.path())?;
fs::remove_file(entry.path())?;
let mail: SerializableEmail = ::serde_json::from_reader(fh)?;
return Ok(Some(String::from_utf8_lossy(&mail.message).to_string()));
}
}
Ok(None)
2019-02-26 10:34:53 +00:00
}
2019-05-03 13:34:34 +00:00
fn vks_publish_submit_multiple<'a>(client: &'a Client, data: &[u8]) {
let mut response = vks_publish_submit_response(client, data);
let response_body = response.body_string().unwrap();
assert_eq!(response.status(), Status::Ok);
assert!(response_body.contains("you must upload them individually"));
}
fn vks_publish_submit_get_token<'a>(client: &'a Client, data: &[u8]) -> String {
let mut response = vks_publish_submit_response(client, data);
let response_body = response.body_string().unwrap();
let pattern = "name=\"token\" value=\"([^\"]*)\"";
let capture_re = regex::bytes::Regex::new(pattern).unwrap();
let capture_content = capture_re .captures(response_body.as_bytes()).unwrap()
.get(1).unwrap().as_bytes();
let token = String::from_utf8_lossy(capture_content).to_string();
assert_eq!(response.status(), Status::Ok);
2019-05-03 13:34:34 +00:00
token
}
fn vks_publish_submit_response<'a>(client: &'a Client, data: &[u8]) ->
LocalResponse<'a> {
2019-02-26 10:34:53 +00:00
let ct = ContentType::with_params(
"multipart", "form-data",
("boundary", "---------------------------14733842173518794281682249499"));
let header =
b"-----------------------------14733842173518794281682249499\r\n\
Content-Disposition: form-data; name=\"csrf\"\r\n\
\r\n\
\r\n\
-----------------------------14733842173518794281682249499\r\n\
Content-Disposition: form-data; name=\"keytext\"; filename=\".k\"\r\n\
Content-Type: application/octet-stream\r\n\
\r\n";
let footer = b"\r\n-----------------------------14733842173518794281682249499--";
let mut body = Vec::new();
body.extend_from_slice(header);
body.extend_from_slice(data);
body.extend_from_slice(footer);
2019-05-23 23:01:24 +00:00
client.post("/upload/submit")
2019-02-26 10:34:53 +00:00
.header(ct)
.body(&body[..])
.dispatch()
2019-02-26 10:34:53 +00:00
}
2019-06-05 12:33:02 +00:00
fn vks_publish_shortcut_get_token<'a>(client: &'a Client, data: &[u8]) -> String {
let mut response = client.put("/")
.body(data)
.dispatch();
let response_body = response.body_string().unwrap();
assert_eq!(response.status(), Status::Ok);
assert!(response_body.contains("Key successfully uploaded"));
let pattern = format!("{}/upload/([^ \t\n]*)", BASE_URI);
let capture_re = regex::bytes::Regex::new(&pattern).unwrap();
let capture_content = capture_re .captures(response_body.as_bytes()).unwrap()
.get(1).unwrap().as_bytes();
String::from_utf8_lossy(capture_content).to_string()
}
2019-05-20 21:17:50 +00:00
fn vks_publish_json_get_token<'a>(client: &'a Client, data: &[u8]) -> String {
2019-05-23 23:01:24 +00:00
let mut response = client.post("/vks/v1/upload")
2019-05-20 21:17:50 +00:00
.header(ContentType::JSON)
.body(format!(r#"{{ "keytext": "{}" }}"#, base64::encode(data)))
.dispatch();
let response_body = response.body_string().unwrap();
2019-05-23 23:01:24 +00:00
let result: vks_api::json::UploadResult = serde_json::from_str(&response_body).unwrap();
2019-05-20 21:17:50 +00:00
assert_eq!(response.status(), Status::Ok);
result.token
}
fn vks_manage<'a>(client: &'a Client, search_term: &str) {
let encoded = ::url::form_urlencoded::Serializer::new(String::new())
.append_pair("search_term", search_term)
.finish();
2019-04-25 18:04:20 +00:00
let response = client.post("/manage")
2019-04-17 10:13:45 +00:00
.header(ContentType::Form)
.body(encoded.as_bytes())
.dispatch();
assert_eq!(response.status(), Status::Ok);
}
fn vks_manage_delete(client: &Client, token: &str, address: &str) {
let encoded = ::url::form_urlencoded::Serializer::new(String::new())
.append_pair("token", token)
.append_pair("address", address)
.finish();
2019-04-25 18:04:20 +00:00
let response = client.post("/manage/unpublish")
.header(ContentType::Form)
.body(encoded.as_bytes())
.dispatch();
assert_eq!(response.status(), Status::Ok);
}
}