453 lines
14 KiB
Rust

// Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::{defense::Level, defense::LevelBuilder};
use serde::{Deserialize, Serialize};
use db_core::TrafficPattern;
use super::create::{runner::create as create_runner, CreateCaptcha};
use super::update::{runner::update_captcha as update_captcha_runner, UpdateCaptcha};
use crate::errors::*;
use crate::settings::DefaultDifficultyStrategy;
use crate::AppData;
pub mod routes {
pub struct Easy {
/// easy is using defaults
pub create: &'static str,
pub update: &'static str,
}
impl Easy {
pub const fn new() -> Self {
Self {
create: "/api/v1/mcaptcha/add/easy",
update: "/api/v1/mcaptcha/update/easy",
}
}
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(update);
cfg.service(create);
}
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
/// User's traffic pattern; used in generating a captcha configuration
pub struct TrafficPatternRequest {
/// average traffic of user's website
pub avg_traffic: u32,
/// the peak traffic that the user's website can handle
pub peak_sustainable_traffic: u32,
/// traffic that bought the user's website down; optional
pub broke_my_site_traffic: Option<u32>,
/// Captcha description
pub description: String,
/// publish benchmarks
pub publish_benchmarks: bool,
}
impl From<&TrafficPatternRequest> for TrafficPattern {
fn from(t: &TrafficPatternRequest) -> Self {
TrafficPattern {
avg_traffic: t.avg_traffic,
peak_sustainable_traffic: t.peak_sustainable_traffic,
broke_my_site_traffic: t.broke_my_site_traffic,
}
}
}
pub fn calculate(
tp: &TrafficPattern,
strategy: &DefaultDifficultyStrategy,
) -> ServiceResult<Vec<Level>> {
let mut levels = vec![
LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty)?
.visitor_threshold(tp.avg_traffic)
.build()?,
LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)?
.visitor_threshold(tp.peak_sustainable_traffic)
.build()?,
];
let mut highest_level = LevelBuilder::default();
highest_level.difficulty_factor(strategy.broke_my_site_traffic_difficulty)?;
match tp.broke_my_site_traffic {
Some(broke_my_site_traffic) => {
highest_level.visitor_threshold(broke_my_site_traffic)
}
None => match tp
.peak_sustainable_traffic
.checked_add(tp.peak_sustainable_traffic / 2)
{
Some(num) => highest_level.visitor_threshold(num),
// TODO check for overflow: database saves these values as i32, so this u32 is cast
// into i32. Should choose bigger number or casts properly
None => highest_level.visitor_threshold(u32::MAX),
},
};
levels.push(highest_level.build()?);
Ok(levels)
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.easy.create",
wrap = "crate::api::v1::get_middleware()"
)]
async fn create(
payload: web::Json<TrafficPatternRequest>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let payload = payload.into_inner();
let pattern = (&payload).into();
let levels =
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?;
let msg = CreateCaptcha {
levels,
duration: data.settings.captcha.default_difficulty_strategy.duration,
description: payload.description,
publish_benchmarks: payload.publish_benchmarks,
};
let mcaptcha_config = create_runner(&msg, &data, &username).await?;
data.db
.add_traffic_pattern(&username, &mcaptcha_config.key, &pattern)
.await?;
Ok(HttpResponse::Ok().json(mcaptcha_config))
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UpdateTrafficPattern {
pub pattern: TrafficPatternRequest,
pub key: String,
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.easy.update",
wrap = "crate::api::v1::get_middleware()"
)]
async fn update(
payload: web::Json<UpdateTrafficPattern>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let payload = payload.into_inner();
let pattern = (&payload.pattern).into();
let levels =
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?;
let msg = UpdateCaptcha {
levels,
duration: data.settings.captcha.default_difficulty_strategy.duration,
description: payload.pattern.description,
key: payload.key,
publish_benchmarks: payload.pattern.publish_benchmarks,
};
update_captcha_runner(&msg, &data, &username).await?;
data.db.delete_traffic_pattern(&username, &msg.key).await?;
data.db
.add_traffic_pattern(&username, &msg.key, &pattern)
.await?;
Ok(HttpResponse::Ok())
}
#[cfg(test)]
pub mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use actix_web::web::Bytes;
use super::*;
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
use crate::api::v1::ROUTES;
use crate::tests::*;
use crate::*;
mod isoloated_test {
use super::{calculate, LevelBuilder};
use db_core::TrafficPattern;
#[test]
fn easy_configuration_works() {
let settings = crate::tests::get_settings();
let mut payload = TrafficPattern {
avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000),
};
let strategy = &settings.captcha.default_difficulty_strategy;
let l1 = LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty)
.unwrap()
.visitor_threshold(payload.avg_traffic)
.build()
.unwrap();
let l2 = LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)
.unwrap()
.visitor_threshold(payload.peak_sustainable_traffic)
.build()
.unwrap();
let l3 = LevelBuilder::default()
.difficulty_factor(strategy.broke_my_site_traffic_difficulty)
.unwrap()
.visitor_threshold(payload.broke_my_site_traffic.unwrap())
.build()
.unwrap();
let levels = vec![l1, l2, l3];
assert_eq!(calculate(&payload, strategy).unwrap(), levels);
let estimated_lmax = LevelBuilder::default()
.difficulty_factor(strategy.broke_my_site_traffic_difficulty)
.unwrap()
.visitor_threshold(1500000)
.build()
.unwrap();
payload.broke_my_site_traffic = None;
assert_eq!(
calculate(&payload, strategy).unwrap(),
vec![l1, l2, estimated_lmax]
);
let lmax = LevelBuilder::default()
.difficulty_factor(strategy.broke_my_site_traffic_difficulty)
.unwrap()
.visitor_threshold(u32::MAX)
.build()
.unwrap();
let very_large_l2_peak_traffic = u32::MAX - 1;
let very_large_l2 = LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)
.unwrap()
.visitor_threshold(very_large_l2_peak_traffic)
.build()
.unwrap();
// payload.broke_my_site_traffic = Some(very_large_l2_peak_traffic);
payload.peak_sustainable_traffic = very_large_l2_peak_traffic;
assert_eq!(
calculate(&payload, strategy).unwrap(),
vec![l1, very_large_l2, lmax]
);
}
}
#[actix_rt::test]
async fn easy_works_pg() {
let data = crate::tests::pg::get_data().await;
easy_works(data).await;
}
#[actix_rt::test]
async fn easy_works_maria() {
let data = crate::tests::maria::get_data().await;
easy_works(data).await;
}
pub async fn easy_works(data: ArcData) {
const NAME: &str = "defaultuserconfgworks";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "defaultuserconfgworks@a.com";
let data = &data;
delete_user(data, NAME).await;
let (_creds, signin_resp) =
register_and_signin(data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let payload = TrafficPatternRequest {
avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000),
description: NAME.into(),
publish_benchmarks: false,
};
let default_levels = calculate(
&(&payload).into(),
&data.settings.captcha.default_difficulty_strategy,
)
.unwrap();
// START create_easy
let add_token_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.captcha.easy.create)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
let get_level_resp = test::call_service(
&app,
post_request!(&token_key, ROUTES.captcha.get)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
assert_eq!(res_levels, default_levels);
assert!(!data
.db
.analytics_captcha_is_published(&token_key.key)
.await
.unwrap());
// END create_easy
// START update_easy
let update_pattern = TrafficPatternRequest {
avg_traffic: 1_000,
peak_sustainable_traffic: 10_000,
broke_my_site_traffic: Some(1_000_000),
description: NAME.into(),
publish_benchmarks: true,
};
let updated_default_values = calculate(
&(&update_pattern).into(),
&data.settings.captcha.default_difficulty_strategy,
)
.unwrap();
let payload = UpdateTrafficPattern {
pattern: update_pattern,
key: token_key.key.clone(),
};
let update_token_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.captcha.easy.update)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(update_token_resp.status(), StatusCode::OK);
assert!(data
.db
.analytics_captcha_is_published(&token_key.key)
.await
.unwrap());
let get_level_resp = test::call_service(
&app,
post_request!(&token_key, ROUTES.captcha.get)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
assert_ne!(res_levels, default_levels);
assert_eq!(res_levels, updated_default_values);
// END update_easy
// test easy edit page
let easy_url = PAGES.panel.sitekey.get_edit_easy(&token_key.key);
let easy_edit_page = test::call_service(
&app,
test::TestRequest::get()
.uri(&easy_url)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(easy_edit_page.status(), StatusCode::OK);
let body: Bytes = test::read_body(easy_edit_page).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains(&token_key.name));
assert!(body.contains(
&payload
.pattern
.broke_my_site_traffic
.as_ref()
.unwrap()
.to_string()
));
assert!(body.contains(&payload.pattern.avg_traffic.to_string()));
assert!(body.contains(&payload.pattern.peak_sustainable_traffic.to_string()));
// START update_easy to delete published results
let mut payload2 = TrafficPatternRequest {
avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000),
description: NAME.into(),
publish_benchmarks: true,
};
let add_token_resp = test::call_service(
&app,
post_request!(&payload2, ROUTES.captcha.easy.create)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
assert!(data
.db
.analytics_captcha_is_published(&token_key.key)
.await
.unwrap());
let token_key2: MCaptchaDetails = test::read_body_json(add_token_resp).await;
payload2.publish_benchmarks = false;
let payload = UpdateTrafficPattern {
pattern: payload2,
key: token_key2.key.clone(),
};
let update_token_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.captcha.easy.update)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(update_token_resp.status(), StatusCode::OK);
assert!(!data
.db
.analytics_captcha_is_published(&token_key2.key)
.await
.unwrap());
}
}