mirror of
https://github.com/mCaptcha/mCaptcha.git
synced 2025-11-27 15:53:46 +00:00
453 lines
14 KiB
Rust
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());
|
|
}
|
|
}
|