diff --git a/src/cache/hashcache.rs b/src/cache/hashcache.rs
index 9508ca6..62984b6 100644
--- a/src/cache/hashcache.rs
+++ b/src/cache/hashcache.rs
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
-
+//! In-memory cache implementation that uses [HashMap]
use std::collections::HashMap;
use actix::prelude::*;
@@ -26,6 +26,7 @@ use crate::errors::*;
use crate::pow::PoWConfig;
#[derive(Clone, Default)]
+/// cache datastructure implementing [Save]
pub struct HashCache(HashMap);
impl HashCache {
@@ -86,21 +87,4 @@ mod tests {
let difficulty_factor = addr.send(Retrive(string)).await.unwrap().unwrap();
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);
- // }
}
diff --git a/src/cache/mod.rs b/src/cache/mod.rs
index fe6a828..c877134 100644
--- a/src/cache/mod.rs
+++ b/src/cache/mod.rs
@@ -15,15 +15,18 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
-
+//! Cache is used to save proofof work details and nonces to prevent replay attacks
+//! and rainbow/dictionary attacks
pub use hashcache::HashCache;
use messages::*;
pub mod hashcache;
+/// Describes actor handler trait impls that are required by a cache implementation
pub trait Save: actix::Actor + actix::Handler + actix::Handler {}
pub mod messages {
+ //! Messages that can be sent to cache data structures implementing [Save][super::Save]
use crate::pow::PoWConfig;
use actix::dev::*;
diff --git a/src/data.rs b/src/data.rs
new file mode 100644
index 0000000..ba47bcd
--- /dev/null
+++ b/src/data.rs
@@ -0,0 +1,166 @@
+/*
+ * 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 .
+ */
+//! 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 {
+ master: Addr>,
+ cache: Addr,
+ pow: Config,
+}
+
+impl Data
+where
+ T: Save,
+ ::Context: ToEnvelope + ToEnvelope,
+{
+ /// utility function to get difficulty factor of site `id` and cache it
+ pub async fn get_pow(&self, id: String) -> Option {
+ 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 {
+ 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 = 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 {
+ 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));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index dbaaafb..f68a93d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -105,6 +105,7 @@ pub mod message {
/// message datatypes to interact with [MCaptcha] actor
pub mod cache;
+pub mod data;
pub mod pow;
mod utils;
diff --git a/src/master.rs b/src/master.rs
index b7a04a1..87e1466 100644
--- a/src/master.rs
+++ b/src/master.rs
@@ -15,6 +15,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
+//! [Master] actor module that manages [MCaptcha] actors
use std::collections::BTreeMap;
use actix::dev::*;
@@ -22,24 +23,29 @@ use derive_builder::Builder;
use crate::mcaptcha::MCaptcha;
-/// This struct represents the mCaptcha state and is used
-/// to configure leaky-bucket lifetime and manage defense
+/// This Actor manages the [MCaptcha] actors.
+/// 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)]
pub struct Master<'a> {
sites: BTreeMap<&'a str, Addr>,
}
impl Master<'static> {
+ /// add [MCaptcha] actor to [Master]
pub fn add_site(&mut self, details: AddSite) {
self.sites.insert(details.id, details.addr.to_owned());
}
+ /// create new master
pub fn new() -> Self {
Master {
sites: BTreeMap::new(),
}
}
+ /// get [MCaptcha] actor from [Master]
pub fn get_site<'a, 'b>(&'a self, id: &'b str) -> Option<&'a Addr> {
self.sites.get(id)
}
@@ -49,7 +55,7 @@ impl Actor for Master<'static> {
type Context = Context;
}
-/// Message to increment the visitor count
+/// Message to get an [MCaptcha] actor from master
#[derive(Message)]
#[rtype(result = "Option>")]
pub struct GetSite(pub String);
@@ -67,7 +73,7 @@ impl Handler for Master<'static> {
}
}
-/// Message to increment the visitor count
+/// Message to add an [MCaptcha] actor to [Master]
#[derive(Message, Builder)]
#[rtype(result = "()")]
pub struct AddSite {
@@ -83,98 +89,28 @@ impl Handler for Master<'static> {
}
}
-///// Message to decrement the visitor count
-//#[derive(Message, Deserialize)]
-//#[rtype(result = "()")]
-//pub struct VerifyPoW {
-// pow: ShaPoW>,
-// id: String,
-//}
-//
-//impl Handler for MCaptcha {
-// type Result = ();
-// fn handle(&mut self, msg: VerifyPoW, _ctx: &mut Self::Context) -> Self::Result {
-// self.decrement_visiotr();
-// }
-//}
-
#[cfg(test)]
mod tests {
use super::*;
- use crate::defense::*;
-
- // 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;
-
- 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()
- }
+ use crate::mcaptcha::tests::*;
#[actix_rt::test]
- async fn master() {
+ async fn master_actor_works() {
let addr = Master::new().start();
let id = "yo";
let mcaptcha = get_counter().start();
let msg = AddSiteBuilder::default()
.id(id)
- .addr(mcaptcha)
+ .addr(mcaptcha.clone())
.build()
.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);
- // }
}
diff --git a/src/mcaptcha.rs b/src/mcaptcha.rs
index c483bd8..e66d5c4 100644
--- a/src/mcaptcha.rs
+++ b/src/mcaptcha.rs
@@ -83,7 +83,7 @@ use crate::defense::Defense;
/// This struct represents the mCaptcha state and is used
/// to configure leaky-bucket lifetime and manage defense
-#[derive(Clone, Builder)]
+#[derive(Clone, Debug, Builder)]
pub struct MCaptcha {
#[builder(default = "0", setter(skip))]
visitor_threshold: u32,
diff --git a/src/pow.rs b/src/pow.rs
index 08cd74c..ed55c19 100644
--- a/src/pow.rs
+++ b/src/pow.rs
@@ -15,23 +15,19 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
-use actix::dev::*;
-use derive_builder::Builder;
-use pow_sha256::{Config, PoW};
+
+//! PoW datatypes used in client-server interaction
+use pow_sha256::PoW;
use serde::Serialize;
-use crate::cache::messages;
-use crate::cache::Save;
-use crate::errors::*;
-use crate::master::Master;
-
-/// PoW Config that will be sent to clients for generating PoW
+/// PoW requirement datatype that is be sent to clients for generating PoW
#[derive(Clone, Serialize, Debug)]
pub struct PoWConfig {
pub string: String,
pub difficulty_factor: u32,
}
impl PoWConfig {
+ /// create new instance of [PoWConfig]
pub fn new(m: u32) -> Self {
use crate::utils::get_random;
@@ -42,58 +38,7 @@ impl PoWConfig {
}
}
-#[derive(Clone, Builder)]
-pub struct Actors {
- master: Addr>,
- cache: Addr,
- pow: Config,
-}
-
-impl Actors
-where
- T: Save,
- ::Context: ToEnvelope + ToEnvelope,
-{
- pub async fn get_pow(&self, id: String) -> Option {
- 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 {
- 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 = 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),
- }
- }
-}
-
+/// PoW datatype that clients send to server
#[derive(Clone, Serialize, Debug)]
pub struct Work {
pub string: String,
@@ -111,87 +56,3 @@ impl From for PoW {
.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 {
- 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));
- }
-}