mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 08:41:41 +00:00
fix: harden gateway control-plane restart protections
This commit is contained in:
79
src/gateway/control-plane-rate-limit.ts
Normal file
79
src/gateway/control-plane-rate-limit.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { GatewayClient } from "./server-methods/types.js";
|
||||
|
||||
const CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS = 3;
|
||||
const CONTROL_PLANE_RATE_LIMIT_WINDOW_MS = 60_000;
|
||||
|
||||
type Bucket = {
|
||||
count: number;
|
||||
windowStartMs: number;
|
||||
};
|
||||
|
||||
const controlPlaneBuckets = new Map<string, Bucket>();
|
||||
|
||||
function normalizePart(value: unknown, fallback: string): string {
|
||||
if (typeof value !== "string") {
|
||||
return fallback;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : fallback;
|
||||
}
|
||||
|
||||
export function resolveControlPlaneRateLimitKey(client: GatewayClient | null): string {
|
||||
const deviceId = normalizePart(client?.connect?.device?.id, "unknown-device");
|
||||
const clientIp = normalizePart(client?.clientIp, "unknown-ip");
|
||||
return `${deviceId}|${clientIp}`;
|
||||
}
|
||||
|
||||
export function consumeControlPlaneWriteBudget(params: {
|
||||
client: GatewayClient | null;
|
||||
nowMs?: number;
|
||||
}): {
|
||||
allowed: boolean;
|
||||
retryAfterMs: number;
|
||||
remaining: number;
|
||||
key: string;
|
||||
} {
|
||||
const nowMs = params.nowMs ?? Date.now();
|
||||
const key = resolveControlPlaneRateLimitKey(params.client);
|
||||
const bucket = controlPlaneBuckets.get(key);
|
||||
|
||||
if (!bucket || nowMs - bucket.windowStartMs >= CONTROL_PLANE_RATE_LIMIT_WINDOW_MS) {
|
||||
controlPlaneBuckets.set(key, {
|
||||
count: 1,
|
||||
windowStartMs: nowMs,
|
||||
});
|
||||
return {
|
||||
allowed: true,
|
||||
retryAfterMs: 0,
|
||||
remaining: CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS - 1,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
if (bucket.count >= CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS) {
|
||||
const retryAfterMs = Math.max(
|
||||
0,
|
||||
bucket.windowStartMs + CONTROL_PLANE_RATE_LIMIT_WINDOW_MS - nowMs,
|
||||
);
|
||||
return {
|
||||
allowed: false,
|
||||
retryAfterMs,
|
||||
remaining: 0,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
bucket.count += 1;
|
||||
return {
|
||||
allowed: true,
|
||||
retryAfterMs: 0,
|
||||
remaining: Math.max(0, CONTROL_PLANE_RATE_LIMIT_MAX_REQUESTS - bucket.count),
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resetControlPlaneRateLimitState() {
|
||||
controlPlaneBuckets.clear();
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user