This commit is contained in:
Aravinth Manivannan 2021-03-08 19:43:26 +05:30
parent d53d35468e
commit b4883c414a
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
7 changed files with 199 additions and 248 deletions

View file

@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
//! In-memory cache implementation that uses [HashMap]
use std::collections::HashMap; use std::collections::HashMap;
use actix::prelude::*; use actix::prelude::*;
@ -26,6 +26,7 @@ use crate::errors::*;
use crate::pow::PoWConfig; use crate::pow::PoWConfig;
#[derive(Clone, Default)] #[derive(Clone, Default)]
/// cache datastructure implementing [Save]
pub struct HashCache(HashMap<String, u32>); pub struct HashCache(HashMap<String, u32>);
impl HashCache { impl HashCache {
@ -86,21 +87,4 @@ mod tests {
let difficulty_factor = addr.send(Retrive(string)).await.unwrap().unwrap(); let difficulty_factor = addr.send(Retrive(string)).await.unwrap().unwrap();
assert_eq!(difficulty_factor.unwrap(), 54); assert_eq!(difficulty_factor.unwrap(), 54);
} }
//
// #[actix_rt::test]
// async fn counter_defense_loosenup_works() {
// use actix::clock::delay_for;
// let addr: MyActor = get_counter().start();
//
// race(addr.clone(), LEVEL_2).await;
// race(addr.clone(), LEVEL_2).await;
// let mut difficulty_factor = addr.send(Visitor).await.unwrap();
// assert_eq!(difficulty_factor.difficulty_factor, LEVEL_2.1);
//
// let duration = Duration::new(DURATION, 0);
// delay_for(duration).await;
//
// difficulty_factor = addr.send(Visitor).await.unwrap();
// assert_eq!(difficulty_factor.difficulty_factor, LEVEL_1.1);
// }
} }

5
src/cache/mod.rs vendored
View file

