1use super::helpers::middleware_gateway_error_response;
4use super::rate_limit_key_policy::effective_requests_per_minute;
5use crate::core::rate_limiter::{RateLimitReservation, get_global_rate_limiter};
6use crate::core::types::context::{RequestContext, SharedRequestContext};
7use crate::server::state::AppState;
8use crate::utils::error::gateway_error::GatewayError;
9use actix_web::body::EitherBody;
10use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready};
11use actix_web::http::{StatusCode, header::HeaderMap};
12use actix_web::web;
13use actix_web::{HttpMessage, HttpRequest, HttpResponse, ResponseError};
14use dashmap::DashMap;
15use futures::future::{Ready, ready};
16use std::fmt;
17use std::future::Future;
18use std::net::{IpAddr, SocketAddr};
19use std::pin::Pin;
20use std::rc::Rc;
21use std::sync::Arc;
22use std::sync::OnceLock;
23use std::time::{Duration, Instant};
24use tracing::{debug, info, warn};
25
26const MAX_FALLBACK_ENTRIES: usize = 10_000;
34
35static GATEWAY_FALLBACK_STORE: OnceLock<Arc<DashMap<String, KeyTracker>>> = OnceLock::new();
36
37const GLOBAL_LIMITER_SOURCE: &str = "global";
38const FALLBACK_LIMITER_SOURCE: &str = "fallback";
39
40struct KeyTracker {
42 timestamps: Vec<Instant>,
43}
44
45impl KeyTracker {
46 fn new() -> Self {
47 Self {
48 timestamps: Vec::new(),
49 }
50 }
51
52 fn release(&mut self, recorded_at: Instant) -> bool {
53 let Some(position) = self.timestamps.iter().position(|&ts| ts == recorded_at) else {
54 return false;
55 };
56
57 self.timestamps.remove(position);
58 true
59 }
60
61 fn check_and_record(&mut self, limit: u32, window: Duration) -> (bool, u64, Option<Instant>) {
63 let now = Instant::now();
64 self.timestamps
65 .retain(|&ts| now.duration_since(ts) < window);
66
67 let count = self.timestamps.len() as u32;
68 if count >= limit {
69 let retry_after = self
70 .timestamps
71 .first()
72 .map(|&ts| {
73 let age = now.duration_since(ts);
74 window.saturating_sub(age).as_secs().max(1)
75 })
76 .unwrap_or(window.as_secs());
77 return (false, retry_after, None);
78 }
79
80 self.timestamps.push(now);
81 (true, 0, Some(now))
82 }
83}
84
85struct RateLimitPass {
86 source: &'static str,
87 limit: u32,
88 remaining: u32,
89 reservation: RecordedRateLimitReservation,
90}
91
92struct RateLimitRejection {
93 source: &'static str,
94 retry_after: u64,
95 limit: u32,
96}
97
98enum RateLimitReservationSource {
99 Global(RateLimitReservation),
100 Fallback {
101 store: Arc<DashMap<String, KeyTracker>>,
102 recorded_at: Instant,
103 },
104 Noop,
105}
106
107enum RecordedRateLimitReservation {
108 Global(RateLimitReservation),
109 Fallback(Instant),
110}
111
112pub(super) struct AuthRateLimitReservation {
113 key: String,
114 source: RateLimitReservationSource,
115}
116
117impl AuthRateLimitReservation {
118 pub(super) fn noop() -> Self {
119 Self {
120 key: String::new(),
121 source: RateLimitReservationSource::Noop,
122 }
123 }
124
125 pub(super) async fn release(self) {
126 match self.source {
127 RateLimitReservationSource::Global(record_source) => {
128 if let Some(global_limiter) = get_global_rate_limiter() {
129 global_limiter
130 .release_recorded(&self.key, record_source)
131 .await;
132 }
133 }
134 RateLimitReservationSource::Fallback { store, recorded_at } => {
135 let _removed_entry = store.remove_if_mut(&self.key, |_, tracker| {
136 tracker.release(recorded_at);
137 tracker.timestamps.is_empty()
138 });
139 }
140 RateLimitReservationSource::Noop => {}
141 }
142 }
143}
144
145fn enforce_fallback_capacity(store: &DashMap<String, KeyTracker>, window: Duration) {
152 let now = Instant::now();
153 store.retain(|_, tracker| {
154 tracker
155 .timestamps
156 .last()
157 .is_some_and(|ts| now.duration_since(*ts) < window)
158 });
159
160 let overflow = store.len().saturating_sub(MAX_FALLBACK_ENTRIES);
161 if overflow == 0 {
162 return;
163 }
164
165 let mut candidates: Vec<(Option<Instant>, String)> = store
166 .iter()
167 .map(|e| (e.value().timestamps.last().copied(), e.key().clone()))
168 .collect();
169 candidates.sort_by_key(|(ts, _)| *ts);
171 for (_, key) in candidates.into_iter().take(overflow) {
172 store.remove(&key);
173 }
174}
175
176async fn check_rate_limit_key(
177 key: &str,
178 requests_per_minute: u32,
179 fallback_store: &DashMap<String, KeyTracker>,
180) -> Result<RateLimitPass, RateLimitRejection> {
181 if let Some(global_limiter) = get_global_rate_limiter() {
182 let (result, reservation) = global_limiter
183 .check_and_record_with_source_and_limit(key, requests_per_minute)
184 .await;
185
186 if !result.allowed {
187 return Err(RateLimitRejection {
188 source: GLOBAL_LIMITER_SOURCE,
189 retry_after: result.retry_after_secs.unwrap_or(60),
190 limit: requests_per_minute,
191 });
192 }
193
194 return Ok(RateLimitPass {
195 source: GLOBAL_LIMITER_SOURCE,
196 limit: requests_per_minute,
197 remaining: result.remaining,
198 reservation: RecordedRateLimitReservation::Global(reservation),
199 });
200 }
201
202 let window = Duration::from_secs(60);
203 let (allowed, retry_after, recorded_at) = {
204 let mut tracker = fallback_store
205 .entry(key.to_string())
206 .or_insert_with(KeyTracker::new);
207 tracker.check_and_record(requests_per_minute, window)
208 };
209 if fallback_store.len() > MAX_FALLBACK_ENTRIES {
210 enforce_fallback_capacity(fallback_store, window);
211 }
212
213 if !allowed {
214 return Err(RateLimitRejection {
215 source: FALLBACK_LIMITER_SOURCE,
216 retry_after,
217 limit: requests_per_minute,
218 });
219 }
220
221 let Some(recorded_at) = recorded_at else {
222 return Err(RateLimitRejection {
223 source: FALLBACK_LIMITER_SOURCE,
224 retry_after: window.as_secs().max(1),
225 limit: requests_per_minute,
226 });
227 };
228
229 let remaining = fallback_store
230 .get(key)
231 .map(|tracker| requests_per_minute.saturating_sub(tracker.timestamps.len() as u32))
232 .unwrap_or(requests_per_minute);
233
234 Ok(RateLimitPass {
235 source: FALLBACK_LIMITER_SOURCE,
236 limit: requests_per_minute,
237 remaining,
238 reservation: RecordedRateLimitReservation::Fallback(recorded_at),
239 })
240}
241
242fn gateway_fallback_store() -> Arc<DashMap<String, KeyTracker>> {
243 GATEWAY_FALLBACK_STORE
244 .get_or_init(|| Arc::new(DashMap::new()))
245 .clone()
246}
247
248#[cfg(feature = "websockets")]
250pub(crate) async fn enforce_socket_request_rate(
251 context: &RequestContext,
252 requests_per_minute: u32,
253) -> Result<(), u64> {
254 let key = client_key_from_context(context).ok_or(60u64)?;
255 check_rate_limit_key(&key, requests_per_minute, &gateway_fallback_store())
256 .await
257 .map(|_| ())
258 .map_err(|rejection| rejection.retry_after)
259}
260
261pub(super) async fn reserve_rate_limit_for_auth_attempt(
262 req: &ServiceRequest,
263 requests_per_minute: u32,
264 trusted_proxies: &[String],
265) -> Result<AuthRateLimitReservation, RateLimitError> {
266 let key = extract_client_key(req, trusted_proxies);
267 let fallback_store = gateway_fallback_store();
268
269 match check_rate_limit_key(&key, requests_per_minute, &fallback_store).await {
270 Ok(pass) => {
271 debug!(
272 client = %key,
273 limit = pass.limit,
274 remaining = pass.remaining,
275 "Rate limit reservation passed for auth attempt ({} limiter)",
276 pass.source
277 );
278 let source = match pass.reservation {
279 RecordedRateLimitReservation::Global(reservation) => {
280 RateLimitReservationSource::Global(reservation)
281 }
282 RecordedRateLimitReservation::Fallback(recorded_at) => {
283 RateLimitReservationSource::Fallback {
284 store: fallback_store,
285 recorded_at,
286 }
287 }
288 };
289 Ok(AuthRateLimitReservation { key, source })
290 }
291 Err(rejection) => {
292 warn!(
293 client = %key,
294 "Rate limit exceeded before auth verification ({} limiter): retry after {}s",
295 rejection.source,
296 rejection.retry_after
297 );
298 Err(RateLimitError::new(rejection.retry_after, rejection.limit))
299 }
300 }
301}
302
303pub(super) async fn enforce_rate_limit_for_rejected_auth(
304 req: &ServiceRequest,
305 requests_per_minute: u32,
306 trusted_proxies: &[String],
307) -> Result<(), RateLimitError> {
308 let key = extract_client_key(req, trusted_proxies);
309 let fallback_store = gateway_fallback_store();
310
311 match check_rate_limit_key(&key, requests_per_minute, &fallback_store).await {
312 Ok(pass) => {
313 debug!(
314 client = %key,
315 limit = pass.limit,
316 remaining = pass.remaining,
317 "Rate limit check passed for rejected auth path ({} limiter)",
318 pass.source
319 );
320 Ok(())
321 }
322 Err(rejection) => {
323 warn!(
324 client = %key,
325 "Rate limit exceeded for rejected auth path ({} limiter): retry after {}s",
326 rejection.source,
327 rejection.retry_after
328 );
329 Err(RateLimitError::new(rejection.retry_after, rejection.limit))
330 }
331 }
332}
333
334#[derive(Debug, Clone, Copy)]
336pub(super) struct RateLimitError {
337 retry_after: u64,
338 limit: u32,
339}
340
341impl RateLimitError {
342 fn new(retry_after: u64, limit: u32) -> Self {
343 Self { retry_after, limit }
344 }
345
346 pub(super) fn gateway_error(&self) -> GatewayError {
347 GatewayError::RateLimit {
348 message: "Rate limit exceeded. Please retry after the indicated seconds.".to_string(),
349 retry_after: Some(self.retry_after),
350 rpm_limit: Some(self.limit),
351 tpm_limit: None,
352 }
353 }
354}
355
356impl fmt::Display for RateLimitError {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 write!(f, "Too Many Requests")
359 }
360}
361
362impl ResponseError for RateLimitError {
363 fn status_code(&self) -> StatusCode {
364 StatusCode::TOO_MANY_REQUESTS
365 }
366
367 fn error_response(&self) -> HttpResponse {
368 HttpResponse::TooManyRequests()
369 .insert_header(("Retry-After", self.retry_after.to_string()))
370 .insert_header(("X-RateLimit-Limit", self.limit.to_string()))
371 .json(serde_json::json!({
372 "error": {
373 "message": "Rate limit exceeded. Please retry after the indicated seconds.",
374 "type": "rate_limit_error",
375 "code": 429
376 }
377 }))
378 }
379}
380
381pub struct RateLimitMiddleware {
383 requests_per_minute: Option<u32>,
384}
385
386impl RateLimitMiddleware {
387 pub fn new(requests_per_minute: u32) -> Self {
388 Self {
389 requests_per_minute: Some(requests_per_minute),
390 }
391 }
392
393 pub fn optional(requests_per_minute: Option<u32>) -> Self {
394 Self {
395 requests_per_minute,
396 }
397 }
398}
399
400impl Default for RateLimitMiddleware {
401 fn default() -> Self {
402 Self::new(60)
403 }
404}
405
406impl<S, B> Transform<S, ServiceRequest> for RateLimitMiddleware
407where
408 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
409 S::Future: 'static,
410 B: 'static,
411{
412 type Response = ServiceResponse<EitherBody<B>>;
413 type Error = actix_web::Error;
414 type InitError = ();
415 type Transform = RateLimitMiddlewareService<S>;
416 type Future = Ready<Result<Self::Transform, Self::InitError>>;
417
418 fn new_transform(&self, service: S) -> Self::Future {
419 ready(Ok(RateLimitMiddlewareService {
420 service: Rc::new(service),
421 requests_per_minute: self.requests_per_minute,
422 fallback_store: gateway_fallback_store(),
423 }))
424 }
425}
426
427pub struct RateLimitMiddlewareService<S> {
429 service: Rc<S>,
430 requests_per_minute: Option<u32>,
431 fallback_store: Arc<DashMap<String, KeyTracker>>,
433}
434
435fn parse_ip(value: &str) -> Option<IpAddr> {
440 let value = value.trim();
441 value
442 .parse::<IpAddr>()
443 .ok()
444 .or_else(|| value.parse::<SocketAddr>().ok().map(|addr| addr.ip()))
445 .map(|ip| ip.to_canonical())
446}
447
448pub(crate) fn is_trusted_proxy(ip: IpAddr, trusted_proxies: &[String]) -> bool {
449 trusted_proxies
450 .iter()
451 .filter_map(|proxy| parse_ip(proxy))
452 .any(|proxy| proxy == ip)
453}
454
455fn extract_client_key(req: &ServiceRequest, trusted_proxies: &[String]) -> String {
463 if let Some(identity) = authenticated_client_key(req) {
464 return identity;
465 }
466
467 network_client_key(req, trusted_proxies)
468}
469
470fn authenticated_client_key(req: &ServiceRequest) -> Option<String> {
471 let extensions = req.extensions();
472 if let Some(context) = extensions.get::<SharedRequestContext>() {
473 return client_key_from_context(context.as_ref());
474 }
475
476 let context = extensions.get::<RequestContext>()?;
477 client_key_from_context(context)
478}
479
480fn client_key_from_context(context: &RequestContext) -> Option<String> {
481 if let Some(api_key_id) = context.api_key_id() {
482 return Some(format!("api_key:{}", api_key_id));
483 }
484
485 context
486 .user_id
487 .as_deref()
488 .map(str::trim)
489 .filter(|user_id| !user_id.is_empty())
490 .map(|user_id| format!("user:{}", user_id))
491}
492
493pub(super) fn network_client_key(req: &ServiceRequest, trusted_proxies: &[String]) -> String {
494 network_client_key_from_parts(req.peer_addr(), req.headers(), trusted_proxies)
495 .unwrap_or_else(|| "ip:unknown".to_string())
496}
497
498pub(crate) fn trusted_network_client_key(
499 req: &HttpRequest,
500 trusted_proxies: &[String],
501) -> Option<String> {
502 network_client_key_from_parts(req.peer_addr(), req.headers(), trusted_proxies)
503}
504
505fn network_client_key_from_parts(
506 peer_addr: Option<SocketAddr>,
507 headers: &HeaderMap,
508 trusted_proxies: &[String],
509) -> Option<String> {
510 let peer_ip = peer_addr?.ip().to_canonical();
511
512 if is_trusted_proxy(peer_ip, trusted_proxies)
514 && let Some(client_ip) = last_untrusted_xff_ip(headers, trusted_proxies)
515 {
516 return Some(format!("ip:{}", client_ip));
517 }
518
519 Some(format!("ip:{}", peer_ip))
520}
521
522fn last_untrusted_xff_ip(headers: &HeaderMap, trusted_proxies: &[String]) -> Option<IpAddr> {
532 let mut fallback = None;
533 for field in headers.get_all("X-Forwarded-For").rev() {
534 for entry in field.as_bytes().rsplit(|byte| *byte == b',') {
535 let entry = std::str::from_utf8(entry).ok()?;
536 let ip = parse_ip(entry)?;
537 fallback.get_or_insert(ip);
538 if !is_trusted_proxy(ip, trusted_proxies) {
539 return Some(ip);
540 }
541 }
542 }
543 fallback
544}
545
546impl<S, B> Service<ServiceRequest> for RateLimitMiddlewareService<S>
547where
548 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
549 S::Future: 'static,
550 B: 'static,
551{
552 type Response = ServiceResponse<EitherBody<B>>;
553 type Error = actix_web::Error;
554 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
555
556 forward_ready!(service);
557
558 fn call(&self, req: ServiceRequest) -> Self::Future {
559 let service = Rc::clone(&self.service);
560 let app_state = req.app_data::<web::Data<AppState>>().cloned();
561 let trusted_proxies: Vec<String> = match app_state.as_ref() {
562 Some(state) => {
563 let cfg = state.config.load();
564 cfg.server().trusted_proxies.clone()
565 }
566 None => Vec::new(),
567 };
568 let start_time = Instant::now();
569 let path = req.path().to_string();
570 let method = req.method().to_string();
571
572 let client_key = extract_client_key(&req, &trusted_proxies);
573 let requests_per_minute = effective_requests_per_minute(&req, self.requests_per_minute);
574
575 let fallback_store = self.fallback_store.clone();
576 let key = client_key.clone();
577
578 Box::pin(async move {
579 if let Some(requests_per_minute) = requests_per_minute {
580 let pass =
581 match check_rate_limit_key(&key, requests_per_minute, &fallback_store).await {
582 Ok(pass) => pass,
583 Err(rejection) => {
584 warn!(
585 client = %key,
586 path = %path,
587 "Rate limit exceeded ({} limiter): retry after {}s",
588 rejection.source,
589 rejection.retry_after
590 );
591 let err = RateLimitError::new(rejection.retry_after, rejection.limit);
592 let gateway_error = err.gateway_error();
593 return Ok(middleware_gateway_error_response(
594 req,
595 actix_web::Error::from(err),
596 gateway_error,
597 ));
598 }
599 };
600
601 debug!(
602 client = %key,
603 limit = pass.limit,
604 remaining = pass.remaining,
605 "Rate limit check passed ({} limiter)",
606 pass.source
607 );
608 }
609
610 let res = service.call(req).await?.map_into_left_body();
611 let duration = start_time.elapsed();
612 info!(
613 "{} {} completed in {:?} with status {}",
614 method,
615 path,
616 duration,
617 res.status()
618 );
619 Ok(res)
620 })
621 }
622}
623
624#[cfg(test)]
625#[path = "rate_limit_tests.rs"]
626mod tests;