1use 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#[derive(Debug, Clone)]
38pub struct Limits {
39 enabled: bool,
40 pub requests_per_minute: u32,
42 pub sign_ins_per_minute: u32,
45 pub body_bytes: usize,
47 pub characters: u32,
49 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 pub connections_per_combat: u32,
58 pub trust_proxy: bool,
61}
62
63impl Default for Limits {
64 fn default() -> Self {
67 Self::off()
68 }
69}
70
71impl Limits {
72 pub fn off() -> Self {
74 Self {
75 enabled: false,
76 ..Self::on()
77 }
78 }
79
80 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 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 pub fn enabled(&self) -> bool {
139 self.enabled
140 }
141
142 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 #[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 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 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#[derive(Clone)]
202struct RateLimiter {
203 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);
211const 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 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 fn client(&self, request: &Request) -> Option<IpAddr> {
247 if self.trust_proxy {
248 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 fn service(limits: &Limits) -> Router {
328 use axum::routing::post;
329 limits.apply(Router::new().route("/", post(|_: String| async { "ok" })))
330 }
331
332 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("as);
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 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 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}