diff --git a/examples/simple.rs b/examples/simple.rs index 09f35d9..b48c191 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,5 +1,5 @@ use libmcaptcha::{ - cache::{messages::VerifyCaptchaResult, HashCache}, + cache::{hashcache::HashCache, messages::VerifyCaptchaResult}, master::embedded::master::Master, master::messages::AddSiteBuilder, pow::{ConfigBuilder, Work}, diff --git a/src/cache/mod.rs b/src/cache/mod.rs index fd20d0c..710c67c 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -17,22 +17,32 @@ */ //! Cache is used to save proofof work details and nonces to prevent replay attacks //! and rainbow/dictionary attacks -pub use hashcache::HashCache; -use messages::*; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] pub mod hashcache; +#[derive(Serialize, Deserialize)] +pub struct AddChallenge { + pub difficulty: usize, + pub duration: u64, + pub challenge: String, +} + /// Describes actor handler trait impls that are required by a cache implementation +#[cfg(feature = "full")] pub trait Save: actix::Actor - + actix::Handler - + actix::Handler - + actix::Handler - + actix::Handler - + actix::Handler - + actix::Handler + + actix::Handler + + actix::Handler + + actix::Handler + + actix::Handler + + actix::Handler + + actix::Handler { } + +#[cfg(feature = "full")] pub mod messages { //! Messages that can be sent to cache data structures implementing [Save][super::Save] use actix::dev::*; @@ -45,23 +55,16 @@ pub mod messages { #[derive(Message, Serialize, Deserialize, Builder, Clone)] #[rtype(result = "CaptchaResult<()>")] pub struct CachePoW { + /// challenge string pub string: String, + /// Difficulty factor of mCaptcha at the time of minting this config pub difficulty_factor: u32, + /// mCaptcha TTL pub duration: u64, + /// Key is mCaptcha name pub key: String, } - // pub fn new(p: &PoWConfig, k: String, v: &AddVisitorResult) -> Self { - // CachePoWBuilder::default() - // .string(p.string.clone()) - // .difficulty_factor(v.difficulty_factor) - // .duration(v.duration) - // .key(k) - // .build() - // .unwrap() - // } - //} - /// Message to retrive the the difficulty factor for the specified /// string from the cache #[derive(Message)] @@ -70,6 +73,7 @@ pub mod messages { #[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)] pub struct CachedPoWConfig { + /// mCaptcha name pub key: String, pub difficulty_factor: u32, pub duration: u64, @@ -87,7 +91,7 @@ pub mod messages { #[rtype(result = "CaptchaResult<()>")] pub struct CacheResult { pub token: String, - // key is Captcha identifier + /// key is mCaptcha identifier pub key: String, pub duration: u64, } diff --git a/src/cache/redis.rs b/src/cache/redis.rs new file mode 100644 index 0000000..ae40835 --- /dev/null +++ b/src/cache/redis.rs @@ -0,0 +1,294 @@ +/* + * mCaptcha - A proof of work based DoS protection system + * Copyright © 2021 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 . + */ +//! Cache implementation that uses Redis +use crate::redis::Redis; +use std::collections::HashMap; + +use actix::prelude::*; + +use super::messages::*; +use super::Save; +use crate::errors::*; + +pub struct RedisCache(Redis); + +#[derive(Clone, Default)] +/// cache datastructure implementing [Save] +pub struct HashCache { + difficulty_map: HashMap, + result_map: HashMap, +} + +impl RedisCache { + // save [PoWConfig] to cache + async fn save_pow_config(&mut self, config: CachePoW) -> CaptchaResult<()> { + let challenge = config.string; + let config: CachedPoWConfig = CachedPoWConfig { + key: config.key, + difficulty_factor: config.difficulty_factor, + duration: config.duration, + }; + + // {MCAPTCHA_NAME}:difficulty_map:challenge (difficulty_factor -> duration) EX duration + + // TODO use hashmap + + self.difficulty_map.insert(challenge, config); + Ok(()) + } + + // retrive [PoWConfig] from cache. Deletes config post retrival + fn retrive_pow_config(&mut self, string: String) -> CaptchaResult> { + if let Some(difficulty_factor) = self.remove_pow_config(&string) { + Ok(Some(difficulty_factor.to_owned())) + } else { + Ok(None) + } + } + + // delete [PoWConfig] from cache + fn remove_pow_config(&mut self, string: &str) -> Option { + self.difficulty_map.remove(string) + } + + // save captcha result + async fn save_captcha_result(&mut self, res: CacheResult) -> CaptchaResult<()> { + self.result_map.insert(res.token, res.key); + + // {MCAPTCHA_NAME}:result_map:token 0 EX duration + Ok(()) + } + + // verify captcha result + fn verify_captcha_result(&mut self, challenge: VerifyCaptchaResult) -> CaptchaResult { + if let Some(captcha_id) = self.remove_cache_result(&challenge.token) { + if captcha_id == challenge.key { + return Ok(true); + } else { + return Ok(false); + } + } else { + Ok(false) + } + } + + // delete cache result + fn remove_cache_result(&mut self, string: &str) -> Option { + self.result_map.remove(string) + } +} + +impl Save for HashCache {} + +impl Actor for HashCache { + type Context = Context; +} + +/// cache a PoWConfig +impl Handler for HashCache { + type Result = MessageResult; + fn handle(&mut self, msg: CachePoW, ctx: &mut Self::Context) -> Self::Result { + //use actix::clock::sleep; + use actix::clock::delay_for; + use std::time::Duration; + + let addr = ctx.address(); + let del_msg = DeletePoW(msg.string.clone()); + + let duration: Duration = Duration::new(msg.duration.clone(), 0); + let wait_for = async move { + //sleep(duration).await; + delay_for(duration).await; + addr.send(del_msg).await.unwrap().unwrap(); + } + .into_actor(self); + ctx.spawn(wait_for); + + MessageResult(self.save_pow_config(msg)) + } +} + +/// Delte a PoWConfig +impl Handler for HashCache { + type Result = MessageResult; + fn handle(&mut self, msg: DeletePoW, _ctx: &mut Self::Context) -> Self::Result { + self.remove_pow_config(&msg.0); + MessageResult(Ok(())) + } +} + +/// Retrive PoW difficulty_factor for a PoW string +impl Handler for HashCache { + type Result = MessageResult; + fn handle(&mut self, msg: RetrivePoW, _ctx: &mut Self::Context) -> Self::Result { + MessageResult(self.retrive_pow_config(msg.0)) + } +} + +/// cache PoW result +impl Handler for HashCache { + type Result = MessageResult; + fn handle(&mut self, msg: CacheResult, ctx: &mut Self::Context) -> Self::Result { + //use actix::clock::sleep; + use actix::clock::delay_for; + use std::time::Duration; + + let addr = ctx.address(); + let del_msg = DeleteCaptchaResult { + token: msg.token.clone(), + }; + + let duration: Duration = Duration::new(msg.duration.clone(), 0); + let wait_for = async move { + //sleep(duration).await; + delay_for(duration).await; + addr.send(del_msg).await.unwrap().unwrap(); + } + .into_actor(self); + ctx.spawn(wait_for); + + MessageResult(self.save_captcha_result(msg)) + } +} + +/// Delte a PoWConfig +impl Handler for HashCache { + type Result = MessageResult; + fn handle(&mut self, msg: DeleteCaptchaResult, _ctx: &mut Self::Context) -> Self::Result { + self.remove_cache_result(&msg.token); + MessageResult(Ok(())) + } +} + +/// Retrive PoW difficulty_factor for a PoW string +impl Handler for HashCache { + type Result = MessageResult; + fn handle(&mut self, msg: VerifyCaptchaResult, _ctx: &mut Self::Context) -> Self::Result { + // MessageResult(self.retrive(msg.0)) + MessageResult(self.verify_captcha_result(msg)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::master::AddVisitorResult; + use crate::pow::PoWConfig; + + // async fn sleep(time: u64) { + // //use actix::clock::sleep; + // use actix::clock::delay_for; + // use std::time::Duration; + + // let duration: Duration = Duration::new(time, 0); + // //sleep(duration).await; + // delay_for(duration).await; + // } + + #[actix_rt::test] + async fn hashcache_pow_cache_works() { + use actix::clock::delay_for; + use actix::clock::Duration; + + const DIFFICULTY_FACTOR: u32 = 54; + const DURATION: u64 = 5; + const KEY: &str = "mcaptchakey"; + let addr = HashCache::default().start(); + let pow: PoWConfig = PoWConfig::new(DIFFICULTY_FACTOR, KEY.into()); //salt is dummy here + let visitor_result = AddVisitorResult { + difficulty_factor: DIFFICULTY_FACTOR, + duration: DURATION, + }; + let string = pow.string.clone(); + + let msg = CachePoWBuilder::default() + .string(pow.string.clone()) + .difficulty_factor(DIFFICULTY_FACTOR) + .duration(visitor_result.duration) + .key(KEY.into()) + .build() + .unwrap(); + + addr.send(msg).await.unwrap().unwrap(); + + let cache_difficulty_factor = addr + .send(RetrivePoW(string.clone())) + .await + .unwrap() + .unwrap(); + assert_eq!( + DIFFICULTY_FACTOR, + cache_difficulty_factor.unwrap().difficulty_factor + ); + + let duration: Duration = Duration::new(5, 0); + //sleep(DURATION + DURATION).await; + delay_for(duration + duration).await; + + let expired_string = addr.send(RetrivePoW(string)).await.unwrap().unwrap(); + assert_eq!(None, expired_string); + } + + #[actix_rt::test] + async fn hashcache_result_cache_works() { + use actix::clock::delay_for; + use actix::clock::Duration; + + const DURATION: u64 = 5; + const KEY: &str = "a"; + const RES: &str = "b"; + let addr = HashCache::default().start(); + // send value to cache + // send another value to cache for auto delete + // verify_captcha_result + // delete + // wait for timeout and verify_captcha_result against second value + + let add_cache = CacheResult { + key: KEY.into(), + token: RES.into(), + duration: DURATION, + }; + + addr.send(add_cache).await.unwrap().unwrap(); + + let verify_msg = VerifyCaptchaResult { + key: KEY.into(), + token: RES.into(), + }; + + assert!(addr.send(verify_msg.clone()).await.unwrap().unwrap()); + // duplicate + assert!(!addr.send(verify_msg).await.unwrap().unwrap()); + + let verify_msg = VerifyCaptchaResult { + key: "cz".into(), + token: RES.into(), + }; + assert!(!addr.send(verify_msg).await.unwrap().unwrap()); + + let duration: Duration = Duration::new(5, 0); + delay_for(duration + duration).await; + + let verify_msg = VerifyCaptchaResult { + key: KEY.into(), + token: RES.into(), + }; + assert!(!addr.send(verify_msg).await.unwrap().unwrap()); + } +} diff --git a/src/lib.rs b/src/lib.rs index e2f8e23..ecf23e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ //! //! ```rust //! use libmcaptcha::{ -//! cache::{messages::VerifyCaptchaResult, HashCache}, +//! cache::{messages::VerifyCaptchaResult, hashcache::HashCache}, //! master::embedded::master:: Master, //! master::messages::AddSiteBuilder, //! pow::{ConfigBuilder, Work}, @@ -191,7 +191,6 @@ pub mod master; mod redis; /// message datatypes to interact with [MCaptcha] actor -#[cfg(feature = "full")] pub mod cache; pub mod mcaptcha; #[cfg(feature = "full")] diff --git a/src/master/embedded/counter.rs b/src/master/embedded/counter.rs index 7e41167..ede5163 100644 --- a/src/master/embedded/counter.rs +++ b/src/master/embedded/counter.rs @@ -22,7 +22,7 @@ //! use libmcaptcha::{ //! master::embedded::counter::{Counter, AddVisitor}, //! MCaptchaBuilder, -//! cache::HashCache, +//! cache::hashcache::HashCache, //! LevelBuilder, //! DefenseBuilder //! }; diff --git a/src/system.rs b/src/system.rs index a785d50..720e21c 100644 --- a/src/system.rs +++ b/src/system.rs @@ -125,7 +125,7 @@ mod tests { use super::System; use super::*; - use crate::cache::HashCache; + use crate::cache::hashcache::HashCache; use crate::master::embedded::counter::tests::*; use crate::master::embedded::master::Master;