fix(#338): Add rate limiting to orchestrator API
- 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>
This commit is contained in:
63
apps/orchestrator/src/common/guards/throttler.guard.ts
Normal file
63
apps/orchestrator/src/common/guards/throttler.guard.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user