diff --git a/config/default.toml b/config/default.toml index 72e0f7b9..51d787df 100644 --- a/config/default.toml +++ b/config/default.toml @@ -34,8 +34,11 @@ enable_stats = true [captcha.default_difficulty_strategy] avg_traffic_difficulty = 50000 # almost instant solution +#avg_traffic_time = 1 # almost instant solution peak_sustainable_traffic_difficulty = 3000000 # roughly 1.5s +#peak_sustainable_traffic_time = 3 broke_my_site_traffic_difficulty = 5000000 # greater than 3.5s +#broke_my_site_traffic_time = 5 duration = 30 # cooldown period in seconds [database] diff --git a/db/db-core/src/lib.rs b/db/db-core/src/lib.rs index 98b8336a..2ae7a17e 100644 --- a/db/db-core/src/lib.rs +++ b/db/db-core/src/lib.rs @@ -202,6 +202,13 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase { captcha_key: &str, ) -> DBResult; + /// Get all easy captcha configurations on instance + async fn get_all_easy_captchas( + &self, + limit: usize, + offset: usize, + ) -> DBResult>; + /// Delete traffic configuration async fn delete_traffic_pattern( &self, @@ -383,6 +390,19 @@ pub struct AddNotification<'a> { pub message: &'a str, } +#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)] +/// Represents Easy captcha configuration +pub struct EasyCaptcha { + /// traffic pattern of easy captcha + pub traffic_pattern: TrafficPattern, + /// captcha key/sitekey + pub key: String, + /// captcha description + pub description: String, + /// Owner of the captcha configuration + pub username: String, +} + #[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)] /// User's traffic pattern; used in generating a captcha configuration pub struct TrafficPattern { diff --git a/db/db-core/src/tests.rs b/db/db-core/src/tests.rs index d0204a1a..ee91d828 100644 --- a/db/db-core/src/tests.rs +++ b/db/db-core/src/tests.rs @@ -223,6 +223,11 @@ pub async fn database_works<'a, T: MCDatabase>( tp ); + // get all traffic patterns + let patterns = db.get_all_easy_captchas(10, 0).await.unwrap(); + assert_eq!(patterns.get(0).as_ref().unwrap().key, c.key); + assert_eq!(&patterns.get(0).unwrap().traffic_pattern, tp); + // delete traffic pattern db.delete_traffic_pattern(p.username, c.key).await.unwrap(); assert!( diff --git a/db/db-sqlx-maria/.sqlx/query-d587844217f202c23d29c3cb4c819551bc204dd459c956c41024fa74aadbba64.json b/db/db-sqlx-maria/.sqlx/query-d587844217f202c23d29c3cb4c819551bc204dd459c956c41024fa74aadbba64.json new file mode 100644 index 00000000..712493aa --- /dev/null +++ b/db/db-sqlx-maria/.sqlx/query-d587844217f202c23d29c3cb4c819551bc204dd459c956c41024fa74aadbba64.json @@ -0,0 +1,80 @@ +{ + "db_name": "MySQL", + "query": "SELECT \n mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic,\n mcaptcha_config.name,\n mcaptcha_users.name as username,\n mcaptcha_config.captcha_key\n FROM \n mcaptcha_sitekey_user_provided_avg_traffic \n INNER JOIN\n mcaptcha_config\n ON\n mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id\n INNER JOIN\n mcaptcha_users\n ON\n mcaptcha_config.user_id = mcaptcha_users.ID\n ORDER BY mcaptcha_config.config_id\n LIMIT ? OFFSET ?", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "avg_traffic", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "peak_sustainable_traffic", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 11 + } + }, + { + "ordinal": 2, + "name": "broke_my_site_traffic", + "type_info": { + "type": "Long", + "flags": "", + "char_set": 63, + "max_size": 11 + } + }, + { + "ordinal": 3, + "name": "name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 400 + } + }, + { + "ordinal": 4, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 400 + } + }, + { + "ordinal": 5, + "name": "captcha_key", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 400 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + false, + false, + false + ] + }, + "hash": "d587844217f202c23d29c3cb4c819551bc204dd459c956c41024fa74aadbba64" +} diff --git a/db/db-sqlx-maria/src/lib.rs b/db/db-sqlx-maria/src/lib.rs index 40e7f463..1c77910a 100644 --- a/db/db-sqlx-maria/src/lib.rs +++ b/db/db-sqlx-maria/src/lib.rs @@ -1273,6 +1273,66 @@ impl MCDatabase for Database { Err(e) => Err(map_row_not_found_err(e, DBError::CaptchaNotFound)), } } + + /// Get all easy captcha configurations on instance + async fn get_all_easy_captchas( + &self, + limit: usize, + offset: usize, + ) -> DBResult> { + struct InnerEasyCaptcha { + captcha_key: String, + name: String, + username: String, + peak_sustainable_traffic: i32, + avg_traffic: i32, + broke_my_site_traffic: Option, + } + let mut inner_res = sqlx::query_as!( + InnerEasyCaptcha, + "SELECT + mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic, + mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic, + mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic, + mcaptcha_config.name, + mcaptcha_users.name as username, + mcaptcha_config.captcha_key + FROM + mcaptcha_sitekey_user_provided_avg_traffic + INNER JOIN + mcaptcha_config + ON + mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id + INNER JOIN + mcaptcha_users + ON + mcaptcha_config.user_id = mcaptcha_users.ID + ORDER BY mcaptcha_config.config_id + LIMIT ? OFFSET ?", + limit as i64, + offset as i64 + ) + .fetch_all(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?; + let mut res = Vec::with_capacity(inner_res.len()); + inner_res.drain(0..).for_each(|v| { + res.push(EasyCaptcha { + key: v.captcha_key, + description: v.name, + username: v.username, + traffic_pattern: TrafficPattern { + broke_my_site_traffic: v + .broke_my_site_traffic + .as_ref() + .map(|v| *v as u32), + avg_traffic: v.avg_traffic as u32, + peak_sustainable_traffic: v.peak_sustainable_traffic as u32, + }, + }) + }); + Ok(res) + } } #[derive(Clone)] diff --git a/db/db-sqlx-postgres/.sqlx/query-f01a9c09c8722bc195f477a8c3ce6466d415e7c74665fa882eff4a8566e70577.json b/db/db-sqlx-postgres/.sqlx/query-f01a9c09c8722bc195f477a8c3ce6466d415e7c74665fa882eff4a8566e70577.json new file mode 100644 index 00000000..cc409744 --- /dev/null +++ b/db/db-sqlx-postgres/.sqlx/query-f01a9c09c8722bc195f477a8c3ce6466d415e7c74665fa882eff4a8566e70577.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic,\n mcaptcha_config.name,\n mcaptcha_users.name as username,\n mcaptcha_config.key\n FROM \n mcaptcha_sitekey_user_provided_avg_traffic \n INNER JOIN\n mcaptcha_config\n ON\n mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id\n INNER JOIN\n mcaptcha_users\n ON\n mcaptcha_config.user_id = mcaptcha_users.ID\n ORDER BY mcaptcha_config.config_id\n OFFSET $1 LIMIT $2; ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "avg_traffic", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "peak_sustainable_traffic", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "broke_my_site_traffic", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "key", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false + ] + }, + "hash": "f01a9c09c8722bc195f477a8c3ce6466d415e7c74665fa882eff4a8566e70577" +} diff --git a/db/db-sqlx-postgres/src/lib.rs b/db/db-sqlx-postgres/src/lib.rs index 835d33e3..f52e1b28 100644 --- a/db/db-sqlx-postgres/src/lib.rs +++ b/db/db-sqlx-postgres/src/lib.rs @@ -669,13 +669,8 @@ impl MCDatabase for Database { username: &str, captcha_key: &str, ) -> DBResult { - struct Traffic { - peak_sustainable_traffic: i32, - avg_traffic: i32, - broke_my_site_traffic: Option, - } let res = sqlx::query_as!( - Traffic, + InnerTraffic, "SELECT avg_traffic, peak_sustainable_traffic, @@ -706,11 +701,67 @@ impl MCDatabase for Database { .fetch_one(&self.pool) .await .map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?; - Ok(TrafficPattern { - broke_my_site_traffic: res.broke_my_site_traffic.as_ref().map(|v| *v as u32), - avg_traffic: res.avg_traffic as u32, - peak_sustainable_traffic: res.peak_sustainable_traffic as u32, - }) + Ok(res.into()) + } + + /// Get all easy captcha configurations on instance + async fn get_all_easy_captchas( + &self, + limit: usize, + offset: usize, + ) -> DBResult> { + struct InnerEasyCaptcha { + key: String, + peak_sustainable_traffic: i32, + avg_traffic: i32, + broke_my_site_traffic: Option, + name: String, + username: String, + } + let mut inner_res = sqlx::query_as!( + InnerEasyCaptcha, + "SELECT + mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic, + mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic, + mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic, + mcaptcha_config.name, + mcaptcha_users.name as username, + mcaptcha_config.key + FROM + mcaptcha_sitekey_user_provided_avg_traffic + INNER JOIN + mcaptcha_config + ON + mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id + INNER JOIN + mcaptcha_users + ON + mcaptcha_config.user_id = mcaptcha_users.ID + ORDER BY mcaptcha_config.config_id + OFFSET $1 LIMIT $2; ", + offset as i32, + limit as i32 + ) + .fetch_all(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?; + let mut res = Vec::with_capacity(inner_res.len()); + inner_res.drain(0..).for_each(|v| { + res.push(EasyCaptcha { + key: v.key, + description: v.name, + username: v.username, + traffic_pattern: TrafficPattern { + broke_my_site_traffic: v + .broke_my_site_traffic + .as_ref() + .map(|v| *v as u32), + avg_traffic: v.avg_traffic as u32, + peak_sustainable_traffic: v.peak_sustainable_traffic as u32, + }, + }) + }); + Ok(res) } /// Delete traffic configuration @@ -1345,3 +1396,19 @@ impl From for Captcha { } } } + +struct InnerTraffic { + peak_sustainable_traffic: i32, + avg_traffic: i32, + broke_my_site_traffic: Option, +} + +impl From for TrafficPattern { + fn from(v: InnerTraffic) -> Self { + TrafficPattern { + broke_my_site_traffic: v.broke_my_site_traffic.as_ref().map(|v| *v as u32), + avg_traffic: v.avg_traffic as u32, + peak_sustainable_traffic: v.peak_sustainable_traffic as u32, + } + } +} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d964a484..e08356d7 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -56,23 +56,22 @@ you will be overriding the values set in the configuration files. ### Captcha -| Name | Value | -| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --- | -| `MCAPTCHA_captcha_SALT` | Salt has to be long and random | -| `MCAPTCHA_captcha_GC` | Garbage collection duration in seconds, requires tuning but 30 is a good starting point | -| `MCAPTCHA_captcha_RUNNERS` | [Performance] Number of runners to use for PoW validation. Defaults to number of CPUs available | -| `MCAPTCHA_captcha_QUEUE_LENGTH` | [Performance] PoW Validation queue length, controls how many pending validation jobs can be held in queue | -| `MCAPTCHA_captcha_ENABLE_STATS` | Record for CAPTCHA events like configuration fetch, solves and authentication of validation token. Useful for commercial deployments. | | -| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_difficulty`% | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for average traffic metric | -| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_difficulty`% | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for peak traffic metric | -| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_difficulty`% | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for traffic that took the website down | -| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration`% | Default duration to use in CAPTCHA configuration in easy mode | +| Name | Value | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `MCAPTCHA_captcha_SALT` | Salt has to be long and random | +| `MCAPTCHA_captcha_GC` | Garbage collection duration in seconds, requires tuning but 30 is a good starting point | +| `MCAPTCHA_captcha_RUNNERS` | [Performance] Number of runners to use for PoW validation. Defaults to number of CPUs available | +| `MCAPTCHA_captcha_QUEUE_LENGTH` | [Performance] PoW Validation queue length, controls how many pending validation jobs can be held in queue | +| `MCAPTCHA_captcha_ENABLE_STATS` | Record for CAPTCHA events like configuration fetch, solves and authentication of validation token. Useful for commercial deployments. | +| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_difficulty` | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for average traffic metric | +| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_time` | This difficulty factor is used in to use in easy mode CAPTCHA configuration estimation for average traffic metric | +| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_difficulty` | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for peak traffic metric | +| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_time` | This difficulty factor is used in to use in easy mode CAPTCHA configuration estimation for peak traffic metric | +| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_difficulty` | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for traffic that took the website down | +| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_time` | Default time (in seconds) to use to compute difficulty factor using stored PoW performance records. | +| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration` | Default duration to use in CAPTCHA configuration in easy mode | -\% See commits -[`54b14291ec140e`](https://github.com/mCaptcha/mCaptcha/commit/54b14291ec140ea4cbbf73462d3d6fc2d39f2d2c) -and -[`42544ec421e0`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065) -for more info. +See commits [`54b14291ec140e`](https://github.com/mCaptcha/mCaptcha/commit/54b14291ec140ea4cbbf73462d3d6fc2d39f2d2c) and [`42544ec421e0`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065) for more info. ### SMTP diff --git a/src/api/v1/mcaptcha/easy.rs b/src/api/v1/mcaptcha/easy.rs index 65698aef..50d66ec2 100644 --- a/src/api/v1/mcaptcha/easy.rs +++ b/src/api/v1/mcaptcha/easy.rs @@ -101,6 +101,79 @@ pub fn calculate( Ok(levels) } +async fn calculate_with_percentile( + data: &AppData, + tp: &TrafficPattern, +) -> ServiceResult>> { + use crate::api::v1::stats::{percentile_bench_runner, PercentileReq}; + + let strategy = &data.settings.captcha.default_difficulty_strategy; + + if strategy.avg_traffic_time.is_none() + && strategy.peak_sustainable_traffic_time.is_none() + && strategy.broke_my_site_traffic_time.is_none() + { + return Ok(None); + } + + let mut req = PercentileReq { + time: strategy.avg_traffic_time.unwrap(), + percentile: 90.00, + }; + let resp = percentile_bench_runner(data, &req).await?; + if resp.difficulty_factor.is_none() { + return Ok(None); + } + let avg_traffic_difficulty = resp.difficulty_factor.unwrap(); + + req.time = strategy.peak_sustainable_traffic_time.unwrap(); + let resp = percentile_bench_runner(data, &req).await?; + if resp.difficulty_factor.is_none() { + return Ok(None); + } + let peak_sustainable_traffic_difficulty = resp.difficulty_factor.unwrap(); + + req.time = strategy.broke_my_site_traffic_time.unwrap(); + let resp = percentile_bench_runner(data, &req).await?; + let broke_my_site_traffic_difficulty = if resp.difficulty_factor.is_none() { + resp.difficulty_factor.unwrap() + } else { + peak_sustainable_traffic_difficulty * 2 + }; + + let mut levels = vec![ + LevelBuilder::default() + .difficulty_factor(avg_traffic_difficulty)? + .visitor_threshold(tp.avg_traffic) + .build()?, + LevelBuilder::default() + .difficulty_factor(peak_sustainable_traffic_difficulty)? + .visitor_threshold(tp.peak_sustainable_traffic) + .build()?, + ]; + let mut highest_level = LevelBuilder::default(); + highest_level.difficulty_factor(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(Some(levels)) +} + #[my_codegen::post( path = "crate::V1_API_ROUTES.captcha.easy.create", wrap = "crate::api::v1::get_middleware()" @@ -113,8 +186,12 @@ async fn create( 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 levels = if let Some(levels) = calculate_with_percentile(&data, &pattern).await? + { + levels + } else { + calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)? + }; let msg = CreateCaptcha { levels, duration: data.settings.captcha.default_difficulty_strategy.duration, @@ -147,6 +224,15 @@ async fn update( ) -> ServiceResult { let username = id.identity().unwrap(); let payload = payload.into_inner(); + update_runner(&data, payload, username).await?; + Ok(HttpResponse::Ok()) +} + +pub async fn update_runner( + data: &AppData, + payload: UpdateTrafficPattern, + username: String, +) -> ServiceResult<()> { let pattern = (&payload.pattern).into(); let levels = calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?; @@ -167,7 +253,7 @@ async fn update( .add_traffic_pattern(&username, &msg.key, &pattern) .await?; - Ok(HttpResponse::Ok()) + Ok(()) } #[cfg(test)] diff --git a/src/demo.rs b/src/demo.rs index 54da1f3c..b1ef9d74 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -8,6 +8,7 @@ use std::time::Duration; use actix::clock::sleep; use actix::spawn; +use tokio::sync::oneshot::{channel, error::TryRecvError, Receiver, Sender}; use tokio::task::JoinHandle; use crate::api::v1::account::delete::runners::delete_user; @@ -23,20 +24,24 @@ pub const DEMO_USER: &str = "aaronsw"; pub const DEMO_PASSWORD: &str = "password"; pub struct DemoUser { - handle: JoinHandle<()>, + tx: Sender<()>, } impl DemoUser { - pub async fn spawn(data: AppData, duration: Duration) -> ServiceResult { - let handle = Self::run(data, duration).await?; - let d = Self { handle }; + pub async fn spawn( + data: AppData, + duration: u32, + ) -> ServiceResult<(Self, JoinHandle<()>)> { + let (tx, rx) = channel(); + let handle = Self::run(data, duration, rx).await?; + let d = Self { tx }; - Ok(d) + Ok((d, handle)) } #[allow(dead_code)] - pub fn abort(&self) { - self.handle.abort(); + pub fn abort(mut self) { + self.tx.send(()); } /// register demo user runner @@ -71,16 +76,38 @@ impl DemoUser { pub async fn run( data: AppData, - duration: Duration, + duration: u32, + mut rx: Receiver<()>, ) -> ServiceResult> { Self::register_demo_user(&data).await?; + fn can_run(rx: &mut Receiver<()>) -> bool { + match rx.try_recv() { + Err(TryRecvError::Empty) => true, + _ => false, + } + } + + let mut exit = false; let fut = async move { loop { - sleep(duration).await; + if exit { + break; + } + for _ in 0..duration { + if can_run(&mut rx) { + sleep(Duration::new(1, 0)).await; + continue; + } else { + exit = true; + break; + } + } + if let Err(e) = Self::delete_demo_user(&data).await { log::error!("Error while deleting demo user: {:?}", e); } + if let Err(e) = Self::register_demo_user(&data).await { log::error!("Error while registering demo user: {:?}", e); } @@ -133,7 +160,7 @@ mod tests { assert!(!username_exists(&payload, &data).await.unwrap().exists); // test the runner - let user = DemoUser::spawn(data, duration).await.unwrap(); + let user = DemoUser::spawn(data, DURATION as u32).await.unwrap(); let (_, signin_resp, token_key) = add_levels_util(data_inner, DEMO_USER, DEMO_PASSWORD).await; let cookies = get_cookie!(signin_resp); @@ -162,6 +189,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); let res_levels: Vec = test::read_body_json(resp).await; assert!(res_levels.is_empty()); - user.abort(); + user.0.abort(); + user.1.await.unwrap(); } } diff --git a/src/easy.rs b/src/easy.rs new file mode 100644 index 00000000..e6d880f1 --- /dev/null +++ b/src/easy.rs @@ -0,0 +1,132 @@ +// Copyright (C) 2024// Copyright (C) 2024 Aravinth Manivannan +// SPDX-FileCopyrightText: 2023 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::time::Duration; +//use std::sync::atomicBool + +use actix::clock::sleep; +use actix::spawn; +use tokio::sync::oneshot::{channel, error::TryRecvError, Receiver, Sender}; +use tokio::task::JoinHandle; + +use crate::api::v1::mcaptcha::easy::{ + update_runner, TrafficPatternRequest, UpdateTrafficPattern, +}; +use crate::*; + +use errors::*; + +pub struct UpdateEasyCaptcha { + tx: Sender<()>, +} + +impl UpdateEasyCaptcha { + pub async fn spawn( + data: AppData, + duration: u32, + ) -> ServiceResult<(Self, JoinHandle<()>)> { + let (tx, rx) = channel(); + let handle = Self::run(data, duration, rx).await?; + let d = Self { tx }; + + Ok((d, handle)) + } + + #[allow(dead_code)] + pub fn abort(mut self) { + self.tx.send(()); + } + + /// update configurations + async fn update_captcha_configurations( + data: &AppData, + rx: &mut Receiver<()>, + ) -> ServiceResult<()> { + let limit = 10; + let mut offset = 0; + let mut page = 0; + loop { + offset = page * limit; + + if !Self::can_run(rx) { + return Ok(()); + } + + let mut patterns = data.db.get_all_easy_captchas(limit, offset).await?; + for pattern in patterns.drain(0..) { + if !Self::can_run(rx) { + return Ok(()); + } + + let publish_benchmarks = + data.db.analytics_captcha_is_published(&pattern.key).await?; + + let req = UpdateTrafficPattern { + pattern: TrafficPatternRequest { + avg_traffic: pattern.traffic_pattern.avg_traffic, + peak_sustainable_traffic: pattern + .traffic_pattern + .peak_sustainable_traffic, + broke_my_site_traffic: pattern + .traffic_pattern + .broke_my_site_traffic, + description: pattern.description, + publish_benchmarks, + }, + key: pattern.key, + }; + if !Self::can_run(rx) { + return Ok(()); + } + + update_runner(&data, req, pattern.username).await?; + } + page += 1; + } + } + + fn can_run(rx: &mut Receiver<()>) -> bool { + match rx.try_recv() { + Err(TryRecvError::Empty) => true, + _ => false, + } + } + + pub async fn run( + data: AppData, + duration: u32, + mut rx: Receiver<()>, + ) -> ServiceResult> { + let mut exit = false; + let fut = async move { + loop { + if exit { + break; + } + for _ in 0..duration { + if Self::can_run(&mut rx) { + sleep(Duration::new(1, 0)).await; + continue; + } else { + exit = true; + break; + } + } + + if let Some(err) = Self::update_captcha_configurations(&data, &mut rx) + .await + .err() + { + log::error!( + "Tried to update easy captcha configurations in background {:?}", + err + ); + } + } + }; + let handle = spawn(fut); + Ok(handle) + } +} diff --git a/src/main.rs b/src/main.rs index c96b475c..2366a2b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use actix_web::{ }; use lazy_static::lazy_static; use log::info; +use tokio::task::JoinHandle; mod api; mod data; @@ -21,6 +22,7 @@ mod date; mod db; mod demo; mod docs; +mod easy; mod email; mod errors; #[macro_use] @@ -110,11 +112,22 @@ async fn main() -> std::io::Result<()> { let data = Data::new(&settings, secrets.clone()).await; let data = actix_web::web::Data::new(data); - let mut demo_user: Option = None; + let mut demo_user: Option<(DemoUser, JoinHandle<()>)> = None; if settings.allow_demo && settings.allow_registration { - demo_user = Some( - DemoUser::spawn(data.clone(), Duration::from_secs(60 * 30)) + demo_user = Some(DemoUser::spawn(data.clone(), 60 * 30).await.unwrap()); + } + + let mut update_easy_captcha: Option<(easy::UpdateEasyCaptcha, JoinHandle<()>)> = + None; + if settings + .captcha + .default_difficulty_strategy + .avg_traffic_time + .is_some() + { + update_easy_captcha = Some( + easy::UpdateEasyCaptcha::spawn(data.clone(), 60 * 30) .await .unwrap(), ); @@ -156,7 +169,13 @@ async fn main() -> std::io::Result<()> { } if let Some(demo_user) = demo_user { - demo_user.abort(); + demo_user.0.abort(); + demo_user.1.await.unwrap(); + } + + if let Some(update_easy_captcha) = update_easy_captcha { + update_easy_captcha.0.abort(); + update_easy_captcha.1.await.unwrap(); } if let Some(survey_upload_handle) = survey_upload_handle { diff --git a/src/settings.rs b/src/settings.rs index f5107def..e0ecf056 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -37,8 +37,11 @@ pub struct Captcha { #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] pub struct DefaultDifficultyStrategy { pub avg_traffic_difficulty: u32, - pub broke_my_site_traffic_difficulty: u32, + pub avg_traffic_time: Option, pub peak_sustainable_traffic_difficulty: u32, + pub peak_sustainable_traffic_time: Option, + pub broke_my_site_traffic_time: Option, + pub broke_my_site_traffic_difficulty: u32, pub duration: u32, } @@ -113,7 +116,7 @@ pub struct Settings { pub smtp: Option, } -const ENV_VAR_CONFIG: [(&str, &str); 29] = [ +const ENV_VAR_CONFIG: [(&str, &str); 32] = [ /* top-level */ ("debug", "MCAPTCHA_debug"), ("commercial", "MCAPTCHA_commercial"), @@ -150,6 +153,9 @@ const ENV_VAR_CONFIG: [(&str, &str); 29] = [ ( "captcha.default_difficulty_strategy.duration", "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration" ), + ("captcha.default_difficulty_strategy.avg_traffic_time", "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_time"), + ("captcha.default_difficulty_strategy.peak_sustainable_traffic_time", "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_time"), + ("captcha.default_difficulty_strategy.broke_my_site_traffic_time", "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_time"), /* SMTP */ @@ -251,6 +257,28 @@ impl Settings { Ok(settings) } + fn check_easy_captcha_config(&self) { + let s = &self.captcha.default_difficulty_strategy; + if s.avg_traffic_time.is_some() { + if s.broke_my_site_traffic_time.is_none() + || s.peak_sustainable_traffic_time.is_none() + { + panic!("if captcha.default_difficulty_strategy.avg_traffic_time is set, then captcha.default_difficulty_strategy.broke_my_site_traffic_time and captcha.default_difficulty_strategy.peak_sustainable_traffic_time must also be set"); + } + } + if s.peak_sustainable_traffic_time.is_some() { + if s.avg_traffic_time.is_none() || s.peak_sustainable_traffic_time.is_none() + { + panic!("if captcha.default_difficulty_strategy.peak_sustainable_traffic_time is set, then captcha.default_difficulty_strategy.broke_my_site_traffic_time and captcha.default_difficulty_strategy.avg_traffic_time must also be set"); + } + } + if s.broke_my_site_traffic_time.is_some() { + if s.avg_traffic_time.is_none() || s.peak_sustainable_traffic_time.is_none() + { + panic!("if captcha.default_difficulty_strategy.broke_my_site_traffic_time is set, then captcha.default_difficulty_strategy.peak_sustainable_traffic_time and captcha.default_difficulty_strategy.avg_traffic_time must also be set"); + } + } + } fn env_override(mut s: ConfigBuilder) -> ConfigBuilder { for (parameter, env_var_name) in DEPRECATED_ENV_VARS.iter() { @@ -538,6 +566,30 @@ mod tests { 999, captcha.default_difficulty_strategy.duration ); + helper!( + "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_time", + "10", + Some(10), + captcha.default_difficulty_strategy.avg_traffic_time + ); + + helper!( + "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_time", + "20", + Some(20), + captcha + .default_difficulty_strategy + .peak_sustainable_traffic_time + ); + + helper!( + "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_time", + "30", + Some(30), + captcha + .default_difficulty_strategy + .broke_my_site_traffic_time + ); /* SMTP */