siderite_core/middleware/
rate_limit.rs1use super::TrustedProxies;
4use super::adapt::{BoxService, Next, from_fn, impl_layer};
5use crate::error::ApiError;
6use crate::extract::Request;
7use crate::response::IntoResponse;
8use http::{HeaderValue, StatusCode, header};
9use std::collections::{HashMap, VecDeque};
10use std::net::IpAddr;
11use std::sync::{Arc, Mutex, PoisonError};
12use std::time::{Duration, Instant};
13use tower::Layer;
14
15const DEFAULT_MAX_CLIENTS: usize = 10_000;
16const EXPIRY_SCAN_BUDGET: usize = 16;
17type Key = Option<IpAddr>;
18type Verdict = Result<(), Duration>;
19
20struct Bucket {
21 tokens: f64,
22 last: Instant,
23}
24
25#[derive(Default)]
26struct Buckets {
27 entries: HashMap<Key, Bucket>,
28 expiry: VecDeque<Key>,
29}
30
31#[derive(Clone)]
39pub struct RateLimit {
40 capacity: f64,
41 refill_per_sec: f64,
42 max_clients: usize,
43 proxies: TrustedProxies,
44 buckets: Arc<Mutex<Buckets>>,
45}
46
47#[derive(Debug, thiserror::Error, PartialEq, Eq)]
49#[error("rate limits require positive capacity and finite positive refill")]
50pub struct RateLimitError;
51
52impl std::fmt::Debug for RateLimit {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("RateLimit")
55 .field("capacity", &self.capacity)
56 .field("refill_per_sec", &self.refill_per_sec)
57 .field("max_clients", &self.max_clients)
58 .field("proxies", &self.proxies)
59 .finish_non_exhaustive()
60 }
61}
62
63impl RateLimit {
64 pub fn new(burst: u32, per_second: f64) -> Self {
73 Self {
74 capacity: f64::from(burst),
75 refill_per_sec: per_second,
76 max_clients: DEFAULT_MAX_CLIENTS,
77 proxies: TrustedProxies::default(),
78 buckets: Arc::default(),
79 }
80 }
81
82 pub fn try_new(burst: u32, rate: f64) -> Result<Self, RateLimitError> {
91 let limiter = Self::new(burst, rate);
92 if limiter.valid() {
93 Ok(limiter)
94 } else {
95 Err(RateLimitError)
96 }
97 }
98
99 #[must_use]
107 pub fn max_clients(mut self, max: usize) -> Self {
108 self.max_clients = max;
109 self.buckets = Arc::default();
110 self
111 }
112
113 #[must_use]
121 pub fn trusted_proxies(mut self, proxies: TrustedProxies) -> Self {
122 self.proxies = proxies;
123 self
124 }
125
126 #[must_use]
134 pub fn trust_forwarded_for(self, trust: bool) -> Self {
135 self.trusted_proxies(if trust {
136 TrustedProxies::dangerously_trust_all()
137 } else {
138 TrustedProxies::default()
139 })
140 }
141
142 pub(super) fn valid(&self) -> bool {
143 self.capacity > 0.0
144 && self.refill_per_sec.is_finite()
145 && self.refill_per_sec > 0.0
146 && self.max_clients > 0
147 }
148
149 fn take(&self, key: Key, now: Instant) -> Verdict {
150 let guard = self.buckets.lock();
151 let mut state = guard.unwrap_or_else(PoisonError::into_inner);
152 if !state.entries.contains_key(&key) {
153 if state.entries.len() >= self.max_clients {
154 self.expire(&mut state, now);
155 }
156 if state.entries.len() >= self.max_clients {
157 return Err(Duration::from_secs(1));
158 }
159 state.expiry.push_back(key);
160 }
161 let bucket = state.entries.entry(key).or_insert(Bucket {
162 tokens: self.capacity,
163 last: now,
164 });
165 let elapsed = now.duration_since(bucket.last).as_secs_f64();
166 let refilled = bucket.tokens + elapsed * self.refill_per_sec;
167 bucket.tokens = refilled.min(self.capacity);
168 bucket.last = now;
169 if bucket.tokens >= 1.0 {
170 bucket.tokens -= 1.0;
171 Ok(())
172 } else {
173 let wait = (1.0 - bucket.tokens) / self.refill_per_sec;
174 let maximum = Duration::from_secs(3600);
175 Err(Duration::try_from_secs_f64(wait).unwrap_or(maximum))
176 }
177 }
178
179 fn expire(&self, state: &mut Buckets, now: Instant) {
180 for _ in 0..state.expiry.len().min(EXPIRY_SCAN_BUDGET) {
181 let Some(key) = state.expiry.pop_front() else {
182 break;
183 };
184 let expired = state.entries.get(&key).is_some_and(|bucket| {
185 let elapsed = now.duration_since(bucket.last).as_secs_f64();
186 bucket.tokens + elapsed * self.refill_per_sec >= self.capacity
187 });
188 if expired {
189 state.entries.remove(&key);
190 } else {
191 state.expiry.push_back(key);
192 }
193 }
194 }
195
196 fn wrap(&self, inner: BoxService) -> BoxService {
197 let this = self.clone();
198 from_fn(move |req: Request, next: Next| {
199 let valid = this.valid();
200 let verdict = if valid {
201 this.proxies
202 .resolve(&req)
203 .map(|context| {
204 let now = Instant::now();
205 this.take(context.client_ip, now)
206 })
207 .map_err(|_| ())
208 } else {
209 Err(())
210 };
211 async move {
212 if !valid {
213 let message = "invalid rate limiter configuration";
214 return ApiError::internal(message).into_response();
215 }
216 match verdict {
217 Err(()) => {
218 let message = "Invalid forwarded request context.";
219 ApiError::bad_request(message).into_response()
220 }
221 Ok(Ok(())) => next.run(req).await,
222 Ok(Err(wait)) => {
223 let status = StatusCode::TOO_MANY_REQUESTS;
224 let message = "Rate limit exceeded.";
225 let error = ApiError::new(status, message);
226 let mut response = error.into_response();
227 let round_up = u64::from(wait.subsec_nanos() > 0);
228 let secs = wait.as_secs().saturating_add(round_up);
229 let seconds = secs.max(1).to_string();
230 let value = HeaderValue::from_str(&seconds);
231 if let Ok(value) = value {
232 let headers = response.headers_mut();
233 headers.insert(header::RETRY_AFTER, value);
234 }
235 response
236 }
237 }
238 }
239 })
240 .layer(inner)
241 }
242}
243
244impl_layer!(RateLimit);
245
246#[cfg(test)]
247#[path = "rate_limit_tests.rs"]
248mod tests;