Add per-client API rate limiting for hosted hardening
This commit is contained in:
parent
60f52742ad
commit
9ee97ffa8e
@ -28,6 +28,10 @@ Version metadata:
|
||||
- Current values: `api_version=0.3.0`, `schema_version=1.0.0`.
|
||||
- Compatibility policy (current): additive, backward-compatible fields may be introduced in the same API minor version.
|
||||
|
||||
Operational limits:
|
||||
- `MAX_BODY_BYTES` (default `2097152`) limits request payload size.
|
||||
- `MAX_REQUESTS_PER_MINUTE` (default `120`) applies per-client throttling for POST API endpoints.
|
||||
|
||||
## REST API
|
||||
|
||||
### `POST /compile`
|
||||
|
||||
@ -8,9 +8,12 @@ import { validateModel } from "./validate.js";
|
||||
const PORT = Number(process.env.PORT ?? "8787");
|
||||
const MAX_BODY_BYTES = Number(process.env.MAX_BODY_BYTES ?? 2 * 1024 * 1024);
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN ?? "*";
|
||||
const MAX_REQUESTS_PER_MINUTE = Number(process.env.MAX_REQUESTS_PER_MINUTE ?? 120);
|
||||
const FRONTEND_ROOT = join(process.cwd(), "frontend");
|
||||
const API_VERSION = "0.3.0";
|
||||
const SCHEMA_VERSION = "1.0.0";
|
||||
const RATE_WINDOW_MS = 60_000;
|
||||
const rateByClient = new Map();
|
||||
|
||||
const MIME_TYPES = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
@ -133,6 +136,42 @@ function withEnvelopeMeta(payload) {
|
||||
};
|
||||
}
|
||||
|
||||
function clientKey(req) {
|
||||
const forwarded = req.headers["x-forwarded-for"];
|
||||
if (typeof forwarded === "string" && forwarded.trim()) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
return req.socket?.remoteAddress ?? "unknown";
|
||||
}
|
||||
|
||||
function pruneClientRate(now, list) {
|
||||
const minTs = now - RATE_WINDOW_MS;
|
||||
let idx = 0;
|
||||
while (idx < list.length && list[idx] < minTs) {
|
||||
idx += 1;
|
||||
}
|
||||
if (idx > 0) {
|
||||
list.splice(0, idx);
|
||||
}
|
||||
}
|
||||
|
||||
function withinRateLimit(req) {
|
||||
if (!(MAX_REQUESTS_PER_MINUTE > 0)) {
|
||||
return true;
|
||||
}
|
||||
const key = clientKey(req);
|
||||
const now = Date.now();
|
||||
const list = rateByClient.get(key) ?? [];
|
||||
pruneClientRate(now, list);
|
||||
if (list.length >= MAX_REQUESTS_PER_MINUTE) {
|
||||
rateByClient.set(key, list);
|
||||
return false;
|
||||
}
|
||||
list.push(now);
|
||||
rateByClient.set(key, list);
|
||||
return true;
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
if (!req.url || !req.method) {
|
||||
return json(res, 400, errorEnvelope("invalid_request", "Invalid request."));
|
||||
@ -172,6 +211,9 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/analyze") {
|
||||
if (!withinRateLimit(req)) {
|
||||
return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly."));
|
||||
}
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const parsed = parsePayloadOptions(body);
|
||||
@ -188,6 +230,9 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/compile") {
|
||||
if (!withinRateLimit(req)) {
|
||||
return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly."));
|
||||
}
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const parsed = parsePayloadOptions(body);
|
||||
@ -204,6 +249,9 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/layout/auto") {
|
||||
if (!withinRateLimit(req)) {
|
||||
return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly."));
|
||||
}
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const parsed = parsePayloadOptions(body);
|
||||
@ -229,6 +277,9 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/layout/tidy") {
|
||||
if (!withinRateLimit(req)) {
|
||||
return json(res, 429, errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly."));
|
||||
}
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const parsed = parsePayloadOptions(body);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user