Skip to main content

headless_lms_server/domain/
rate_limit_middleware_builder.rs

1use crate::config::server_runtime_config;
2use crate::domain::authentication::{constant_time_eq_str, session_user_id};
3use actix_session::SessionExt;
4use actix_web::{
5    Error, HttpMessage, HttpResponse,
6    body::{EitherBody, MessageBody},
7    dev::{Service, ServiceRequest, ServiceResponse, Transform},
8    http::{StatusCode, header},
9};
10use futures_util::future::{LocalBoxFuture, Ready, ready};
11use governor::{
12    Quota, RateLimiter,
13    clock::{Clock, DefaultClock},
14    state::keyed::DefaultKeyedStateStore,
15};
16use headless_lms_models::library::oauth::bearer::TokenSubject;
17use secrecy::ExposeSecret;
18#[cfg(test)]
19use secrecy::SecretString;
20use std::{
21    num::NonZeroU32,
22    sync::{
23        Arc,
24        atomic::{AtomicU64, Ordering},
25    },
26    task::{Context, Poll},
27    time::Duration,
28};
29
30#[derive(Clone, Debug, Default)]
31pub struct RateLimitConfig {
32    pub per_second: Option<u64>,
33    pub per_minute: Option<u64>,
34    pub per_hour: Option<u64>,
35    pub per_day: Option<u64>,
36    pub per_month: Option<u64>,
37}
38
39/// Whose requests share one quota.
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum RateLimitKey {
42    #[default]
43    ClientIp,
44    /// The user of the request's access token, else the signed-in user, falling back to the client
45    /// IP for anonymous requests. For endpoints that act on the caller's own data, where users
46    /// behind one NAT address must not share a quota.
47    User,
48    /// The grant of the request's access token. Must sit inside the middleware that authenticates
49    /// the token and puts its [`TokenSubject`] in the request extensions; falls back to the client
50    /// IP without one.
51    Grant,
52}
53
54/// Request header whose value, when equal to `RATELIMIT_PROTECTION_SAFE_API_KEY`, skips a
55/// [`RateLimit::bypassable`] limiter. tmc-server sends it, and we send it to tmc-server.
56pub const RATELIMIT_PROTECTION_SAFE_API_KEY_HEADER: &str = "RATELIMIT-PROTECTION-SAFE-API-KEY";
57
58/// Which key, if any, lets a request skip the limiter.
59#[derive(Clone)]
60enum Bypass {
61    Off,
62    RuntimeConfigKey,
63    #[cfg(test)]
64    Key(SecretString),
65}
66
67type Key = String;
68type Limiter = RateLimiter<Key, DefaultKeyedStateStore<Key>, DefaultClock>;
69
70#[derive(Clone)]
71struct EndpointLimiters {
72    month: Option<Arc<Limiter>>,
73    day: Option<Arc<Limiter>>,
74    hour: Option<Arc<Limiter>>,
75    minute: Option<Arc<Limiter>>,
76    second: Option<Arc<Limiter>>,
77}
78
79impl EndpointLimiters {
80    fn from_config(cfg: &RateLimitConfig) -> Self {
81        Self {
82            month: cfg.per_month.and_then(|n| {
83                build_custom_period_limiter(n, Duration::from_secs(30 * 24 * 60 * 60))
84            }),
85            day: cfg
86                .per_day
87                .and_then(|n| build_custom_period_limiter(n, Duration::from_secs(24 * 60 * 60))),
88            hour: cfg.per_hour.and_then(|n| build_limiter(n, Quota::per_hour)),
89            minute: cfg
90                .per_minute
91                .and_then(|n| build_limiter(n, Quota::per_minute)),
92            second: cfg
93                .per_second
94                .and_then(|n| build_limiter(n, Quota::per_second)),
95        }
96    }
97
98    fn iter(&self) -> impl Iterator<Item = &Arc<Limiter>> {
99        self.month
100            .iter()
101            .chain(self.day.iter())
102            .chain(self.hour.iter())
103            .chain(self.minute.iter())
104            .chain(self.second.iter())
105    }
106
107    fn is_empty(&self) -> bool {
108        self.second.is_none()
109            && self.minute.is_none()
110            && self.hour.is_none()
111            && self.day.is_none()
112            && self.month.is_none()
113    }
114}
115
116fn build_limiter<F>(n: u64, quota_fn: F) -> Option<Arc<Limiter>>
117where
118    F: FnOnce(NonZeroU32) -> Quota,
119{
120    let n32 = NonZeroU32::new(u32::try_from(n).ok()?)?;
121    Some(Arc::new(RateLimiter::keyed(quota_fn(n32))))
122}
123
124fn build_custom_period_limiter(n: u64, period: Duration) -> Option<Arc<Limiter>> {
125    let n32 = NonZeroU32::new(u32::try_from(n).ok()?)?;
126    let quota = Quota::with_period(period)?.allow_burst(n32);
127    Some(Arc::new(RateLimiter::keyed(quota)))
128}
129
130#[derive(Clone)]
131pub struct RateLimit {
132    limiters: Arc<EndpointLimiters>,
133    key: RateLimitKey,
134    bypass: Bypass,
135    calls: Arc<AtomicU64>,
136}
137
138impl RateLimit {
139    /// Global `/api/v0` limits aligned with nginx ingress `limit-rps` and `limit-rpm`; relaxed when `TEST_MODE` is set.
140    pub fn global_api_rate_limit_config(test_mode: bool) -> RateLimitConfig {
141        if test_mode {
142            RateLimitConfig {
143                per_second: Some(10000),
144                per_minute: Some(200000),
145                ..Default::default()
146            }
147        } else {
148            RateLimitConfig {
149                per_second: Some(20),
150                per_minute: Some(1000),
151                per_hour: Some(10000),
152                ..Default::default()
153            }
154        }
155    }
156
157    pub fn new(cfg: RateLimitConfig) -> Self {
158        Self {
159            limiters: Arc::new(EndpointLimiters::from_config(&cfg)),
160            key: RateLimitKey::default(),
161            bypass: Bypass::Off,
162            calls: Arc::new(AtomicU64::new(0)),
163        }
164    }
165
166    /// Shares each quota by `key` instead of by client IP.
167    pub fn keyed_by(self, key: RateLimitKey) -> Self {
168        Self { key, ..self }
169    }
170
171    /// Lets requests carrying the configured `RATELIMIT_PROTECTION_SAFE_API_KEY` in
172    /// [`RATELIMIT_PROTECTION_SAFE_API_KEY_HEADER`] skip this limiter, for trusted servers that
173    /// call on behalf of many users from one IP. Never use it on brute-force limiters.
174    pub fn bypassable(self) -> Self {
175        Self {
176            bypass: Bypass::RuntimeConfigKey,
177            ..self
178        }
179    }
180
181    #[cfg(test)]
182    fn bypassable_with(self, safe_api_key: SecretString) -> Self {
183        Self {
184            bypass: Bypass::Key(safe_api_key),
185            ..self
186        }
187    }
188}
189
190impl<S, B> Transform<S, ServiceRequest> for RateLimit
191where
192    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
193    B: MessageBody + 'static,
194{
195    type Response = ServiceResponse<EitherBody<B>>;
196    type Error = Error;
197    type InitError = ();
198    type Transform = RateLimitInner<S>;
199    type Future = Ready<Result<Self::Transform, Self::InitError>>;
200
201    fn new_transform(&self, service: S) -> Self::Future {
202        ready(Ok(RateLimitInner {
203            service,
204            limiters: self.limiters.clone(),
205            key: self.key,
206            bypass: self.bypass.clone(),
207            calls: self.calls.clone(),
208        }))
209    }
210}
211
212pub struct RateLimitInner<S> {
213    service: S,
214    limiters: Arc<EndpointLimiters>,
215    key: RateLimitKey,
216    bypass: Bypass,
217    calls: Arc<AtomicU64>,
218}
219
220impl<S> RateLimitInner<S> {
221    fn carries_bypass_key(&self, req: &ServiceRequest) -> bool {
222        let Some(provided) = req
223            .headers()
224            .get(RATELIMIT_PROTECTION_SAFE_API_KEY_HEADER)
225            .and_then(|value| value.to_str().ok())
226        else {
227            return false;
228        };
229        let expected = match &self.bypass {
230            Bypass::Off => return false,
231            Bypass::RuntimeConfigKey => server_runtime_config()
232                .ratelimit_protection_safe_api_key
233                .expose_secret(),
234            #[cfg(test)]
235            Bypass::Key(key) => key.expose_secret(),
236        };
237        !expected.is_empty() && constant_time_eq_str(provided, expected)
238    }
239}
240
241impl<S, B> Service<ServiceRequest> for RateLimitInner<S>
242where
243    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
244    B: MessageBody + 'static,
245{
246    type Response = ServiceResponse<EitherBody<B>>;
247    type Error = Error;
248    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
249
250    fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
251        self.service.poll_ready(cx)
252    }
253
254    fn call(&self, req: ServiceRequest) -> Self::Future {
255        if self.limiters.is_empty() || self.carries_bypass_key(&req) {
256            let fut = self.service.call(req);
257            return Box::pin(async move { fut.await.map(|r| r.map_into_left_body()) });
258        }
259
260        const RETAIN_EVERY: u64 = 1024;
261        const SHRINK_EVERY: u64 = 65_536;
262
263        let n = self.calls.fetch_add(1, Ordering::Relaxed) + 1;
264        if n.is_multiple_of(RETAIN_EVERY) {
265            for limiter in self.limiters.iter() {
266                limiter.retain_recent();
267            }
268            if n.is_multiple_of(SHRINK_EVERY) {
269                for limiter in self.limiters.iter() {
270                    limiter.shrink_to_fit();
271                }
272            }
273        }
274
275        let clock = DefaultClock::default();
276        let token_subject = req.extensions().get::<TokenSubject>().copied();
277        let key = match self.key {
278            RateLimitKey::User => token_subject
279                .map(|subject| subject.user_id)
280                .or_else(|| session_user_id(&req.get_session()))
281                .map(|id| format!("user:{id}"))
282                .unwrap_or_else(|| extract_client_ip_key(&req)),
283            RateLimitKey::Grant => token_subject
284                .map(|subject| format!("grant:{}", subject.grant_id))
285                .unwrap_or_else(|| extract_client_ip_key(&req)),
286            RateLimitKey::ClientIp => extract_client_ip_key(&req),
287        };
288
289        let mut retry_after: Option<Duration> = None;
290        for limiter in self.limiters.iter() {
291            if let Err(negative) = limiter.check_key(&key) {
292                let wait = negative.wait_time_from(clock.now());
293                retry_after = Some(retry_after.map_or(wait, |cur| cur.max(wait)));
294            }
295        }
296
297        if let Some(wait) = retry_after {
298            let secs = wait.as_secs().max(1);
299            let resp = HttpResponse::build(StatusCode::TOO_MANY_REQUESTS)
300                .insert_header((header::RETRY_AFTER, secs.to_string()))
301                .content_type("application/json")
302                .body(r#"{"type":"rate_limit","message_key":"rate_limited","message":"Too many requests. Please try again later."}"#.to_string());
303            return Box::pin(async move { Ok(req.into_response(resp).map_into_right_body()) });
304        }
305
306        let fut = self.service.call(req);
307        Box::pin(async move { fut.await.map(|r| r.map_into_left_body()) })
308    }
309}
310
311fn extract_client_ip_key(req: &ServiceRequest) -> String {
312    if let Some(s) = req.connection_info().realip_remote_addr() {
313        let s = s.trim();
314        if !s.is_empty() {
315            return s.to_string();
316        }
317    }
318
319    if let Some(sa) = req.peer_addr() {
320        return sa.ip().to_string();
321    }
322
323    format!("unknown:{}|{}", req.connection_info().host(), req.path())
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use actix_http::Request;
330    use actix_web::{
331        App, HttpResponse,
332        body::{BoxBody, EitherBody},
333        dev::{Service, ServiceResponse},
334        http::header,
335        test, web,
336    };
337    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
338
339    fn mw(
340        per_minute: Option<u64>,
341        per_hour: Option<u64>,
342        per_day: Option<u64>,
343        per_month: Option<u64>,
344    ) -> RateLimit {
345        RateLimit::new(RateLimitConfig {
346            per_minute,
347            per_hour,
348            per_day,
349            per_month,
350            ..Default::default()
351        })
352    }
353
354    #[actix_web::test]
355    async fn global_api_rate_limit_config_uses_test_mode_argument() {
356        let test_cfg = RateLimit::global_api_rate_limit_config(true);
357        assert_eq!(test_cfg.per_second, Some(10000));
358        assert_eq!(test_cfg.per_minute, Some(200000));
359        assert_eq!(test_cfg.per_hour, None);
360
361        let production_cfg = RateLimit::global_api_rate_limit_config(false);
362        assert_eq!(production_cfg.per_second, Some(20));
363        assert_eq!(production_cfg.per_minute, Some(1000));
364        assert_eq!(production_cfg.per_hour, Some(10000));
365    }
366
367    async fn call_get<S>(
368        app: &S,
369        uri: &str,
370        xff: Option<&str>,
371        peer: Option<SocketAddr>,
372    ) -> ServiceResponse<EitherBody<BoxBody>>
373    where
374        S: Service<Request, Response = ServiceResponse<EitherBody<BoxBody>>, Error = Error>,
375    {
376        let mut tr = test::TestRequest::get().uri(uri);
377        if let Some(v) = xff {
378            tr = tr.insert_header(("x-forwarded-for", v));
379        }
380        if let Some(p) = peer {
381            tr = tr.peer_addr(p);
382        }
383        test::call_service(app, tr.to_request()).await
384    }
385
386    fn retry_after_secs(resp: &ServiceResponse<EitherBody<BoxBody>>) -> Option<u64> {
387        resp.headers()
388            .get(header::RETRY_AFTER)
389            .and_then(|v| v.to_str().ok())
390            .and_then(|s| s.parse::<u64>().ok())
391    }
392
393    async fn app(
394        mw: RateLimit,
395    ) -> impl Service<Request, Response = ServiceResponse<EitherBody<BoxBody>>, Error = Error> {
396        test::init_service(
397            App::new()
398                .wrap(mw)
399                .route("/", web::get().to(|| async { HttpResponse::Ok().finish() }))
400                .route(
401                    "/other",
402                    web::get().to(|| async { HttpResponse::Ok().finish() }),
403                ),
404        )
405        .await
406    }
407
408    #[actix_web::test]
409    async fn key_trims_realip_value() {
410        let req = test::TestRequest::get()
411            .uri("/x")
412            .insert_header(("x-forwarded-for", " 9.9.9.9 "))
413            .to_srv_request();
414
415        assert_eq!(super::extract_client_ip_key(&req), "9.9.9.9");
416    }
417
418    #[actix_web::test]
419    async fn key_empty_realip_falls_back_to_peer_ip() {
420        let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 5555);
421        let req = test::TestRequest::get()
422            .uri("/x")
423            .insert_header(("x-forwarded-for", "     "))
424            .peer_addr(peer)
425            .to_srv_request();
426
427        assert_eq!(super::extract_client_ip_key(&req), "1.2.3.4");
428    }
429
430    #[actix_web::test]
431    async fn key_no_realip_uses_peer_ip() {
432        let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 9)), 1234);
433        let req = test::TestRequest::get()
434            .uri("/x")
435            .peer_addr(peer)
436            .to_srv_request();
437        assert_eq!(super::extract_client_ip_key(&req), "10.0.0.9");
438    }
439
440    #[actix_web::test]
441    async fn key_no_realip_and_no_peer_uses_unknown_host_and_path() {
442        let req = test::TestRequest::get().uri("/path123").to_srv_request();
443        let k = super::extract_client_ip_key(&req);
444        assert!(k.starts_with("unknown:"), "key={k}");
445        assert!(k.contains("|/path123"), "key={k}");
446    }
447
448    #[actix_web::test]
449    async fn passthrough_when_no_limits() {
450        let app = app(mw(None, None, None, None)).await;
451
452        let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
453        let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
454
455        assert_eq!(r1.status(), StatusCode::OK);
456        assert_eq!(r2.status(), StatusCode::OK);
457        assert!(r2.headers().get(header::RETRY_AFTER).is_none());
458    }
459
460    #[actix_web::test]
461    async fn per_minute_zero_disables_window() {
462        let app = app(mw(Some(0), None, None, None)).await;
463
464        let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
465        let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
466
467        assert_eq!(r1.status(), StatusCode::OK);
468        assert_eq!(r2.status(), StatusCode::OK);
469    }
470
471    #[actix_web::test]
472    async fn per_minute_over_u32_disables_window() {
473        let app = app(mw(Some(u64::from(u32::MAX) + 1), None, None, None)).await;
474
475        let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
476        let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
477
478        assert_eq!(r1.status(), StatusCode::OK);
479        assert_eq!(r2.status(), StatusCode::OK);
480    }
481
482    #[actix_web::test]
483    async fn blocks_second_request_same_key() {
484        let app = app(mw(Some(1), None, None, None)).await;
485
486        let ok = call_get(&app, "/", Some("1.2.3.4"), None).await;
487        assert_eq!(ok.status(), StatusCode::OK);
488
489        let blocked = call_get(&app, "/", Some("1.2.3.4"), None).await;
490        assert_eq!(blocked.status(), StatusCode::TOO_MANY_REQUESTS);
491
492        let ra = retry_after_secs(&blocked).expect("missing Retry-After");
493        assert!(ra >= 1);
494    }
495
496    #[actix_web::test]
497    async fn retry_after_is_integer_seconds_and_body_is_json() {
498        let app = app(mw(Some(1), None, None, None)).await;
499
500        let _ = call_get(&app, "/", Some("1.2.3.4"), None).await;
501        let blocked = call_get(&app, "/", Some("1.2.3.4"), None).await;
502
503        assert_eq!(blocked.status(), StatusCode::TOO_MANY_REQUESTS);
504
505        let ra_hdr = blocked.headers().get(header::RETRY_AFTER).unwrap();
506        let ra_str = ra_hdr.to_str().unwrap();
507        assert!(
508            ra_str.parse::<u64>().is_ok(),
509            "Retry-After not int: {ra_str}"
510        );
511
512        let bytes = test::read_body(blocked).await;
513        let body = std::str::from_utf8(&bytes).unwrap();
514        assert!(body.contains(r#""type":"rate_limit""#), "body={body}");
515        let v: serde_json::Value = serde_json::from_str(body).unwrap();
516        assert_eq!(v["type"], "rate_limit");
517        assert_eq!(v["message_key"], "rate_limited");
518        assert!(v.get("retry_after").is_none());
519    }
520
521    #[actix_web::test]
522    async fn different_keys_independent() {
523        let app = app(mw(Some(1), None, None, None)).await;
524
525        let a1 = call_get(&app, "/", Some("10.0.0.1"), None).await;
526        let b1 = call_get(&app, "/", Some("10.0.0.2"), None).await;
527        assert_eq!(a1.status(), StatusCode::OK);
528        assert_eq!(b1.status(), StatusCode::OK);
529
530        let a2 = call_get(&app, "/", Some("10.0.0.1"), None).await;
531        assert_eq!(a2.status(), StatusCode::TOO_MANY_REQUESTS);
532    }
533
534    #[actix_web::test]
535    async fn same_key_shared_across_routes_in_same_app() {
536        let app = app(mw(Some(1), None, None, None)).await;
537
538        let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
539        assert_eq!(r1.status(), StatusCode::OK);
540
541        let r2 = call_get(&app, "/other", Some("1.2.3.4"), None).await;
542        assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
543    }
544
545    #[actix_web::test]
546    async fn max_retry_after_prefers_longer_window() {
547        let app = app(mw(Some(1), Some(1), None, None)).await;
548
549        let r1 = call_get(&app, "/", Some("1.2.3.4"), None).await;
550        assert_eq!(r1.status(), StatusCode::OK);
551
552        let r2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
553        assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
554
555        let ra = retry_after_secs(&r2).expect("missing Retry-After");
556        // hour window should dominate minute window; be tolerant but meaningful
557        assert!(ra >= 120, "expected hour-dominated Retry-After, got {ra}");
558    }
559
560    #[actix_web::test]
561    async fn peer_addr_used_when_no_forwarded_headers() {
562        let app = app(mw(Some(1), None, None, None)).await;
563
564        let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), 9999);
565        let r1 = call_get(&app, "/", None, Some(peer)).await;
566        let r2 = call_get(&app, "/", None, Some(peer)).await;
567
568        assert_eq!(r1.status(), StatusCode::OK);
569        assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
570    }
571
572    #[actix_web::test]
573    async fn empty_forwarded_header_does_not_create_empty_key_bucket() {
574        let app = app(mw(Some(1), None, None, None)).await;
575
576        let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 1111);
577        let r1 = call_get(&app, "/", Some("   "), Some(peer)).await;
578        let r2 = call_get(&app, "/", Some("   "), Some(peer)).await;
579
580        assert_eq!(r1.status(), StatusCode::OK);
581        assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
582    }
583
584    #[actix_web::test]
585    async fn unknown_bucket_includes_path_to_reduce_collisions() {
586        let app = app(mw(Some(1), None, None, None)).await;
587
588        let r1 = call_get(&app, "/", None, None).await;
589        let r2 = call_get(&app, "/other", None, None).await;
590
591        assert_eq!(r1.status(), StatusCode::OK);
592        assert_eq!(r2.status(), StatusCode::OK);
593
594        let r1b = call_get(&app, "/", None, None).await;
595        assert_eq!(r1b.status(), StatusCode::TOO_MANY_REQUESTS);
596    }
597
598    #[actix_web::test]
599    async fn housekeeping_retain_recent_path_executes() {
600        // Trigger retain_recent() at 1024 calls; keep limit huge to avoid 429.
601        let app = app(mw(Some(1_000_000), None, None, None)).await;
602
603        for i in 0..=1024 {
604            let ip = format!("192.0.2.{}", (i % 250) + 1);
605            let resp = call_get(&app, "/", Some(&ip), None).await;
606            assert_eq!(resp.status(), StatusCode::OK, "i={i} ip={ip}");
607        }
608    }
609
610    #[actix_web::test]
611    async fn keys_are_exact_strings_no_normalization_means_ports_are_distinct() {
612        let app = app(mw(Some(1), None, None, None)).await;
613
614        let a = call_get(&app, "/", Some("1.2.3.4"), None).await;
615        let b = call_get(&app, "/", Some("1.2.3.4:12345"), None).await;
616
617        assert_eq!(a.status(), StatusCode::OK);
618        assert_eq!(b.status(), StatusCode::OK);
619
620        let a2 = call_get(&app, "/", Some("1.2.3.4"), None).await;
621        let b2 = call_get(&app, "/", Some("1.2.3.4:12345"), None).await;
622
623        assert_eq!(a2.status(), StatusCode::TOO_MANY_REQUESTS);
624        assert_eq!(b2.status(), StatusCode::TOO_MANY_REQUESTS);
625    }
626
627    async fn call_get_with_safe_api_key<S>(
628        app: &S,
629        safe_api_key: &str,
630    ) -> ServiceResponse<EitherBody<BoxBody>>
631    where
632        S: Service<Request, Response = ServiceResponse<EitherBody<BoxBody>>, Error = Error>,
633    {
634        let req = test::TestRequest::get()
635            .uri("/")
636            .insert_header(("x-forwarded-for", "1.2.3.4"))
637            .insert_header((RATELIMIT_PROTECTION_SAFE_API_KEY_HEADER, safe_api_key))
638            .to_request();
639        test::call_service(app, req).await
640    }
641
642    #[actix_web::test]
643    async fn bypassable_limiter_skips_requests_with_safe_api_key() {
644        let app = app(mw(Some(1), None, None, None).bypassable_with("safe-key".into())).await;
645
646        for _ in 0..3 {
647            let resp = call_get_with_safe_api_key(&app, "safe-key").await;
648            assert_eq!(resp.status(), StatusCode::OK);
649        }
650    }
651
652    #[actix_web::test]
653    async fn bypassable_limiter_limits_wrong_safe_api_key() {
654        let app = app(mw(Some(1), None, None, None).bypassable_with("safe-key".into())).await;
655
656        let r1 = call_get_with_safe_api_key(&app, "wrong-key").await;
657        let r2 = call_get_with_safe_api_key(&app, "wrong-key").await;
658
659        assert_eq!(r1.status(), StatusCode::OK);
660        assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
661    }
662
663    #[actix_web::test]
664    async fn non_bypassable_limiter_limits_safe_api_key() {
665        let app = app(mw(Some(1), None, None, None)).await;
666
667        let r1 = call_get_with_safe_api_key(&app, "safe-key").await;
668        let r2 = call_get_with_safe_api_key(&app, "safe-key").await;
669
670        assert_eq!(r1.status(), StatusCode::OK);
671        assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
672    }
673
674    #[actix_web::test]
675    async fn empty_configured_safe_api_key_never_bypasses() {
676        let app = app(mw(Some(1), None, None, None).bypassable_with("".into())).await;
677
678        let r1 = call_get_with_safe_api_key(&app, "").await;
679        let r2 = call_get_with_safe_api_key(&app, "").await;
680
681        assert_eq!(r1.status(), StatusCode::OK);
682        assert_eq!(r2.status(), StatusCode::TOO_MANY_REQUESTS);
683    }
684}