mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add rate limiting for web interface endpoints
- Install @fastify/rate-limit package - Configure rate limiting with sensible defaults: - 100 requests/minute for GET endpoints - 30 requests/minute for POST endpoints (more restrictive) - Add RateLimitConfig interface for configuration - Apply rate limiting to all /web/* routes - Add /web/api/rate-limit endpoint to check current limits - Skip rate limiting for /health endpoint - Custom error response with retry-after information - Support for X-Forwarded-For header for proxied requests
This commit is contained in:
47
package-lock.json
generated
47
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/websocket": "^9.0.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
@@ -1203,6 +1204,43 @@
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/rate-limit": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz",
|
||||
"integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lukeed/ms": "^2.0.2",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"toad-cache": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/rate-limit/node_modules/fastify-plugin": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
|
||||
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fastify/websocket": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-9.0.0.tgz",
|
||||
@@ -1396,6 +1434,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/ms": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
|
||||
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@malept/cross-spawn-promise": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/websocket": "^9.0.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import websocket from '@fastify/websocket';
|
||||
import rateLimit from '@fastify/rate-limit';
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { WebSocket } from 'ws';
|
||||
import crypto from 'crypto';
|
||||
@@ -25,6 +26,18 @@ export interface WebAuthConfig {
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
// Rate limiting configuration
|
||||
export interface RateLimitConfig {
|
||||
// Maximum requests per time window
|
||||
max: number;
|
||||
// Time window in milliseconds
|
||||
timeWindow: number;
|
||||
// Maximum requests for POST endpoints (typically lower)
|
||||
maxPost: number;
|
||||
// Enable/disable rate limiting
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebServer - HTTP and WebSocket server for remote access
|
||||
*
|
||||
@@ -81,6 +94,14 @@ export interface WebTheme {
|
||||
// Callback type for fetching current theme
|
||||
export type GetThemeCallback = () => WebTheme | null;
|
||||
|
||||
// Default rate limit configuration
|
||||
const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = {
|
||||
max: 100, // 100 requests per minute for GET endpoints
|
||||
timeWindow: 60000, // 1 minute in milliseconds
|
||||
maxPost: 30, // 30 requests per minute for POST endpoints (more restrictive)
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export class WebServer {
|
||||
private server: FastifyInstance;
|
||||
private port: number;
|
||||
@@ -88,6 +109,7 @@ export class WebServer {
|
||||
private webClients: Map<string, WebClient> = new Map();
|
||||
private clientIdCounter: number = 0;
|
||||
private authConfig: WebAuthConfig = { enabled: false, token: null };
|
||||
private rateLimitConfig: RateLimitConfig = { ...DEFAULT_RATE_LIMIT_CONFIG };
|
||||
private getSessionsCallback: GetSessionsCallback | null = null;
|
||||
private getThemeCallback: GetThemeCallback | null = null;
|
||||
|
||||
@@ -134,6 +156,21 @@ export class WebServer {
|
||||
return { ...this.authConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rate limiting configuration
|
||||
*/
|
||||
setRateLimitConfig(config: Partial<RateLimitConfig>) {
|
||||
this.rateLimitConfig = { ...this.rateLimitConfig, ...config };
|
||||
console.log(`Web server rate limiting ${this.rateLimitConfig.enabled ? 'enabled' : 'disabled'} (max: ${this.rateLimitConfig.max}/min, maxPost: ${this.rateLimitConfig.maxPost}/min)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current rate limiting configuration
|
||||
*/
|
||||
getRateLimitConfig(): RateLimitConfig {
|
||||
return { ...this.rateLimitConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new random authentication token
|
||||
*/
|
||||
@@ -198,6 +235,44 @@ export class WebServer {
|
||||
|
||||
// Enable WebSocket support
|
||||
await this.server.register(websocket);
|
||||
|
||||
// Enable rate limiting for web interface endpoints to prevent abuse
|
||||
// Rate limiting is applied globally but can be overridden per-route
|
||||
await this.server.register(rateLimit, {
|
||||
global: false, // Don't apply to all routes by default (we'll apply selectively)
|
||||
max: this.rateLimitConfig.max,
|
||||
timeWindow: this.rateLimitConfig.timeWindow,
|
||||
// Custom error response
|
||||
errorResponseBuilder: (
|
||||
_request: FastifyRequest,
|
||||
context: { statusCode: number; ban: boolean; after: string; max: number; ttl: number }
|
||||
) => {
|
||||
return {
|
||||
statusCode: 429,
|
||||
error: 'Too Many Requests',
|
||||
message: `Rate limit exceeded. You can make ${context.max} requests per ${context.after}. Try again later.`,
|
||||
retryAfter: context.after,
|
||||
};
|
||||
},
|
||||
// Allow list function to skip rate limiting for certain requests
|
||||
allowList: (request: FastifyRequest) => {
|
||||
// Skip rate limiting if disabled
|
||||
if (!this.rateLimitConfig.enabled) return true;
|
||||
// Allow health checks without rate limiting
|
||||
if (request.url === '/health') return true;
|
||||
return false;
|
||||
},
|
||||
// Use IP address as the rate limit key
|
||||
keyGenerator: (request: FastifyRequest) => {
|
||||
// Use X-Forwarded-For if available (for proxied requests), otherwise use IP
|
||||
const forwarded = request.headers['x-forwarded-for'];
|
||||
if (forwarded) {
|
||||
const ip = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0].trim();
|
||||
return ip;
|
||||
}
|
||||
return request.ip;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes() {
|
||||
@@ -267,10 +342,32 @@ export class WebServer {
|
||||
* - /web/mobile - Mobile web interface entry point
|
||||
* - /web/api/* - REST API endpoints for web clients
|
||||
* - /ws/web - WebSocket endpoint for real-time updates to web clients
|
||||
*
|
||||
* Rate limiting is applied to all web interface endpoints to prevent abuse.
|
||||
*/
|
||||
private setupWebInterfaceRoutes() {
|
||||
// Rate limit configuration for GET endpoints
|
||||
const getRateLimitConfig = {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: this.rateLimitConfig.max,
|
||||
timeWindow: this.rateLimitConfig.timeWindow,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Rate limit configuration for POST endpoints (more restrictive)
|
||||
const postRateLimitConfig = {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: this.rateLimitConfig.maxPost,
|
||||
timeWindow: this.rateLimitConfig.timeWindow,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Web interface root - returns info about available interfaces
|
||||
this.server.get('/web', async () => {
|
||||
this.server.get('/web', getRateLimitConfig, async () => {
|
||||
return {
|
||||
name: 'Maestro Web Interface',
|
||||
version: '1.0.0',
|
||||
@@ -285,7 +382,7 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Desktop web interface entry point (placeholder)
|
||||
this.server.get('/web/desktop', async () => {
|
||||
this.server.get('/web/desktop', getRateLimitConfig, async () => {
|
||||
return {
|
||||
message: 'Desktop web interface - Coming soon',
|
||||
description: 'Full-featured collaborative interface for hackathons/team coding',
|
||||
@@ -293,7 +390,7 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Desktop web interface with wildcard for client-side routing
|
||||
this.server.get('/web/desktop/*', async () => {
|
||||
this.server.get('/web/desktop/*', getRateLimitConfig, async () => {
|
||||
return {
|
||||
message: 'Desktop web interface - Coming soon',
|
||||
description: 'Full-featured collaborative interface for hackathons/team coding',
|
||||
@@ -301,7 +398,7 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Mobile web interface entry point (placeholder)
|
||||
this.server.get('/web/mobile', async () => {
|
||||
this.server.get('/web/mobile', getRateLimitConfig, async () => {
|
||||
return {
|
||||
message: 'Mobile web interface - Coming soon',
|
||||
description: 'Lightweight remote control for sending commands from your phone',
|
||||
@@ -309,7 +406,7 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Mobile web interface with wildcard for client-side routing
|
||||
this.server.get('/web/mobile/*', async () => {
|
||||
this.server.get('/web/mobile/*', getRateLimitConfig, async () => {
|
||||
return {
|
||||
message: 'Mobile web interface - Coming soon',
|
||||
description: 'Lightweight remote control for sending commands from your phone',
|
||||
@@ -317,13 +414,34 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Web API namespace root
|
||||
this.server.get('/web/api', async () => {
|
||||
this.server.get('/web/api', getRateLimitConfig, async () => {
|
||||
return {
|
||||
name: 'Maestro Web API',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
sessions: '/web/api/sessions',
|
||||
theme: '/web/api/theme',
|
||||
rateLimit: '/web/api/rate-limit',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
});
|
||||
|
||||
// Rate limit status endpoint - allows clients to check current limits
|
||||
this.server.get('/web/api/rate-limit', getRateLimitConfig, async () => {
|
||||
return {
|
||||
enabled: this.rateLimitConfig.enabled,
|
||||
limits: {
|
||||
get: {
|
||||
max: this.rateLimitConfig.max,
|
||||
timeWindowMs: this.rateLimitConfig.timeWindow,
|
||||
timeWindowDescription: `${this.rateLimitConfig.timeWindow / 1000} seconds`,
|
||||
},
|
||||
post: {
|
||||
max: this.rateLimitConfig.maxPost,
|
||||
timeWindowMs: this.rateLimitConfig.timeWindow,
|
||||
timeWindowDescription: `${this.rateLimitConfig.timeWindow / 1000} seconds`,
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
@@ -477,7 +595,7 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Authentication status endpoint - allows checking if auth is enabled
|
||||
this.server.get('/web/api/auth/status', async () => {
|
||||
this.server.get('/web/api/auth/status', getRateLimitConfig, async () => {
|
||||
return {
|
||||
enabled: this.authConfig.enabled,
|
||||
timestamp: Date.now(),
|
||||
@@ -485,7 +603,8 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Authentication verification endpoint - checks if a token is valid
|
||||
this.server.post('/web/api/auth/verify', async (request) => {
|
||||
// Uses more restrictive POST rate limit to prevent brute force attacks
|
||||
this.server.post('/web/api/auth/verify', postRateLimitConfig, async (request) => {
|
||||
const body = request.body as { token?: string } | undefined;
|
||||
const token = body?.token;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user