From dfa35daf0fecacc1bf90b399a6d9f485e80d0ca0 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Sat, 10 Aug 2024 15:32:29 +0000 Subject: [PATCH] refactor: Split the finders into several modules Signed-off-by: Awiteb --- src/finder.rs | 340 ------------------------------------ src/finder/form_finder.rs | 173 ++++++++++++++++++ src/finder/header_finder.rs | 162 +++++++++++++++++ src/finder/mod.rs | 43 +++++ 4 files changed, 378 insertions(+), 340 deletions(-) delete mode 100644 src/finder.rs create mode 100644 src/finder/form_finder.rs create mode 100644 src/finder/header_finder.rs create mode 100644 src/finder/mod.rs diff --git a/src/finder.rs b/src/finder.rs deleted file mode 100644 index f26cd71..0000000 --- a/src/finder.rs +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (c) 2024, Awiteb -// A captcha middleware for Salvo framework. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -use salvo_core::http::header::HeaderName; -use salvo_core::http::Request; - -/// Trait to find the captcha token and answer from the request. -pub trait CaptchaFinder: Send + Sync { - /// Find the captcha token from the request. - /// - /// ### Returns - /// - None: If the token is not found - /// - Some(None): If the token is found but is invalid (e.g. not a valid string) - /// - Some(Some(token)): If the token is found - fn find_token( - &self, - req: &mut Request, - ) -> impl std::future::Future>> + std::marker::Send; - - /// Find the captcha answer from the request. - /// - /// ### Returns - /// - None: If the answer is not found - /// - Some(None): If the answer is found but is invalid (e.g. not a valid string) - /// - Some(Some(answer)): If the answer is found - fn find_answer( - &self, - req: &mut Request, - ) -> impl std::future::Future>> + std::marker::Send; -} - -/// Find the captcha token and answer from the header -#[derive(Debug)] -pub struct CaptchaHeaderFinder { - /// The header name of the captcha token - /// - /// Default: "x-captcha-token" - pub token_header: HeaderName, - - /// The header name of the captcha answer - /// - /// Default: "x-captcha-answer" - pub answer_header: HeaderName, -} - -/// Find the captcha token and answer from the form -#[derive(Debug)] -pub struct CaptchaFormFinder { - /// The form name of the captcha token - /// - /// Default: "captcha_token" - pub token_name: String, - - /// The form name of the captcha answer - /// - /// Default: "captcha_answer" - pub answer_name: String, -} - -impl CaptchaHeaderFinder { - /// Create a new CaptchaHeaderFinder - pub fn new() -> Self { - Self::default() - } - - /// Set the token header name - pub fn token_header(mut self, token_header: HeaderName) -> Self { - self.token_header = token_header; - self - } - - /// Set the answer header name - pub fn answer_header(mut self, answer_header: HeaderName) -> Self { - self.answer_header = answer_header; - self - } -} - -impl CaptchaFormFinder { - /// Create a new CaptchaFormFinder - pub fn new() -> Self { - Self::default() - } - - /// Set the token form name - pub fn token_name(mut self, token_name: String) -> Self { - self.token_name = token_name; - self - } - - /// Set the answer form name - pub fn answer_name(mut self, answer_name: String) -> Self { - self.answer_name = answer_name; - self - } -} - -impl Default for CaptchaHeaderFinder { - /// Create a default CaptchaHeaderFinder with: - /// - token_header: "x-captcha-token" - /// - answer_header: "x-captcha-answer" - fn default() -> Self { - Self { - token_header: HeaderName::from_static("x-captcha-token"), - answer_header: HeaderName::from_static("x-captcha-answer"), - } - } -} - -impl Default for CaptchaFormFinder { - /// Create a default CaptchaFormFinder with: - /// - token_name: "captcha_token" - /// - answer_name: "captcha_answer" - fn default() -> Self { - Self { - token_name: "captcha_token".to_string(), - answer_name: "captcha_answer".to_string(), - } - } -} - -impl CaptchaFinder for CaptchaHeaderFinder { - async fn find_token(&self, req: &mut Request) -> Option> { - req.headers() - .get(&self.token_header) - .map(|t| t.to_str().map(ToString::to_string).ok()) - } - - async fn find_answer(&self, req: &mut Request) -> Option> { - req.headers() - .get(&self.answer_header) - .map(|a| a.to_str().map(ToString::to_string).ok()) - } -} - -impl CaptchaFinder for CaptchaFormFinder { - async fn find_token(&self, req: &mut Request) -> Option> { - req.form_data() - .await - .map(|form| form.fields.get(&self.token_name).cloned()) - .ok() - } - - async fn find_answer(&self, req: &mut Request) -> Option> { - req.form_data() - .await - .map(|form| form.fields.get(&self.answer_name).cloned()) - .ok() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use salvo_core::http::headers::ContentType; - use salvo_core::http::Request; - use salvo_core::http::{header::*, ReqBody}; - - #[tokio::test] - async fn test_captcha_header_finder() { - let finder = CaptchaHeaderFinder::new(); - let mut req = Request::default(); - let headers = req.headers_mut(); - - headers.insert( - HeaderName::from_static("x-captcha-token"), - HeaderValue::from_str("token").unwrap(), - ); - headers.insert( - HeaderName::from_static("x-captcha-answer"), - HeaderValue::from_static("answer"), - ); - - assert_eq!( - finder.find_token(&mut req).await, - Some(Some("token".to_owned())) - ); - assert_eq!( - finder.find_answer(&mut req).await, - Some(Some("answer".to_owned())) - ); - } - - #[tokio::test] - async fn test_captcha_header_finder_customized() { - let finder = CaptchaHeaderFinder::new() - .token_header(HeaderName::from_static("token")) - .answer_header(HeaderName::from_static("answer")); - - let mut req = Request::default(); - let headers = req.headers_mut(); - - headers.insert( - HeaderName::from_static("token"), - HeaderValue::from_str("token").unwrap(), - ); - headers.insert( - HeaderName::from_static("answer"), - HeaderValue::from_static("answer"), - ); - - assert_eq!( - finder.find_token(&mut req).await, - Some(Some("token".to_owned())) - ); - assert_eq!( - finder.find_answer(&mut req).await, - Some(Some("answer".to_owned())) - ); - } - - #[tokio::test] - async fn test_captcha_header_finder_none() { - let finder = CaptchaHeaderFinder::new(); - let mut req = Request::default(); - - assert_eq!(finder.find_token(&mut req).await, None); - assert_eq!(finder.find_answer(&mut req).await, None); - } - - #[tokio::test] - async fn test_captcha_header_finder_customized_none() { - let finder = CaptchaHeaderFinder::new() - .token_header(HeaderName::from_static("token")) - .answer_header(HeaderName::from_static("answer")); - let mut req = Request::default(); - let headers = req.headers_mut(); - - headers.insert( - HeaderName::from_static("x-captcha-token"), - HeaderValue::from_str("token").unwrap(), - ); - headers.insert( - HeaderName::from_static("x-captcha-answer"), - HeaderValue::from_static("answer"), - ); - - assert_eq!(finder.find_token(&mut req).await, None); - assert_eq!(finder.find_answer(&mut req).await, None); - } - - #[tokio::test] - async fn test_captcha_form_finder() { - let finder = CaptchaFormFinder::new(); - let mut req = Request::default(); - *req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into()); - let headers = req.headers_mut(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), - ); - - assert_eq!( - finder.find_token(&mut req).await, - Some(Some("token".to_owned())) - ); - assert_eq!( - finder.find_answer(&mut req).await, - Some(Some("answer".to_owned())) - ); - } - - #[tokio::test] - async fn test_captcha_form_finder_customized() { - let finder = CaptchaFormFinder::new() - .token_name("token".to_string()) - .answer_name("answer".to_string()); - let mut req = Request::default(); - *req.body_mut() = ReqBody::Once("token=token&answer=answer".into()); - let headers = req.headers_mut(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), - ); - - assert_eq!( - finder.find_token(&mut req).await, - Some(Some("token".to_owned())) - ); - assert_eq!( - finder.find_answer(&mut req).await, - Some(Some("answer".to_owned())) - ); - } - - #[tokio::test] - async fn test_captcha_form_finder_none() { - let finder = CaptchaFormFinder::new(); - let mut req = Request::default(); - *req.body_mut() = ReqBody::Once("".into()); - let headers = req.headers_mut(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), - ); - - assert_eq!(finder.find_token(&mut req).await, Some(None)); - assert_eq!(finder.find_answer(&mut req).await, Some(None)); - } - - #[tokio::test] - async fn test_captcha_form_finder_customized_none() { - let finder = CaptchaFormFinder::new() - .token_name("token".to_string()) - .answer_name("answer".to_string()); - let mut req = Request::default(); - *req.body_mut() = ReqBody::Once("".into()); - let headers = req.headers_mut(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), - ); - - assert_eq!(finder.find_token(&mut req).await, Some(None)); - assert_eq!(finder.find_answer(&mut req).await, Some(None)); - } - - #[tokio::test] - async fn test_captcha_form_finder_invalid() { - let finder = CaptchaFormFinder::new(); - let mut req = Request::default(); - *req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into()); - let headers = req.headers_mut(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_str(&ContentType::json().to_string()).unwrap(), - ); - - assert_eq!(finder.find_token(&mut req).await, None); - assert_eq!(finder.find_answer(&mut req).await, None); - } -} diff --git a/src/finder/form_finder.rs b/src/finder/form_finder.rs new file mode 100644 index 0000000..23c593e --- /dev/null +++ b/src/finder/form_finder.rs @@ -0,0 +1,173 @@ +// Copyright (c) 2024, Awiteb +// A captcha middleware for Salvo framework. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +use salvo_core::http::Request; + +use crate::CaptchaFinder; + +/// Find the captcha token and answer from the form +#[derive(Debug)] +pub struct CaptchaFormFinder { + /// The form name of the captcha token + /// + /// Default: "captcha_token" + pub token_name: String, + + /// The form name of the captcha answer + /// + /// Default: "captcha_answer" + pub answer_name: String, +} + +impl CaptchaFormFinder { + /// Create a new CaptchaFormFinder + pub fn new() -> Self { + Self::default() + } + + /// Set the token form name + pub fn token_name(mut self, token_name: String) -> Self { + self.token_name = token_name; + self + } + + /// Set the answer form name + pub fn answer_name(mut self, answer_name: String) -> Self { + self.answer_name = answer_name; + self + } +} + +impl Default for CaptchaFormFinder { + /// Create a default CaptchaFormFinder with: + /// - token_name: "captcha_token" + /// - answer_name: "captcha_answer" + fn default() -> Self { + Self { + token_name: "captcha_token".to_string(), + answer_name: "captcha_answer".to_string(), + } + } +} + +impl CaptchaFinder for CaptchaFormFinder { + async fn find_token(&self, req: &mut Request) -> Option> { + req.form_data() + .await + .map(|form| form.fields.get(&self.token_name).cloned()) + .ok() + } + + async fn find_answer(&self, req: &mut Request) -> Option> { + req.form_data() + .await + .map(|form| form.fields.get(&self.answer_name).cloned()) + .ok() + } +} + +#[cfg(test)] +mod tests { + use salvo_core::http::{header, headers::ContentType, HeaderValue, ReqBody}; + + use super::*; + + #[tokio::test] + async fn test_captcha_form_finder() { + let finder = CaptchaFormFinder::new(); + let mut req = Request::default(); + *req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into()); + let headers = req.headers_mut(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), + ); + + assert_eq!( + finder.find_token(&mut req).await, + Some(Some("token".to_owned())) + ); + assert_eq!( + finder.find_answer(&mut req).await, + Some(Some("answer".to_owned())) + ); + } + + #[tokio::test] + async fn test_captcha_form_finder_customized() { + let finder = CaptchaFormFinder::new() + .token_name("token".to_string()) + .answer_name("answer".to_string()); + let mut req = Request::default(); + *req.body_mut() = ReqBody::Once("token=token&answer=answer".into()); + let headers = req.headers_mut(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), + ); + + assert_eq!( + finder.find_token(&mut req).await, + Some(Some("token".to_owned())) + ); + assert_eq!( + finder.find_answer(&mut req).await, + Some(Some("answer".to_owned())) + ); + } + + #[tokio::test] + async fn test_captcha_form_finder_none() { + let finder = CaptchaFormFinder::new(); + let mut req = Request::default(); + *req.body_mut() = ReqBody::Once("".into()); + let headers = req.headers_mut(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), + ); + + assert_eq!(finder.find_token(&mut req).await, Some(None)); + assert_eq!(finder.find_answer(&mut req).await, Some(None)); + } + + #[tokio::test] + async fn test_captcha_form_finder_customized_none() { + let finder = CaptchaFormFinder::new() + .token_name("token".to_string()) + .answer_name("answer".to_string()); + let mut req = Request::default(); + *req.body_mut() = ReqBody::Once("".into()); + let headers = req.headers_mut(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(), + ); + + assert_eq!(finder.find_token(&mut req).await, Some(None)); + assert_eq!(finder.find_answer(&mut req).await, Some(None)); + } + + #[tokio::test] + async fn test_captcha_form_finder_invalid() { + let finder = CaptchaFormFinder::new(); + let mut req = Request::default(); + *req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into()); + let headers = req.headers_mut(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str(&ContentType::json().to_string()).unwrap(), + ); + + assert_eq!(finder.find_token(&mut req).await, None); + assert_eq!(finder.find_answer(&mut req).await, None); + } +} diff --git a/src/finder/header_finder.rs b/src/finder/header_finder.rs new file mode 100644 index 0000000..a30e5a5 --- /dev/null +++ b/src/finder/header_finder.rs @@ -0,0 +1,162 @@ +// Copyright (c) 2024, Awiteb +// A captcha middleware for Salvo framework. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +use salvo_core::http::{HeaderName, Request}; + +use crate::CaptchaFinder; + +/// Find the captcha token and answer from the header +#[derive(Debug)] +pub struct CaptchaHeaderFinder { + /// The header name of the captcha token + /// + /// Default: "x-captcha-token" + pub token_header: HeaderName, + + /// The header name of the captcha answer + /// + /// Default: "x-captcha-answer" + pub answer_header: HeaderName, +} + +impl CaptchaHeaderFinder { + /// Create a new CaptchaHeaderFinder + pub fn new() -> Self { + Self::default() + } + + /// Set the token header name + pub fn token_header(mut self, token_header: HeaderName) -> Self { + self.token_header = token_header; + self + } + + /// Set the answer header name + pub fn answer_header(mut self, answer_header: HeaderName) -> Self { + self.answer_header = answer_header; + self + } +} + +impl Default for CaptchaHeaderFinder { + /// Create a default CaptchaHeaderFinder with: + /// - token_header: "x-captcha-token" + /// - answer_header: "x-captcha-answer" + fn default() -> Self { + Self { + token_header: HeaderName::from_static("x-captcha-token"), + answer_header: HeaderName::from_static("x-captcha-answer"), + } + } +} + +impl CaptchaFinder for CaptchaHeaderFinder { + async fn find_token(&self, req: &mut Request) -> Option> { + req.headers() + .get(&self.token_header) + .map(|t| t.to_str().map(ToString::to_string).ok()) + } + + async fn find_answer(&self, req: &mut Request) -> Option> { + req.headers() + .get(&self.answer_header) + .map(|a| a.to_str().map(ToString::to_string).ok()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use salvo_core::http::HeaderValue; + + #[tokio::test] + async fn test_captcha_header_finder() { + let finder = CaptchaHeaderFinder::new(); + let mut req = Request::default(); + let headers = req.headers_mut(); + + headers.insert( + HeaderName::from_static("x-captcha-token"), + HeaderValue::from_str("token").unwrap(), + ); + headers.insert( + HeaderName::from_static("x-captcha-answer"), + HeaderValue::from_static("answer"), + ); + + assert_eq!( + finder.find_token(&mut req).await, + Some(Some("token".to_owned())) + ); + assert_eq!( + finder.find_answer(&mut req).await, + Some(Some("answer".to_owned())) + ); + } + + #[tokio::test] + async fn test_captcha_header_finder_customized() { + let finder = CaptchaHeaderFinder::new() + .token_header(HeaderName::from_static("token")) + .answer_header(HeaderName::from_static("answer")); + + let mut req = Request::default(); + let headers = req.headers_mut(); + + headers.insert( + HeaderName::from_static("token"), + HeaderValue::from_str("token").unwrap(), + ); + headers.insert( + HeaderName::from_static("answer"), + HeaderValue::from_static("answer"), + ); + + assert_eq!( + finder.find_token(&mut req).await, + Some(Some("token".to_owned())) + ); + assert_eq!( + finder.find_answer(&mut req).await, + Some(Some("answer".to_owned())) + ); + } + + #[tokio::test] + async fn test_captcha_header_finder_none() { + let finder = CaptchaHeaderFinder::new(); + let mut req = Request::default(); + + assert_eq!(finder.find_token(&mut req).await, None); + assert_eq!(finder.find_answer(&mut req).await, None); + } + + #[tokio::test] + async fn test_captcha_header_finder_customized_none() { + let finder = CaptchaHeaderFinder::new() + .token_header(HeaderName::from_static("token")) + .answer_header(HeaderName::from_static("answer")); + let mut req = Request::default(); + let headers = req.headers_mut(); + + headers.insert( + HeaderName::from_static("x-captcha-token"), + HeaderValue::from_str("token").unwrap(), + ); + headers.insert( + HeaderName::from_static("x-captcha-answer"), + HeaderValue::from_static("answer"), + ); + + assert_eq!(finder.find_token(&mut req).await, None); + assert_eq!(finder.find_answer(&mut req).await, None); + } +} diff --git a/src/finder/mod.rs b/src/finder/mod.rs new file mode 100644 index 0000000..e0568c7 --- /dev/null +++ b/src/finder/mod.rs @@ -0,0 +1,43 @@ +// Copyright (c) 2024, Awiteb +// A captcha middleware for Salvo framework. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +use salvo_core::http::Request; + +mod form_finder; +mod header_finder; + +pub use form_finder::*; +pub use header_finder::*; + +/// Trait to find the captcha token and answer from the request. +pub trait CaptchaFinder: Send + Sync { + /// Find the captcha token from the request. + /// + /// ### Returns + /// - None: If the token is not found + /// - Some(None): If the token is found but is invalid (e.g. not a valid string) + /// - Some(Some(token)): If the token is found + fn find_token( + &self, + req: &mut Request, + ) -> impl std::future::Future>> + std::marker::Send; + + /// Find the captcha answer from the request. + /// + /// ### Returns + /// - None: If the answer is not found + /// - Some(None): If the answer is found but is invalid (e.g. not a valid string) + /// - Some(Some(answer)): If the answer is found + fn find_answer( + &self, + req: &mut Request, + ) -> impl std::future::Future>> + std::marker::Send; +}