- Add @nestjs/throttler for rate limiting support - Configure multiple throttle profiles: default (100/min), strict (10/min for spawn/kill), status (200/min for polling) - Apply strict rate limits to spawn and kill endpoints to prevent DoS - Apply higher rate limits to status/health endpoints for monitoring - Add OrchestratorThrottlerGuard with X-Forwarded-For support for proxy setups - Add unit tests for throttler guard Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
64 lines
2.0 KiB
TypeScript
64 lines
2.0 KiB
TypeScript
import { Injectable, ExecutionContext } from "@nestjs/common";
|
|
import { ThrottlerGuard, ThrottlerException } from "@nestjs/throttler";
|
|
|
|
interface RequestWithHeaders {
|
|
headers?: Record<string, string | string[]>;
|
|
ip?: string;
|
|
connection?: { remoteAddress?: string };
|
|
url?: string;
|
|
}
|
|
|
|
/**
|
|
* OrchestratorThrottlerGuard - Rate limiting guard for orchestrator API endpoints
|
|
*
|
|
* Uses the X-Forwarded-For header for client IP identification when behind a proxy,
|
|
* falling back to the direct connection IP.
|
|
*
|
|
* Usage:
|
|
* @UseGuards(OrchestratorThrottlerGuard)
|
|
* @Controller('agents')
|
|
* export class AgentsController { ... }
|
|
*/
|
|
@Injectable()
|
|
export class OrchestratorThrottlerGuard extends ThrottlerGuard {
|
|
/**
|
|
* Get the client IP address for rate limiting tracking
|
|
* Prioritizes X-Forwarded-For header for proxy setups
|
|
*/
|
|
protected getTracker(req: Record<string, unknown>): Promise<string> {
|
|
const request = req as RequestWithHeaders;
|
|
const headers = request.headers;
|
|
|
|
// Check X-Forwarded-For header first (for proxied requests)
|
|
if (headers) {
|
|
const forwardedFor = headers["x-forwarded-for"];
|
|
if (forwardedFor) {
|
|
// Get the first IP in the chain (original client)
|
|
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
if (ips) {
|
|
const clientIp = ips.split(",")[0]?.trim();
|
|
if (clientIp) {
|
|
return Promise.resolve(clientIp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to direct connection IP
|
|
const ip = request.ip ?? request.connection?.remoteAddress ?? "unknown";
|
|
return Promise.resolve(ip);
|
|
}
|
|
|
|
/**
|
|
* Custom error message for rate limit exceeded
|
|
*/
|
|
protected throwThrottlingException(context: ExecutionContext): Promise<void> {
|
|
const request = context.switchToHttp().getRequest<RequestWithHeaders>();
|
|
const endpoint = request.url ?? "unknown";
|
|
|
|
throw new ThrottlerException(
|
|
`Rate limit exceeded for endpoint ${endpoint}. Please try again later.`
|
|
);
|
|
}
|
|
}
|