Merge pull request #128 from mCaptcha/feat-auto-captcha

Use time (in seconds) instead of difficulty factor to describe PoW
This commit is contained in:
Aravinth Manivannan 2024-01-05 01:25:19 +05:30 committed by GitHub
commit 5722a5327c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 651 additions and 47 deletions

View file

@ -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]

View file

@ -202,6 +202,13 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
captcha_key: &str,
) -> DBResult<TrafficPattern>;
/// Get all easy captcha configurations on instance
async fn get_all_easy_captchas(
&self,
limit: usize,
offset: usize,
) -> DBResult<Vec<EasyCaptcha>>;
/// 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 {

View file

@ -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!(

View file

@ -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"
}

View file

@ -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<Vec<EasyCaptcha>> {
struct InnerEasyCaptcha {
captcha_key: String,
name: String,
username: String,
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
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)]

View file

@ -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"
}

View file

@ -669,13 +669,8 @@ impl MCDatabase for Database {
username: &str,
captcha_key: &str,
) -> DBResult<TrafficPattern> {
struct Traffic {
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
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<Vec<EasyCaptcha>> {
struct InnerEasyCaptcha {
key: String,
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
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<InternaleCaptchaConfig> for Captcha {
}
}
}
struct InnerTraffic {
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
impl From<InnerTraffic> 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,
}
}
}

View file

@ -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

View file

@ -101,6 +101,79 @@ pub fn calculate(
Ok(levels)
}
async fn calculate_with_percentile(
data: &AppData,
tp: &TrafficPattern,
) -> ServiceResult<Option<Vec<Level>>> {
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<impl Responder> {
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)]

View file

@ -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<Self> {
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<JoinHandle<()>> {
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<Level> = test::read_body_json(resp).await;
assert!(res_levels.is_empty());
user.abort();
user.0.abort();
user.1.await.unwrap();
}
}

132
src/easy.rs Normal file
View file

@ -0,0 +1,132 @@
// Copyright (C) 2024// Copyright (C) 2024 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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<JoinHandle<()>> {
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)
}
}

View file

@ -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<DemoUser> = 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 {

View file

@ -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<u32>,
pub peak_sustainable_traffic_difficulty: u32,
pub peak_sustainable_traffic_time: Option<u32>,
pub broke_my_site_traffic_time: Option<u32>,
pub broke_my_site_traffic_difficulty: u32,
pub duration: u32,
}
@ -113,7 +116,7 @@ pub struct Settings {
pub smtp: Option<Smtp>,
}
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<DefaultState>) -> ConfigBuilder<DefaultState> {
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 */