Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

211 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { readFileSync } from "node:fs";
3import { test } from "node:test";
4
5import { LIMIT_PERIOD_SECONDS, RATE_LIMITS, type RateLimitBinding, checkLimit, isLimited, secretKey } from "@g1t/contracts/rate-limits";
6
7import { archiveLink, gitLimited, heavy, pageLimited, sessionCookie, unlimited, usercontentLimited } from "./front-door-limits.ts";
8
9/** A binding that lets `allow` requests through per key, recording each key it was asked. */
10function binding(allow: number): RateLimitBinding & { keys: string[] } {
11 const counts = new Map<string, number>();
12 const keys: string[] = [];
13 return {
14 keys,
15 async limit({ key }) {
16 keys.push(key);
17 const count = (counts.get(key) ?? 0) + 1;
18 counts.set(key, count);
19 return { success: count <= allow };
20 },
21 };
22}
23
24const broken: RateLimitBinding = {
25 async limit() {
26 throw new Error("the binding is down");
27 },
28};
29
30function request(path: string, headers: Record<string, string> = {}): Request {
31 return new Request(`https://g1t.sh${path}`, { headers: { "cf-connecting-ip": "203.0.113.9", ...headers } });
32}
33
34test("a limit lets requests through until it is reached", async () => {
35 const limit = binding(2);
36 assert.equal(await checkLimit(limit, "ip:1"), "allowed");
37 assert.equal(await checkLimit(limit, "ip:1"), "allowed");
38 assert.equal(await checkLimit(limit, "ip:1"), "limited");
39 assert.equal(await checkLimit(limit, "ip:2"), "allowed", "each key counts apart");
40});
41
42test("a missing or failing binding fails open", async () => {
43 const logged = console.error;
44 console.error = () => {};
45 try {
46 assert.equal(await checkLimit(undefined, "ip:1"), "unavailable");
47 assert.equal(await checkLimit(broken, "ip:1"), "unavailable");
48 assert.equal(await isLimited(broken, "ip:1"), false);
49 assert.equal(await gitLimited({ GIT_ANONYMOUS_LIMIT: broken }, request("/acme/rocket.git/info/refs")), null);
50 assert.equal(await pageLimited({ WEB_ADDRESS_LIMIT: broken, WEB_ANONYMOUS_LIMIT: broken }, request("/acme/rocket"), "/acme/rocket"), null);
51 } finally {
52 console.error = logged;
53 }
54});
55
56test("secrets are keyed by a short hash, never as they are", async () => {
57 const key = await secretKey("session", "s3cret-session");
58 assert.match(key, /^session:[0-9a-f]{16}$/);
59 assert.equal(await secretKey("session", "s3cret-session"), key);
60 assert.notEqual(await secretKey("session", "another"), key);
61});
62
63test("git without credentials is limited by address, with a message git shows", async () => {
64 const env = { GIT_ANONYMOUS_LIMIT: binding(1), GIT_SIGNED_LIMIT: binding(1) };
65 const clone = () => request("/acme/rocket.git/info/refs");
66 assert.equal(await gitLimited(env, clone()), null);
67 const refused = await gitLimited(env, clone());
68 assert.equal(refused?.status, 429);
69 assert.equal(refused?.headers.get("retry-after"), String(LIMIT_PERIOD_SECONDS));
70 assert.match(refused?.headers.get("content-type") ?? "", /^text\/plain/);
71 assert.match((await refused?.text()) ?? "", /Too many git requests/);
72 assert.deepEqual(env.GIT_ANONYMOUS_LIMIT.keys, ["ip:203.0.113.9", "ip:203.0.113.9"]);
73});
74
75test("git with credentials counts by a hash of them, apart from the address", async () => {
76 const env = { GIT_ANONYMOUS_LIMIT: binding(0), GIT_SIGNED_LIMIT: binding(5) };
77 const authorization = `Basic ${btoa("ada:g1t_token")}`;
78 assert.equal(await gitLimited(env, request("/acme/rocket.git/git-upload-pack", { authorization })), null);
79 assert.equal(env.GIT_ANONYMOUS_LIMIT.keys.length, 0);
80 assert.match(env.GIT_SIGNED_LIMIT.keys[0]!, /^git:[0-9a-f]{16}$/);
81 assert.ok(!env.GIT_SIGNED_LIMIT.keys[0]!.includes("g1t_token"));
82});
83
84test("signed-out pages count by address, and costly ones against a tighter limit too", async () => {
85 const env = { WEB_ADDRESS_LIMIT: binding(100), WEB_ANONYMOUS_LIMIT: binding(100), WEB_HEAVY_LIMIT: binding(1) };
86 assert.equal(await pageLimited(env, request("/acme/rocket"), "/acme/rocket"), null);
87 assert.equal(env.WEB_HEAVY_LIMIT.keys.length, 0);
88 const archive = "/acme/rocket/archive/main.zip";
89 assert.equal(await pageLimited(env, request(archive), archive), null);
90 const refused = await pageLimited(env, request(archive), archive);
91 assert.equal(refused?.status, 429);
92 assert.match((await refused?.text()) ?? "", /Signed-in accounts have a higher limit/);
93 assert.equal(await pageLimited(env, request("/acme/rocket/issues"), "/acme/rocket/issues"), null, "other pages go on");
94});
95
96test("signed-in requests count by session, and every request by address", async () => {
97 const env = { WEB_ADDRESS_LIMIT: binding(1), WEB_SESSION_LIMIT: binding(100), WEB_ANONYMOUS_LIMIT: binding(0) };
98 const signedIn = () => request("/acme/rocket/archive/main.zip", { cookie: "theme=dark; g1t_session=abc123" });
99 assert.equal(await pageLimited(env, signedIn(), "/acme/rocket/archive/main.zip"), null);
100 assert.equal(env.WEB_ANONYMOUS_LIMIT.keys.length, 0);
101 assert.match(env.WEB_SESSION_LIMIT.keys[0]!, /^session:[0-9a-f]{16}$/);
102 // Made-up cookies still meet the ceiling per address.
103 const refused = await pageLimited(env, request("/", { cookie: "g1t_session=made-up" }), "/");
104 assert.equal(refused?.status, 429);
105});
106
107test("requests with an access token count by a hash of the token, at the API's limit", async () => {
108 const env = { WEB_ADDRESS_LIMIT: binding(100), WEB_TOKEN_LIMIT: binding(1), WEB_SESSION_LIMIT: binding(100), WEB_ANONYMOUS_LIMIT: binding(0) };
109 const withToken = () => request("/acme/rocket", { authorization: "Bearer g1t_secret", cookie: "g1t_session=abc123" });
110 assert.equal(await pageLimited(env, withToken(), "/acme/rocket"), null);
111 assert.equal(env.WEB_SESSION_LIMIT.keys.length, 0, "the token counts, not a cookie beside it");
112 assert.equal(env.WEB_ANONYMOUS_LIMIT.keys.length, 0);
113 assert.match(env.WEB_TOKEN_LIMIT.keys[0]!, /^token:[0-9a-f]{16}$/);
114 assert.ok(!env.WEB_TOKEN_LIMIT.keys[0]!.includes("g1t_secret"));
115 const refused = await pageLimited(env, withToken(), "/acme/rocket");
116 assert.equal(refused?.status, 429);
117 assert.match((await refused?.text()) ?? "", /this access token/);
118 assert.equal(RATE_LIMITS.WEB_TOKEN_LIMIT.limit, RATE_LIMITS.API_TOKEN_LIMIT.limit);
119});
120
121test("files the Worker serves itself are never limited", async () => {
122 const env = { WEB_ADDRESS_LIMIT: binding(0), WEB_ANONYMOUS_LIMIT: binding(0) };
123 for (const path of ["/assets/app-1a2b.js", "/fonts/hanken.woff2", "/favicon.ico", "/robots.txt", "/llms.txt", "/sitemap.xml"]) {
124 assert.ok(unlimited(path), path);
125 assert.equal(await pageLimited(env, request(path), path), null, path);
126 }
127 assert.ok(!unlimited("/acme/rocket/blob/main/logo.png"), "a file in a repository is a page");
128});
129
130test("costly pages are recognised", () => {
131 for (const path of [
132 "/search",
133 "/search.data",
134 "/acme/rocket/archive/main.zip",
135 "/acme/rocket/actions/runs/run_1",
136 "/acme/rocket/actions/runs/run_1.data",
137 "/acme/rocket/actions/runs/run_1/logs.zip",
138 "/acme/rocket/actions/runs/run_1/artifacts/dist",
139 "/acme/rocket/actions/jobs/job_1/log.txt",
140 "/acme/rocket/security/report",
141 "/acme/rocket/security/report.data",
142 ]) {
143 assert.ok(heavy(path), path);
144 }
145 // A running job's live log is asked for every second; it counts as a page.
146 for (const path of [
147 "/",
148 "/acme/rocket",
149 "/acme/rocket/actions",
150 "/acme/rocket/issues/1",
151 "/acme/search",
152 "/acme/rocket/security",
153 "/acme/rocket/security/reports",
154 "/acme/rocket/actions/jobs/job_1/log",
155 ]) {
156 assert.ok(!heavy(path), path);
157 }
158});
159
160test("the session cookie is read from among others", () => {
161 assert.equal(sessionCookie("theme=dark; g1t_session=abc; x=1"), "abc");
162 assert.equal(sessionCookie("g1t_session=abc"), "abc");
163 assert.equal(sessionCookie("not_g1t_session=abc"), null);
164 assert.equal(sessionCookie(null), null);
165});
166
167/** A wrangler.jsonc as JSON: comments and trailing commas out, strings kept. */
168function readJsonc(path: string): { ratelimits?: { name: string; namespace_id: string; simple: { limit: number; period: number } }[] } {
169 const text = readFileSync(new URL(path, import.meta.url), "utf8")
170 .replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, string: string | undefined) => string ?? "")
171 .replace(/,(\s*[}\]])/g, "$1");
172 return JSON.parse(text);
173}
174
175test("every wrangler.jsonc declares the rate limits RATE_LIMITS lists, and only those", () => {
176 const ids = new Set<number>();
177 const workers = new Set(Object.values(RATE_LIMITS).map((spec) => spec.worker));
178 for (const worker of workers) {
179 const declared = readJsonc(`../../../../${worker}/wrangler.jsonc`).ratelimits ?? [];
180 const listed = Object.entries(RATE_LIMITS).filter(([, spec]) => spec.worker === worker);
181 assert.deepEqual(declared.map((binding) => binding.name).sort(), listed.map(([name]) => name).sort(), `${worker}'s bindings`);
182 for (const [name, spec] of listed) {
183 const binding = declared.find((b) => b.name === name)!;
184 assert.equal(Number(binding.namespace_id), spec.namespaceId, `${name}'s namespace id`);
185 assert.equal(binding.simple.limit, spec.limit, `${name}'s limit`);
186 assert.equal(binding.simple.period, LIMIT_PERIOD_SECONDS, `${name}'s period`);
187 }
188 }
189 for (const spec of Object.values(RATE_LIMITS)) {
190 assert.ok(!ids.has(spec.namespaceId), `namespace id ${spec.namespaceId} is used once`);
191 ids.add(spec.namespaceId);
192 }
193});
194
195test("repository files on the usercontent origin count per address; avatars do not", async () => {
196 const env = { WEB_ANONYMOUS_LIMIT: binding(1) };
197 const raw = "/acme/rocket/raw/main/logo.png";
198 assert.equal(await usercontentLimited(env, request(raw), raw), null);
199 assert.equal((await usercontentLimited(env, request(raw), raw))?.status, 429);
200 assert.equal(await usercontentLimited(env, request("/avatars/" + "a".repeat(64)), "/avatars/" + "a".repeat(64)), null);
201 const image = "/production-images/" + "b".repeat(64);
202 assert.equal(await usercontentLimited(env, request(image), image), null);
203 assert.deepEqual(env.WEB_ANONYMOUS_LIMIT.keys, ["ip:203.0.113.9", "ip:203.0.113.9"]);
204});
205
206test("a signed archive link is known by its token, and only on an archive", () => {
207 assert.equal(archiveLink("/acme/web/archive/abc.tar.gz", "https://g1t.sh/acme/web/archive/abc.tar.gz?token=t1"), "t1");
208 assert.equal(archiveLink("/acme/web/archive/main.zip", "https://g1t.sh/acme/web/archive/main.zip?token=t2"), "t2");
209 assert.equal(archiveLink("/acme/web/archive/main.zip", "https://g1t.sh/acme/web/archive/main.zip"), null);
210 assert.equal(archiveLink("/acme/web/issues", "https://g1t.sh/acme/web/issues?token=t3"), null);
211});