use crate::error::Decision;
use crate::governor::Governor;
use crate::matchers::RequestContext;
use http::{Request, Response, StatusCode};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tower::Service;
use tower_layer::Layer;
use super::headers::{RateLimitHeaderValues, inject_rate_limit_headers};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
fn into_box_error<E: Into<BoxError>>(e: E) -> BoxError {
e.into()
}
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
pub return_429_on_reject: bool,
pub return_403_on_ban: bool,
pub reject_body: String,
pub ban_body: String,
pub skip_health_checks: bool,
pub health_check_paths: Vec<String>,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
return_429_on_reject: true,
return_403_on_ban: true,
reject_body: "Rate limit exceeded".to_string(),
ban_body: "Access denied".to_string(),
skip_health_checks: true,
health_check_paths: vec![
"/health".to_string(),
"/healthz".to_string(),
"/ready".to_string(),
],
}
}
}
impl RateLimitConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_return_429_on_reject(mut self, value: bool) -> Self {
self.return_429_on_reject = value;
self
}
pub fn with_return_403_on_ban(mut self, value: bool) -> Self {
self.return_403_on_ban = value;
self
}
pub fn with_reject_body(mut self, body: &str) -> Self {
self.reject_body = body.to_string();
self
}
pub fn with_ban_body(mut self, body: &str) -> Self {
self.ban_body = body.to_string();
self
}
pub fn with_skip_health_checks(mut self, skip: bool) -> Self {
self.skip_health_checks = skip;
self
}
pub fn with_health_check_path(mut self, path: &str) -> Self {
self.health_check_paths.push(path.to_string());
self
}
pub fn is_health_check_path(&self, path: &str) -> bool {
self.skip_health_checks && self.health_check_paths.iter().any(|p| p == path)
}
}
pub trait IntoRequestContext<B> {
#[allow(clippy::wrong_self_convention)]
fn into_request_context(&self, request: &Request<B>) -> RequestContext;
}
#[derive(Debug, Clone, Default)]
pub struct DefaultRequestContextConverter;
impl<B> IntoRequestContext<B> for DefaultRequestContextConverter {
fn into_request_context(&self, request: &Request<B>) -> RequestContext {
let mut context = RequestContext::new()
.with_path(request.uri().path())
.with_method(request.method().as_str());
if let Some(user_id) = request.headers().get("x-user-id") {
if let Ok(value) = user_id.to_str() {
context = context.with_header("X-User-Id", value);
}
}
if let Some(ip) = request.headers().get("x-real-ip") {
if let Ok(value) = ip.to_str() {
context = context.with_client_ip(value);
}
} else if let Some(forwarded) = request.headers().get("x-forwarded-for") {
if let Ok(value) = forwarded.to_str() {
if let Some(first_ip) = value.split(',').next() {
context = context.with_client_ip(first_ip.trim());
}
}
}
if let Some(api_key) = request.headers().get("x-api-key") {
if let Ok(value) = api_key.to_str() {
context = context.with_header("X-API-Key", value);
}
}
for (name, value) in request.headers().iter() {
if let Ok(value_str) = value.to_str() {
context = context.with_header(name.as_str(), value_str);
}
}
context
}
}
pub struct RateLimitLayer<C = DefaultRequestContextConverter> {
governor: Arc<Governor>,
config: RateLimitConfig,
context_converter: C,
}
impl RateLimitLayer {
pub fn new(governor: Arc<Governor>, config: RateLimitConfig) -> Self {
Self {
governor,
config,
context_converter: DefaultRequestContextConverter,
}
}
}
impl<C> RateLimitLayer<C> {
pub fn with_converter(governor: Arc<Governor>, config: RateLimitConfig, converter: C) -> Self {
Self {
governor,
config,
context_converter: converter,
}
}
}
impl<S, C> Layer<S> for RateLimitLayer<C>
where
C: Clone,
{
type Service = RateLimitService<S, C>;
fn layer(&self, inner: S) -> Self::Service {
RateLimitService {
governor: self.governor.clone(),
inner,
config: self.config.clone(),
context_converter: self.context_converter.clone(),
}
}
}
pub struct RateLimitService<S, C = DefaultRequestContextConverter> {
governor: Arc<Governor>,
inner: S,
config: RateLimitConfig,
context_converter: C,
}
impl<S, C, ReqBody, ResBody> Service<Request<ReqBody>> for RateLimitService<S, C>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
S::Future: Send + 'static,
C: IntoRequestContext<ReqBody> + Send + Sync + Clone + 'static,
ReqBody: Send + 'static,
ResBody: Default + Send + 'static,
{
type Response = Response<ResBody>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner
.poll_ready(cx)
.map_err(into_box_error)
.map_ok(|_| ())
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let governor = self.governor.clone();
let config = self.config.clone();
let context_converter = self.context_converter.clone();
let path = req.uri().path().to_string();
if config.is_health_check_path(&path) {
let future = self.inner.call(req);
return Box::pin(async move { future.await.map_err(into_box_error) });
}
let context = context_converter.into_request_context(&req);
let inner = self.inner.clone().call(req);
Box::pin(async move {
match governor.check(&context).await {
Ok(Decision::Allowed(metadata)) => {
let response = inner.await.map_err(into_box_error)?;
let header_values = RateLimitHeaderValues {
limit: metadata.limit,
remaining: metadata.remaining,
reset_at: metadata.reset_at,
retry_after: metadata.retry_after,
policy: metadata.policy,
};
Ok(inject_rate_limit_headers(response, &header_values))
}
Ok(Decision::Rejected(metadata)) => {
if config.return_429_on_reject {
let mut response = Response::new(ResBody::default());
*response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
let header_values = RateLimitHeaderValues {
limit: metadata.limit,
remaining: 0,
reset_at: metadata.reset_at,
retry_after: Some(metadata.retry_after),
policy: String::new(),
};
Ok(inject_rate_limit_headers(response, &header_values))
} else {
inner.await.map_err(into_box_error)
}
}
Ok(Decision::Banned(_)) => {
if config.return_403_on_ban {
let mut response = Response::new(ResBody::default());
*response.status_mut() = StatusCode::FORBIDDEN;
Ok(response)
} else {
inner.await.map_err(into_box_error)
}
}
Err(e) => {
log::error!("Rate limit check failed: {}", e);
inner.await.map_err(into_box_error)
}
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Action, ActionConfig, FlowControlConfig, LimiterConfig, Matcher, Rule};
use crate::storage::{MemoryBanStorage, MemoryStorage};
#[derive(Clone)]
struct MockService;
impl<B> Service<Request<B>> for MockService {
type Response = Response<()>;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<B>) -> Self::Future {
Box::pin(async { Ok(Response::new(())) })
}
}
async fn make_governor(
config: FlowControlConfig,
l1_enabled: bool,
) -> (Arc<Governor>, Arc<dyn crate::storage::BanStorage>) {
let storage: Arc<dyn crate::storage::Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn crate::storage::BanStorage> = Arc::new(MemoryBanStorage::new());
let bs = ban_storage.clone();
let gov = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.with_l1_cache_enabled(l1_enabled)
.build()
.await
.expect("Governor build");
(Arc::new(gov), bs)
}
fn gen_config(capacity: u64, refill_rate: u64) -> FlowControlConfig {
FlowControlConfig {
rules: vec![Rule {
id: "r".into(),
name: "R".into(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".into()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity,
refill_rate,
}],
action: ActionConfig {
on_exceed: Action::Reject,
ban: None,
},
}],
..FlowControlConfig::default()
}
}
fn make_req(path: &str, user: &str) -> Request<()> {
Request::builder()
.uri(path)
.method("GET")
.header("X-User-Id", user)
.body(())
.unwrap()
}
#[test]
fn test_rate_limit_config_default() {
let config = RateLimitConfig::default();
assert!(config.return_429_on_reject);
assert!(config.return_403_on_ban);
assert!(config.skip_health_checks);
assert_eq!(config.health_check_paths.len(), 3);
assert!(config.is_health_check_path("/health"));
assert!(config.is_health_check_path("/healthz"));
assert!(config.is_health_check_path("/ready"));
assert!(!config.is_health_check_path("/api/users"));
}
#[test]
fn test_rate_limit_config_builder() {
let config = RateLimitConfig::new()
.with_return_429_on_reject(false)
.with_return_403_on_ban(false)
.with_reject_body("Custom reject")
.with_ban_body("Custom ban")
.with_skip_health_checks(false)
.with_health_check_path("/ping");
assert!(!config.return_429_on_reject);
assert!(!config.return_403_on_ban);
assert_eq!(config.reject_body, "Custom reject");
assert_eq!(config.ban_body, "Custom ban");
assert!(!config.skip_health_checks);
assert!(!config.is_health_check_path("/health")); assert!(config.health_check_paths.contains(&"/ping".to_string()));
}
#[test]
fn test_default_request_context_converter() {
use http::Request;
let converter = DefaultRequestContextConverter;
let request = Request::builder()
.uri("/api/users")
.method("GET")
.header("X-User-Id", "user123")
.header("X-Real-IP", "192.168.1.1")
.header("X-API-Key", "my-api-key")
.body(())
.unwrap();
let context = converter.into_request_context(&request);
assert_eq!(context.path, "/api/users");
assert_eq!(context.method, "GET");
}
#[tokio::test]
async fn test_rate_limit_layer_creation() {
use crate::config::{
Action, ActionConfig, FlowControlConfig, LimiterConfig, Matcher, Rule,
};
use crate::storage::{MemoryBanStorage, MemoryStorage};
use std::sync::Arc;
let config = FlowControlConfig {
rules: vec![Rule {
id: "test_rule".to_string(),
name: "Test Rule".to_string(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig {
on_exceed: Action::Reject,
ban: None,
},
}],
..FlowControlConfig::default()
};
let storage: Arc<dyn crate::storage::Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn crate::storage::BanStorage> = Arc::new(MemoryBanStorage::new());
let governor = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.build()
.await
.expect("Governor build should succeed");
let layer = RateLimitLayer::new(Arc::new(governor), RateLimitConfig::default());
assert!(layer.config.skip_health_checks);
}
#[test]
fn test_into_box_error_with_string() {
let result = into_box_error("custom error".to_string());
assert_eq!(result.to_string(), "custom error");
}
#[test]
fn test_default_converter_x_forwarded_for() {
let c = DefaultRequestContextConverter;
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Forwarded-For", "10.0.0.1")
.body(())
.unwrap();
let ctx = c.into_request_context(&req);
assert_eq!(ctx.client_ip.as_deref(), Some("10.0.0.1"));
}
#[test]
fn test_default_converter_x_forwarded_for_multiple_ips() {
let c = DefaultRequestContextConverter;
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Forwarded-For", "192.168.1.1, 10.0.0.1, 172.16.0.1")
.body(())
.unwrap();
let ctx = c.into_request_context(&req);
assert_eq!(ctx.client_ip.as_deref(), Some("192.168.1.1"));
}
#[test]
fn test_default_converter_x_real_ip_overrides_forwarded_for() {
let c = DefaultRequestContextConverter;
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Real-IP", "192.168.1.1")
.header("X-Forwarded-For", "10.0.0.1")
.body(())
.unwrap();
let ctx = c.into_request_context(&req);
assert_eq!(ctx.client_ip.as_deref(), Some("192.168.1.1"));
}
#[test]
fn test_default_converter_no_ip_headers() {
let c = DefaultRequestContextConverter;
let req = Request::builder()
.uri("/api")
.method("GET")
.body(())
.unwrap();
let ctx = c.into_request_context(&req);
assert!(ctx.client_ip.is_none());
}
#[test]
fn test_default_converter_headers_iterated() {
let c = DefaultRequestContextConverter;
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Custom-Header", "custom-value")
.body(())
.unwrap();
let ctx = c.into_request_context(&req);
assert_eq!(ctx.headers.get("x-custom-header").unwrap(), "custom-value");
}
#[tokio::test]
async fn test_rate_limit_layer_with_converter() {
#[derive(Clone)]
struct CustomConverter;
impl<B> IntoRequestContext<B> for CustomConverter {
fn into_request_context(&self, _req: &Request<B>) -> RequestContext {
RequestContext::new()
.with_path("/custom")
.with_method("POST")
}
}
let (gov, _) = make_governor(gen_config(100, 10), true).await;
let layer =
RateLimitLayer::with_converter(gov, RateLimitConfig::default(), CustomConverter);
let mut svc: RateLimitService<MockService, CustomConverter> = layer.layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_layer_creates_service() {
let (gov, _) = make_governor(gen_config(100, 10), true).await;
let layer = RateLimitLayer::new(gov, RateLimitConfig::default());
let _svc: RateLimitService<MockService> = layer.layer(MockService);
}
#[tokio::test]
async fn test_service_health_check_path() {
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let ctx = RequestContext::new()
.with_path("/api")
.with_method("GET")
.with_header("x-user-id", "u");
assert!(matches!(
gov.check(&ctx).await.unwrap(),
Decision::Allowed(_)
));
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/health", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[tokio::test]
async fn test_service_allowed() {
let (gov, _) = make_governor(gen_config(100, 10), false).await;
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.headers().get("RateLimit-Limit").unwrap(), "0");
assert_eq!(resp.headers().get("RateLimit-Remaining").unwrap(), "0");
assert!(resp.headers().get("Retry-After").is_none());
}
#[tokio::test]
async fn test_service_rejected_with_429() {
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let ctx = RequestContext::new()
.with_path("/api")
.with_method("GET")
.with_header("x-user-id", "u");
assert!(matches!(
gov.check(&ctx).await.unwrap(),
Decision::Allowed(_)
));
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(resp.headers().get("RateLimit-Remaining").unwrap(), "0");
assert_eq!(resp.headers().get("Retry-After").unwrap(), "60");
}
#[tokio::test]
async fn test_service_rejected_pass_through() {
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let ctx = RequestContext::new()
.with_path("/api")
.with_method("GET")
.with_header("x-user-id", "u");
assert!(matches!(
gov.check(&ctx).await.unwrap(),
Decision::Allowed(_)
));
let cfg = RateLimitConfig {
return_429_on_reject: false,
..RateLimitConfig::default()
};
let mut svc = RateLimitLayer::new(gov, cfg).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[cfg(feature = "parallel-checker")]
#[tokio::test]
async fn test_service_banned_with_403() {
use crate::storage::BanTarget;
use chrono::Utc;
let (gov, ban_storage) = make_governor(gen_config(100, 10), false).await;
let record = crate::storage::BanRecord {
target: BanTarget::UserId("u".into()),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
is_manual: true,
reason: "test".into(),
};
ban_storage.save(&record).await.unwrap();
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[cfg(feature = "parallel-checker")]
#[tokio::test]
async fn test_service_banned_pass_through() {
use crate::storage::BanTarget;
use chrono::Utc;
let (gov, ban_storage) = make_governor(gen_config(100, 10), false).await;
let record = crate::storage::BanRecord {
target: BanTarget::UserId("u".into()),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
is_manual: true,
reason: "test".into(),
};
ban_storage.save(&record).await.unwrap();
let cfg = RateLimitConfig {
return_403_on_ban: false,
..RateLimitConfig::default()
};
let mut svc = RateLimitLayer::new(gov, cfg).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[tokio::test]
async fn test_service_poll_ready_ok() {
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
let (gov, _) = make_governor(gen_config(100, 10), false).await;
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let poll =
<RateLimitService<MockService> as Service<Request<()>>>::poll_ready(&mut svc, &mut cx);
match poll {
Poll::Ready(Ok(())) => {}
_ => panic!("expected Ready(Ok(()))"),
}
}
#[derive(Clone)]
struct ErrorMockService;
impl<B> Service<Request<B>> for ErrorMockService {
type Response = Response<()>;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Err("service not ready".into()))
}
fn call(&mut self, _req: Request<B>) -> Self::Future {
Box::pin(async { Ok(Response::new(())) })
}
}
#[tokio::test]
async fn test_service_poll_ready_error() {
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
let (gov, _) = make_governor(gen_config(100, 10), false).await;
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(ErrorMockService);
let poll = <RateLimitService<ErrorMockService> as Service<Request<()>>>::poll_ready(
&mut svc, &mut cx,
);
match poll {
Poll::Ready(Err(_)) => {}
_ => panic!("expected Ready(Err)"),
}
}
}