1use crate::problem_response::write_problem;
12use std::collections::HashMap;
13use std::net::IpAddr;
14use std::ops::ControlFlow;
15use std::str::FromStr;
16use std::time::Instant;
17use tokio::sync::Mutex;
18use webx_core::config::RateLimitSection;
19use webx_core::error::Result;
20use webx_core::http::IHttpContext;
21use webx_core::middleware::IMiddleware;
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 pub fn retry_after_secs(&self) -> u64 {
97 if self.rate <= 0.0 {
98 return 1;
99 }
100 ((1.0 / self.rate).ceil() as u64).max(1)
101 }
102}
103
104fn evict_oldest(buckets: &mut HashMap<IpAddr, TokenBucket>) {
105 if buckets.is_empty() {
106 return;
107 }
108 let oldest = buckets
109 .iter()
110 .min_by_key(|(_, b)| b.last_refill)
111 .map(|(ip, _)| *ip);
112 if let Some(ip) = oldest {
113 buckets.remove(&ip);
114 }
115}
116
117pub struct RateLimitMiddleware {
118 limiter: RateLimiter,
119 trust_proxy: bool,
120 trusted_proxies: Vec<IpCidr>,
122}
123
124impl RateLimitMiddleware {
125 pub fn new(requests_per_second: f64, burst_size: u32) -> Self {
126 Self::new_with_max_ips(requests_per_second, burst_size, 10_000)
127 }
128
129 pub fn new_with_max_ips(
130 requests_per_second: f64,
131 burst_size: u32,
132 max_tracked_ips: usize,
133 ) -> Self {
134 Self {
135 limiter: RateLimiter::new(requests_per_second, burst_size, max_tracked_ips),
136 trust_proxy: false,
137 trusted_proxies: Vec::new(),
138 }
139 }
140
141 pub fn from_config(cfg: &RateLimitSection) -> Self {
142 let trusted_proxies: Vec<IpCidr> = cfg
143 .trusted_proxies
144 .iter()
145 .filter_map(|value| match IpCidr::parse(value) {
146 Some(cidr) => Some(cidr),
147 None => {
148 tracing::warn!(
149 value,
150 "RateLimit.TrustedProxies entry is not a valid CIDR; ignoring it"
151 );
152 None
153 }
154 })
155 .collect();
156 if cfg.trust_proxy && trusted_proxies.is_empty() {
157 tracing::warn!(
158 "RateLimit.TrustProxy is enabled but TrustedProxies is empty; \
159 forwarded headers are ignored (a client could otherwise spoof them)"
160 );
161 }
162 Self {
163 limiter: RateLimiter::new(cfg.requests_per_second, cfg.burst_size, cfg.max_tracked_ips),
164 trust_proxy: cfg.trust_proxy,
165 trusted_proxies,
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy)]
172struct IpCidr {
173 network: IpAddr,
174 prefix: u8,
175}
176
177impl IpCidr {
178 fn parse(value: &str) -> Option<Self> {
179 let (addr, prefix) = value.split_once('/')?;
180 let network = IpAddr::from_str(addr.trim()).ok()?;
181 let prefix: u8 = prefix.trim().parse().ok()?;
182 let max_prefix = match network {
183 IpAddr::V4(_) => 32,
184 IpAddr::V6(_) => 128,
185 };
186 (prefix <= max_prefix).then_some(Self { network, prefix })
187 }
188
189 fn contains(&self, ip: IpAddr) -> bool {
190 match (self.network, ip) {
191 (IpAddr::V4(network), IpAddr::V4(ip)) => {
192 let mask = if self.prefix == 0 {
193 0
194 } else {
195 u32::MAX << (32 - self.prefix)
196 };
197 (u32::from(network) & mask) == (u32::from(ip) & mask)
198 }
199 (IpAddr::V6(network), IpAddr::V6(ip)) => {
200 let mask = if self.prefix == 0 {
201 0
202 } else {
203 u128::MAX << (128 - self.prefix)
204 };
205 (u128::from(network) & mask) == (u128::from(ip) & mask)
206 }
207 _ => false,
208 }
209 }
210}
211
212#[async_trait::async_trait]
213impl IMiddleware for RateLimitMiddleware {
214 async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
215 let Some(ip) = extract_client_ip(ctx, self.trust_proxy, &self.trusted_proxies) else {
220 tracing::debug!("rate limit skipped: request has no peer address");
221 return Ok(ControlFlow::Continue(()));
222 };
223
224 if !self.limiter.allow(ip).await {
225 let retry_after = self.limiter.retry_after_secs().to_string();
226 ctx.response_mut().set_header("retry-after", &retry_after);
227 write_problem(ctx, 429, "Too Many Requests").await;
228 return Ok(ControlFlow::Break(()));
229 }
230
231 Ok(ControlFlow::Continue(()))
232 }
233}
234
235fn extract_client_ip(
243 ctx: &dyn IHttpContext,
244 trust_proxy: bool,
245 trusted_proxies: &[IpCidr],
246) -> Option<IpAddr> {
247 let peer = ctx.request().remote_addr().map(|addr| addr.ip());
248
249 if trust_proxy && !trusted_proxies.is_empty() {
250 let peer_is_trusted = peer.is_some_and(|ip| trusted_proxies.iter().any(|c| c.contains(ip)));
251 if peer_is_trusted {
252 if let Some(forwarded) = ctx.request().header("x-forwarded-for") {
253 if let Some(client) = client_from_forwarded_for(forwarded, trusted_proxies) {
254 return Some(client);
255 }
256 }
257 if let Some(real) = ctx.request().header("x-real-ip") {
258 if let Ok(ip) = IpAddr::from_str(real.trim()) {
259 return Some(ip);
260 }
261 }
262 }
263 }
264
265 peer
266}
267
268fn client_from_forwarded_for(value: &str, trusted_proxies: &[IpCidr]) -> Option<IpAddr> {
273 let hops: Vec<IpAddr> = value
274 .split(',')
275 .filter_map(|part| IpAddr::from_str(part.trim()).ok())
276 .collect();
277
278 hops.iter()
279 .rev()
280 .find(|ip| !trusted_proxies.iter().any(|cidr| cidr.contains(**ip)))
281 .copied()
282 .or_else(|| hops.first().copied())
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use std::net::Ipv4Addr;
289
290 #[tokio::test]
291 async fn rate_limiter_evicts_oldest_when_max_ips_reached() {
292 let limiter = RateLimiter::new(100.0, 10, 2);
293 assert!(limiter.allow(IpAddr::V4(Ipv4Addr::new(1, 0, 0, 1))).await);
294 assert!(limiter.allow(IpAddr::V4(Ipv4Addr::new(2, 0, 0, 1))).await);
295 assert_eq!(limiter.tracked_ip_count().await, 2);
296
297 assert!(limiter.allow(IpAddr::V4(Ipv4Addr::new(3, 0, 0, 1))).await);
298 assert_eq!(limiter.tracked_ip_count().await, 2);
299 }
300
301 #[tokio::test]
302 async fn retry_after_matches_the_refill_rate() {
303 let fast = RateLimiter::new(10.0, 1, 10);
306 assert_eq!(fast.retry_after_secs(), 1);
307
308 let slow = RateLimiter::new(0.5, 1, 10);
310 assert_eq!(slow.retry_after_secs(), 2);
311
312 let unset = RateLimiter::new(0.0, 1, 10);
314 assert_eq!(unset.retry_after_secs(), 1);
315 }
316
317 #[test]
318 fn cidr_matches_only_inside_the_range() {
319 let cidr = IpCidr::parse("10.0.0.0/8").expect("valid cidr");
320 assert!(cidr.contains("10.1.2.3".parse().unwrap()));
321 assert!(!cidr.contains("11.0.0.1".parse().unwrap()));
322 assert!(IpCidr::parse("0.0.0.0/0")
323 .unwrap()
324 .contains("203.0.113.9".parse().unwrap()));
325 assert!(IpCidr::parse("not-a-cidr").is_none());
326 assert!(IpCidr::parse("10.0.0.0/33").is_none());
327
328 let v6 = IpCidr::parse("2001:db8::/32").expect("valid v6 cidr");
329 assert!(v6.contains("2001:db8::1".parse().unwrap()));
330 assert!(!v6.contains("2001:db9::1".parse().unwrap()));
331 }
332
333 #[test]
334 fn forwarded_for_skips_trusted_proxies_from_the_right() {
335 let proxies = vec![IpCidr::parse("10.0.0.0/8").unwrap()];
336 assert_eq!(
337 client_from_forwarded_for("203.0.113.7, 10.0.0.2, 10.0.0.3", &proxies),
338 Some("203.0.113.7".parse().unwrap()),
339 "the right-most untrusted hop is the client"
340 );
341 assert_eq!(
342 client_from_forwarded_for("1.1.1.1, 203.0.113.7, 10.0.0.3", &proxies),
343 Some("203.0.113.7".parse().unwrap()),
344 "a spoofed left-most entry must not win"
345 );
346 assert_eq!(
347 client_from_forwarded_for("10.0.0.1, 10.0.0.3", &proxies),
348 Some("10.0.0.1".parse().unwrap()),
349 "when every hop is a trusted proxy fall back to the left-most"
350 );
351 }
352}