diff --git a/README.md b/README.md index 8fcffd2..be2b728 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

-[![dependency status](https://deps.rs/repo/github/mCaptcha/cache/status.svg?style=flat-square)](https://deps.rs/repo/github/mCaptcha/cache) +[![dependency status](https://deps.rs/repo/github/mCaptcha/cache/status.svg)](https://deps.rs/repo/github/mCaptcha/cache) [![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg?style=flat-square)](http://www.gnu.org/licenses/agpl-3.0) [![Chat](https://img.shields.io/badge/matrix-+mcaptcha:matrix.batsense.net-purple?style=flat-square)](https://matrix.to/#/+mcaptcha:matrix.batsense.net) @@ -21,7 +21,7 @@ bucket](https://en.wikipedia.org/wiki/Leaky_bucket)-enabled counter to keep track of traffic/challenge requests. -- At `t=0`, if someone is visiting an mCaptcha-protected website, the +- At `t=0`(where `t` is time), if someone is visiting an mCaptcha-protected website, the counter for that website will be initialized and set to 1. - It should also automatically decrement(by 1) after a certain period, say @@ -29,7 +29,8 @@ keep track of traffic/challenge requests. website. - If at `t=x`(where `x. */ +use redis_module::{redis_command, redis_module}; +use redis_module::{Context, NextArg, RedisResult}; + +mod pocket; +fn timer_create(ctx: &Context, args: Vec) -> RedisResult { + let mut args = args.into_iter().skip(1); + // mcaptcha captcha key name + let key_name = args.next_string()?; + // expiry + let duration = args.next_u64()?; + pocket::Pocket::increment(ctx, duration, &key_name)?; + + //return Ok("OK".into()); + return Ok(format!("{}{}", key_name, duration).into()); +} + +////////////////////////////////////////////////////// + +redis_module! { + name: "mcaptcha_cahce", + version: 1, + data_types: [], + commands: [ + ["mcaptcha_cahce.create", timer_create, "", 0, 0, 0], + ], +} diff --git a/src/pocket.rs b/src/pocket.rs new file mode 100644 index 0000000..41854cf --- /dev/null +++ b/src/pocket.rs @@ -0,0 +1,250 @@ +/* + * Copyright (C) 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 . + */ + +use std::collections::HashMap; +use std::os::raw::c_void; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use redis_module::native_types::RedisType; +use redis_module::raw::KeyType; +use redis_module::RedisError; +use redis_module::{raw, Context}; + +pub const PREFIX_COUNTER: &str = "mcaptcha_cache:captcha:"; +pub const PREFIX_TIME_POCKET: &str = "mcaptcha_cache:pocket:"; + +#[derive(Debug, Clone)] +pub struct Pocket { + timer: Option, + pocket_instant: u64, + decrement: HashMap, +} + +#[inline] +/// duration in seconds +fn get_pocket_name(pocket_instant: u64) -> String { + format!("{}{}", PREFIX_TIME_POCKET, pocket_instant) +} + +#[inline] +fn pocket_instant(duration: u64) -> Result { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(val) => Ok(val.as_secs() + duration), + Err(_) => Err(RedisError::String("SystemTime before UNIX EPOCH!".into())), + } +} + +#[inline] +fn get_captcha_key(name: &str) -> String { + format!("{}{}", PREFIX_COUNTER, name) +} + +impl Pocket { + /// creates new pocket and sets off timer to go off at `duration` + pub fn new(ctx: &Context, duration: u64) -> Result { + let decrement = HashMap::new(); + + let pocket_instant = pocket_instant(duration)?; + let timer = Some(ctx.create_timer( + Duration::from_secs(duration), + Self::decrement, + pocket_instant, + )); + + let pocket = Pocket { + timer, + pocket_instant, + decrement, + }; + Ok(pocket) + } + + /// increments count of key = captcha and registers for auto decrement + pub fn increment(ctx: &Context, duration: u64, captcha: &str) -> Result<(), RedisError> { + let captcha_name = get_captcha_key(captcha); + // increment + let captcha = ctx.open_key_writable(&captcha_name); + match captcha.read()? { + Some(val) => { + if val.trim().is_empty() { + captcha.write("1")?; + } else { + let mut val: usize = val.parse()?; + val += 1; + captcha.write(&val.to_string())?; + } + } + None => { + captcha.write("1")?; + } + } + + let pocket_instant = pocket_instant(duration)?; + let pocket_name = get_pocket_name(pocket_instant); + + ctx.log_warning(&format!("Pocket name: {}", &pocket_name)); + + // get pocket + let pocket = ctx.open_key_writable(&pocket_name); + + match pocket.get_value::(&MCAPTCHA_POCKET_TYPE)? { + Some(pocket) => match pocket.decrement.get_mut(&captcha_name) { + Some(count) => *count += 1, + None => { + pocket.decrement.insert(captcha_name, 1); + } + }, + + None => { + let mut counter = Pocket::new(ctx, duration)?; + counter.decrement.insert(captcha_name, 1); + pocket.set_value(&MCAPTCHA_POCKET_TYPE, counter)?; + pocket.set_expire(Duration::from_secs(duration + 10))?; + } + }; + + // return Ok("OK".into()); + Ok(()) + } + + /// executes when timer goes off. Decrements all registered counts and cleans itself up + fn decrement(ctx: &Context, pocket_instant: u64) { + // get pocket + let key = ctx.open_key_writable(&get_pocket_name(pocket_instant)); + + ctx.log_warning(&format!("Pocket instant: {}", &pocket_instant)); + let val = key.get_value::(&MCAPTCHA_POCKET_TYPE).unwrap(); + ctx.log_warning(&format!("read hashmap ")); + match val { + Some(pocket) => { + ctx.log_warning(&format!("entering loop hashmap ")); + for (captcha, count) in pocket.decrement.drain() { + ctx.log_warning(&format!( + "reading captcha: {} with decr count {}", + &captcha, count + )); + let stored_captcha = ctx.open_key_writable(&captcha); + if stored_captcha.key_type() == KeyType::Empty { + continue; + } + + let mut stored_count: usize = + stored_captcha.read().unwrap().unwrap().parse().unwrap(); + stored_count -= count; + stored_captcha.write(&stored_count.to_string()).unwrap(); + } + } + None => { + ctx.log_warning(&format!("pocket not found, can't decrement")); + } + } + + ctx.log_warning(&format!("loop exited")); + let res = key.delete(); + + ctx.log_warning(&format!( + "enountered error while deleting hashmap: {:?}", + res + )); + //res.unwrap(); + } +} + +static MCAPTCHA_POCKET_TYPE: RedisType = RedisType::new( + "mcaptcha_cache_pocket", + 0, + raw::RedisModuleTypeMethods { + version: raw::REDISMODULE_TYPE_METHOD_VERSION as u64, + rdb_load: None, + rdb_save: None, + aof_rewrite: None, + free: Some(free), + + // Currently unused by Redis + mem_usage: None, + digest: None, + + // Aux data + aux_load: None, + aux_save: None, + aux_save_triggers: 0, + + free_effort: None, + unlink: None, + copy: None, + defrag: None, + }, +); + +unsafe extern "C" fn free(value: *mut c_void) { + let val = value as *mut Pocket; + Box::from_raw(val); + // drop(value as *mut Pocket); +} + +//fn alloc_set(ctx: &Context, args: Vec) -> RedisResult { +// let mut args = args.into_iter().skip(1); +// let key = args.next_string()?; +// let size = args.next_i64()?; +// +// ctx.log_debug(format!("key: {}, size: {}", key, size).as_str()); +// +// let key = ctx.open_key_writable(&key); +// +// match key.get_value::(&MCAPTCHA_POCKET_TYPE)? { +// Some(value) => { +// value.data = "B".repeat(size as usize); +// } +// None => { +// let value = MyType { +// data: "A".repeat(size as usize), +// }; +// +// key.set_value(&MCAPTCHA_POCKET_TYPE, value)?; +// } +// } +// +// Ok(size.into()) +//} + +//fn alloc_get(ctx: &Context, args: Vec) -> RedisResult { +// let mut args = args.into_iter().skip(1); +// let key = args.next_string()?; +// +// let key = ctx.open_key(&key); +// +// let value = match key.get_value::(&MCAPTCHA_POCKET_TYPE)? { +// Some(value) => value.data.as_str().into(), +// None => ().into(), +// }; +// +// Ok(value) +//} + +////////////////////////////////////////////////////// + +//redis_module! { +// name: "alloc", +// version: 1, +// data_types: [ +// MY_REDIS_TYPE, +// ], +// commands: [ +// ["alloc.set", alloc_set, "write", 1, 1, 1], +// ["alloc.get", alloc_get, "readonly", 1, 1, 1], +// ], +//}