#
mcaptcha/api/v1/pow/
verify_pow.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//! PoW Verification module
    7
    8use actix_web::HttpRequest;
    9use actix_web::{web, HttpResponse, Responder};
    10use libmcaptcha::pow::Work;
    11use serde::{Deserialize, Serialize};
    12
    13use crate::errors::*;
    14use crate::AppData;
    15use crate::V1_API_ROUTES;
    16
    17#[derive(Clone, Debug, Deserialize, Serialize)]
    18/// validation token that clients receive as proof for submiting
    19/// valid PoW
    20pub struct ValidationToken {
    21    pub token: String,
    22}
    23
    24#[derive(Clone, Debug, Deserialize, Serialize)]
    25pub struct ApiWork {
    26    pub string: String,
    27    pub result: String,
    28    pub nonce: u64,
    29    pub key: String,
    30    pub time: Option<u32>,
    31    pub worker_type: Option<String>,
    32}
    33
    34impl From<ApiWork> for Work {
    35    fn from(value: ApiWork) -> Self {
    36        Self {
    37            string: value.string,
    38            nonce: value.nonce,
    39            result: value.result,
    40            key: value.key,
    41        }
    42    }
    43}
    44
    45// API keys are mcaptcha actor names
    46
    47/// route handler that verifies PoW and issues a solution token
    48/// if verification is successful
    49#[my_codegen::post(path = "V1_API_ROUTES.pow.verify_pow()")]
    50pub async fn verify_pow(
    51    req: HttpRequest,
    52    payload: web::Json<ApiWork>,
    53    data: AppData,
    54) -> ServiceResult<impl Responder> {
    55    #[cfg(not(test))]
    56    let ip = req.connection_info().peer_addr().unwrap().to_string();
    57    // From actix-web docs:
    58    //  Will only return None when called in unit tests unless TestRequest::peer_addr is used.
    59    //
    60    // ref: https://docs.rs/actix-web/latest/actix_web/struct.HttpRequest.html#method.peer_addr
    61    #[cfg(test)]
    62    let ip = "127.0.1.1".into();
    63
    64    let key = payload.key.clone();
    65    let payload = payload.into_inner();
    66    let worker_type = payload.worker_type.clone();
    67    let time = payload.time;
    68    let nonce = payload.nonce;
    69    let (res, difficulty_factor) = data.captcha.verify_pow(payload.into(), ip).await?;
    70    data.stats.record_solve(&data, &key).await?;
    71    if let (Some(time), Some(worker_type)) = (time, worker_type) {
    72        let analytics = db_core::CreatePerformanceAnalytics {
    73            difficulty_factor,
    74            time,
    75            worker_type,
    76        };
    77        data.db.analysis_save(&key, &analytics).await?;
    78    }
    79    data.db
    80        .update_max_nonce_for_level(&key, difficulty_factor, nonce as u32)
    81        .await?;
    82    let payload = ValidationToken { token: res };
    83    Ok(HttpResponse::Ok().json(payload))
    84}
    85
    86#[cfg(test)]
    87pub mod tests {
    88    use actix_web::http::StatusCode;
    89    use actix_web::test;
    90    use libmcaptcha::pow::PoWConfig;
    91
    92    use super::*;
    93    use crate::api::v1::pow::get_config::GetConfigPayload;
    94    use crate::tests::*;
    95    use crate::*;
    96
    97    #[actix_rt::test]
    98    async fn verify_pow_works_pg() {
    99        let data = crate::tests::pg::get_data().await;
    100        verify_pow_works(data).await;
    101    }
    102
    103    #[actix_rt::test]
    104    async fn verify_pow_works_maria() {
    105        let data = crate::tests::maria::get_data().await;
    106        verify_pow_works(data).await;
    107    }
    108
    109    #[actix_rt::test]
    110    async fn verify_analytics_pow_works_pg() {
    111        let data = crate::tests::pg::get_data().await;
    112        verify_analytics_pow_works(data).await;
    113    }
    114
    115    #[actix_rt::test]
    116    async fn verify_analytics_pow_works_maria() {
    117        let data = crate::tests::maria::get_data().await;
    118        verify_analytics_pow_works(data).await;
    119    }
    120
    121    pub async fn verify_analytics_pow_works(data: ArcData) {
    122        const NAME: &str = "powanalyticsuser";
    123        const PASSWORD: &str = "testingpas";
    124        const EMAIL: &str = "powanalyticsuser@a.com";
    125        let data = &data;
    126
    127        delete_user(data, NAME).await;
    128
    129        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    130        let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
    131        let app = get_app!(data).await;
    132
    133        let get_config_payload = GetConfigPayload {
    134            key: token_key.key.clone(),
    135        };
    136
    137        // update and check changes
    138
    139        let get_config_resp = test::call_service(
    140            &app,
    141            post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
    142                .to_request(),
    143        )
    144        .await;
    145        assert_eq!(get_config_resp.status(), StatusCode::OK);
    146        let config: PoWConfig = test::read_body_json(get_config_resp).await;
    147
    148        let pow = mcaptcha_pow_sha256::ConfigBuilder::default()
    149            .salt(config.salt)
    150            .build()
    151            .unwrap();
    152        let work = pow
    153            .prove_work(&config.string.clone(), config.difficulty_factor)
    154            .unwrap();
    155
    156        let work = ApiWork {
    157            string: config.string.clone(),
    158            result: work.result,
    159            nonce: work.nonce,
    160            key: token_key.key.clone(),
    161            time: Some(100),
    162            worker_type: Some("wasm".into()),
    163        };
    164
    165        let pow_verify_resp = test::call_service(
    166            &app,
    167            post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
    168        )
    169        .await;
    170        assert_eq!(pow_verify_resp.status(), StatusCode::OK);
    171        let limit = 50;
    172        let offset = 0;
    173        let mut analytics = data
    174            .db
    175            .analytics_fetch(&token_key.key, limit, offset)
    176            .await
    177            .unwrap();
    178        assert_eq!(analytics.len(), 1);
    179        let a = analytics.pop().unwrap();
    180        assert_eq!(a.time, work.time.unwrap());
    181        assert_eq!(a.worker_type, work.worker_type.unwrap());
    182    }
    183
    184    pub async fn verify_pow_works(data: ArcData) {
    185        const NAME: &str = "powverifyusr";
    186        const PASSWORD: &str = "testingpas";
    187        const EMAIL: &str = "verifyuser@a.com";
    188        let data = &data;
    189
    190        delete_user(data, NAME).await;
    191
    192        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    193        let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
    194        let app = get_app!(data).await;
    195
    196        let get_config_payload = GetConfigPayload {
    197            key: token_key.key.clone(),
    198        };
    199
    200        // update and check changes
    201
    202        let get_config_resp = test::call_service(
    203            &app,
    204            post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
    205                .to_request(),
    206        )
    207        .await;
    208        assert_eq!(get_config_resp.status(), StatusCode::OK);
    209        let config: PoWConfig = test::read_body_json(get_config_resp).await;
    210
    211        let pow = mcaptcha_pow_sha256::ConfigBuilder::default()
    212            .salt(config.salt)
    213            .build()
    214            .unwrap();
    215        let work = pow
    216            .prove_work(&config.string.clone(), config.difficulty_factor)
    217            .unwrap();
    218
    219        let work = Work {
    220            string: config.string.clone(),
    221            result: work.result,
    222            nonce: work.nonce,
    223            key: token_key.key.clone(),
    224        };
    225
    226        let pow_verify_resp = test::call_service(
    227            &app,
    228            post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
    229        )
    230        .await;
    231        assert_eq!(pow_verify_resp.status(), StatusCode::OK);
    232        assert!(data
    233            .db
    234            .analytics_fetch(&token_key.key, 50, 0)
    235            .await
    236            .unwrap()
    237            .is_empty());
    238
    239        let string_not_found = test::call_service(
    240            &app,
    241            post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
    242        )
    243        .await;
    244        assert_eq!(string_not_found.status(), StatusCode::BAD_REQUEST);
    245        let err: ErrorToResponse = test::read_body_json(string_not_found).await;
    246        assert_eq!(err.error, "Challenge: not found");
    247
    248        // let pow_config_resp = test::call_service(
    249        //     &app,
    250        //     post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config).to_request(),
    251        // )
    252        // .await;
    253        // assert_eq!(pow_config_resp.status(), StatusCode::OK);
    254        // I'm not checking for errors because changing work.result triggered
    255        // InssuficientDifficulty, which is possible because libmcaptcha calculates
    256        // difficulty with the submitted result. Besides, this endpoint is merely
    257        // propagating errors from libmcaptcha and libmcaptcha has tests covering the
    258        // pow aspects ¯\_(ツ)_/¯
    259    }
    260}