Compare commits
3 commits
60ae95ea5f
...
4be7a66d03
Author | SHA1 | Date | |
---|---|---|---|
4be7a66d03 | |||
dfa35daf0f | |||
42d598e0e4 |
9 changed files with 474 additions and 426 deletions
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||||
authors = ["Awiteb <a@4rs.nl>"]
|
authors = ["Awiteb <a@4rs.nl>"]
|
||||||
description = "A captcha middleware for Salvo framework."
|
description = "A captcha middleware for Salvo framework."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/TheAwiteb/salvo-captcha"
|
repository = "https://git.4rs.nl/awiteb/salvo-captcha"
|
||||||
documentation = "https://docs.rs/salvo-captcha"
|
documentation = "https://docs.rs/salvo-captcha"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["salvo", "captcha", "middleware"]
|
keywords = ["salvo", "captcha", "middleware"]
|
||||||
|
|
|
@ -152,16 +152,13 @@ fn index_page(captcha_image: String, captcha_token: String) -> String {
|
||||||
</form>
|
</form>
|
||||||
<srong>Or you can skip the captcha</strong>
|
<srong>Or you can skip the captcha</strong>
|
||||||
<form action="/skipped" method="post">
|
<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" />
|
<input type="text" name="username" placeholder="Username" />
|
||||||
<br/>
|
<br/>
|
||||||
<input type="password" name="password" placeholder="Password" />
|
<input type="password" name="password" placeholder="Password" />
|
||||||
<br/>
|
<br/>
|
||||||
<input type="submit" value="Skip Captcha" />
|
<input type="submit" value="Skip Captcha" />
|
||||||
</form>
|
</form>
|
||||||
<a href="https://github.com/TheAwiteb/salvo-captcha">Source Code</a>
|
<a href="https://git.4rs.nl/awiteb/salvo-captcha">Source Code</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"#
|
"#
|
||||||
|
@ -182,7 +179,7 @@ fn captcha_result_page(captcha_result: String) -> String {
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<h1>Salvo Captcha Example</h1>
|
<h1>Salvo Captcha Example</h1>
|
||||||
<h2>Sign In</h2>
|
<h2>Result page</h2>
|
||||||
<strong>{captcha_result}</strong>
|
<strong>{captcha_result}</strong>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="/">Go Back</a>
|
<a href="/">Go Back</a>
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub trait CaptchaGenerator: CaptchaStorage {
|
||||||
///
|
///
|
||||||
/// The returned captcha image is 220x110 pixels.
|
/// The returned captcha image is 220x110 pixels.
|
||||||
///
|
///
|
||||||
/// For more information about the captcha name and difficulty, see the [`README.md`](https://github.com/TheAwiteb/salvo-captcha/#captcha-name-and-difficulty).
|
/// For more information about the captcha name and difficulty, see the [`README.md`](https://git.4rs.nl/awiteb/salvo-captcha/#captcha-name-and-difficulty).
|
||||||
fn new_captcha(
|
fn new_captcha(
|
||||||
&self,
|
&self,
|
||||||
name: CaptchaName,
|
name: CaptchaName,
|
||||||
|
|
340
src/finder.rs
340
src/finder.rs
|
@ -1,340 +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::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);
|
|
||||||
}
|
|
||||||
}
|
|
173
src/finder/form_finder.rs
Normal file
173
src/finder/form_finder.rs
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
162
src/finder/header_finder.rs
Normal file
162
src/finder/header_finder.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
43
src/finder/mod.rs
Normal file
43
src/finder/mod.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// 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;
|
||||||
|
}
|
|
@ -9,62 +9,22 @@
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
#[cfg(feature = "cacache-storage")]
|
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::SystemTime,
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Trait to store the captcha token and answer. is also clear the expired captcha.
|
use crate::CaptchaStorage;
|
||||||
///
|
|
||||||
/// 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.
|
/// The [`cacache`] storage.
|
||||||
///
|
///
|
||||||
/// [`cacache`]: https://github.com/zkat/cacache-rs
|
/// [`cacache`]: https://github.com/zkat/cacache-rs
|
||||||
#[cfg(feature = "cacache-storage")]
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CacacheStorage {
|
pub struct CacacheStorage {
|
||||||
/// The cacache cache directory.
|
/// The cacache cache directory.
|
||||||
cache_dir: PathBuf,
|
cache_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cacache-storage")]
|
|
||||||
impl CacacheStorage {
|
impl CacacheStorage {
|
||||||
/// Create a new CacacheStorage
|
/// Create a new CacacheStorage
|
||||||
pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
|
pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
|
||||||
|
@ -79,7 +39,6 @@ impl CacacheStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cacache-storage")]
|
|
||||||
impl CaptchaStorage for CacacheStorage {
|
impl CaptchaStorage for CacacheStorage {
|
||||||
type Error = cacache::Error;
|
type Error = cacache::Error;
|
||||||
|
|
||||||
|
@ -144,43 +103,7 @@ 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(test)]
|
||||||
#[cfg(feature = "cacache-storage")]
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
90
src/storage/mod.rs
Normal file
90
src/storage/mod.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue