Skip to main content

libmcaptcha/master/redis/
master.rs

1/*
2 * mCaptcha - A proof of work based DoS protection system
3 * Copyright © 2021 Aravinth Manivannan <realravinth@batsense.net>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18use actix::dev::*;
19use tokio::sync::oneshot;
20
21use crate::errors::*;
22use crate::master::messages::{
23    AddSite, AddVisitor, GetInternalData, RemoveCaptcha, Rename, SetInternalData,
24};
25use crate::master::Master as MasterTrait;
26use crate::redis::mcaptcha_redis::MCaptchaRedis;
27use crate::redis::RedisConfig;
28
29#[derive(Clone)]
30pub struct Master {
31    pub redis: MCaptchaRedis,
32}
33
34impl Master {
35    pub async fn new(redis: RedisConfig) -> CaptchaResult<Self> {
36        let redis = MCaptchaRedis::new(redis).await?;
37        let master = Self { redis };
38        Ok(master)
39    }
40}
41
42impl MasterTrait for Master {}
43
44impl Actor for Master {
45    type Context = Context<Self>;
46}
47
48impl Handler<AddVisitor> for Master {
49    type Result = MessageResult<AddVisitor>;
50
51    fn handle(&mut self, m: AddVisitor, ctx: &mut Self::Context) -> Self::Result {
52        let (tx, rx) = oneshot::channel();
53
54        let con = self.redis.get_client();
55        let fut = async move {
56            let res = con.add_visitor(m).await;
57            let _ = tx.send(res);
58        }
59        .into_actor(self);
60        ctx.wait(fut);
61        MessageResult(rx)
62    }
63}
64
65impl Handler<AddSite> for Master {
66    type Result = MessageResult<AddSite>;
67
68    fn handle(&mut self, m: AddSite, ctx: &mut Self::Context) -> Self::Result {
69        let (tx, rx) = oneshot::channel();
70        let con = self.redis.get_client();
71        let fut = async move {
72            let res = con.add_mcaptcha(m).await;
73            let _ = tx.send(res);
74        }
75        .into_actor(self);
76        ctx.wait(fut);
77        MessageResult(rx)
78    }
79}
80
81impl Handler<Rename> for Master {
82    type Result = MessageResult<Rename>;
83
84    fn handle(&mut self, m: Rename, ctx: &mut Self::Context) -> Self::Result {
85        let (tx, rx) = oneshot::channel();
86
87        let con = self.redis.get_client();
88        let fut = async move {
89            let res = con.rename_captcha(&m.name, &m.rename_to).await;
90            let _ = tx.send(res);
91        }
92        .into_actor(self);
93        ctx.wait(fut);
94        MessageResult(rx)
95    }
96}
97
98impl Handler<RemoveCaptcha> for Master {
99    type Result = MessageResult<RemoveCaptcha>;
100
101    fn handle(&mut self, m: RemoveCaptcha, ctx: &mut Self::Context) -> Self::Result {
102        let (tx, rx) = oneshot::channel();
103
104        let con = self.redis.get_client();
105        let fut = async move {
106            let res = con.delete_captcha(&m.0).await;
107            let _ = tx.send(res);
108        }
109        .into_actor(self);
110        ctx.wait(fut);
111        MessageResult(rx)
112    }
113}
114
115impl Handler<GetInternalData> for Master {
116    type Result = MessageResult<GetInternalData>;
117
118    fn handle(&mut self, m: GetInternalData, ctx: &mut Self::Context) -> Self::Result {
119        todo!()
120    }
121}
122
123impl Handler<SetInternalData> for Master {
124    type Result = MessageResult<SetInternalData>;
125
126    fn handle(&mut self, m: SetInternalData, ctx: &mut Self::Context) -> Self::Result {
127        todo!()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::master::embedded::counter::tests::get_mcaptcha;
135    use crate::master::messages::RenameBuilder;
136    use crate::master::redis::master::Master;
137    use crate::redis::RedisConfig;
138    use std::{env, sync::OnceLock};
139
140    fn redis_url() -> &'static str {
141        static REDIS_URL: OnceLock<String> = OnceLock::new();
142        REDIS_URL.get_or_init(|| {
143            env::var("LIBMCAPTCHA_TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.1.1/".into())
144        })
145    }
146
147    #[actix_rt::test]
148    async fn redis_master_works() {
149        const CAPTCHA_NAME: &str = "REDIS_MASTER_CAPTCHA_TEST";
150        const RENAME_CAPTCHA_NAME: &str = "RENAME_REDIS_MASTER_CAPTCHA_TEST";
151
152        let master = Master::new(RedisConfig::Single(redis_url().into())).await;
153        let sec_master = Master::new(RedisConfig::Single(redis_url().into())).await;
154        let r = sec_master.unwrap().redis.get_client();
155
156        assert!(master.is_ok());
157        let master = master.unwrap();
158        {
159            let _ = master.redis.get_client().delete_captcha(CAPTCHA_NAME).await;
160            let _ = master
161                .redis
162                .get_client()
163                .delete_captcha(RENAME_CAPTCHA_NAME)
164                .await;
165        }
166
167        let addr = master.start();
168
169        let mcaptcha = get_mcaptcha();
170        let duration = mcaptcha.get_duration();
171
172        let add_mcaptcha_msg = AddSite {
173            id: CAPTCHA_NAME.into(),
174            mcaptcha,
175        };
176        addr.send(add_mcaptcha_msg).await.unwrap();
177
178        let add_visitor_msg = AddVisitor(CAPTCHA_NAME.into());
179        addr.send(add_visitor_msg).await.unwrap();
180        let visitors = r.get_visitors(CAPTCHA_NAME).await.unwrap();
181        assert_eq!(visitors, 1);
182
183        let timer_expire = std::time::Duration::new(duration, 0);
184        actix::clock::sleep(timer_expire).await;
185        let visitors = r.get_visitors(CAPTCHA_NAME).await.unwrap();
186        assert_eq!(visitors, 0);
187
188        let rename = RenameBuilder::default()
189            .name(CAPTCHA_NAME.into())
190            .rename_to(RENAME_CAPTCHA_NAME.into())
191            .build()
192            .unwrap();
193        assert!(addr.send(rename).await.is_ok());
194        assert!(addr
195            .send(RemoveCaptcha(RENAME_CAPTCHA_NAME.into()))
196            .await
197            .is_ok());
198    }
199
200    #[actix_rt::test]
201    async fn race_redis_master() {
202        const CAPTCHA_NAME: &str = "REDIS_MASTER_CAPTCHA_RACE";
203
204        let master = Master::new(RedisConfig::Single(redis_url().into())).await;
205        let sec_master = Master::new(RedisConfig::Single(redis_url().into())).await;
206        let r = sec_master.unwrap().redis.get_client();
207
208        assert!(master.is_ok());
209        let master = master.unwrap();
210        {
211            let _ = master.redis.get_client().delete_captcha(CAPTCHA_NAME).await;
212        }
213
214        let addr = master.start();
215
216        let mcaptcha = get_mcaptcha();
217        let duration = mcaptcha.get_duration();
218
219        let add_mcaptcha_msg = AddSite {
220            id: CAPTCHA_NAME.into(),
221            mcaptcha,
222        };
223        addr.send(add_mcaptcha_msg).await.unwrap();
224
225        let add_visitor_msg = AddVisitor(CAPTCHA_NAME.into());
226        for _ in 0..500 {
227            addr.send(add_visitor_msg.clone()).await.unwrap();
228        }
229        let visitors = r.get_visitors(CAPTCHA_NAME).await.unwrap();
230        assert_eq!(visitors, 500);
231
232        let timer_expire = std::time::Duration::new(duration, 0);
233        actix::clock::sleep(timer_expire).await;
234        let visitors = r.get_visitors(CAPTCHA_NAME).await.unwrap();
235        assert_eq!(visitors, 0);
236    }
237}