Compare commits

..

No commits in common. "4be7a66d03875f0f07082f7a34c5453434de7eeb" and "60ae95ea5f8649b22d4c2f83928ee764dde9748b" have entirely different histories.

9 changed files with 426 additions and 474 deletions

View file

@ -6,7 +6,7 @@ edition = "2021"
authors = ["Awiteb <a@4rs.nl>"]
description = "A captcha middleware for Salvo framework."
license = "MIT"
repository = "https://git.4rs.nl/awiteb/salvo-captcha"
repository = "https://github.com/TheAwiteb/salvo-captcha"
documentation = "https://docs.rs/salvo-captcha"
readme = "README.md"
keywords = ["salvo", "captcha", "middleware"]

View file

@ -152,13 +152,16 @@ fn index_page(captcha_image: String, captcha_token: String) -> String {
</form>
<srong>Or you can skip the captcha</strong>
<form action="/skipped" method="post">
<input type="hidden" name="captcha_token" value="{captcha_token}" />
<input type="hidden" name="captcha_answer"/>
<input type="text" name="username" placeholder="Username" />
<br/>
<input type="password" name="password" placeholder="Password" />
<br/>
<input type="submit" value="Skip Captcha" />
</form>
<a href="https://git.4rs.nl/awiteb/salvo-captcha">Source Code</a>
<a href="https://github.com/TheAwiteb/salvo-captcha">Source Code</a>
</body>
</html>
"#
@ -179,7 +182,7 @@ fn captcha_result_page(captcha_result: String) -> String {
</style>
<body>
<h1>Salvo Captcha Example</h1>
<h2>Result page</h2>
<h2>Sign In</h2>
<strong>{captcha_result}</strong>
<br/>
<a href="/">Go Back</a>

View file

@ -17,7 +17,7 @@ pub trait CaptchaGenerator: CaptchaStorage {
///
/// The returned captcha image is 220x110 pixels.
///
/// For more information about the captcha name and difficulty, see the [`README.md`](https://git.4rs.nl/awiteb/salvo-captcha/#captcha-name-and-difficulty).
/// For more information about the captcha name and difficulty, see the [`README.md`](https://github.com/TheAwiteb/salvo-captcha/#captcha-name-and-difficulty).
fn new_captcha(
&self,
name: CaptchaName,

340
src/finder.rs Normal file
View file

@ -0,0 +1,340 @@
// Copyright (c) 2024, Awiteb <a@4rs.nl>
// 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<Output = Option<Option<String>>> + 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<Output = Option<Option<String>>> + 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<Option<String>> {
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<Option<String>> {
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<Option<String>> {
req.form_data()
.await
.map(|form| form.fields.get(&self.token_name).cloned())
.ok()
}
async fn find_answer(&self, req: &mut Request) -> Option<Option<String>> {
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);
}
}

View file

@ -1,173 +0,0 @@
// Copyright (c) 2024, Awiteb <a@4rs.nl>
// 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<Option<String>> {
req.form_data()
.await
.map(|form| form.fields.get(&self.token_name).cloned())
.ok()
}
async fn find_answer(&self, req: &mut Request) -> Option<Option<String>> {
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);
}
}

View file

@ -1,162 +0,0 @@
// Copyright (c) 2024, Awiteb <a@4rs.nl>
// 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<Option<String>> {
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<Option<String>> {
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);
}
}

View file

@ -1,43 +0,0 @@
// Copyright (c) 2024, Awiteb <a@4rs.nl>
// 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<Output = Option<Option<String>>> + 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<Output = Option<Option<String>>> + std::marker::Send;
}

View file

