g1t/services/packages/src/limits.rs

75 lines3,121 bytesCodeBlame

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 member1//! 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
10use worker::Method;
11
12use crate::names::Route;
13
14/// Which limit a request counts against.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Limit {
17 Anonymous,
18 Signed,
19}
20
21impl 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.
31pub 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.
35pub 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.
43pub 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)]
51mod 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}