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}