#![doc = include_str!("../README.md")]
#![deny(warnings)]
#![deny(missing_docs)]
#![deny(clippy::print_stdout)]
mod captcha_gen;
mod finder;
mod storage;
use salvo_core::{
handler::{none_skipper, Skipper},
Depot, FlowCtrl, Handler, Request, Response,
};
pub use {captcha_gen::*, finder::*, storage::*};
pub use captcha::{CaptchaName, Difficulty as CaptchaDifficulty};
pub const CAPTCHA_STATE_KEY: &str = "::salvo_captcha::captcha_state";
#[non_exhaustive]
#[allow(clippy::type_complexity)]
pub struct Captcha<S, F>
where
S: CaptchaStorage,
F: CaptchaFinder<Token = S::Token, Answer = S::Answer>,
{
finder: F,
storage: S,
skipper: Box<dyn Skipper>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptchaState {
Passed,
Skipped,
TokenNotFound,
AnswerNotFound,
WrongToken,
WrongAnswer,
StorageError,
}
impl<S, F> Captcha<S, F>
where
S: CaptchaStorage,
F: CaptchaFinder<Token = S::Token, Answer = S::Answer>,
{
pub fn new(storage: impl Into<S>, finder: impl Into<F>) -> Self {
Self {
finder: finder.into(),
storage: storage.into(),
skipper: Box::new(none_skipper),
}
}
pub fn storage(&self) -> &S {
&self.storage
}
pub fn skipper(mut self, skipper: impl Skipper) -> Self {
self.skipper = Box::new(skipper);
self
}
}
#[easy_ext::ext(CaptchaDepotExt)]
impl Depot {
pub fn get_captcha_state(&self) -> Option<&CaptchaState> {
self.get(CAPTCHA_STATE_KEY).ok()
}
}
#[async_trait::async_trait]
impl<S, F> Handler for Captcha<S, F>
where
S: CaptchaStorage,
F: CaptchaFinder<Token = S::Token, Answer = S::Answer> + 'static,
{
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
_: &mut Response,
_: &mut FlowCtrl,
) {
if self.skipper.skipped(req, depot) {
log::info!("Captcha check is skipped");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::Skipped);
return;
}
let token = match self.finder.find_token(req).await {
Ok(Some(token)) => token,
Ok(None) => {
log::info!("Captcha token is not found in request");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::TokenNotFound);
return;
}
Err(err) => {
log::error!("Failed to find captcha token from request: {err:?}");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongToken);
return;
}
};
let answer = match self.finder.find_answer(req).await {
Ok(Some(answer)) => answer,
Ok(None) => {
log::info!("Captcha answer is not found in request");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::AnswerNotFound);
return;
}
Err(err) => {
log::error!("Failed to find captcha answer from request: {err:?}");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongAnswer);
return;
}
};
match self.storage.get_answer(&token).await {
Ok(Some(captch_answer)) => {
log::info!("Captcha answer is exist in storage for token: {token}");
if captch_answer == answer {
log::info!("Captcha answer is correct for token: {token}");
self.storage.clear_by_token(&token).await.ok();
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::Passed);
} else {
log::info!("Captcha answer is wrong for token: {token}");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongAnswer);
}
}
Ok(None) => {
log::info!("Captcha answer is not exist in storage for token: {token}");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongToken);
}
Err(err) => {
log::error!("Failed to get captcha answer from storage: {err}");
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::StorageError);
}
};
}
}