@ -9,22 +9,62 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
use std::{sync::Arc, time::Duration};
#[cfg(feature = "cacache-storage")]
use std::{
path::{Path, PathBuf},
time::{Duration, SystemTime},
time::SystemTime,
};
use crate::CaptchaStorage;
/// Trait to store the captcha token and answer. is also clear the expired captcha.
///
/// The trait will be implemented for `Arc<T>` if `T` implements the trait.
///
/// The trait is thread safe, so the storage can be shared between threads.
pub trait CaptchaStorage: Send + Sync + 'static
where
Self: Clone + std::fmt::Debug,
{
/// The error type of the storage.
type Error: std::fmt::Display + std::fmt::Debug + Send;
/// Store the captcha token and answer.
fn store_answer(
&self,
answer: String,
) -> impl std::future::Future<Output = Result<String, Self::Error>> + Send;
/// Returns the answer of the captcha token. This method will return None if the token is not exist.
fn get_answer(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send;
/// Clear the expired captcha.
fn clear_expired(
&self,
expired_after: Duration,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
/// Clear the captcha by token.
fn clear_by_token(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
}
/// The [`cacache`] storage.
///
/// [`cacache`]: https://github.com/zkat/cacache-rs
#[cfg(feature = "cacache-storage")]
#[derive(Debug, Clone)]
pub struct CacacheStorage {
/// The cacache cache directory.
cache_dir: PathBuf,
}
#[cfg(feature = "cacache-storage")]
impl CacacheStorage {
/// Create a new CacacheStorage
pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
@ -39,6 +79,7 @@ impl CacacheStorage {
}
}
#[cfg(feature = "cacache-storage")]
impl CaptchaStorage for CacacheStorage {
type Error = cacache::Error;
@ -103,7 +144,43 @@ impl CaptchaStorage for CacacheStorage {
}
}
impl<T> CaptchaStorage for Arc<T>
where
T: CaptchaStorage,
{
type Error = T::Error;
fn store_answer(
&self,
answer: String,
) -> impl std::future::Future<Output = Result<String, Self::Error>> + Send {
self.as_ref().store_answer(answer)
}
fn get_answer(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send {
self.as_ref().get_answer(token)
}
fn clear_expired(
&self,
expired_after: Duration,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
self.as_ref().clear_expired(expired_after)
}
fn clear_by_token(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
self.as_ref().clear_by_token(token)
}
}
#[cfg(test)]
#[cfg(feature = "cacache-storage")]
mod tests {
use super::*;

View file

@ -1,90 +0,0 @@
// Copyright (c) 2024, Awiteb <a@4rs.nl>
// 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 std::{sync::Arc, time::Duration};
#[cfg(feature = "cacache-storage")]
mod cacache_storage;
#[cfg(feature = "cacache-storage")]
pub use cacache_storage::*;
/// Trait to store the captcha token and answer. is also clear the expired captcha.
///
/// The trait will be implemented for `Arc<T>` if `T` implements the trait.
///
/// The trait is thread safe, so the storage can be shared between threads.
pub trait CaptchaStorage: Send + Sync + 'static
where
Self: Clone + std::fmt::Debug,
{
/// The error type of the storage.
type Error: std::fmt::Display + std::fmt::Debug + Send;
/// Store the captcha token and answer.
fn store_answer(
&self,
answer: String,
) -> impl std::future::Future<Output = Result<String, Self::Error>> + Send;
/// Returns the answer of the captcha token. This method will return None if the token is not exist.
fn get_answer(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send;
/// Clear the expired captcha.
fn clear_expired(
&self,
expired_after: Duration,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
/// Clear the captcha by token.
fn clear_by_token(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
}
impl<T> CaptchaStorage for Arc<T>
where
T: CaptchaStorage,
{
type Error = T::Error;
fn store_answer(
&self,
answer: String,
) -> impl std::future::Future<Output = Result<String, Self::Error>> + Send {
self.as_ref().store_answer(answer)
}
fn get_answer(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send {
self.as_ref().get_answer(token)
}
fn clear_expired(
&self,
expired_after: Duration,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
self.as_ref().clear_expired(expired_after)
}
fn clear_by_token(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
self.as_ref().clear_by_token(token)
}
}