#[cfg(feature = "parallel-checker")]
pub mod parallel_checker;
#[cfg(feature = "parallel-checker")]
pub use parallel_checker::ParallelBanChecker;
use crate::error::{ConsumeResult, StorageError};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
#[async_trait]
pub trait Storage: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<String>, StorageError>;
async fn set(&self, key: &str, value: &str, ttl: Option<u64>) -> Result<(), StorageError>;
async fn delete(&self, key: &str) -> Result<(), StorageError>;
}
#[derive(Debug, Clone)]
pub struct QuotaInfo {
pub consumed: u64,
pub limit: u64,
pub window_start: DateTime<Utc>,
pub window_end: DateTime<Utc>,
}
#[async_trait]
pub trait QuotaStorage: Send + Sync {
async fn get_quota(
&self,
user_id: &str,
resource: &str,
) -> Result<Option<QuotaInfo>, StorageError>;
async fn consume(
&self,
user_id: &str,
resource: &str,
cost: u64,
limit: u64,
window: Duration,
) -> Result<ConsumeResult, StorageError>;
async fn reset(
&self,
user_id: &str,
resource: &str,
limit: u64,
window: Duration,
) -> Result<(), StorageError>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum BanTarget {
#[serde(rename = "ip")]
Ip(String),
#[serde(rename = "user")]
UserId(String),
#[serde(rename = "mac")]
Mac(String),
#[serde(rename = "geo")]
Geo { country_code: String },
#[serde(rename = "cidr")]
Cidr(String),
}
impl BanTarget {
pub fn contains_ip(&self, ip: &std::net::IpAddr) -> bool {
match self {
BanTarget::Cidr(cidr) => cidr
.parse::<ipnet::IpNet>()
.map(|net| net.contains(ip))
.unwrap_or(false),
_ => false,
}
}
pub fn prefix_len(&self) -> Option<u8> {
match self {
BanTarget::Cidr(cidr) => cidr
.parse::<ipnet::IpNet>()
.ok()
.map(|net| net.prefix_len()),
_ => None,
}
}
}
#[cfg(feature = "multi-tenant")]
pub(crate) fn ban_target_value(target: &BanTarget) -> Option<&str> {
match target {
BanTarget::Ip(v) | BanTarget::UserId(v) | BanTarget::Mac(v) => Some(v),
BanTarget::Geo { .. } | BanTarget::Cidr(_) => None,
}
}
#[cfg(feature = "multi-tenant")]
pub(crate) fn qualify_ban_target(
target: &BanTarget,
namespace: &crate::tenant::Namespace,
) -> Option<BanTarget> {
let qualified = namespace.qualify_key(ban_target_value(target)?);
Some(match target {
BanTarget::Ip(_) => BanTarget::Ip(qualified),
BanTarget::UserId(_) => BanTarget::UserId(qualified),
BanTarget::Mac(_) => BanTarget::Mac(qualified),
BanTarget::Geo { .. } | BanTarget::Cidr(_) => return None,
})
}
#[derive(Debug, Clone)]
pub struct BanRecord {
pub target: BanTarget,
pub ban_times: u32,
pub duration: Duration,
pub banned_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub is_manual: bool,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct BanHistory {
pub ban_times: u32,
pub last_banned_at: DateTime<Utc>,
}
#[async_trait]
pub trait BanStorage: Send + Sync {
async fn is_banned(&self, target: &BanTarget) -> Result<Option<BanRecord>, StorageError>;
async fn get_ban(&self, target: &BanTarget) -> Result<Option<BanRecord>, StorageError> {
self.is_banned(target).await
}
async fn add_ban(&self, record: &BanRecord) -> Result<(), StorageError> {
self.save(record).await
}
async fn save(&self, record: &BanRecord) -> Result<(), StorageError>;
async fn get_history(&self, target: &BanTarget) -> Result<Option<BanHistory>, StorageError>;
async fn increment_ban_times(&self, target: &BanTarget) -> Result<u64, StorageError>;
async fn upsert_ban_record(&self, record: &BanRecord) -> Result<u64, StorageError> {
let current = u32::try_from(self.get_ban_times(&record.target).await?)
.unwrap_or(u32::MAX)
.saturating_add(1);
let mut stored = record.clone();
stored.ban_times = stored.ban_times.max(current);
self.save(&stored).await?;
Ok(u64::from(stored.ban_times))
}
async fn get_ban_times(&self, target: &BanTarget) -> Result<u64, StorageError>;
async fn remove_ban(&self, target: &BanTarget) -> Result<(), StorageError>;
async fn cleanup_expired_bans(&self) -> Result<u64, StorageError>;
async fn list_bans(
&self,
active_only: bool,
offset: u64,
limit: u64,
) -> Result<Vec<BanRecord>, StorageError>;
fn as_any(&self) -> &dyn std::any::Any;
}
use ahash::AHashMap as HashMap;
use tokio::sync::RwLock;
pub struct MemoryStorage {
data: RwLock<HashMap<String, String>>,
expiration: RwLock<HashMap<String, u64>>,
}
pub struct MemoryBanStorage {
bans: RwLock<HashMap<BanTarget, BanRecord>>,
expiration: RwLock<HashMap<BanTarget, i64>>,
}
mod storage_impl;