Skip to main content

rws_server/rate_limit/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use std::collections::{HashMap, VecDeque};
5use std::sync::{Mutex, OnceLock};
6#[cfg(not(target_arch = "wasm32"))]
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11/// A sliding-window per-key rate limiter.
12///
13/// Each call to [`RateLimiter::check`] records a timestamp for the given key
14/// and returns `true` when the number of calls within the current window is
15/// still below `max_requests`. Returns `false` once the limit is exceeded.
16///
17/// Thread-safe: the internal state is behind a `Mutex` so it can be shared
18/// across threads via [`global`] or wrapped in an `Arc`.
19///
20/// # Example
21///
22/// ```rust,no_run
23/// # extern crate rws_server as rust_web_server;
24/// use rust_web_server::rate_limit::RateLimiter;
25///
26/// let limiter = RateLimiter::new(100, 60); // 100 req / 60 s
27///
28/// if limiter.check("192.168.1.1") {
29///     // process request
30/// } else {
31///     // return 429 Too Many Requests
32/// }
33/// ```
34pub struct RateLimiter {
35    state: Mutex<HashMap<String, VecDeque<Instant>>>,
36    max_requests: AtomicU32,
37    window_secs: AtomicU64,
38}
39
40impl RateLimiter {
41    /// Create a new limiter allowing `max_requests` per `window_secs`-second window.
42    pub fn new(max_requests: u32, window_secs: u64) -> Self {
43        RateLimiter {
44            state: Mutex::new(HashMap::new()),
45            max_requests: AtomicU32::new(max_requests),
46            window_secs: AtomicU64::new(window_secs),
47        }
48    }
49
50    /// Update the limits on a live limiter without restarting.
51    ///
52    /// Changes take effect on the next call to [`check`] or [`remaining`].
53    /// Called automatically by [`crate::config_reload::reload`] on SIGHUP.
54    pub fn set_limits(&self, max_requests: u32, window_secs: u64) {
55        self.max_requests.store(max_requests, Ordering::Relaxed);
56        self.window_secs.store(window_secs, Ordering::Relaxed);
57    }
58
59    /// The current `(max_requests, window_secs)`.
60    pub fn limits(&self) -> (u32, u64) {
61        (self.max_requests.load(Ordering::Relaxed), self.window_secs.load(Ordering::Relaxed))
62    }
63
64    fn window(&self) -> Duration {
65        Duration::from_secs(self.window_secs.load(Ordering::Relaxed))
66    }
67
68    fn max(&self) -> u32 {
69        self.max_requests.load(Ordering::Relaxed)
70    }
71
72    /// Returns `true` if `key` (typically a client IP) is within the rate limit,
73    /// or `false` if the limit has been exceeded.
74    ///
75    /// A permitted call is always recorded so it counts toward future limits.
76    pub fn check(&self, key: &str) -> bool {
77        let now = Instant::now();
78        let window = self.window();
79        let max = self.max();
80        let mut guard = self.state.lock().unwrap();
81        let timestamps = guard.entry(key.to_string()).or_default();
82
83        // Drop timestamps older than the window.
84        while timestamps.front().map(|t| now.duration_since(*t) > window).unwrap_or(false) {
85            timestamps.pop_front();
86        }
87
88        if (timestamps.len() as u32) < max {
89            timestamps.push_back(now);
90            true
91        } else {
92            false
93        }
94    }
95
96    /// Seconds a rejected `key` should wait before trying again.
97    ///
98    /// The window length, a safe upper bound: the oldest request in the
99    /// window may expire sooner, so a client that waits this long is never
100    /// rejected again for the same burst — it may only wait longer than
101    /// strictly needed.
102    pub fn retry_after(&self, _key: &str) -> u64 {
103        self.window_secs.load(Ordering::Relaxed)
104    }
105
106    /// Number of remaining requests `key` may make within the current window.
107    pub fn remaining(&self, key: &str) -> u32 {
108        let now = Instant::now();
109        let window = self.window();
110        let max = self.max();
111        let mut guard = self.state.lock().unwrap();
112        let timestamps = guard.entry(key.to_string()).or_default();
113        while timestamps.front().map(|t| now.duration_since(*t) > window).unwrap_or(false) {
114            timestamps.pop_front();
115        }
116        max.saturating_sub(timestamps.len() as u32)
117    }
118
119    /// Remove all tracked state for `key`. Useful in tests.
120    pub fn reset(&self, key: &str) {
121        self.state.lock().unwrap().remove(key);
122    }
123}
124
125// ── RedisRateLimiter ──────────────────────────────────────────────────────────
126
127#[cfg(not(target_arch = "wasm32"))]
128use crate::redis_protocol::{RespConn, RespReply};
129
130/// A distributed, fixed-window rate limiter backed by a Redis server.
131///
132/// [`RateLimiter`] only protects a single process: two `rws` instances behind
133/// a load balancer each track their own in-memory counters, so the effective
134/// limit for a client doubles (or worse) as replicas scale out. `RedisRateLimiter`
135/// keys the counter on a shared Redis server instead, so every instance enforces
136/// the same budget.
137///
138/// Unlike [`RateLimiter`]'s sliding window (a deque of timestamps), this uses a
139/// fixed-window counter: `key`'s count lives under one Redis key with a TTL equal
140/// to the window length, incremented atomically via `INCR`. This is the standard
141/// distributed rate-limiting pattern — Redis guarantees `INCR` is atomic across
142/// concurrent callers, so counters stay correct even under heavy concurrent load
143/// from multiple `rws` processes. The tradeoff versus a sliding window: a client
144/// can burst up to `2x max_requests` across a window boundary (once near the end
145/// of one window, once at the start of the next).
146///
147/// Cloning is cheap — all clones share the same underlying TCP connection (one
148/// persistent connection per `RedisRateLimiter` instance).
149///
150/// # Example
151///
152/// ```rust,no_run
153/// # extern crate rws_server as rust_web_server;
154/// use rust_web_server::rate_limit::RedisRateLimiter;
155///
156/// let limiter = RedisRateLimiter::new("127.0.0.1:6379", None, 100, 60); // 100 req / 60s
157///
158/// match limiter.check("192.168.1.1") {
159///     Ok(true) => { /* process request */ }
160///     Ok(false) => { /* return 429 Too Many Requests */ }
161///     Err(e) => { /* Redis unreachable — decide fail-open vs fail-closed */ }
162/// }
163/// ```
164#[cfg(not(target_arch = "wasm32"))]
165pub struct RedisRateLimiter {
166    conn: Arc<RespConn>,
167    max_requests: AtomicU32,
168    window_secs: AtomicU64,
169}
170
171#[cfg(not(target_arch = "wasm32"))]
172impl Clone for RedisRateLimiter {
173    fn clone(&self) -> Self {
174        RedisRateLimiter {
175            conn: Arc::clone(&self.conn),
176            max_requests: AtomicU32::new(self.max()),
177            window_secs: AtomicU64::new(self.window().as_secs()),
178        }
179    }
180}
181
182#[cfg(not(target_arch = "wasm32"))]
183impl RedisRateLimiter {
184    /// Create a limiter that connects to `addr` (e.g. `"127.0.0.1:6379"`),
185    /// allowing `max_requests` per `window_secs`-second window.
186    /// `password` is passed to Redis `AUTH` if `Some`.
187    pub fn new(addr: impl Into<String>, password: Option<String>, max_requests: u32, window_secs: u64) -> Self {
188        RedisRateLimiter {
189            conn: Arc::new(RespConn::new(addr, password)),
190            max_requests: AtomicU32::new(max_requests),
191            window_secs: AtomicU64::new(window_secs),
192        }
193    }
194
195    /// Build a limiter from environment variables:
196    /// - `RWS_REDIS_HOST` (default `127.0.0.1`)
197    /// - `RWS_REDIS_PORT` (default `6379`)
198    /// - `RWS_REDIS_PASSWORD` (optional)
199    /// - `RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS` (default `1000`)
200    /// - `RWS_CONFIG_RATE_LIMIT_WINDOW_SECS` (default `60`)
201    pub fn from_env() -> Self {
202        let host = std::env::var("RWS_REDIS_HOST").unwrap_or_else(|_| "127.0.0.1".into());
203        let port = std::env::var("RWS_REDIS_PORT").unwrap_or_else(|_| "6379".into());
204        let addr = format!("{}:{}", host, port);
205        let password = std::env::var("RWS_REDIS_PASSWORD").ok();
206        let max: u32 = std::env::var("RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS")
207            .ok()
208            .and_then(|v| v.parse().ok())
209            .unwrap_or(1000);
210        let window: u64 = std::env::var("RWS_CONFIG_RATE_LIMIT_WINDOW_SECS")
211            .ok()
212            .and_then(|v| v.parse().ok())
213            .unwrap_or(60);
214        Self::new(addr, password, max, window)
215    }
216
217    /// Update the limits on a live limiter without restarting.
218    pub fn set_limits(&self, max_requests: u32, window_secs: u64) {
219        self.max_requests.store(max_requests, Ordering::Relaxed);
220        self.window_secs.store(window_secs, Ordering::Relaxed);
221    }
222
223    fn window(&self) -> Duration {
224        Duration::from_secs(self.window_secs.load(Ordering::Relaxed))
225    }
226
227    fn max(&self) -> u32 {
228        self.max_requests.load(Ordering::Relaxed)
229    }
230
231    /// Seconds a rejected `key` should wait before trying again: the window
232    /// length, a safe upper bound.
233    pub fn retry_after(&self, _key: &str) -> u64 {
234        self.window_secs.load(Ordering::Relaxed)
235    }
236
237    fn redis_key(key: &str) -> Vec<u8> {
238        format!("rws:ratelimit:{}", key).into_bytes()
239    }
240
241    /// Returns `Ok(true)` if `key` (typically a client IP) is within the rate
242    /// limit, `Ok(false)` if the limit has been exceeded, or `Err` if the
243    /// Redis server could not be reached.
244    ///
245    /// A call always increments the shared counter, whether permitted or not,
246    /// so callers must decide for themselves whether to fail open (allow the
247    /// request) or fail closed (deny it) on `Err` — this limiter does not
248    /// silently pick one.
249    pub fn check(&self, key: &str) -> std::io::Result<bool> {
250        let redis_key = Self::redis_key(key);
251        let window = self.window().as_secs().to_string();
252        // Atomically create the key with a TTL the first time it's seen. If
253        // it already exists, `SET ... NX` is a no-op — this only races once,
254        // at creation, and `SET NX` is itself atomic on the Redis server.
255        self.conn.cmd(&[b"SET", &redis_key, b"0", b"EX", window.as_bytes(), b"NX"])?;
256        let count = match self.conn.cmd(&[b"INCR", &redis_key])? {
257            RespReply::Int(n) => n,
258            _ => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected INCR reply")),
259        };
260        Ok(count as u32 <= self.max())
261    }
262
263    /// Number of remaining requests `key` may make within the current window,
264    /// or `Err` if the Redis server could not be reached.
265    pub fn remaining(&self, key: &str) -> std::io::Result<u32> {
266        match self.conn.cmd(&[b"GET", &Self::redis_key(key)])? {
267            RespReply::Bulk(Some(bytes)) => {
268                let count: u32 = String::from_utf8_lossy(&bytes).parse().unwrap_or(0);
269                Ok(self.max().saturating_sub(count))
270            }
271            _ => Ok(self.max()),
272        }
273    }
274
275    /// Remove all tracked state for `key`. Useful in tests.
276    pub fn reset(&self, key: &str) -> std::io::Result<()> {
277        self.conn.cmd(&[b"DEL", &Self::redis_key(key)])?;
278        Ok(())
279    }
280}
281
282static GLOBAL_LIMITER: OnceLock<RateLimiter> = OnceLock::new();
283
284/// `true` when the operator set a global rate limit
285/// (`RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS`, e.g. `[rate_limit] max_requests` in
286/// `rws.config.toml`) to a positive number. The `rws` binary enforces
287/// [`global()`] only then — an unset limit means "no global limit", not
288/// "the 1000/60s default".
289pub fn is_configured() -> bool {
290    std::env::var("RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS")
291        .ok()
292        .and_then(|v| v.trim().parse::<u32>().ok())
293        .map_or(false, |max| max > 0)
294}
295
296/// Return the process-wide rate limiter, initialized from environment variables.
297///
298/// | Variable | Default | Meaning |
299/// |---|---|---|
300/// | `RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS` | `1000` | Requests allowed per window |
301/// | `RWS_CONFIG_RATE_LIMIT_WINDOW_SECS` | `60` | Window length in seconds |
302///
303/// Returns `None` when rate limiting is disabled (`RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS=0`).
304pub fn global() -> &'static RateLimiter {
305    GLOBAL_LIMITER.get_or_init(|| {
306        let max: u32 = std::env::var("RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS")
307            .ok()
308            .and_then(|v| v.parse().ok())
309            .unwrap_or(1000);
310        let window: u64 = std::env::var("RWS_CONFIG_RATE_LIMIT_WINDOW_SECS")
311            .ok()
312            .and_then(|v| v.parse().ok())
313            .unwrap_or(60);
314        RateLimiter::new(max, window)
315    })
316}