/* * Copyright (C) 2023 Aravinth Manivannan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use actix_web::web::ServiceConfig; use actix_web::{web, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; use url::Url; use crate::api::v1::ROUTES; use crate::errors::*; use crate::mcaptcha::Secret; use crate::AppData; pub fn services(cfg: &mut ServiceConfig) { cfg.service(register); cfg.service(upload); cfg.service(download); } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub struct MCaptchaInstance { pub url: Url, pub auth_token: String, } #[actix_web_codegen_const_routes::post(path = "ROUTES.mcaptcha.register")] async fn register( data: AppData, payload: web::Json, ) -> ServiceResult { /* Summary * 1. Check if secret exists * 2. If not, add hostname and create secret * 3. Post to mCaptcha */ let url_str = payload.url.to_string(); let secret = if data.mcaptcha_url_exists(&url_str).await? { data.mcaptcha_update_secret(&url_str).await? } else { data.mcaptcha_register_instance(&url_str).await? }; let payload = payload.into_inner(); data.mcaptcha .share_secret(payload.url, secret, payload.auth_token) .await?; Ok(HttpResponse::Ok()) } #[actix_web_codegen_const_routes::post(path = "ROUTES.mcaptcha.upload")] async fn upload( data: AppData, campaign: web::Path, payload: web::Json, ) -> ServiceResult { /* TODO * 1. Authenticate: Get URL from secret * 2. Check if campaign exists * 3. If not: create campaign * 4. Get last known sync point * 5. Download results * 6. Update sync point */ let url = data .mcaptcha_authenticate_and_get_url(&payload.secret) .await?; let campaign_str = campaign.to_string(); if !data .mcaptcha_campaign_is_registered(&campaign, &payload.secret) .await? { data.mcaptcha_register_campaign(&campaign, &payload.secret) .await?; } let checkpoint = data .mcaptcha_get_checkpoint(&campaign, &payload.secret) .await?; const LIMIT: usize = 50; let mut page = 1 + (checkpoint / LIMIT); loop { let mut res = data .mcaptcha .download_benchmarks(url.clone(), &campaign_str, page) .await?; let skip = checkpoint - ((page - 1) * LIMIT); let new_records = res.len() - skip as usize; let mut skip = skip as isize; for r in res.drain(0..) { if skip > 0 { skip -= 1; continue; } data.mcaptcha_insert_analytics(&campaign, &payload.secret, &r) .await?; } data.mcaptcha_set_checkpoint(&campaign, &payload.secret, new_records) .await?; page += 1; if res.len() < LIMIT { break; } } Ok(HttpResponse::Ok()) } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub struct Page { pub page: usize, } #[actix_web_codegen_const_routes::get(path = "ROUTES.mcaptcha.download")] async fn download( data: AppData, page: web::Query, public_id: web::Path, ) -> ServiceResult { const LIMIT: usize = 50; let offset = LIMIT as isize * ((page.page as isize) - 1); let offset = if offset < 0 { 0 } else { offset }; let public_id = public_id.into_inner(); let resp = data .mcaptcha_analytics_fetch(&public_id, LIMIT, offset as usize) .await?; Ok(HttpResponse::Ok().json(resp)) } #[cfg(test)] mod tests { use crate::api::v1::bench::Submission; use crate::api::v1::bench::SubmissionType; use crate::api::v1::get_random; use crate::errors::*; use crate::mcaptcha::PerformanceAnalytics; use crate::mcaptcha::Secret; use crate::tests::*; use crate::*; use actix_web::{http::header, test}; #[actix_rt::test] async fn mcaptcha_hooks_work() { let mcaptcha_instance = url::Url::parse("http://mcaptcha_hooks_work.example.org").unwrap(); let mcaptcha_instance_str = mcaptcha_instance.to_string(); let campaign_id = uuid::Uuid::new_v4(); let (data, client) = get_test_data_with_mcaptcha_client().await; let app = get_app!(data).await; if data .mcaptcha_url_exists(&mcaptcha_instance_str) .await .unwrap() { let secret = data .mcaptcha_update_secret(&mcaptcha_instance_str) .await .unwrap(); data.mcaptcha_delete_mcaptcha_instance(&mcaptcha_instance_str, &secret) .await .unwrap(); } let payload = super::MCaptchaInstance { url: mcaptcha_instance.clone(), auth_token: get_random(23), }; let resp = test::call_service( &app, post_request!(&payload, V1_API_ROUTES.mcaptcha.register).to_request(), ) .await; assert_eq!(resp.status(), StatusCode::OK); let secret = { let mut mcaptcha = payload.url.clone(); mcaptcha.set_path("/api/v1/survey/secret"); let mut x = client.client.write().unwrap(); x.remove(&mcaptcha.to_string()).unwrap() }; let resp2 = test::call_service( &app, post_request!(&payload, V1_API_ROUTES.mcaptcha.register).to_request(), ) .await; assert_eq!(resp2.status(), StatusCode::OK); let secret2 = { let mut mcaptcha = payload.url.clone(); mcaptcha.set_path("/api/v1/survey/secret"); let mut x = client.client.write().unwrap(); x.remove(&mcaptcha.to_string()).unwrap() }; assert_ne!(secret, secret2); let secret = secret2; let payload = Secret { secret: secret.clone(), }; if data .mcaptcha_campaign_is_registered(&campaign_id, &secret) .await .unwrap() { data.mcaptcha_delete_mcaptcha_campaign(&campaign_id, &secret) .await .unwrap(); } let resp = test::call_service( &app, post_request!( &payload, &V1_API_ROUTES .mcaptcha .get_upload_route(&campaign_id.to_string()) ) .to_request(), ) .await; assert_eq!(resp.status(), StatusCode::OK); let public_id = data .mcaptcha_get_campaign_public_id(&campaign_id, &secret) .await .unwrap(); let expected = crate::mcaptcha::tests::BENCHMARK.clone(); let got = data .mcaptcha_analytics_fetch(&public_id, 50, 0) .await .unwrap(); for i in 0..2 { assert_eq!(got[i].time, expected[i].time); assert_eq!(got[i].difficulty_factor, expected[i].difficulty_factor); assert_eq!(got[i].worker_type, expected[i].worker_type); } let resp = get_request!( &app, &V1_API_ROUTES .mcaptcha .get_download_route(&public_id.to_string(), 0) ); assert_eq!(resp.status(), StatusCode::OK); let resp: Vec = test::read_body_json(resp).await; assert_eq!(resp.len(), 2); assert_eq!(resp, got); } }