#
mcaptcha/api/v1/mcaptcha/
easy.rs

    1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
    2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
    3//
    4// SPDX-License-Identifier: AGPL-3.0-or-later
    5
    6use actix_identity::Identity;
    7use actix_web::{web, HttpResponse, Responder};
    8use libmcaptcha::{defense::Level, defense::LevelBuilder};
    9use serde::{Deserialize, Serialize};
    10
    11use db_core::TrafficPattern;
    12
    13use super::create::{runner::create as create_runner, CreateCaptcha};
    14use super::update::{runner::update_captcha as update_captcha_runner, UpdateCaptcha};
    15use crate::errors::*;
    16use crate::settings::DefaultDifficultyStrategy;
    17use crate::AppData;
    18
    19pub mod routes {
    20    pub struct Easy {
    21        /// easy is using defaults
    22        pub create: &'static str,
    23        pub update: &'static str,
    24    }
    25
    26    impl Easy {
    27        pub const fn new() -> Self {
    28            Self {
    29                create: "/api/v1/mcaptcha/add/easy",
    30                update: "/api/v1/mcaptcha/update/easy",
    31            }
    32        }
    33    }
    34}
    35
    36pub fn services(cfg: &mut web::ServiceConfig) {
    37    cfg.service(update);
    38    cfg.service(create);
    39}
    40
    41#[derive(Default, Serialize, Deserialize, Clone, Debug)]
    42/// User's traffic pattern; used in generating a captcha configuration
    43pub struct TrafficPatternRequest {
    44    /// average traffic of user's website
    45    pub avg_traffic: u32,
    46    /// the peak traffic that the user's website can handle
    47    pub peak_sustainable_traffic: u32,
    48    /// traffic that bought the user's website down; optional
    49    pub broke_my_site_traffic: Option<u32>,
    50    /// Captcha description
    51    pub description: String,
    52
    53    /// publish benchmarks
    54    pub publish_benchmarks: bool,
    55}
    56
    57impl From<&TrafficPatternRequest> for TrafficPattern {
    58    fn from(t: &TrafficPatternRequest) -> Self {
    59        TrafficPattern {
    60            avg_traffic: t.avg_traffic,
    61            peak_sustainable_traffic: t.peak_sustainable_traffic,
    62            broke_my_site_traffic: t.broke_my_site_traffic,
    63        }
    64    }
    65}
    66
    67pub fn calculate(
    68    tp: &TrafficPattern,
    69    strategy: &DefaultDifficultyStrategy,
    70) -> ServiceResult<Vec<Level>> {
    71    let mut levels = vec![
    72        LevelBuilder::default()
    73            .difficulty_factor(strategy.avg_traffic_difficulty)?
    74            .visitor_threshold(tp.avg_traffic)
    75            .build()?,
    76        LevelBuilder::default()
    77            .difficulty_factor(strategy.peak_sustainable_traffic_difficulty)?
    78            .visitor_threshold(tp.peak_sustainable_traffic)
    79            .build()?,
    80    ];
    81    let mut highest_level = LevelBuilder::default();
    82    highest_level.difficulty_factor(strategy.broke_my_site_traffic_difficulty)?;
    83
    84    match tp.broke_my_site_traffic {
    85        Some(broke_my_site_traffic) => {
    86            highest_level.visitor_threshold(broke_my_site_traffic)
    87        }
    88        None => match tp
    89            .peak_sustainable_traffic
    90            .checked_add(tp.peak_sustainable_traffic / 2)
    91        {
    92            Some(num) => highest_level.visitor_threshold(num),
    93            // TODO check for overflow: database saves these values as i32, so this u32 is cast
    94            // into i32. Should choose bigger number or casts properly
    95            None => highest_level.visitor_threshold(u32::MAX),
    96        },
    97    };
    98
    99    levels.push(highest_level.build()?);
    100
    101    Ok(levels)
    102}
    103
    104async fn calculate_with_percentile(
    105    data: &AppData,
    106    tp: &TrafficPattern,
    107) -> ServiceResult<Option<Vec<Level>>> {
    108    use crate::api::v1::stats::{percentile_bench_runner, PercentileReq};
    109
    110    let strategy = &data.settings.captcha.default_difficulty_strategy;
    111
    112    if strategy.avg_traffic_time.is_none()
    113        && strategy.peak_sustainable_traffic_time.is_none()
    114        && strategy.broke_my_site_traffic_time.is_none()
    115    {
    116        return Ok(None);
    117    }
    118
    119    let mut req = PercentileReq {
    120        time: strategy.avg_traffic_time.unwrap(),
    121        percentile: 90.00,
    122    };
    123    let resp = percentile_bench_runner(data, &req).await?;
    124    if resp.difficulty_factor.is_none() {
    125        return Ok(None);
    126    }
    127    let avg_traffic_difficulty = resp.difficulty_factor.unwrap();
    128
    129    req.time = strategy.peak_sustainable_traffic_time.unwrap();
    130    let resp = percentile_bench_runner(data, &req).await?;
    131    if resp.difficulty_factor.is_none() {
    132        return Ok(None);
    133    }
    134    let peak_sustainable_traffic_difficulty = resp.difficulty_factor.unwrap();
    135
    136    req.time = strategy.broke_my_site_traffic_time.unwrap();
    137    let resp = percentile_bench_runner(data, &req).await?;
    138    let broke_my_site_traffic_difficulty = if resp.difficulty_factor.is_none() {
    139        resp.difficulty_factor.unwrap()
    140    } else {
    141        peak_sustainable_traffic_difficulty * 2
    142    };
    143
    144    let mut levels = vec![
    145        LevelBuilder::default()
    146            .difficulty_factor(avg_traffic_difficulty)?
    147            .visitor_threshold(tp.avg_traffic)
    148            .build()?,
    149        LevelBuilder::default()
    150            .difficulty_factor(peak_sustainable_traffic_difficulty)?
    151            .visitor_threshold(tp.peak_sustainable_traffic)
    152            .build()?,
    153    ];
    154    let mut highest_level = LevelBuilder::default();
    155    highest_level.difficulty_factor(broke_my_site_traffic_difficulty)?;
    156
    157    match tp.broke_my_site_traffic {
    158        Some(broke_my_site_traffic) => {
    159            highest_level.visitor_threshold(broke_my_site_traffic)
    160        }
    161        None => match tp
    162            .peak_sustainable_traffic
    163            .checked_add(tp.peak_sustainable_traffic / 2)
    164        {
    165            Some(num) => highest_level.visitor_threshold(num),
    166            // TODO check for overflow: database saves these values as i32, so this u32 is cast
    167            // into i32. Should choose bigger number or casts properly
    168            None => highest_level.visitor_threshold(u32::MAX),
    169        },
    170    };
    171
    172    levels.push(highest_level.build()?);
    173
    174    Ok(Some(levels))
    175}
    176
    177#[my_codegen::post(
    178    path = "crate::V1_API_ROUTES.captcha.easy.create",
    179    wrap = "crate::api::v1::get_middleware()"
    180)]
    181async fn create(
    182    payload: web::Json<TrafficPatternRequest>,
    183    data: AppData,
    184    id: Identity,
    185) -> ServiceResult<impl Responder> {
    186    let username = id.identity().unwrap();
    187    let payload = payload.into_inner();
    188    let pattern = (&payload).into();
    189    let levels = if let Some(levels) = calculate_with_percentile(&data, &pattern).await?
    190    {
    191        levels
    192    } else {
    193        calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?
    194    };
    195    let msg = CreateCaptcha {
    196        levels,
    197        duration: data.settings.captcha.default_difficulty_strategy.duration,
    198        description: payload.description,
    199        publish_benchmarks: payload.publish_benchmarks,
    200    };
    201
    202    let mcaptcha_config = create_runner(&msg, &data, &username).await?;
    203    data.db
    204        .add_traffic_pattern(&username, &mcaptcha_config.key, &pattern)
    205        .await?;
    206
    207    Ok(HttpResponse::Ok().json(mcaptcha_config))
    208}
    209
    210#[derive(Serialize, Deserialize, Clone, Debug)]
    211pub struct UpdateTrafficPattern {
    212    pub pattern: TrafficPatternRequest,
    213    pub key: String,
    214}
    215
    216#[my_codegen::post(
    217    path = "crate::V1_API_ROUTES.captcha.easy.update",
    218    wrap = "crate::api::v1::get_middleware()"
    219)]
    220async fn update(
    221    payload: web::Json<UpdateTrafficPattern>,
    222    data: AppData,
    223    id: Identity,
    224) -> ServiceResult<impl Responder> {
    225    let username = id.identity().unwrap();
    226    let payload = payload.into_inner();
    227    update_runner(&data, payload, username).await?;
    228    Ok(HttpResponse::Ok())
    229}
    230
    231pub async fn update_runner(
    232    data: &AppData,
    233    payload: UpdateTrafficPattern,
    234    username: String,
    235) -> ServiceResult<()> {
    236    let pattern = (&payload.pattern).into();
    237    let levels =
    238        calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?;
    239
    240    let msg = UpdateCaptcha {
    241        levels,
    242        duration: data.settings.captcha.default_difficulty_strategy.duration,
    243        description: payload.pattern.description,
    244        key: payload.key,
    245        publish_benchmarks: payload.pattern.publish_benchmarks,
    246    };
    247
    248    update_captcha_runner(&msg, &data, &username).await?;
    249
    250    data.db.delete_traffic_pattern(&username, &msg.key).await?;
    251
    252    data.db
    253        .add_traffic_pattern(&username, &msg.key, &pattern)
    254        .await?;
    255
    256    Ok(())
    257}
    258
    259#[cfg(test)]
    260pub mod tests {
    261    use actix_web::http::StatusCode;
    262    use actix_web::test;
    263    use actix_web::web::Bytes;
    264
    265    use super::*;
    266    use crate::api::v1::mcaptcha::create::MCaptchaDetails;
    267    use crate::api::v1::ROUTES;
    268    use crate::tests::*;
    269    use crate::*;
    270
    271    mod isoloated_test {
    272        use super::{calculate, LevelBuilder};
    273
    274        use db_core::TrafficPattern;
    275
    276        #[test]
    277        fn easy_configuration_works() {
    278            let settings = crate::tests::get_settings();
    279
    280            let mut payload = TrafficPattern {
    281                avg_traffic: 100_000,
    282                peak_sustainable_traffic: 1_000_000,
    283                broke_my_site_traffic: Some(10_000_000),
    284            };
    285
    286            let strategy = &settings.captcha.default_difficulty_strategy;
    287            let l1 = LevelBuilder::default()
    288                .difficulty_factor(strategy.avg_traffic_difficulty)
    289                .unwrap()
    290                .visitor_threshold(payload.avg_traffic)
    291                .build()
    292                .unwrap();
    293
    294            let l2 = LevelBuilder::default()
    295                .difficulty_factor(strategy.peak_sustainable_traffic_difficulty)
    296                .unwrap()
    297                .visitor_threshold(payload.peak_sustainable_traffic)
    298                .build()
    299                .unwrap();
    300            let l3 = LevelBuilder::default()
    301                .difficulty_factor(strategy.broke_my_site_traffic_difficulty)
    302                .unwrap()
    303                .visitor_threshold(payload.broke_my_site_traffic.unwrap())
    304                .build()
    305                .unwrap();
    306
    307            let levels = vec![l1, l2, l3];
    308            assert_eq!(calculate(&payload, strategy).unwrap(), levels);
    309
    310            let estimated_lmax = LevelBuilder::default()
    311                .difficulty_factor(strategy.broke_my_site_traffic_difficulty)
    312                .unwrap()
    313                .visitor_threshold(1500000)
    314                .build()
    315                .unwrap();
    316            payload.broke_my_site_traffic = None;
    317            assert_eq!(
    318                calculate(&payload, strategy).unwrap(),
    319                vec![l1, l2, estimated_lmax]
    320            );
    321
    322            let lmax = LevelBuilder::default()
    323                .difficulty_factor(strategy.broke_my_site_traffic_difficulty)
    324                .unwrap()
    325                .visitor_threshold(u32::MAX)
    326                .build()
    327                .unwrap();
    328
    329            let very_large_l2_peak_traffic = u32::MAX - 1;
    330            let very_large_l2 = LevelBuilder::default()
    331                .difficulty_factor(strategy.peak_sustainable_traffic_difficulty)
    332                .unwrap()
    333                .visitor_threshold(very_large_l2_peak_traffic)
    334                .build()
    335                .unwrap();
    336
    337            //        payload.broke_my_site_traffic = Some(very_large_l2_peak_traffic);
    338            payload.peak_sustainable_traffic = very_large_l2_peak_traffic;
    339            assert_eq!(
    340                calculate(&payload, strategy).unwrap(),
    341                vec![l1, very_large_l2, lmax]
    342            );
    343        }
    344    }
    345
    346    #[actix_rt::test]
    347    async fn easy_works_pg() {
    348        let data = crate::tests::pg::get_data().await;
    349        easy_works(data).await;
    350    }
    351
    352    #[actix_rt::test]
    353    async fn easy_works_maria() {
    354        let data = crate::tests::maria::get_data().await;
    355        easy_works(data).await;
    356    }
    357
    358    pub async fn easy_works(data: ArcData) {
    359        const NAME: &str = "defaultuserconfgworks";
    360        const PASSWORD: &str = "longpassworddomain";
    361        const EMAIL: &str = "defaultuserconfgworks@a.com";
    362        let data = &data;
    363
    364        delete_user(data, NAME).await;
    365
    366        let (_creds, signin_resp) =
    367            register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    368        let cookies = get_cookie!(signin_resp);
    369        let app = get_app!(data).await;
    370
    371        let payload = TrafficPatternRequest {
    372            avg_traffic: 100_000,
    373            peak_sustainable_traffic: 1_000_000,
    374            broke_my_site_traffic: Some(10_000_000),
    375            description: NAME.into(),
    376            publish_benchmarks: false,
    377        };
    378
    379        let default_levels = calculate(
    380            &(&payload).into(),
    381            &data.settings.captcha.default_difficulty_strategy,
    382        )
    383        .unwrap();
    384
    385        // START create_easy
    386
    387        let add_token_resp = test::call_service(
    388            &app,
    389            post_request!(&payload, ROUTES.captcha.easy.create)
    390                .cookie(cookies.clone())
    391                .to_request(),
    392        )
    393        .await;
    394        assert_eq!(add_token_resp.status(), StatusCode::OK);
    395        let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
    396
    397        let get_level_resp = test::call_service(
    398            &app,
    399            post_request!(&token_key, ROUTES.captcha.get)
    400                .cookie(cookies.clone())
    401                .to_request(),
    402        )
    403        .await;
    404
    405        assert_eq!(get_level_resp.status(), StatusCode::OK);
    406        let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
    407        assert_eq!(res_levels, default_levels);
    408        assert!(!data
    409            .db
    410            .analytics_captcha_is_published(&token_key.key)
    411            .await
    412            .unwrap());
    413        // END create_easy
    414
    415        // START update_easy
    416        let update_pattern = TrafficPatternRequest {
    417            avg_traffic: 1_000,
    418            peak_sustainable_traffic: 10_000,
    419            broke_my_site_traffic: Some(1_000_000),
    420            description: NAME.into(),
    421            publish_benchmarks: true,
    422        };
    423
    424        let updated_default_values = calculate(
    425            &(&update_pattern).into(),
    426            &data.settings.captcha.default_difficulty_strategy,
    427        )
    428        .unwrap();
    429
    430        let payload = UpdateTrafficPattern {
    431            pattern: update_pattern,
    432            key: token_key.key.clone(),
    433        };
    434
    435        let update_token_resp = test::call_service(
    436            &app,
    437            post_request!(&payload, ROUTES.captcha.easy.update)
    438                .cookie(cookies.clone())
    439                .to_request(),
    440        )
    441        .await;
    442        assert_eq!(update_token_resp.status(), StatusCode::OK);
    443        assert!(data
    444            .db
    445            .analytics_captcha_is_published(&token_key.key)
    446            .await
    447            .unwrap());
    448
    449        let get_level_resp = test::call_service(
    450            &app,
    451            post_request!(&token_key, ROUTES.captcha.get)
    452                .cookie(cookies.clone())
    453                .to_request(),
    454        )
    455        .await;
    456
    457        assert_eq!(get_level_resp.status(), StatusCode::OK);
    458        let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
    459        assert_ne!(res_levels, default_levels);
    460        assert_eq!(res_levels, updated_default_values);
    461        // END update_easy
    462
    463        // test easy edit page
    464        let easy_url = PAGES.panel.sitekey.get_edit_easy(&token_key.key);
    465
    466        let easy_edit_page = test::call_service(
    467            &app,
    468            test::TestRequest::get()
    469                .uri(&easy_url)
    470                .cookie(cookies.clone())
    471                .to_request(),
    472        )
    473        .await;
    474        assert_eq!(easy_edit_page.status(), StatusCode::OK);
    475
    476        let body: Bytes = test::read_body(easy_edit_page).await;
    477        let body = String::from_utf8(body.to_vec()).unwrap();
    478        assert!(body.contains(&token_key.name));
    479
    480        assert!(body.contains(
    481            &payload
    482                .pattern
    483                .broke_my_site_traffic
    484                .as_ref()
    485                .unwrap()
    486                .to_string()
    487        ));
    488        assert!(body.contains(&payload.pattern.avg_traffic.to_string()));
    489        assert!(body.contains(&payload.pattern.peak_sustainable_traffic.to_string()));
    490
    491        // START update_easy to delete published results
    492        let mut payload2 = TrafficPatternRequest {
    493            avg_traffic: 100_000,
    494            peak_sustainable_traffic: 1_000_000,
    495            broke_my_site_traffic: Some(10_000_000),
    496            description: NAME.into(),
    497            publish_benchmarks: true,
    498        };
    499
    500        let add_token_resp = test::call_service(
    501            &app,
    502            post_request!(&payload2, ROUTES.captcha.easy.create)
    503                .cookie(cookies.clone())
    504                .to_request(),
    505        )
    506        .await;
    507        assert_eq!(add_token_resp.status(), StatusCode::OK);
    508
    509        assert!(data
    510            .db
    511            .analytics_captcha_is_published(&token_key.key)
    512            .await
    513            .unwrap());
    514
    515        let token_key2: MCaptchaDetails = test::read_body_json(add_token_resp).await;
    516
    517        payload2.publish_benchmarks = false;
    518
    519        let payload = UpdateTrafficPattern {
    520            pattern: payload2,
    521            key: token_key2.key.clone(),
    522        };
    523
    524        let update_token_resp = test::call_service(
    525            &app,
    526            post_request!(&payload, ROUTES.captcha.easy.update)
    527                .cookie(cookies.clone())
    528                .to_request(),
    529        )
    530        .await;
    531        assert_eq!(update_token_resp.status(), StatusCode::OK);
    532        assert!(!data
    533            .db
    534            .analytics_captcha_is_published(&token_key2.key)
    535            .await
    536            .unwrap());
    537    }
    538}