Skip to content
58 linesCodeBlameRaw
1//! What an anonymous clone can cost a repository's owner.
2//!
3//! Anyone may clone a public repository, and each clone or fetch the git
4//! store answers is an operation counted for the repository's workspace
5//! (git_ops.rs). The site limits git requests per address (apps/web's
6//! front-door-limits.ts); here, per repository:
7//!
8//! - ANONYMOUS_FETCH_LIMIT: anonymous fetches that reach the store. Clones
9//! answered from the pack cache or kept refs never count, so the same
10//! commit cloned by many costs nothing; a flood of different requests
11//! from many addresses is answered 429 once past it. Signed-in clones
12//! are never limited here.
13//! - PACK_FILL_LIMIT: packs written to the pack cache (pack_cache.rs). Past
14//! it the pack still goes to git, only not kept, so a flood of distinct
15//! clones cannot turn into a flood of writes to the bucket.
16//!
17//! Both are Workers Rate Limiting bindings keyed by the repository's id;
18//! their limits are in `RATE_LIMITS` (packages/contracts). Without them
19//! (self-hosted) nothing is limited, and one that fails lets the request
20//! through (g1t_kit::limits).
21
22use g1t_kit::limits::PERIOD_SECONDS;
23use worker::{Response, Result};
24
25use crate::git_ops::GitCall;
26
27pub const ANONYMOUS_FETCH: &str = "ANONYMOUS_FETCH_LIMIT";
28pub const PACK_FILL: &str = "PACK_FILL_LIMIT";
29
30/// Whether a request counts against ANONYMOUS_FETCH_LIMIT: a fetch of
31/// objects, by no one, that the store is about to be asked for.
32pub fn counts_as_anonymous_fetch(call: GitCall, anonymous: bool) -> bool {
33 anonymous && call == GitCall::Fetch
34}
35
36/// What git is told past the limit: a plain-text answer, which git shows.
37pub fn too_many_anonymous_fetches(path: &str) -> Result<Response> {
38 let message = format!(
39 "Too many anonymous clones of {path} right now. Wait a minute and try again, or clone with credentials: https://docs.g1t.sh/reference/rate-limits/\n"
40 );
41 let response = Response::error(message, 429)?;
42 response.headers().set("retry-after", &PERIOD_SECONDS.to_string())?;
43 Ok(response)
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn only_anonymous_fetches_of_objects_count() {
52 assert!(counts_as_anonymous_fetch(GitCall::Fetch, true));
53 assert!(!counts_as_anonymous_fetch(GitCall::Fetch, false), "signed-in clones are not limited here");
54 assert!(!counts_as_anonymous_fetch(GitCall::RefAdvertisement, true));
55 assert!(!counts_as_anonymous_fetch(GitCall::LsRefs, true));
56 assert!(!counts_as_anonymous_fetch(GitCall::ReceivePack, true));
57 }
58}