fix(hub): rate limit public code browsing
This commit is contained in:
parent
a7fd517d58
commit
f87cd30dab
3 changed files with 108 additions and 1 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const net = require("node:net");
|
||||
const path = require("node:path");
|
||||
const os = require("node:os");
|
||||
|
||||
|
|
@ -14,6 +15,42 @@ function json(response, status, value) {
|
|||
response.end(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function clientAddress(request) {
|
||||
const real = String(request.headers["x-real-ip"] || "").trim();
|
||||
if (net.isIP(real)) return real;
|
||||
const forwarded = String(request.headers["x-forwarded-for"] || "")
|
||||
.split(",")
|
||||
.map(value => value.trim())
|
||||
.filter(Boolean);
|
||||
const last = forwarded.at(-1) || "";
|
||||
if (net.isIP(last)) return last;
|
||||
return String(request.socket.remoteAddress || "unknown").slice(0, 64);
|
||||
}
|
||||
|
||||
function createFixedWindowLimiter({ max, windowMs = 60000, now = Date.now }) {
|
||||
const entries = new Map();
|
||||
return key => {
|
||||
const timestamp = now();
|
||||
let entry = entries.get(key);
|
||||
if (!entry || timestamp >= entry.resetAt) {
|
||||
entry = { count: 0, resetAt: timestamp + windowMs };
|
||||
entries.set(key, entry);
|
||||
}
|
||||
entry.count += 1;
|
||||
if (entries.size > 4096) {
|
||||
for (const [storedKey, stored] of entries) {
|
||||
if (timestamp >= stored.resetAt) entries.delete(storedKey);
|
||||
}
|
||||
if (entries.size > 4096) entries.delete(entries.keys().next().value);
|
||||
}
|
||||
return {
|
||||
allowed: entry.count <= max,
|
||||
remaining: Math.max(0, max - entry.count),
|
||||
retryAfter: Math.max(1, Math.ceil((entry.resetAt - timestamp) / 1000))
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function proxyCodeChannel(request, response, options = {}) {
|
||||
const internalPath = request.url.replace(/^\/code(?=\/|$)/, "") || "/";
|
||||
const upstream = http.request({
|
||||
|
|
@ -84,13 +121,34 @@ function readCandidateStatus(response, options = {}) {
|
|||
}
|
||||
|
||||
function createApp(options = {}) {
|
||||
const codeLimiter = createFixedWindowLimiter({
|
||||
max: Number(options.codeRateLimitMax || process.env.CODE_RATE_LIMIT_MAX || 600),
|
||||
windowMs: Number(options.codeRateLimitWindowMs || 60000),
|
||||
now: options.now || Date.now
|
||||
});
|
||||
const browseLimiter = createFixedWindowLimiter({
|
||||
max: Number(options.codeBrowseRateLimitMax || process.env.CODE_BROWSE_RATE_LIMIT_MAX || 120),
|
||||
windowMs: Number(options.codeRateLimitWindowMs || 60000),
|
||||
now: options.now || Date.now
|
||||
});
|
||||
return http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://localhost");
|
||||
if (url.pathname === "/code") {
|
||||
response.writeHead(308, { Location: "/code/", "Cache-Control": "no-store" });
|
||||
return response.end();
|
||||
}
|
||||
if (url.pathname.startsWith("/code/")) return proxyCodeChannel(request, response, options);
|
||||
if (url.pathname.startsWith("/code/")) {
|
||||
const address = clientAddress(request);
|
||||
const general = codeLimiter(address);
|
||||
const expensive = /\/(?:archive|src\/commit|raw\/commit|blame)(?:\/|$)/.test(url.pathname)
|
||||
? browseLimiter(address)
|
||||
: { allowed: true, remaining: general.remaining, retryAfter: general.retryAfter };
|
||||
if (!general.allowed || !expensive.allowed) {
|
||||
response.setHeader("Retry-After", String(Math.max(general.retryAfter, expensive.retryAfter)));
|
||||
return json(response, 429, { ok: false, error: "code_channel_rate_limited" });
|
||||
}
|
||||
return proxyCodeChannel(request, response, options);
|
||||
}
|
||||
if (request.method !== "GET" && request.method !== "HEAD") return json(response, 405, { ok: false, error: "method_not_allowed" });
|
||||
if (url.pathname === "/api/code-channel-status") return readCandidateStatus(response, options);
|
||||
if (url.pathname === "/api/status") {
|
||||
|
|
|
|||
Loading…
Reference in a new issue