331 lines
11 KiB
Rust
331 lines
11 KiB
Rust
/*
|
|
* mCaptcha - A proof of work based DoS protection system
|
|
* Copyright © 2021 Aravinth Manivannan <realravinth@batsense.net>
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
//! In-memory cache implementation that uses [HashMap]
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use dashmap::DashMap;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use libmcaptcha::cache::messages::*;
|
|
use libmcaptcha::errors::*;
|
|
|
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
|
/// cache datastructure implementing [Save]
|
|
pub struct HashCache {
|
|
difficulty_map: Arc<DashMap<String, CachedPoWConfig>>,
|
|
result_map: Arc<DashMap<String, (String, u64)>>,
|
|
}
|
|
|
|
impl HashCache {
|
|
// save [PoWConfig] to cache
|
|
fn save_pow_config(&self, config: CachePoW) -> CaptchaResult<()> {
|
|
let challenge = config.string;
|
|
let config: CachedPoWConfig = CachedPoWConfig {
|
|
key: config.key,
|
|
difficulty_factor: config.difficulty_factor,
|
|
duration: config.duration,
|
|
};
|
|
|
|
if self.difficulty_map.get(&challenge).is_none() {
|
|
self.difficulty_map.insert(challenge, config);
|
|
Ok(())
|
|
} else {
|
|
Err(CaptchaError::InvalidPoW)
|
|
}
|
|
}
|
|
|
|
pub async fn clean_all_after_cold_start(&self, updated: HashCache) {
|
|
updated.difficulty_map.iter().for_each(|x| {
|
|
self.difficulty_map
|
|
.insert(x.key().to_owned(), x.value().to_owned());
|
|
});
|
|
updated.result_map.iter().for_each(|x| {
|
|
self.result_map
|
|
.insert(x.key().to_owned(), x.value().to_owned());
|
|
});
|
|
let cache = self.clone();
|
|
let fut = async move {
|
|
for values in cache.result_map.iter() {
|
|
let inner_cache = cache.clone();
|
|
let duration = values.value().1;
|
|
let key = values.key().to_owned();
|
|
let inner_fut = async move {
|
|
tokio::time::sleep(Duration::new(duration, 0)).await;
|
|
inner_cache.remove_cache_result(&key);
|
|
};
|
|
tokio::spawn(inner_fut);
|
|
}
|
|
|
|
for values in cache.difficulty_map.iter() {
|
|
let inner_cache = cache.clone();
|
|
let duration = values.value().duration;
|
|
let key = values.key().to_owned();
|
|
let inner_fut = async move {
|
|
tokio::time::sleep(Duration::new(duration, 0)).await;
|
|
inner_cache.remove_pow_config(&key);
|
|
};
|
|
tokio::spawn(inner_fut);
|
|
}
|
|
};
|
|
|
|
tokio::spawn(fut);
|
|
}
|
|
|
|
// retrieve [PoWConfig] from cache. Deletes config post retrival
|
|
pub fn retrieve_pow_config(&self, msg: VerifyCaptchaResult) -> Option<CachedPoWConfig> {
|
|
if let Some(difficulty_factor) = self.remove_pow_config(&msg.token) {
|
|
Some(difficulty_factor)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// delete [PoWConfig] from cache
|
|
pub fn remove_pow_config(&self, string: &str) -> Option<CachedPoWConfig> {
|
|
self.difficulty_map.remove(string).map(|x| x.1)
|
|
}
|
|
|
|
// save captcha result
|
|
fn save_captcha_result(&self, res: CacheResult) {
|
|
self.result_map.insert(res.token, (res.key, res.duration));
|
|
}
|
|
|
|
// verify captcha result
|
|
pub fn verify_captcha_result(&self, challenge: VerifyCaptchaResult) -> bool {
|
|
if let Some(captcha_id) = self.remove_cache_result(&challenge.token) {
|
|
if captcha_id == challenge.key {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
// delete cache result
|
|
pub fn remove_cache_result(&self, string: &str) -> Option<String> {
|
|
self.result_map.remove(string).map(|x| x.1 .0)
|
|
}
|
|
|
|
pub fn cache_pow(&self, msg: CachePoW) {
|
|
use std::time::Duration;
|
|
use tokio::time::sleep;
|
|
|
|
let duration: Duration = Duration::new(msg.duration, 0);
|
|
let string = msg.string.clone();
|
|
let cache = self.clone();
|
|
let wait_for = async move {
|
|
sleep(duration).await;
|
|
//delay_for(duration).await;
|
|
cache.remove_pow_config(&string);
|
|
};
|
|
let _ = self.save_pow_config(msg);
|
|
tokio::spawn(wait_for);
|
|
}
|
|
|
|
/// cache PoW result
|
|
pub fn cache_result(&self, msg: CacheResult) {
|
|
use std::time::Duration;
|
|
use tokio::time::sleep;
|
|
|
|
let token = msg.token.clone();
|
|
msg.token.clone();
|
|
msg.token.clone();
|
|
msg.token.clone();
|
|
|
|
let duration: Duration = Duration::new(msg.duration, 0);
|
|
let cache = self.clone();
|
|
let wait_for = async move {
|
|
sleep(duration).await;
|
|
//delay_for(duration).await;
|
|
cache.remove_cache_result(&token);
|
|
};
|
|
tokio::spawn(wait_for);
|
|
|
|
let _ = self.save_captcha_result(msg);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use libmcaptcha::master::AddVisitorResult;
|
|
use libmcaptcha::pow::PoWConfig;
|
|
|
|
use std::time::Duration;
|
|
#[actix_rt::test]
|
|
async fn merge_works() {
|
|
const DIFFICULTY_FACTOR: u32 = 54;
|
|
const RES: &str = "b";
|
|
const DURATION: u64 = 5;
|
|
const KEY: &str = "mcaptchakey";
|
|
let pow: PoWConfig = PoWConfig::new(DIFFICULTY_FACTOR, KEY.into()); //salt is dummy here
|
|
|
|
let cache = HashCache::default();
|
|
let new_cache = HashCache::default();
|
|
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();
|
|
|
|
cache.cache_pow(msg);
|
|
|
|
let add_cache = CacheResult {
|
|
key: KEY.into(),
|
|
token: RES.into(),
|
|
duration: DURATION,
|
|
};
|
|
|
|
cache.cache_result(add_cache.clone());
|
|
|
|
new_cache.clean_all_after_cold_start(cache.clone()).await;
|
|
|
|
let msg = VerifyCaptchaResult {
|
|
token: string.clone(),
|
|
key: KEY.into(),
|
|
};
|
|
let cache_difficulty_factor = cache.retrieve_pow_config(msg.clone()).unwrap();
|
|
let new_cache_difficulty_factor = new_cache.retrieve_pow_config(msg.clone()).unwrap();
|
|
|
|
assert_eq!(DIFFICULTY_FACTOR, cache_difficulty_factor.difficulty_factor);
|
|
assert_eq!(
|
|
DIFFICULTY_FACTOR,
|
|
new_cache_difficulty_factor.difficulty_factor
|
|
);
|
|
|
|
let verify_msg = VerifyCaptchaResult {
|
|
key: KEY.into(),
|
|
token: RES.into(),
|
|
};
|
|
|
|
assert!(new_cache.verify_captcha_result(verify_msg.clone()));
|
|
assert!(!new_cache.verify_captcha_result(verify_msg.clone()));
|
|
|
|
let duration: Duration = Duration::new(5, 0);
|
|
//sleep(DURATION + DURATION).await;
|
|
tokio::time::sleep(duration + duration).await;
|
|
|
|
let expired_string = cache.retrieve_pow_config(msg.clone());
|
|
assert_eq!(None, expired_string);
|
|
let expired_string = new_cache.retrieve_pow_config(msg);
|
|
assert_eq!(None, expired_string);
|
|
|
|
cache.cache_result(add_cache);
|
|
new_cache.clean_all_after_cold_start(cache.clone()).await;
|
|
tokio::time::sleep(duration + duration).await;
|
|
assert!(!new_cache.verify_captcha_result(verify_msg.clone()));
|
|
assert!(!cache.verify_captcha_result(verify_msg));
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn hashcache_pow_cache_works() {
|
|
const DIFFICULTY_FACTOR: u32 = 54;
|
|
const DURATION: u64 = 5;
|
|
const KEY: &str = "mcaptchakey";
|
|
let cache = HashCache::default();
|
|
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();
|
|
|
|
cache.cache_pow(msg);
|
|
|
|
let msg = VerifyCaptchaResult {
|
|
token: string.clone(),
|
|
key: KEY.into(),
|
|
};
|
|
let cache_difficulty_factor = cache.retrieve_pow_config(msg.clone()).unwrap();
|
|
|
|
assert_eq!(DIFFICULTY_FACTOR, cache_difficulty_factor.difficulty_factor);
|
|
|
|
let duration: Duration = Duration::new(5, 0);
|
|
//sleep(DURATION + DURATION).await;
|
|
tokio::time::sleep(duration + duration).await;
|
|
|
|
let expired_string = cache.retrieve_pow_config(msg);
|
|
assert_eq!(None, expired_string);
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn hashcache_result_cache_works() {
|
|
const DURATION: u64 = 5;
|
|
const KEY: &str = "a";
|
|
const RES: &str = "b";
|
|
let cache = HashCache::default();
|
|
// 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,
|
|
};
|
|
|
|
cache.cache_result(add_cache);
|
|
|
|
let verify_msg = VerifyCaptchaResult {
|
|
key: KEY.into(),
|
|
token: RES.into(),
|
|
};
|
|
|
|
assert!(cache.verify_captcha_result(verify_msg.clone()));
|
|
|
|
// duplicate
|
|
assert!(!cache.verify_captcha_result(verify_msg));
|
|
|
|
let verify_msg = VerifyCaptchaResult {
|
|
key: "cz".into(),
|
|
token: RES.into(),
|
|
};
|
|
assert!(!cache.verify_captcha_result(verify_msg));
|
|
|
|
let duration: Duration = Duration::new(5, 0);
|
|
tokio::time::sleep(duration + duration).await;
|
|
|
|
let verify_msg = VerifyCaptchaResult {
|
|
key: KEY.into(),
|
|
token: RES.into(),
|
|
};
|
|
assert!(!cache.verify_captcha_result(verify_msg));
|
|
}
|
|
}
|