Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1 | //! How often a client may pull and ask for tokens. |
| 2 | //! | |
| 3 | //! Anonymous requests are limited by the address they come from | |
| 4 | //! (`CF-Connecting-IP`), signed-in ones by who they are, with a much higher | |
| 5 | //! limit. Both are Workers Rate Limiting bindings, ANONYMOUS_LIMIT and | |
| 6 | //! SIGNED_LIMIT; without them (self-hosted) nothing is limited. Pushes are | |
| 7 | //! not limited here: they need an account, and the store's own limits | |
| 8 | //! apply. | |
| 9 | ||
| 10 | use worker::Method; | |
| 11 | ||
| 12 | use crate::names::Route; | |
| 13 | ||
| 14 | /// Which limit a request counts against. | |
| 15 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 16 | pub enum Limit { | |
| 17 | Anonymous, | |
| 18 | Signed, | |
| 19 | } | |
| 20 | ||
| 21 | impl Limit { | |
| 22 | pub fn binding(self) -> &'static str { | |
| 23 | match self { | |
| 24 | Limit::Anonymous => "ANONYMOUS_LIMIT", | |
| 25 | Limit::Signed => "SIGNED_LIMIT", | |
| 26 | } | |
| 27 | } | |
| 28 | } | |
| 29 | ||
| 30 | /// How long a client limited is told to wait: the limits' period. | |
| 31 | pub const RETRY_AFTER_SECONDS: u32 = 60; | |
| 32 | ||
| 33 | /// Whether a request counts: pulls (reads of any kind) and asking for a | |
| 34 | /// token, which is also where a wrong password is tried again and again. | |
| 35 | pub fn counts(method: &Method, route: &Route) -> bool { | |
| 36 | matches!(route, Route::Token) || matches!(method, Method::Get | Method::Head) | |
| 37 | } | |
| 38 | ||
| 39 | /// The limit a request counts against and its key there. `subject` is who | |
| 40 | /// the credentials name (a person's, workspace's or agent's id), absent | |
| 41 | /// for anonymous requests and for credentials that turned out wrong, which | |
| 42 | /// count as anonymous so guessing is limited by address. | |
| 43 | pub fn key(subject: Option<&str>, address: Option<&str>) -> (Limit, String) { | |
| 44 | match subject.filter(|s| !s.is_empty()) { | |
| 45 | Some(subject) => (Limit::Signed, format!("sub:{subject}")), | |
| 46 | None => (Limit::Anonymous, format!("ip:{}", address.map(str::trim).filter(|a| !a.is_empty()).unwrap_or("unknown"))), | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | #[cfg(test)] | |
| 51 | mod tests { | |
| 52 | use super::*; | |
| 53 | ||
| 54 | #[test] | |
| 55 | fn pulls_and_tokens_count_and_pushes_do_not() { | |
| 56 | let manifest = Route::Manifest { name: "acme/web".into(), reference: "v1".into() }; | |
| 57 | assert!(counts(&Method::Get, &manifest)); | |
| 58 | assert!(counts(&Method::Head, &Route::Blob { name: "acme/web".into(), digest: "d".into() })); | |
| 59 | assert!(counts(&Method::Get, &Route::Base)); | |
| 60 | assert!(counts(&Method::Post, &Route::Token), "docker's OAuth form"); | |
| 61 | assert!(!counts(&Method::Put, &manifest)); | |
| 62 | assert!(!counts(&Method::Patch, &Route::Upload { name: "acme/web".into(), id: "u".into() })); | |
| 63 | assert!(!counts(&Method::Delete, &manifest)); | |
| 64 | } | |
| 65 | ||
| 66 | #[test] | |
| 67 | fn anonymous_clients_are_counted_by_address_and_others_by_who_they_are() { | |
| 68 | assert_eq!(key(None, Some("203.0.113.9")), (Limit::Anonymous, "ip:203.0.113.9".to_owned())); | |
| 69 | assert_eq!(key(None, None), (Limit::Anonymous, "ip:unknown".to_owned())); | |
| 70 | assert_eq!(key(Some(""), Some("2001:db8::1")), (Limit::Anonymous, "ip:2001:db8::1".to_owned())); | |
| 71 | assert_eq!(key(Some("usr_1"), Some("203.0.113.9")), (Limit::Signed, "sub:usr_1".to_owned())); | |
| 72 | assert_eq!(Limit::Anonymous.binding(), "ANONYMOUS_LIMIT"); | |
| 73 | assert_eq!(Limit::Signed.binding(), "SIGNED_LIMIT"); | |
| 74 | } | |
| 75 | } |