#
mcaptcha/
data.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//! App data: redis cache, database connections, etc.
    7use std::collections::HashMap;
    8use std::sync::Arc;
    9use std::thread;
    10use std::time::Duration;
    11
    12use actix::prelude::*;
    13use argon2_creds::{Config, ConfigBuilder, PasswordPolicy};
    14use lettre::transport::smtp::authentication::Mechanism;
    15use lettre::{
    16    transport::smtp::authentication::Credentials, AsyncSmtpTransport, Tokio1Executor,
    17};
    18use libmcaptcha::cache::hashcache::HashCache;
    19use libmcaptcha::cache::redis::RedisCache;
    20use libmcaptcha::master::redis::master::Master as RedisMaster;
    21use libmcaptcha::redis::RedisConfig;
    22use libmcaptcha::{
    23    cache::messages::VerifyCaptchaResult,
    24    cache::Save,
    25    errors::CaptchaResult,
    26    master::messages::{AddSite, RemoveCaptcha, Rename},
    27    master::{embedded::master::Master as EmbeddedMaster, Master as MasterTrait},
    28    pow::ConfigBuilder as PoWConfigBuilder,
    29    pow::PoWConfig,
    30    pow::Work,
    31    system::{System, SystemBuilder},
    32};
    33use reqwest::Client;
    34use serde::{Deserialize, Serialize};
    35use tokio::task::JoinHandle;
    36use tokio::time::sleep;
    37
    38use crate::db::{self, BoxDB};
    39use crate::errors::ServiceResult;
    40use crate::settings::Settings;
    41use crate::stats::{Dummy, Real, Stats};
    42use crate::survey::SecretsStore;
    43use crate::AppData;
    44
    45macro_rules! enum_system_actor {
    46    ($name:ident, $type:ident) => {
    47        pub async fn $name(&self, msg: $type) -> ServiceResult<()> {
    48            match self {
    49                Self::Embedded(val) => val.master.send(msg).await?.await??,
    50                Self::Redis(val) => val.master.send(msg).await?.await??,
    51            };
    52            Ok(())
    53        }
    54    };
    55}
    56
    57macro_rules! enum_system_wrapper {
    58    ($name:ident, $type:ty, $return_type:ty) => {
    59        pub async fn $name(&self, msg: $type) -> $return_type {
    60            match self {
    61                Self::Embedded(val) => val.$name(msg).await,
    62                Self::Redis(val) => val.$name(msg).await,
    63            }
    64        }
    65    };
    66}
    67
    68/// Represents mCaptcha cache and master system.
    69/// When Redis is configured, [SystemGroup::Redis] is used and
    70/// in its absence, [SystemGroup::Embedded] is used
    71pub enum SystemGroup {
    72    Embedded(System<HashCache, EmbeddedMaster>),
    73    Redis(System<RedisCache, RedisMaster>),
    74}
    75
    76#[allow(unused_doc_comments)]
    77impl SystemGroup {
    78    // TODO find a way to document these methods
    79
    80    // utility function to get difficulty factor of site `id` and cache it
    81    enum_system_wrapper!(get_pow, String, CaptchaResult<Option<PoWConfig>>);
    82
    83    // utility function to verify [Work]
    84    pub async fn verify_pow(
    85        &self,
    86        msg: Work,
    87        ip: String,
    88    ) -> CaptchaResult<(String, u32)> {
    89        match self {
    90            Self::Embedded(val) => val.verify_pow(msg, ip).await,
    91            Self::Redis(val) => val.verify_pow(msg, ip).await,
    92        }
    93    }
    94
    95    // utility function to validate verification tokens
    96    enum_system_wrapper!(
    97        validate_verification_tokens,
    98        VerifyCaptchaResult,
    99        CaptchaResult<bool>
    100    );
    101
    102    // utility function to AddSite
    103    enum_system_actor!(add_site, AddSite);
    104
    105    // utility function to rename captcha
    106    enum_system_actor!(rename, Rename);
    107
    108    // utility function to remove captcha
    109    enum_system_actor!(remove, RemoveCaptcha);
    110
    111    fn new_system<A: Save, B: MasterTrait>(
    112        s: &Settings,
    113        m: Addr<B>,
    114        c: Addr<A>,
    115    ) -> System<A, B> {
    116        let pow = PoWConfigBuilder::default()
    117            .salt(s.captcha.salt.clone())
    118            .build()
    119            .unwrap();
    120
    121        let runners = if let Some(runners) = s.captcha.runners {
    122            runners
    123        } else {
    124            num_cpus::get_physical()
    125        };
    126        SystemBuilder::default()
    127            .pow(pow)
    128            .cache(c)
    129            .master(m)
    130            .runners(runners)
    131            .queue_length(s.captcha.queue_length)
    132            .build()
    133    }
    134
    135    // read settings, if Redis is configured then produce a Redis mCaptcha cache
    136    // based SystemGroup
    137    async fn new(s: &Settings) -> Self {
    138        match &s.redis {
    139            Some(val) => {
    140                let master = RedisMaster::new(RedisConfig::Single(val.url.clone()))
    141                    .await
    142                    .unwrap()
    143                    .start();
    144                let cache = RedisCache::new(RedisConfig::Single(val.url.clone()))
    145                    .await
    146                    .unwrap()
    147                    .start();
    148                let captcha = Self::new_system(s, master, cache);
    149
    150                SystemGroup::Redis(captcha)
    151            }
    152            None => {
    153                let master = EmbeddedMaster::new(s.captcha.gc).start();
    154                let cache = HashCache::default().start();
    155                let captcha = Self::new_system(s, master, cache);
    156
    157                SystemGroup::Embedded(captcha)
    158            }
    159        }
    160    }
    161}
    162
    163/// App data
    164pub struct Data {
    165    /// database ops defined by db crates
    166    pub db: BoxDB,
    167    /// credential management configuration
    168    pub creds: Config,
    169    /// mCaptcha system: Redis cache, etc.
    170    pub captcha: SystemGroup,
    171    /// email client
    172    pub mailer: Option<Mailer>,
    173    /// app settings
    174    pub settings: Settings,
    175    /// stats recorder
    176    pub stats: Box<dyn Stats>,
    177    /// survey secret store
    178    pub survey_secrets: SecretsStore,
    179}
    180
    181impl Data {
    182    pub fn get_creds() -> Config {
    183        ConfigBuilder::default()
    184            .username_case_mapped(true)
    185            .profanity(true)
    186            .blacklist(true)
    187            .password_policy(PasswordPolicy::default())
    188            .build()
    189            .unwrap()
    190    }
    191    /// create new instance of app data
    192    pub async fn new(s: &Settings, survey_secrets: SecretsStore) -> Arc<Self> {
    193        let creds = Self::get_creds();
    194        let c = creds.clone();
    195
    196        #[allow(unused_variables)]
    197        let init = thread::spawn(move || {
    198            log::info!("Initializing credential manager");
    199            c.init();
    200            log::info!("Initialized credential manager");
    201        });
    202
    203        let db = match s.database.database_type {
    204            crate::settings::DBType::Maria => db::maria::get_data(Some(s.clone())).await,
    205            crate::settings::DBType::Postgres => db::pg::get_data(Some(s.clone())).await,
    206        };
    207
    208        let stats: Box<dyn Stats> = if s.captcha.enable_stats {
    209            Box::<Real>::default()
    210        } else {
    211            Box::<Dummy>::default()
    212        };
    213
    214        let data = Data {
    215            creds,
    216            db,
    217            captcha: SystemGroup::new(s).await,
    218            mailer: Self::get_mailer(s),
    219            settings: s.clone(),
    220            stats,
    221            survey_secrets,
    222        };
    223
    224        #[cfg(not(debug_assertions))]
    225        init.join().unwrap();
    226
    227        Arc::new(data)
    228    }
    229
    230    fn get_mailer(s: &Settings) -> Option<Mailer> {
    231        if let Some(smtp) = s.smtp.as_ref() {
    232            let creds =
    233                Credentials::new(smtp.username.to_string(), smtp.password.to_string()); // "smtp_username".to_string(), "smtp_password".to_string());
    234
    235            let mailer: Mailer =
    236                AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&smtp.url)
    237                    .port(smtp.port)
    238                    .credentials(creds)
    239                    .authentication(vec![
    240                        Mechanism::Login,
    241                        Mechanism::Xoauth2,
    242                        Mechanism::Plain,
    243                    ])
    244                    .build();
    245
    246            //            let mailer: Mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.url) //"smtp.gmail.com")
    247            //                .unwrap()
    248            //                .credentials(creds)
    249            //                .build();
    250            Some(mailer)
    251        } else {
    252            None
    253        }
    254    }
    255
    256    async fn upload_survey_job(&self) -> ServiceResult<()> {
    257        unimplemented!()
    258    }
    259    async fn register_survey(&self) -> ServiceResult<()> {
    260        unimplemented!()
    261    }
    262}
    263
    264/// Mailer data type AsyncSmtpTransport<Tokio1Executor>
    265pub type Mailer = AsyncSmtpTransport<Tokio1Executor>;