#
mcaptcha/api/v1/pow/
get_config.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
    6//use actix::prelude::*;
    7use actix_web::{web, HttpResponse, Responder};
    8use libmcaptcha::pow::PoWConfig;
    9use libmcaptcha::{
    10    defense::LevelBuilder, master::messages::AddSiteBuilder, DefenseBuilder,
    11    MCaptchaBuilder,
    12};
    13use serde::{Deserialize, Serialize};
    14
    15use crate::errors::*;
    16//use crate::stats::record::record_fetch;
    17use crate::AppData;
    18use crate::V1_API_ROUTES;
    19
    20#[derive(Clone, Debug, Deserialize, Serialize)]
    21pub struct GetConfigPayload {
    22    pub key: String,
    23}
    24
    25#[derive(Clone, Serialize, Deserialize, Debug)]
    26pub struct ApiPoWConfig {
    27    pub string: String,
    28    pub difficulty_factor: u32,
    29    pub salt: String,
    30    pub max_recorded_nonce: u32,
    31}
    32
    33/// get PoW configuration for an mcaptcha key
    34#[my_codegen::post(path = "V1_API_ROUTES.pow.get_config()")]
    35pub async fn get_config(
    36    payload: web::Json<GetConfigPayload>,
    37    data: AppData,
    38) -> ServiceResult<impl Responder> {
    39    //if res.exists.is_none() {
    40    if !data.db.captcha_exists(None, &payload.key).await? {
    41        return Err(ServiceError::TokenNotFound);
    42    }
    43    let payload = payload.into_inner();
    44
    45    let config: ServiceResult<PoWConfig> =
    46        match data.captcha.get_pow(payload.key.clone()).await {
    47            Ok(Some(config)) => Ok(config),
    48            Ok(None) => {
    49                init_mcaptcha(&data, &payload.key).await?;
    50                let config = data
    51                    .captcha
    52                    .get_pow(payload.key.clone())
    53                    .await
    54                    .expect("mcaptcha should be initialized and ready to go");
    55                Ok(config.unwrap())
    56            }
    57            Err(e) => Err(e.into()),
    58        };
    59    let config = config?;
    60    let max_nonce = data
    61        .db
    62        .get_max_nonce_for_level(&payload.key, config.difficulty_factor)
    63        .await?;
    64    data.stats.record_fetch(&data, &payload.key).await?;
    65
    66    let config = ApiPoWConfig {
    67        string: config.string,
    68        difficulty_factor: config.difficulty_factor,
    69        salt: config.salt,
    70        max_recorded_nonce: max_nonce,
    71    };
    72    Ok(HttpResponse::Ok().json(config))
    73}
    74/// Call this when [MCaptcha][libmcaptcha::MCaptcha] is not in master.
    75///
    76/// This fn gets mcaptcha config from database, builds [Defense][libmcaptcha::Defense],
    77/// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][libmcaptcha::Defense]
    78pub async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
    79    println!("Initializing captcha");
    80    // get levels
    81    let levels = data.db.get_captcha_levels(None, key).await?;
    82    let duration = data.db.get_captcha_cooldown(key).await?;
    83
    84    // build defense
    85    let mut defense = DefenseBuilder::default();
    86
    87    for level in levels.iter() {
    88        let level = LevelBuilder::default()
    89            .visitor_threshold(level.visitor_threshold)
    90            .difficulty_factor(level.difficulty_factor)
    91            .unwrap()
    92            .build()
    93            .unwrap();
    94        defense.add_level(level).unwrap();
    95    }
    96
    97    let defense = defense.build()?;
    98    println!("{:?}", defense);
    99
    100    // create captcha
    101    let mcaptcha = MCaptchaBuilder::default()
    102        .defense(defense)
    103        // leaky bucket algorithm's emission interval
    104        .duration(duration as u64)
    105        //   .cache(cache)
    106        .build()
    107        .unwrap();
    108
    109    // add captcha to master
    110    let msg = AddSiteBuilder::default()
    111        .id(key.into())
    112        .mcaptcha(mcaptcha)
    113        .build()
    114        .unwrap();
    115
    116    data.captcha.add_site(msg).await?;
    117
    118    Ok(())
    119}
    120
    121#[cfg(test)]
    122pub mod tests {
    123    use crate::*;
    124    use libmcaptcha::pow::PoWConfig;
    125
    126    #[actix_rt::test]
    127    async fn get_pow_config_works_pg() {
    128        let data = crate::tests::pg::get_data().await;
    129        get_pow_config_works(data).await;
    130    }
    131
    132    #[actix_rt::test]
    133    async fn get_pow_config_works_maria() {
    134        let data = crate::tests::maria::get_data().await;
    135        get_pow_config_works(data).await;
    136    }
    137
    138    pub async fn get_pow_config_works(data: ArcData) {
    139        use super::*;
    140        use crate::tests::*;
    141        use crate::*;
    142        use actix_web::test;
    143
    144        const NAME: &str = "powusrworks";
    145        const PASSWORD: &str = "testingpas";
    146        const EMAIL: &str = "randomuser@a.com";
    147
    148        let data = &data;
    149
    150        delete_user(data, NAME).await;
    151
    152        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    153        let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
    154        let app = get_app!(data).await;
    155
    156        let get_config_payload = GetConfigPayload {
    157            key: token_key.key.clone(),
    158        };
    159
    160        // update and check changes
    161
    162        let url = V1_API_ROUTES.pow.get_config;
    163        println!("{}", &url);
    164        let get_config_resp = test::call_service(
    165            &app,
    166            post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
    167                .to_request(),
    168        )
    169        .await;
    170        assert_eq!(get_config_resp.status(), StatusCode::OK);
    171        let config: PoWConfig = test::read_body_json(get_config_resp).await;
    172        assert_eq!(config.difficulty_factor, L1.difficulty_factor);
    173    }
    174
    175    #[actix_rt::test]
    176    async fn pow_difficulty_factor_increases_on_visitor_count_increase_pg() {
    177        let data = crate::tests::pg::get_data().await;
    178        pow_difficulty_factor_increases_on_visitor_count_increase(data).await;
    179    }
    180
    181    #[actix_rt::test]
    182    async fn pow_difficulty_factor_increases_on_visitor_count_increase_maria() {
    183        let data = crate::tests::maria::get_data().await;
    184        pow_difficulty_factor_increases_on_visitor_count_increase(data).await;
    185    }
    186
    187    pub async fn pow_difficulty_factor_increases_on_visitor_count_increase(
    188        data: ArcData,
    189    ) {
    190        use super::*;
    191        use crate::tests::*;
    192        use crate::*;
    193        use actix_web::test;
    194
    195        use libmcaptcha::defense::Level;
    196
    197        use crate::api::v1::mcaptcha::create::CreateCaptcha;
    198        use crate::api::v1::mcaptcha::create::MCaptchaDetails;
    199
    200        const NAME: &str = "powusrworks2";
    201        const PASSWORD: &str = "testingpas";
    202        const EMAIL: &str = "randomuser2@a.com";
    203        pub const L1: Level = Level {
    204            difficulty_factor: 10,
    205            visitor_threshold: 10,
    206        };
    207        pub const L2: Level = Level {
    208            difficulty_factor: 20,
    209            visitor_threshold: 20,
    210        };
    211
    212        pub const L3: Level = Level {
    213            difficulty_factor: 30,
    214            visitor_threshold: 30,
    215        };
    216
    217        let data = &data;
    218        let levels = [L1, L2, L3];
    219
    220        delete_user(data, NAME).await;
    221
    222        let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    223        let cookies = get_cookie!(signin_resp);
    224        let app = get_app!(data).await;
    225
    226        let create_captcha = CreateCaptcha {
    227            levels: levels.into(),
    228            duration: 30,
    229            description: "dummy".into(),
    230            publish_benchmarks: true,
    231        };
    232
    233        // 1. add level
    234        let add_token_resp = test::call_service(
    235            &app,
    236            post_request!(&create_captcha, V1_API_ROUTES.captcha.create)
    237                .cookie(cookies.clone())
    238                .to_request(),
    239        )
    240        .await;
    241        assert_eq!(add_token_resp.status(), StatusCode::OK);
    242        let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
    243
    244        let get_config_payload = GetConfigPayload {
    245            key: token_key.key.clone(),
    246        };
    247
    248        let _url = V1_API_ROUTES.pow.get_config;
    249        let mut prev = 0;
    250        for (count, l) in levels.iter().enumerate() {
    251            for _l in prev..l.visitor_threshold * 2 {
    252                let _get_config_resp = test::call_service(
    253                    &app,
    254                    post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
    255                        .to_request(),
    256                )
    257                .await;
    258            }
    259
    260            let get_config_resp = test::call_service(
    261                &app,
    262                post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
    263                    .to_request(),
    264            )
    265            .await;
    266
    267            let config: PoWConfig = test::read_body_json(get_config_resp).await;
    268            println!(
    269                "[{count}] received difficulty_factor: {} prev difficulty_factor {}",
    270                config.difficulty_factor, prev
    271            );
    272            if count == levels.len() - 1 {
    273                assert!(config.difficulty_factor == prev);
    274            } else {
    275                assert!(config.difficulty_factor > prev);
    276            }
    277            prev = config.difficulty_factor;
    278        }
    279        // update and check changes
    280    }
    281}