Skip to main content

edda_server/
limits.rs

1//! Limits on how much one client or one campaign can ask of a service, so
2//! a service open to the internet can't be flooded or filled up: requests
3//! per minute from each client address (fewer for signing in), how big a
4//! request may be, and how much each campaign may keep.
5//!
6//! All on by default. `LIMITS=off` turns every one off, for running on
7//! your own machine, and `LIMIT_<NAME>=<number>` changes one, such as
8//! `LIMIT_CHARACTERS=1000` (the names are the fields below, in capitals).
9//!
10//! Requests are counted per instance of a service, in memory, by client
11//! address. Behind the HTTPS proxy every request comes from the proxy, so
12//! `TRUST_PROXY=1` takes the client's address from the `X-Forwarded-For`
13//! header the proxy adds instead, and counts only requests that came
14//! through it: the services' calls to each other (the combat service's to
15//! the character service, say) aren't limited. Only set it where nothing
16//! but the proxy and the other services can reach the service, or clients
17//! could claim any address, or none.
18
19use std::{
20    collections::HashMap,
21    net::{IpAddr, SocketAddr},
22    sync::{Arc, Mutex},
23    time::{Duration, Instant},
24};
25
26use axum::{
27    Json, Router,
28    extract::{ConnectInfo, DefaultBodyLimit, Request, State},
29    http::{HeaderValue, StatusCode, header},
30    middleware::{self, Next},
31    response::{IntoResponse, Response},
32};
33use edda_auth::protocol::Quotas;
34use serde_json::json;
35
36/// What a service allows; see the module's description.
37#[derive(Debug, Clone)]
38pub struct Limits {
39    enabled: bool,
40    /// Requests per minute from one client address, to any route.
41    pub requests_per_minute: u32,
42    /// Requests per minute from one client address to the routes that
43    /// need no token: logging in, setting up a campaign, joining a pairing.
44    pub sign_ins_per_minute: u32,
45    /// The largest request body accepted.
46    pub body_bytes: usize,
47    /// What one campaign may keep.
48    pub characters: u32,
49    /// Its own monsters, and its own spells and its own conditions (each).
50    pub monsters: u32,
51    pub rules_texts: u32,
52    pub combats: u32,
53    pub plans: u32,
54    pub devices: u32,
55    pub events_per_combat: u32,
56    /// Devices watching one combat at once (per instance).
57    pub connections_per_combat: u32,
58    /// Whether a request's client is the last `X-Forwarded-For` address
59    /// (`TRUST_PROXY=1`); see the module's description.
60    pub trust_proxy: bool,
61}
62
63impl Default for Limits {
64    /// No limits at all: what tests, and a service on your own machine,
65    /// want. A service reads its own with [`Limits::from_env`].
66    fn default() -> Self {
67        Self::off()
68    }
69}
70
71impl Limits {
72    /// No limits at all.
73    pub fn off() -> Self {
74        Self {
75            enabled: false,
76            ..Self::on()
77        }
78    }
79
80    /// The limits a service open to others starts with.
81    pub fn on() -> Self {
82        Self {
83            enabled: true,
84            requests_per_minute: 600,
85            sign_ins_per_minute: 20,
86            body_bytes: 1024 * 1024,
87            characters: 200,
88            monsters: 1000,
89            rules_texts: 1000,
90            combats: 500,
91            plans: 200,
92            devices: 50,
93            events_per_combat: 20_000,
94            connections_per_combat: 50,
95            trust_proxy: false,
96        }
97    }
98
99    /// The limits from the environment: on unless `LIMITS=off`, each
100    /// changed by `LIMIT_<NAME>`, and `TRUST_PROXY`.
101    pub fn from_env() -> Result<Self, String> {
102        Self::from_vars(|name| std::env::var(name).ok())
103    }
104
105    fn from_vars(var: impl Fn(&str) -> Option<String>) -> Result<Self, String> {
106        let on = match var("LIMITS").as_deref() {
107            None | Some("on") => true,
108            Some("off") => false,
109            Some(other) => return Err(format!("LIMITS must be on or off, not {other:?}")),
110        };
111        let mut limits = if on { Self::on() } else { Self::off() };
112        let number = |name: &str, current: u32| -> Result<u32, String> {
113            match var(&format!("LIMIT_{name}")) {
114                None => Ok(current),
115                Some(value) => value
116                    .parse()
117                    .map_err(|_| format!("LIMIT_{name} must be a number, not {value:?}")),
118            }
119        };
120        limits.requests_per_minute = number("REQUESTS_PER_MINUTE", limits.requests_per_minute)?;
121        limits.sign_ins_per_minute = number("SIGN_INS_PER_MINUTE", limits.sign_ins_per_minute)?;
122        let body = u32::try_from(limits.body_bytes).unwrap_or(u32::MAX);
123        limits.body_bytes = usize::try_from(number("BODY_BYTES", body)?).unwrap_or(usize::MAX);
124        limits.characters = number("CHARACTERS", limits.characters)?;
125        limits.monsters = number("MONSTERS", limits.monsters)?;
126        limits.rules_texts = number("RULES_TEXTS", limits.rules_texts)?;
127        limits.combats = number("COMBATS", limits.combats)?;
128        limits.plans = number("PLANS", limits.plans)?;
129        limits.devices = number("DEVICES", limits.devices)?;
130        limits.events_per_combat = number("EVENTS_PER_COMBAT", limits.events_per_combat)?;
131        limits.connections_per_combat =
132            number("CONNECTIONS_PER_COMBAT", limits.connections_per_combat)?;
133        limits.trust_proxy = matches!(var("TRUST_PROXY").as_deref(), Some("1" | "true" | "on"));
134        Ok(limits)
135    }
136
137    /// Whether the limits are on (`LIMITS` isn't `off`).
138    pub fn enabled(&self) -> bool {
139        self.enabled
140    }
141
142    /// Whether a campaign that keeps `used` of something may add `more`,
143    /// given its limit `max`; if not, what to tell it.
144    pub fn room_for(&self, used: i64, more: u32, max: u32, what: &str) -> Result<(), String> {
145        if self.enabled && used.saturating_add(i64::from(more)) > i64::from(max) {
146            crate::measure::refused(what);
147            return Err(format!(
148                "this campaign has reached its limit of {max} {what}"
149            ));
150        }
151        Ok(())
152    }
153
154    /// These, with a campaign's own `quotas` in place of the ones they set.
155    #[must_use]
156    pub fn with(&self, quotas: &Quotas) -> Limits {
157        let mut limits = self.clone();
158        let own = |mine: &mut u32, theirs: Option<u32>| {
159            if let Some(max) = theirs {
160                *mine = max;
161            }
162        };
163        own(&mut limits.characters, quotas.characters);
164        own(&mut limits.monsters, quotas.monsters);
165        own(&mut limits.rules_texts, quotas.rules_texts);
166        own(&mut limits.combats, quotas.combats);
167        own(&mut limits.plans, quotas.plans);
168        own(&mut limits.devices, quotas.devices);
169        own(&mut limits.events_per_combat, quotas.events_per_combat);
170        own(
171            &mut limits.connections_per_combat,
172            quotas.connections_per_combat,
173        );
174        limits
175    }
176
177    /// `router` with the limits on every request: how many a client may
178    /// send, and how big. Applied by [`crate::serve`].
179    pub(crate) fn apply(&self, router: Router) -> Router {
180        if !self.enabled {
181            return router;
182        }
183        let limiter = RateLimiter::new("requests", self.requests_per_minute, self.trust_proxy);
184        router
185            .layer(middleware::from_fn_with_state(limiter, limit))
186            .layer(DefaultBodyLimit::max(self.body_bytes))
187    }
188
189    /// `router` (the routes that need no token) with the stricter limit on
190    /// signing in.
191    pub fn guard_sign_ins<S: Clone + Send + Sync + 'static>(&self, router: Router<S>) -> Router<S> {
192        if !self.enabled {
193            return router;
194        }
195        let limiter = RateLimiter::new("sign-ins", self.sign_ins_per_minute, self.trust_proxy);
196        router.route_layer(middleware::from_fn_with_state(limiter, limit))
197    }
198}
199
200/// Counts each client address's requests in one-minute windows.
201#[derive(Clone)]
202struct RateLimiter {
203    /// Which limit it keeps, for the metrics.
204    name: &'static str,
205    per_minute: u32,
206    trust_proxy: bool,
207    windows: Arc<Mutex<HashMap<IpAddr, (Instant, u32)>>>,
208}
209
210const WINDOW: Duration = Duration::from_mins(1);
211/// Windows remembered before the finished ones are dropped.
212const REMEMBERED: usize = 10_000;
213
214impl RateLimiter {
215    fn new(name: &'static str, per_minute: u32, trust_proxy: bool) -> Self {
216        Self {
217            name,
218            per_minute,
219            trust_proxy,
220            windows: Arc::default(),
221        }
222    }
223
224    /// Counts a request from `client`: `Err` with the seconds to wait if
225    /// it's one too many.
226    fn count(&self, client: IpAddr, now: Instant) -> Result<(), u64> {
227        let mut windows = self.windows.lock().expect("not poisoned");
228        if windows.len() > REMEMBERED {
229            windows.retain(|_, (start, _)| now.duration_since(*start) < WINDOW);
230        }
231        let (start, count) = windows.entry(client).or_insert((now, 0));
232        if now.duration_since(*start) >= WINDOW {
233            (*start, *count) = (now, 0);
234        }
235        if *count >= self.per_minute {
236            let left = WINDOW.saturating_sub(now.duration_since(*start));
237            return Err(left.as_secs().max(1));
238        }
239        *count += 1;
240        Ok(())
241    }
242
243    /// Where `request` comes from, to count it: behind a trusted proxy, its
244    /// word for it (none for what didn't come through it); else the
245    /// connection's address (unknown when served without it).
246    fn client(&self, request: &Request) -> Option<IpAddr> {
247        if self.trust_proxy {
248            // The proxy adds the address it saw last.
249            return request
250                .headers()
251                .get("x-forwarded-for")?
252                .to_str()
253                .ok()?
254                .rsplit(',')
255                .next()?
256                .trim()
257                .parse()
258                .ok();
259        }
260        request
261            .extensions()
262            .get::<ConnectInfo<SocketAddr>>()
263            .map(|ConnectInfo(address)| address.ip())
264    }
265}
266
267async fn limit(State(limiter): State<RateLimiter>, request: Request, next: Next) -> Response {
268    if let Some(client) = limiter.client(&request)
269        && let Err(wait) = limiter.count(client, Instant::now())
270    {
271        tracing::debug!(%client, "too many requests");
272        crate::measure::refused(limiter.name);
273        let mut response = (
274            StatusCode::TOO_MANY_REQUESTS,
275            Json(json!({ "error": format!("too many requests; try again in {wait} s") })),
276        )
277            .into_response();
278        response
279            .headers_mut()
280            .insert(header::RETRY_AFTER, HeaderValue::from(wait));
281        return response;
282    }
283    next.run(request).await
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    fn vars(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
291        let pairs: HashMap<String, String> = pairs
292            .iter()
293            .map(|(k, v)| (k.to_string(), v.to_string()))
294            .collect();
295        move |name| pairs.get(name).cloned()
296    }
297
298    #[test]
299    fn limits_are_on_unless_turned_off_and_each_can_change() {
300        let limits = Limits::from_vars(vars(&[])).unwrap();
301        assert!(limits.enabled() && !limits.trust_proxy);
302        let limits =
303            Limits::from_vars(vars(&[("LIMIT_CHARACTERS", "5"), ("TRUST_PROXY", "1")])).unwrap();
304        assert_eq!(limits.characters, 5);
305        assert!(limits.trust_proxy);
306        assert!(
307            !Limits::from_vars(vars(&[("LIMITS", "off")]))
308                .unwrap()
309                .enabled()
310        );
311        assert!(Limits::from_vars(vars(&[("LIMITS", "maybe")])).is_err());
312        assert!(Limits::from_vars(vars(&[("LIMIT_PLANS", "lots")])).is_err());
313    }
314
315    #[test]
316    fn quotas_say_when_a_campaign_is_full() {
317        let limits = Limits::on();
318        assert_eq!(limits.room_for(199, 1, 200, "characters"), Ok(()));
319        assert_eq!(
320            limits.room_for(200, 1, 200, "characters"),
321            Err("this campaign has reached its limit of 200 characters".into())
322        );
323        assert_eq!(Limits::off().room_for(10_000, 1, 200, "characters"), Ok(()));
324    }
325
326    /// A service with `limits`.
327    fn service(limits: &Limits) -> Router {
328        use axum::routing::post;
329        limits.apply(Router::new().route("/", post(|_: String| async { "ok" })))
330    }
331
332    /// A request to `app` from `client`, with a body `body` bytes long.
333    async fn send(app: &Router, client: &str, body: usize) -> (StatusCode, Option<HeaderValue>) {
334        use axum::body::Body;
335        use tower::ServiceExt;
336
337        let mut request = Request::post("/")
338            .body(Body::from("x".repeat(body)))
339            .expect("a valid request");
340        let address: SocketAddr = format!("{client}:1234").parse().expect("an address");
341        request.extensions_mut().insert(ConnectInfo(address));
342        let response = app.clone().oneshot(request).await.expect("infallible");
343        (
344            response.status(),
345            response.headers().get(header::RETRY_AFTER).cloned(),
346        )
347    }
348
349    #[test]
350    fn a_campaigns_own_quotas_replace_only_what_they_set() {
351        let quotas = Quotas {
352            characters: Some(5),
353            devices: Some(3),
354            ..Quotas::default()
355        };
356        let mine = Limits::on().with(&quotas);
357        assert_eq!((mine.characters, mine.devices), (5, 3));
358        assert_eq!(
359            mine.monsters,
360            Limits::on().monsters,
361            "the rest as they were"
362        );
363        assert_eq!(Limits::on().with(&Quotas::default()).characters, 200);
364    }
365
366    #[tokio::test]
367    async fn a_client_too_busy_or_too_big_is_turned_away() {
368        let mut limits = Limits::on();
369        limits.requests_per_minute = 2;
370        limits.body_bytes = 100;
371        let app = service(&limits);
372        assert_eq!(send(&app, "192.0.2.1", 10).await.0, StatusCode::OK);
373        assert_eq!(send(&app, "192.0.2.1", 10).await.0, StatusCode::OK);
374        let (status, wait) = send(&app, "192.0.2.1", 10).await;
375        assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
376        assert!(wait.is_some(), "says when to try again");
377        assert_eq!(send(&app, "192.0.2.2", 10).await.0, StatusCode::OK);
378        assert_eq!(
379            send(&app, "192.0.2.3", 200).await.0,
380            StatusCode::PAYLOAD_TOO_LARGE
381        );
382        // Behind the proxy, the services' calls to each other don't count.
383        limits.trust_proxy = true;
384        let app = service(&limits);
385        for _ in 0..5 {
386            assert_eq!(send(&app, "192.0.2.1", 10).await.0, StatusCode::OK);
387        }
388        // Off, neither.
389        let app = service(&Limits::off());
390        for _ in 0..5 {
391            assert_eq!(send(&app, "192.0.2.1", 200).await.0, StatusCode::OK);
392        }
393    }
394
395    #[test]
396    fn each_address_gets_its_requests_per_minute() {
397        let limiter = RateLimiter::new("requests", 3, false);
398        let (a, b): (IpAddr, IpAddr) = ("192.0.2.1".parse().unwrap(), "192.0.2.2".parse().unwrap());
399        let now = Instant::now();
400        for _ in 0..3 {
401            assert_eq!(limiter.count(a, now), Ok(()));
402        }
403        assert_eq!(limiter.count(a, now + Duration::from_secs(20)), Err(40));
404        assert_eq!(limiter.count(b, now), Ok(()), "another address has its own");
405        assert_eq!(limiter.count(a, now + WINDOW), Ok(()), "a new minute");
406    }
407}