@ -15,15 +15,18 @@
* You should have received a copy of the GNU Affero General Public License * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
//! Cache is used to save proofof work details and nonces to prevent replay attacks
//! and rainbow/dictionary attacks
pub use hashcache::HashCache; pub use hashcache::HashCache;
use messages::*; use messages::*;
pub mod hashcache; pub mod hashcache;
/// Describes actor handler trait impls that are required by a cache implementation
pub trait Save: actix::Actor + actix::Handler<Retrive> + actix::Handler<Cache> {} pub trait Save: actix::Actor + actix::Handler<Retrive> + actix::Handler<Cache> {}
pub mod messages { pub mod messages {
//! Messages that can be sent to cache data structures implementing [Save][super::Save]
use crate::pow::PoWConfig; use crate::pow::PoWConfig;
use actix::dev::*; use actix::dev::*;

166
src/data.rs Normal file
View file

@ -0,0 +1,166 @@
/*
* 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/>.
*/
//! module describing various bits of data required for an mCaptcha system
use actix::dev::*;
use derive_builder::Builder;
use pow_sha256::{Config, PoW};
use crate::cache::messages;
use crate::cache::Save;
use crate::errors::*;
use crate::master::Master;
use crate::pow::*;
/// struct describing various bits of data required for an mCaptcha system
#[derive(Clone, Builder)]
pub struct Data<T: Save> {
master: Addr<Master<'static>>,
cache: Addr<T>,
pow: Config,
}
impl<T> Data<T>
where
T: Save,
<T as actix::Actor>::Context: ToEnvelope<T, messages::Cache> + ToEnvelope<T, messages::Retrive>,
{
/// utility function to get difficulty factor of site `id` and cache it
pub async fn get_pow(&self, id: String) -> Option<PoWConfig> {
use crate::cache::messages::Cache;
use crate::master::GetSite;
use crate::mcaptcha::Visitor;
let site_addr = self.master.send(GetSite(id)).await.unwrap();
if site_addr.is_none() {
return None;
}
let difficulty_factor = site_addr.unwrap().send(Visitor).await.unwrap();
let pow_config = PoWConfig::new(difficulty_factor);
self.cache
.send(Cache(pow_config.clone()))
.await
.unwrap()
.unwrap();
Some(pow_config)
}
/// utility function to verify [Work]
pub async fn verify_pow(&self, work: Work) -> CaptchaResult<bool> {
use crate::cache::messages::Retrive;
let string = work.string.clone();
let msg = Retrive(string.clone());
let difficulty = self.cache.send(msg).await.unwrap();
let pow: PoW<String> = work.into();
match difficulty {
Ok(Some(difficulty)) => {
if self.pow.is_sufficient_difficulty(&pow, difficulty) {
Ok(self.pow.is_valid_proof(&pow, &string))
} else {
Err(CaptchaError::InsuffiencientDifficulty)
}
}
Ok(None) => Err(CaptchaError::StringNotFound),
Err(_) => Err(CaptchaError::Default),
}
}
}
#[cfg(test)]
mod tests {
use pow_sha256::ConfigBuilder;
use super::*;
use crate::cache::HashCache;
use crate::master::*;
use crate::mcaptcha::tests::*;
const MCAPTCHA_NAME: &str = "batsense.net";
async fn boostrap_system() -> Data<HashCache> {
let master = Master::new().start();
let mcaptcha = get_counter().start();
let pow = get_config();
let cache = HashCache::default().start();
let msg = AddSiteBuilder::default()
.id(MCAPTCHA_NAME.into())
.addr(mcaptcha.clone())
.build()
.unwrap();
master.send(msg).await.unwrap();
DataBuilder::default()
.master(master)
.cache(cache)
.pow(pow)
.build()
.unwrap()
}
fn get_config() -> Config {
ConfigBuilder::default()
.salt("myrandomsaltisnotlongenoug".into())
.build()
.unwrap()
}
#[actix_rt::test]
async fn get_pow_works() {
let actors = boostrap_system().await;
let pow = actors.get_pow(MCAPTCHA_NAME.into()).await.unwrap();
assert_eq!(pow.difficulty_factor, LEVEL_1.0);
}
#[actix_rt::test]
async fn verify_pow_works() {
let actors = boostrap_system().await;
let work_req = actors.get_pow(MCAPTCHA_NAME.into()).await.unwrap();
let config = get_config();
let work = config
.prove_work(&work_req.string, work_req.difficulty_factor)
.unwrap();
let insufficient_work = config.prove_work(&work_req.string, 1).unwrap();
let insufficient_work_payload = Work {
string: work_req.string.clone(),
result: insufficient_work.result,
nonce: insufficient_work.nonce,
};
let mut payload = Work {
string: work_req.string,
result: work.result,
nonce: work.nonce,
};
let res = actors.verify_pow(payload.clone()).await.unwrap();
assert!(res);
payload.string = "wrongstring".into();
let res = actors.verify_pow(payload.clone()).await;
assert_eq!(res, Err(CaptchaError::StringNotFound));
let res = actors.verify_pow(insufficient_work_payload.clone()).await;
assert_eq!(res, Err(CaptchaError::InsuffiencientDifficulty));
}
}

View file

@ -105,6 +105,7 @@ pub mod message {
/// message datatypes to interact with [MCaptcha] actor /// message datatypes to interact with [MCaptcha] actor
pub mod cache; pub mod cache;
pub mod data;
pub mod pow; pub mod pow;
mod utils; mod utils;

View file

@ -15,6 +15,7 @@
* You should have received a copy of the GNU Affero General Public License * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
//! [Master] actor module that manages [MCaptcha] actors
use std::collections::BTreeMap; use std::collections::BTreeMap;
use actix::dev::*; use actix::dev::*;
@ -22,24 +23,29 @@ use derive_builder::Builder;
use crate::mcaptcha::MCaptcha; use crate::mcaptcha::MCaptcha;
/// This struct represents the mCaptcha state and is used /// This Actor manages the [MCaptcha] actors.
/// to configure leaky-bucket lifetime and manage defense /// A service can have several [MCaptcha] actors with
/// varying [Defense][crate::defense::Defense] configurations
/// so a "master" actor is needed to manage them all
#[derive(Clone)] #[derive(Clone)]
pub struct Master<'a> { pub struct Master<'a> {
sites: BTreeMap<&'a str, Addr<MCaptcha>>, sites: BTreeMap<&'a str, Addr<MCaptcha>>,
} }
impl Master<'static> { impl Master<'static> {
/// add [MCaptcha] actor to [Master]
pub fn add_site(&mut self, details: AddSite) { pub fn add_site(&mut self, details: AddSite) {
self.sites.insert(details.id, details.addr.to_owned()); self.sites.insert(details.id, details.addr.to_owned());
} }
/// create new master
pub fn new() -> Self { pub fn new() -> Self {
Master { Master {
sites: BTreeMap::new(), sites: BTreeMap::new(),
} }
} }
/// get [MCaptcha] actor from [Master]
pub fn get_site<'a, 'b>(&'a self, id: &'b str) -> Option<&'a Addr<MCaptcha>> { pub fn get_site<'a, 'b>(&'a self, id: &'b str) -> Option<&'a Addr<MCaptcha>> {
self.sites.get(id) self.sites.get(id)
} }
@ -49,7 +55,7 @@ impl Actor for Master<'static> {
type Context = Context<Self>; type Context = Context<Self>;
} }
/// Message to increment the visitor count /// Message to get an [MCaptcha] actor from master
#[derive(Message)] #[derive(Message)]
#[rtype(result = "Option<Addr<MCaptcha>>")] #[rtype(result = "Option<Addr<MCaptcha>>")]
pub struct GetSite(pub String); pub struct GetSite(pub String);
@ -67,7 +73,7 @@ impl Handler<GetSite> for Master<'static> {
} }
} }
/// Message to increment the visitor count /// Message to add an [MCaptcha] actor to [Master]
#[derive(Message, Builder)] #[derive(Message, Builder)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct AddSite { pub struct AddSite {
@ -83,98 +89,28 @@ impl Handler<AddSite> for Master<'static> {
} }
} }
///// Message to decrement the visitor count
//#[derive(Message, Deserialize)]
//#[rtype(result = "()")]
//pub struct VerifyPoW {
// pow: ShaPoW<Vec<u8>>,
// id: String,
//}
//
//impl Handler<VerifyPoW> for MCaptcha {
// type Result = ();
// fn handle(&mut self, msg: VerifyPoW, _ctx: &mut Self::Context) -> Self::Result {
// self.decrement_visiotr();
// }
//}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::defense::*; use crate::mcaptcha::tests::*;
// use crate::cache::HashCache;
//
// // constants for testing
// // (visitor count, level)
const LEVEL_1: (u32, u32) = (50, 50);
const LEVEL_2: (u32, u32) = (500, 500);
const DURATION: u64 = 10;
type MyActor = Addr<MCaptcha>;
fn get_defense() -> Defense {
DefenseBuilder::default()
.add_level(
LevelBuilder::default()
.visitor_threshold(LEVEL_1.0)
.difficulty_factor(LEVEL_1.1)
.unwrap()
.build()
.unwrap(),
)
.unwrap()
.add_level(
LevelBuilder::default()
.visitor_threshold(LEVEL_2.0)
.difficulty_factor(LEVEL_2.1)
.unwrap()
.build()
.unwrap(),
)
.unwrap()
.build()
.unwrap()
}
fn get_counter() -> MCaptcha {
use crate::MCaptchaBuilder;
MCaptchaBuilder::default()
.defense(get_defense())
.duration(DURATION)
.build()
.unwrap()
}
#[actix_rt::test] #[actix_rt::test]
async fn master() { async fn master_actor_works() {
let addr = Master::new().start(); let addr = Master::new().start();
let id = "yo"; let id = "yo";
let mcaptcha = get_counter().start(); let mcaptcha = get_counter().start();
let msg = AddSiteBuilder::default() let msg = AddSiteBuilder::default()
.id(id) .id(id)
.addr(mcaptcha) .addr(mcaptcha.clone())
.build() .build()
.unwrap(); .unwrap();
addr.send(msg).await.unwrap(); addr.send(msg).await.unwrap();
let mcaptcha_addr = addr.send(GetSite(id.into())).await.unwrap();
assert_eq!(mcaptcha_addr, Some(mcaptcha));
let addr_doesnt_exist = addr.send(GetSite("a".into())).await.unwrap();
assert!(addr_doesnt_exist.is_none());
} }
//
// #[actix_rt::test]
// async fn counter_defense_loosenup_works() {
// use actix::clock::delay_for;
// let addr: MyActor = get_counter().start();
//
// race(addr.clone(), LEVEL_2).await;
// race(addr.clone(), LEVEL_2).await;
// let mut difficulty_factor = addr.send(Visitor).await.unwrap().unwrap();
// assert_eq!(difficulty_factor, LEVEL_2.1);
//
// let duration = Duration::new(DURATION, 0);
// delay_for(duration).await;
//
// difficulty_factor = addr.send(Visitor).await.unwrap().unwrap();
// assert_eq!(difficulty_factor, LEVEL_1.1);
// }
} }

