rust_webx_host/
rate_limit.rs1use crate::problem_response::write_problem;
12use rust_webx_core::config::RateLimitSection;
13use rust_webx_core::error::Result;
14use rust_webx_core::http::IHttpContext;
15use rust_webx_core::middleware::IMiddleware;
16use std::collections::HashMap;
17use std::net::IpAddr;
18use std::ops::ControlFlow;
19use std::str::FromStr;
20use std::time::Instant;
21use tokio::sync::Mutex;
22
23struct TokenBucket {
24 tokens: f64,
25 last_refill: Instant,
26}
27
28impl TokenBucket {
29 fn new(burst_size: f64) -> Self {
30 Self {
31 tokens: burst_size,
32 last_refill: Instant::now(),
33 }
34 }
35
36 fn try_consume(&mut self, rate: f64, burst: f64) -> bool {
37 let now = Instant::now();
38 let elapsed = now.duration_since(self.last_refill).as_secs_f64();
39 self.tokens = (self.tokens + elapsed * rate).min(burst);
40 self.last_refill = now;
41
42 if self.tokens >= 1.0 {
43 self.tokens -= 1.0;
44 true
45 } else {
46 false
47 }
48 }
49}
50
51pub struct RateLimiter {
53 buckets: Mutex<HashMap<IpAddr, TokenBucket>>,
54 rate: f64,
55 burst: f64,
56 max_ips: usize,
57 trust_proxy: bool,
58}
59
60impl RateLimiter {
61 pub fn new(requests_per_second: f64, burst_size: u32, max_tracked_ips: usize) -> Self {
62 Self {
63 buckets: Mutex::new(HashMap::new()),
64 rate: requests_per_second,
65 burst: burst_size as f64,
66 max_ips: max_tracked_ips.max(1),
67 trust_proxy: false,
68 }
69 }
70
71 pub fn with_trust_proxy(mut self, trust: bool) -> Self {
72 self.trust_proxy = trust;
73 self
74 }
75
76 async fn allow(&self, ip: IpAddr) -> bool {
77 let mut buckets = self.buckets.lock().await;
78 if !buckets.contains_key(&ip) && buckets.len() >= self.max_ips {
79 evict_oldest(&mut buckets);
80 }
81 let bucket = buckets
82 .entry(ip)
83 .or_insert_with(|| TokenBucket::new(self.burst));
84 bucket.try_consume(self.rate, self.burst)
85 }
86
87 pub async fn tracked_ip_count(&self) -> usize {
89 self.buckets.lock().await.len()
90 }
91}
92
93fn evict_oldest(buckets: &mut HashMap<IpAddr, TokenBucket>) {
94 if buckets.is_empty() {
95 return;
96 }
97 let oldest = buckets
98 .iter()
99 .min_by_key(|(_, b)| b.last_refill)
100 .map(|(ip, _)| *ip);
101 if let Some(ip) = oldest {
102 buckets.remove(&ip);
103 }
104}
105
106pub struct RateLimitMiddleware {
107 limiter: RateLimiter,
108 trust_proxy: bool,
109}
110
111impl RateLimitMiddleware {
112 pub fn new(requests_per_second: f64, burst_size: u32) -> Self {
113 Self::new_with_max_ips(requests_per_second, burst_size, 10_000)
114 }
115
116 pub fn new_with_max_ips(requests_per_second: f64, burst_size: u32, max_tracked_ips: usize) -> Self {
117 Self {
118 limiter: RateLimiter::new(requests_per_second, burst_size, max_tracked_ips),
119 trust_proxy: false,
120 }
121 }
122
123 pub fn from_config(cfg: &RateLimitSection) -> Self {
124 Self {
125 limiter: RateLimiter::new(
126 cfg.requests_per_second,
127 cfg.burst_size,
128 cfg.max_tracked_ips,
129 ),
130 trust_proxy: cfg.trust_proxy,
131 }
132 }
133}
134
135#[async_trait::async_trait]
136impl IMiddleware for RateLimitMiddleware {
137 async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
138 let ip = extract_client_ip(ctx, self.trust_proxy);
139
140 if !self.limiter.allow(ip).await {
141 write_problem(ctx, 429, "Too Many Requests").await;
142 return Ok(ControlFlow::Break(()));
143 }
144
145 Ok(ControlFlow::Continue(()))
146 }
147}
148
149fn extract_client_ip(ctx: &dyn IHttpContext, trust_proxy: bool) -> IpAddr {
150 if trust_proxy {
151 if let Some(fwd) = ctx.request().header("x-forwarded-for") {
152 let first = fwd.split(',').next().unwrap_or("").trim();
153 if let Ok(ip) = IpAddr::from_str(first) {
154 return ip;
155 }
156 }
157
158 if let Some(real) = ctx.request().header("x-real-ip") {
159 if let Ok(ip) = IpAddr::from_str(real.trim()) {
160 return ip;
161 }
162 }
163 }
164
165 IpAddr::from_str("127.0.0.1").unwrap()
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use std::net::Ipv4Addr;
172
173 #[tokio::test]
174 async fn rate_limiter_evicts_oldest_when_max_ips_reached() {
175 let limiter = RateLimiter::new(100.0, 10, 2);
176 assert!(limiter.allow(IpAddr::V4(Ipv4Addr::new(1, 0, 0, 1))).await);
177 assert!(limiter.allow(IpAddr::V4(Ipv4Addr::new(2, 0, 0, 1))).await);
178 assert_eq!(limiter.tracked_ip_count().await, 2);
179
180 assert!(limiter.allow(IpAddr::V4(Ipv4Addr::new(3, 0, 0, 1))).await);
181 assert_eq!(limiter.tracked_ip_count().await, 2);
182 }
183}