use std::{sync::Arc, time::Duration};
#[cfg(feature = "cacache-storage")]
use std::{
path::{Path, PathBuf},
time::SystemTime,
};
pub trait CaptchaStorage: Send + Sync + 'static
where
Self: Clone + std::fmt::Debug,
{
type Token: TryFrom<String> + std::fmt::Display + Send + Sync;
type Answer: std::cmp::PartialEq + From<String> + Send + Sync;
type Error: std::fmt::Display + std::fmt::Debug + Send;
fn store_answer(
&self,
answer: Self::Answer,
) -> impl std::future::Future<Output = Result<Self::Token, Self::Error>> + Send;
fn get_answer(
&self,
token: &Self::Token,
) -> impl std::future::Future<Output = Result<Option<Self::Answer>, Self::Error>> + Send;
fn clear_expired(
&self,
expired_after: Duration,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
fn clear_by_token(
&self,
token: &Self::Token,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
}
#[cfg(feature = "cacache-storage")]
#[derive(Debug, Clone)]
pub struct CacacheStorage {
cache_dir: PathBuf,
}
#[cfg(feature = "cacache-storage")]
impl CacacheStorage {
pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
Self {
cache_dir: cache_dir.into(),
}
}
pub fn cache_dir(&self) -> &Path {
&self.cache_dir
}
}
#[cfg(feature = "cacache-storage")]
impl CaptchaStorage for CacacheStorage {
type Error = cacache::Error;
type Token = String;
type Answer = String;
async fn store_answer(&self, answer: Self::Answer) -> Result<Self::Token, Self::Error> {
let token = uuid::Uuid::new_v4();
log::info!("Storing captcha answer to cacache for token: {token}");
cacache::write(&self.cache_dir, token.to_string(), answer.as_bytes()).await?;
Ok(token.to_string())
}
async fn get_answer(&self, token: &Self::Token) -> Result<Option<Self::Answer>, Self::Error> {
log::info!("Getting captcha answer from cacache for token: {token}");
match cacache::read(&self.cache_dir, token.to_string()).await {
Ok(answer) => {
log::info!("Captcha answer is exist in cacache for token: {token}");
Ok(Some(
String::from_utf8(answer)
.expect("All the stored captcha answer should be utf8"),
))
}
Err(cacache::Error::EntryNotFound(_, _)) => {
log::info!("Captcha answer is not exist in cacache for token: {token}");
Ok(None)
}
Err(err) => {
log::error!("Failed to get captcha answer from cacache for token: {token}");
Err(err)
}
}
}
async fn clear_expired(&self, expired_after: Duration) -> Result<(), Self::Error> {
let now = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("SystemTime must be later than UNIX_EPOCH")
.as_millis();
let expired_after = expired_after.as_millis();
let expr_keys = cacache::index::ls(&self.cache_dir).filter_map(|meta| {
if let Ok(meta) = meta {
if now >= (meta.time + expired_after) {
return Some(meta.key);
}
}
None
});
for key in expr_keys {
cacache::RemoveOpts::new()
.remove_fully(true)
.remove(&self.cache_dir, &key)
.await
.ok();
}
Ok(())
}
async fn clear_by_token(&self, token: &Self::Token) -> Result<(), Self::Error> {
log::info!("Clearing captcha token from cacache: {token}");
let remove_opts = cacache::RemoveOpts::new().remove_fully(true);
remove_opts.remove(&self.cache_dir, token.to_string()).await
}
}
impl<T> CaptchaStorage for Arc<T>
where
T: CaptchaStorage,
{
type Error = T::Error;
type Token = T::Token;
type Answer = T::Answer;
fn store_answer(
&self,
answer: Self::Answer,
) -> impl std::future::Future<Output = Result<Self::Token, Self::Error>> + Send {
self.as_ref().store_answer(answer)
}
fn get_answer(
&self,
token: &Self::Token,
) -> impl std::future::Future<Output = Result<Option<Self::Answer>, 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: &Self::Token,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
self.as_ref().clear_by_token(token)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[cfg(feature = "cacache-storage")]
async fn cacache_store_captcha() {
let storage = CacacheStorage::new(
tempfile::tempdir()
.expect("failed to create temp file")
.path()
.to_owned(),
);
let token = storage
.store_answer("answer".to_owned())
.await
.expect("failed to store captcha");
assert_eq!(
storage
.get_answer(&token)
.await
.expect("failed to get captcha answer"),
Some("answer".to_owned())
);
}
#[tokio::test]
#[cfg(feature = "cacache-storage")]
async fn cacache_clear_expired() {
let storage = CacacheStorage::new(
tempfile::tempdir()
.expect("failed to create temp file")
.path()
.to_owned(),
);
let token = storage
.store_answer("answer".to_owned())
.await
.expect("failed to store captcha");
storage
.clear_expired(Duration::from_secs(0))
.await
.expect("failed to clear expired captcha");
assert!(storage
.get_answer(&token)
.await
.expect("failed to get captcha answer")
.is_none());
}
#[tokio::test]
#[cfg(feature = "cacache-storage")]
async fn cacache_clear_by_token() {
let storage = CacacheStorage::new(
tempfile::tempdir()
.expect("failed to create temp file")
.path()
.to_owned(),
);
let token = storage
.store_answer("answer".to_owned())
.await
.expect("failed to store captcha");
storage
.clear_by_token(&token)
.await
.expect("failed to clear captcha by token");
assert!(storage
.get_answer(&token)
.await
.expect("failed to get captcha answer")
.is_none());
}
#[tokio::test]
#[cfg(feature = "cacache-storage")]
async fn cacache_is_token_exist() {
let storage = CacacheStorage::new(
tempfile::tempdir()
.expect("failed to create temp file")
.path()
.to_owned(),
);
let token = storage
.store_answer("answer".to_owned())
.await
.expect("failed to store captcha");
assert!(storage
.get_answer(&token)
.await
.expect("failed to check if token is exist")
.is_some());
assert!(storage
.get_answer(&"token".to_owned())
.await
.expect("failed to check if token is exist")
.is_none());
}
#[tokio::test]
#[cfg(feature = "cacache-storage")]
async fn cacache_get_answer() {
let storage = CacacheStorage::new(
tempfile::tempdir()
.expect("failed to create temp file")
.path()
.to_owned(),
);
let token = storage
.store_answer("answer".to_owned())
.await
.expect("failed to store captcha");
assert_eq!(
storage
.get_answer(&token)
.await
.expect("failed to get captcha answer"),
Some("answer".to_owned())
);
assert!(storage
.get_answer(&"token".to_owned())
.await
.expect("failed to get captcha answer")
.is_none());
}
#[tokio::test]
#[cfg(feature = "cacache-storage")]
async fn cacache_cache_dir() {
let cache_dir = tempfile::tempdir()
.expect("failed to create temp file")
.path()
.to_owned();
let storage = CacacheStorage::new(cache_dir.clone());
assert_eq!(storage.cache_dir(), &cache_dir);
}
#[tokio::test]
#[cfg(feature = "cacache-storage")]
async fn cacache_clear_expired_with_expired_after() {
let storage = CacacheStorage::new(
tempfile::tempdir()
.expect("failed to create temp file")
.path()
.to_owned(),
);
let token = storage
.store_answer("answer".to_owned())
.await
.expect("failed to store captcha");
storage
.clear_expired(Duration::from_secs(1))
.await
.expect("failed to clear expired captcha");
assert_eq!(
storage
.get_answer(&token)
.await
.expect("failed to get captcha answer"),
Some("answer".to_owned())
);
tokio::time::sleep(Duration::from_secs(1)).await;
storage
.clear_expired(Duration::from_secs(1))
.await
.expect("failed to clear expired captcha");
assert!(storage
.get_answer(&token)
.await
.expect("failed to get captcha answer")
.is_none());
}
}