View file

@ -83,7 +83,7 @@ use crate::defense::Defense;
/// This struct represents the mCaptcha state and is used /// This struct represents the mCaptcha state and is used
/// to configure leaky-bucket lifetime and manage defense /// to configure leaky-bucket lifetime and manage defense
#[derive(Clone, Builder)] #[derive(Clone, Debug, Builder)]
pub struct MCaptcha { pub struct MCaptcha {
#[builder(default = "0", setter(skip))] #[builder(default = "0", setter(skip))]
visitor_threshold: u32, visitor_threshold: u32,

View file

@ -15,23 +15,19 @@
* You should have received a copy of the GNU Affero General Public License * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
use actix::dev::*;
use derive_builder::Builder; //! PoW datatypes used in client-server interaction
use pow_sha256::{Config, PoW}; use pow_sha256::PoW;
use serde::Serialize; use serde::Serialize;
use crate::cache::messages; /// PoW requirement datatype that is be sent to clients for generating PoW
use crate::cache::Save;
use crate::errors::*;
use crate::master::Master;
/// PoW Config that will be sent to clients for generating PoW
#[derive(Clone, Serialize, Debug)] #[derive(Clone, Serialize, Debug)]
pub struct PoWConfig { pub struct PoWConfig {
pub string: String, pub string: String,
pub difficulty_factor: u32, pub difficulty_factor: u32,
} }
impl PoWConfig { impl PoWConfig {
/// create new instance of [PoWConfig]
pub fn new(m: u32) -> Self { pub fn new(m: u32) -> Self {
use crate::utils::get_random; use crate::utils::get_random;
@ -42,58 +38,7 @@ impl PoWConfig {
} }
} }
#[derive(Clone, Builder)] /// PoW datatype that clients send to server
pub struct Actors<T: Save> {
master: Addr<Master<'static>>,
cache: Addr<T>,
pow: Config,
}
impl<T> Actors<T>
where
T: Save,
<T as actix::Actor>::Context: ToEnvelope<T, messages::Cache> + ToEnvelope<T, messages::Retrive>,
{
pub async fn get_pow(&self, id: String) -> Option<PoWConfig> {
use crate::cache::messages::Cache;
use crate::master::GetSite;
use crate::mcaptcha::Visitor;
let site_addr = self.master.send(GetSite(id)).await.unwrap();
if site_addr.is_none() {
return None;
}
let difficulty_factor = site_addr.unwrap().send(Visitor).await.unwrap();
let pow_config = PoWConfig::new(difficulty_factor);
self.cache
.send(Cache(pow_config.clone()))
.await
.unwrap()
.unwrap();
Some(pow_config)
}
pub async fn verify_pow(&self, work: Work) -> CaptchaResult<bool> {
use crate::cache::messages::Retrive;
let string = work.string.clone();
let msg = Retrive(string.clone());
let difficulty = self.cache.send(msg).await.unwrap();
let pow: PoW<String> = work.into();
match difficulty {
Ok(Some(difficulty)) => {
if self.pow.is_sufficient_difficulty(&pow, difficulty) {
Ok(self.pow.is_valid_proof(&pow, &string))
} else {
Err(CaptchaError::InsuffiencientDifficulty)
}
}
Ok(None) => Err(CaptchaError::StringNotFound),
Err(_) => Err(CaptchaError::Default),
}
}
}
#[derive(Clone, Serialize, Debug)] #[derive(Clone, Serialize, Debug)]
pub struct Work { pub struct Work {
pub string: String, pub string: String,
@ -111,87 +56,3 @@ impl From<Work> for PoW<String> {
.unwrap() .unwrap()
} }
} }
#[cfg(test)]
mod tests {
use pow_sha256::ConfigBuilder;
use super::*;
use crate::cache::HashCache;
use crate::master::*;
use crate::mcaptcha::tests::*;
const MCAPTCHA_NAME: &str = "batsense.net";
async fn boostrap_system() -> Actors<HashCache> {
let master = Master::new().start();
let mcaptcha = get_counter().start();
let pow = get_config();
let cache = HashCache::default().start();
let msg = AddSiteBuilder::default()
.id(MCAPTCHA_NAME.into())
.addr(mcaptcha.clone())
.build()
.unwrap();
master.send(msg).await.unwrap();
ActorsBuilder::default()
.master(master)
.cache(cache)
.pow(pow)
.build()
.unwrap()
}
fn get_config() -> Config {
ConfigBuilder::default()
.salt("myrandomsaltisnotlongenoug".into())
.build()
.unwrap()
}
#[actix_rt::test]
async fn get_pow_works() {
let actors = boostrap_system().await;
let pow = actors.get_pow(MCAPTCHA_NAME.into()).await.unwrap();
assert_eq!(pow.difficulty_factor, LEVEL_1.0);
}
#[actix_rt::test]
async fn verify_pow_works() {
let actors = boostrap_system().await;
let work_req = actors.get_pow(MCAPTCHA_NAME.into()).await.unwrap();
let config = get_config();
let work = config
.prove_work(&work_req.string, work_req.difficulty_factor)
.unwrap();
let insufficient_work = config.prove_work(&work_req.string, 1).unwrap();
let insufficient_work_payload = Work {
string: work_req.string.clone(),
result: insufficient_work.result,
nonce: insufficient_work.nonce,
};
let mut payload = Work {
string: work_req.string,
result: work.result,
nonce: work.nonce,
};
let res = actors.verify_pow(payload.clone()).await.unwrap();
assert!(res);
payload.string = "wrongstring".into();
let res = actors.verify_pow(payload.clone()).await;
assert_eq!(res, Err(CaptchaError::StringNotFound));
let res = actors.verify_pow(insufficient_work_payload.clone()).await;
assert_eq!(res, Err(CaptchaError::InsuffiencientDifficulty));
}
}