| 1 | //! Rate limits: Workers Rate Limiting bindings, asked once per request with |
| 2 | //! a key (a client's address, a repository's id, a hash of a token). |
| 3 | //! |
| 4 | //! Every binding, its namespace id and its limit is listed in `RATE_LIMITS` |
| 5 | //! (packages/contracts/src/rate-limits.ts), which apps/web's tests check |
| 6 | //! each wrangler.jsonc against; docs/RATE-LIMITS.md says why each is what |
| 7 | //! it is. |
| 8 | //! |
| 9 | //! A limit fails open: a binding that is not there (self-hosted) or that |
| 10 | //! fails lets the request through. A limit guards against floods; it is |
| 11 | //! never a reason for g1t to stop answering. |
| 12 | |
| 13 | use worker::Env; |
| 14 | |
| 15 | /// Every limit's window, in seconds, and how long a client past one is |
| 16 | /// told to wait (`Retry-After`). Workers Rate Limiting counts over 10 or 60. |
| 17 | pub const PERIOD_SECONDS: u32 = 60; |
| 18 | |
| 19 | /// What asking a limit said. Only `Limited` refuses the request. |
| 20 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 21 | pub enum Verdict { |
| 22 | Allowed, |
| 23 | Limited, |
| 24 | /// No binding, or it failed: let through. |
| 25 | Unavailable, |
| 26 | } |
| 27 | |
| 28 | impl Verdict { |
| 29 | pub fn limited(self) -> bool { |
| 30 | self == Verdict::Limited |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | /// The verdict from what the binding answered: `None` when there is no |
| 35 | /// binding, `Some(Err)` when asking it failed, else whether it let the |
| 36 | /// request through. |
| 37 | pub fn verdict<E>(answered: Option<std::result::Result<bool, E>>) -> Verdict { |
| 38 | match answered { |
| 39 | Some(Ok(true)) => Verdict::Allowed, |
| 40 | Some(Ok(false)) => Verdict::Limited, |
| 41 | Some(Err(_)) | None => Verdict::Unavailable, |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | /// Counts one request against the binding named `binding` under `key`. |
| 46 | /// Never fails; a failure to ask is logged and lets the request through. |
| 47 | pub async fn check(env: &Env, binding: &str, key: String) -> Verdict { |
| 48 | let Ok(limiter) = env.rate_limiter(binding) else { |
| 49 | return Verdict::Unavailable; |
| 50 | }; |
| 51 | let answered = limiter.limit(key).await.map(|outcome| outcome.success); |
| 52 | if let Err(problem) = &answered { |
| 53 | worker::console_error!("rate limit {binding} could not be asked: {problem}"); |
| 54 | } |
| 55 | verdict(Some(answered)) |
| 56 | } |
| 57 | |
| 58 | /// A client's address for a key: `ip:` and `CF-Connecting-IP`, or |
| 59 | /// `ip:unknown` when there is none (local development). |
| 60 | pub fn address_key(address: Option<&str>) -> String { |
| 61 | format!("ip:{}", address.map(str::trim).filter(|a| !a.is_empty()).unwrap_or("unknown")) |
| 62 | } |
| 63 | |
| 64 | #[cfg(test)] |
| 65 | mod tests { |
| 66 | use super::*; |
| 67 | |
| 68 | #[test] |
| 69 | fn only_a_limit_the_binding_says_is_reached_refuses() { |
| 70 | assert_eq!(verdict::<()>(Some(Ok(true))), Verdict::Allowed); |
| 71 | assert_eq!(verdict::<()>(Some(Ok(false))), Verdict::Limited); |
| 72 | assert!(verdict::<()>(Some(Ok(false))).limited()); |
| 73 | } |
| 74 | |
| 75 | #[test] |
| 76 | fn no_binding_or_a_failing_one_lets_requests_through() { |
| 77 | assert_eq!(verdict::<()>(None), Verdict::Unavailable); |
| 78 | assert_eq!(verdict(Some(Err("binding threw"))), Verdict::Unavailable); |
| 79 | assert!(!verdict::<()>(None).limited()); |
| 80 | assert!(!verdict(Some(Err("binding threw"))).limited()); |
| 81 | } |
| 82 | |
| 83 | #[test] |
| 84 | fn addresses_are_keyed_as_given_or_unknown() { |
| 85 | assert_eq!(address_key(Some(" 203.0.113.9 ")), "ip:203.0.113.9"); |
| 86 | assert_eq!(address_key(Some("")), "ip:unknown"); |
| 87 | assert_eq!(address_key(None), "ip:unknown"); |
| 88 | } |
| 89 | } |