chore: Update configuration and dependencies for enhanced MCP server functionality
- Add RATE_LIMIT_MAX_AUTH_REQUESTS to .env.example for improved rate limiting - Update bun.lock and package.json to include new dependencies: @anthropic-ai/sdk, express-rate-limit, and their type definitions - Modify bunfig.toml for build settings and output configuration - Refactor src/config.ts to incorporate rate limiting settings - Implement security middleware for enhanced request validation and sanitization - Introduce rate limiting middleware for API and authentication endpoints - Add tests for configuration validation and rate limiting functionality
This commit is contained in:
106
src/__tests__/config.test.ts
Normal file
106
src/__tests__/config.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { expect, test, describe, beforeEach, afterEach } from 'bun:test';
|
||||
import { MCPServerConfigSchema } from '../schemas/config.schema.js';
|
||||
|
||||
describe('Configuration Validation', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment variables before each test
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment after each test
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
test('validates default configuration', () => {
|
||||
const config = MCPServerConfigSchema.parse({});
|
||||
expect(config).toBeDefined();
|
||||
expect(config.port).toBe(3000);
|
||||
expect(config.environment).toBe('development');
|
||||
});
|
||||
|
||||
test('validates custom port', () => {
|
||||
const config = MCPServerConfigSchema.parse({ port: 8080 });
|
||||
expect(config.port).toBe(8080);
|
||||
});
|
||||
|
||||
test('rejects invalid port', () => {
|
||||
expect(() => MCPServerConfigSchema.parse({ port: 0 })).toThrow();
|
||||
expect(() => MCPServerConfigSchema.parse({ port: 70000 })).toThrow();
|
||||
});
|
||||
|
||||
test('validates environment values', () => {
|
||||
expect(() => MCPServerConfigSchema.parse({ environment: 'development' })).not.toThrow();
|
||||
expect(() => MCPServerConfigSchema.parse({ environment: 'production' })).not.toThrow();
|
||||
expect(() => MCPServerConfigSchema.parse({ environment: 'test' })).not.toThrow();
|
||||
expect(() => MCPServerConfigSchema.parse({ environment: 'invalid' })).toThrow();
|
||||
});
|
||||
|
||||
test('validates rate limiting configuration', () => {
|
||||
const config = MCPServerConfigSchema.parse({
|
||||
rateLimit: {
|
||||
maxRequests: 50,
|
||||
maxAuthRequests: 10
|
||||
}
|
||||
});
|
||||
expect(config.rateLimit.maxRequests).toBe(50);
|
||||
expect(config.rateLimit.maxAuthRequests).toBe(10);
|
||||
});
|
||||
|
||||
test('rejects invalid rate limit values', () => {
|
||||
expect(() => MCPServerConfigSchema.parse({
|
||||
rateLimit: {
|
||||
maxRequests: 0,
|
||||
maxAuthRequests: 5
|
||||
}
|
||||
})).toThrow();
|
||||
|
||||
expect(() => MCPServerConfigSchema.parse({
|
||||
rateLimit: {
|
||||
maxRequests: 100,
|
||||
maxAuthRequests: -1
|
||||
}
|
||||
})).toThrow();
|
||||
});
|
||||
|
||||
test('validates execution timeout', () => {
|
||||
const config = MCPServerConfigSchema.parse({ executionTimeout: 5000 });
|
||||
expect(config.executionTimeout).toBe(5000);
|
||||
});
|
||||
|
||||
test('rejects invalid execution timeout', () => {
|
||||
expect(() => MCPServerConfigSchema.parse({ executionTimeout: 500 })).toThrow();
|
||||
expect(() => MCPServerConfigSchema.parse({ executionTimeout: 400000 })).toThrow();
|
||||
});
|
||||
|
||||
test('validates transport settings', () => {
|
||||
const config = MCPServerConfigSchema.parse({
|
||||
useStdioTransport: true,
|
||||
useHttpTransport: false
|
||||
});
|
||||
expect(config.useStdioTransport).toBe(true);
|
||||
expect(config.useHttpTransport).toBe(false);
|
||||
});
|
||||
|
||||
test('validates CORS settings', () => {
|
||||
const config = MCPServerConfigSchema.parse({
|
||||
corsOrigin: 'https://example.com'
|
||||
});
|
||||
expect(config.corsOrigin).toBe('https://example.com');
|
||||
});
|
||||
|
||||
test('validates debug settings', () => {
|
||||
const config = MCPServerConfigSchema.parse({
|
||||
debugMode: true,
|
||||
debugStdio: true,
|
||||
debugHttp: true,
|
||||
silentStartup: false
|
||||
});
|
||||
expect(config.debugMode).toBe(true);
|
||||
expect(config.debugStdio).toBe(true);
|
||||
expect(config.debugHttp).toBe(true);
|
||||
expect(config.silentStartup).toBe(false);
|
||||
});
|
||||
});
|
||||
85
src/__tests__/rate-limit.test.ts
Normal file
85
src/__tests__/rate-limit.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { expect, test, describe, beforeAll, afterAll } from 'bun:test';
|
||||
import express from 'express';
|
||||
import { apiLimiter, authLimiter } from '../middleware/rate-limit.middleware.js';
|
||||
import supertest from 'supertest';
|
||||
|
||||
describe('Rate Limiting Middleware', () => {
|
||||
let app: express.Application;
|
||||
let request: supertest.SuperTest<supertest.Test>;
|
||||
|
||||
beforeAll(() => {
|
||||
app = express();
|
||||
|
||||
// Set up test routes with rate limiting
|
||||
app.use('/api', apiLimiter);
|
||||
app.use('/auth', authLimiter);
|
||||
|
||||
// Test endpoints
|
||||
app.get('/api/test', (req, res) => {
|
||||
res.json({ message: 'API test successful' });
|
||||
});
|
||||
|
||||
app.post('/auth/login', (req, res) => {
|
||||
res.json({ message: 'Login successful' });
|
||||
});
|
||||
|
||||
request = supertest(app);
|
||||
});
|
||||
|
||||
test('allows requests within API rate limit', async () => {
|
||||
// Make multiple requests within the limit
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const response = await request.get('/api/test');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('API test successful');
|
||||
}
|
||||
});
|
||||
|
||||
test('enforces API rate limit', async () => {
|
||||
// Make more requests than the limit allows
|
||||
const requests = Array(150).fill(null).map(() =>
|
||||
request.get('/api/test')
|
||||
);
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
// Some requests should be successful, others should be rate limited
|
||||
const successfulRequests = responses.filter(r => r.status === 200);
|
||||
const limitedRequests = responses.filter(r => r.status === 429);
|
||||
|
||||
expect(successfulRequests.length).toBeGreaterThan(0);
|
||||
expect(limitedRequests.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('allows requests within auth rate limit', async () => {
|
||||
// Make multiple requests within the limit
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const response = await request.post('/auth/login');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Login successful');
|
||||
}
|
||||
});
|
||||
|
||||
test('enforces stricter auth rate limit', async () => {
|
||||
// Make more requests than the auth limit allows
|
||||
const requests = Array(10).fill(null).map(() =>
|
||||
request.post('/auth/login')
|
||||
);
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
// Some requests should be successful, others should be rate limited
|
||||
const successfulRequests = responses.filter(r => r.status === 200);
|
||||
const limitedRequests = responses.filter(r => r.status === 429);
|
||||
|
||||
expect(successfulRequests.length).toBeLessThan(10);
|
||||
expect(limitedRequests.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('includes rate limit headers', async () => {
|
||||
const response = await request.get('/api/test');
|
||||
expect(response.headers['ratelimit-limit']).toBeDefined();
|
||||
expect(response.headers['ratelimit-remaining']).toBeDefined();
|
||||
expect(response.headers['ratelimit-reset']).toBeDefined();
|
||||
});
|
||||
});
|
||||
169
src/__tests__/security.test.ts
Normal file
169
src/__tests__/security.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { describe, expect, test, beforeEach } from 'bun:test';
|
||||
import express, { Request, Response } from 'express';
|
||||
import request from 'supertest';
|
||||
import { SecurityMiddleware } from '../security/enhanced-middleware';
|
||||
|
||||
describe('SecurityMiddleware', () => {
|
||||
const app = express();
|
||||
|
||||
// Apply security middleware
|
||||
app.use(SecurityMiddleware.createRouter());
|
||||
|
||||
// Test routes
|
||||
app.get('/test', (_req: Request, res: Response) => {
|
||||
res.status(200).json({ message: 'Test successful' });
|
||||
});
|
||||
|
||||
app.post('/test', (req: Request, res: Response) => {
|
||||
res.status(200).json(req.body);
|
||||
});
|
||||
|
||||
app.post('/auth/login', (_req: Request, res: Response) => {
|
||||
res.status(200).json({ message: 'Auth successful' });
|
||||
});
|
||||
|
||||
describe('Security Headers', () => {
|
||||
test('should set security headers correctly', async () => {
|
||||
const response = await request(app).get('/test');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['x-frame-options']).toBe('DENY');
|
||||
expect(response.headers['x-xss-protection']).toBe('1; mode=block');
|
||||
expect(response.headers['x-content-type-options']).toBe('nosniff');
|
||||
expect(response.headers['referrer-policy']).toBe('strict-origin-when-cross-origin');
|
||||
expect(response.headers['strict-transport-security']).toBe('max-age=31536000; includeSubDomains; preload');
|
||||
expect(response.headers['x-permitted-cross-domain-policies']).toBe('none');
|
||||
expect(response.headers['cross-origin-embedder-policy']).toBe('require-corp');
|
||||
expect(response.headers['cross-origin-opener-policy']).toBe('same-origin');
|
||||
expect(response.headers['cross-origin-resource-policy']).toBe('same-origin');
|
||||
expect(response.headers['origin-agent-cluster']).toBe('?1');
|
||||
expect(response.headers['x-powered-by']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should set Content-Security-Policy header correctly', async () => {
|
||||
const response = await request(app).get('/test');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['content-security-policy']).toContain("default-src 'self'");
|
||||
expect(response.headers['content-security-policy']).toContain("script-src 'self' 'unsafe-inline'");
|
||||
expect(response.headers['content-security-policy']).toContain("style-src 'self' 'unsafe-inline'");
|
||||
expect(response.headers['content-security-policy']).toContain("img-src 'self' data: https:");
|
||||
expect(response.headers['content-security-policy']).toContain("font-src 'self'");
|
||||
expect(response.headers['content-security-policy']).toContain("connect-src 'self'");
|
||||
expect(response.headers['content-security-policy']).toContain("frame-ancestors 'none'");
|
||||
expect(response.headers['content-security-policy']).toContain("form-action 'self'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Validation', () => {
|
||||
test('should reject requests with long URLs', async () => {
|
||||
const longUrl = '/test?' + 'x'.repeat(2500);
|
||||
const response = await request(app).get(longUrl);
|
||||
expect(response.status).toBe(413);
|
||||
expect(response.body.error).toBe(true);
|
||||
expect(response.body.message).toContain('URL too long');
|
||||
});
|
||||
|
||||
test('should reject large request bodies', async () => {
|
||||
const largeBody = { data: 'x'.repeat(2 * 1024 * 1024) }; // 2MB
|
||||
const response = await request(app)
|
||||
.post('/test')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(largeBody);
|
||||
expect(response.status).toBe(413);
|
||||
expect(response.body.error).toBe(true);
|
||||
expect(response.body.message).toContain('Request body too large');
|
||||
});
|
||||
|
||||
test('should require correct content type for POST requests', async () => {
|
||||
const response = await request(app)
|
||||
.post('/test')
|
||||
.set('Content-Type', 'text/plain')
|
||||
.send('test data');
|
||||
expect(response.status).toBe(415);
|
||||
expect(response.body.error).toBe(true);
|
||||
expect(response.body.message).toContain('Content-Type must be application/json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Sanitization', () => {
|
||||
test('should sanitize string input with HTML', async () => {
|
||||
const response = await request(app)
|
||||
.post('/test')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({ text: '<script>alert("xss")</script>Hello<img src="x" onerror="alert(1)">' });
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.text).toBe('Hello');
|
||||
});
|
||||
|
||||
test('should sanitize nested object input', async () => {
|
||||
const response = await request(app)
|
||||
.post('/test')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({
|
||||
user: {
|
||||
name: '<script>alert("xss")</script>John',
|
||||
bio: '<img src="x" onerror="alert(1)">Developer'
|
||||
}
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.user.name).toBe('John');
|
||||
expect(response.body.user.bio).toBe('Developer');
|
||||
});
|
||||
|
||||
test('should sanitize array input', async () => {
|
||||
const response = await request(app)
|
||||
.post('/test')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({
|
||||
items: [
|
||||
'<script>alert(1)</script>Hello',
|
||||
'<img src="x" onerror="alert(1)">World'
|
||||
]
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.items[0]).toBe('Hello');
|
||||
expect(response.body.items[1]).toBe('World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting', () => {
|
||||
beforeEach(() => {
|
||||
SecurityMiddleware.clearRateLimits();
|
||||
});
|
||||
|
||||
test('should enforce regular rate limits', async () => {
|
||||
// Make 50 requests (should succeed)
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const response = await request(app).get('/test');
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
// 51st request should fail
|
||||
const response = await request(app).get('/test');
|
||||
expect(response.status).toBe(429);
|
||||
expect(response.body.error).toBe(true);
|
||||
expect(response.body.message).toContain('Too many requests');
|
||||
});
|
||||
|
||||
test('should enforce stricter auth rate limits', async () => {
|
||||
// Make 3 auth requests (should succeed)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const response = await request(app)
|
||||
.post('/auth/login')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({});
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
// 4th auth request should fail
|
||||
const response = await request(app)
|
||||
.post('/auth/login')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({});
|
||||
expect(response.status).toBe(429);
|
||||
expect(response.body.error).toBe(true);
|
||||
expect(response.body.message).toContain('Too many authentication requests');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,50 +3,59 @@
|
||||
* Values can be overridden using environment variables
|
||||
*/
|
||||
|
||||
export interface MCPServerConfig {
|
||||
// Server configuration
|
||||
port: number;
|
||||
environment: string;
|
||||
import { MCPServerConfigSchema, MCPServerConfigType } from './schemas/config.schema.js';
|
||||
import { logger } from './utils/logger.js';
|
||||
|
||||
// Execution settings
|
||||
executionTimeout: number;
|
||||
streamingEnabled: boolean;
|
||||
function loadConfig(): MCPServerConfigType {
|
||||
try {
|
||||
const rawConfig = {
|
||||
// Server configuration
|
||||
port: parseInt(process.env.PORT || '3000', 10),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
|
||||
// Transport settings
|
||||
useStdioTransport: boolean;
|
||||
useHttpTransport: boolean;
|
||||
// Execution settings
|
||||
executionTimeout: parseInt(process.env.EXECUTION_TIMEOUT || '30000', 10),
|
||||
streamingEnabled: process.env.STREAMING_ENABLED === 'true',
|
||||
|
||||
// Debug and logging
|
||||
debugMode: boolean;
|
||||
debugStdio: boolean;
|
||||
debugHttp: boolean;
|
||||
silentStartup: boolean;
|
||||
// Transport settings
|
||||
useStdioTransport: process.env.USE_STDIO_TRANSPORT === 'true',
|
||||
useHttpTransport: process.env.USE_HTTP_TRANSPORT === 'true',
|
||||
|
||||
// CORS settings
|
||||
corsOrigin: string;
|
||||
// Debug and logging
|
||||
debugMode: process.env.DEBUG_MODE === 'true',
|
||||
debugStdio: process.env.DEBUG_STDIO === 'true',
|
||||
debugHttp: process.env.DEBUG_HTTP === 'true',
|
||||
silentStartup: process.env.SILENT_STARTUP === 'true',
|
||||
|
||||
// CORS settings
|
||||
corsOrigin: process.env.CORS_ORIGIN || '*',
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: {
|
||||
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10),
|
||||
maxAuthRequests: parseInt(process.env.RATE_LIMIT_MAX_AUTH_REQUESTS || '5', 10),
|
||||
},
|
||||
};
|
||||
|
||||
// Validate and parse configuration
|
||||
const validatedConfig = MCPServerConfigSchema.parse(rawConfig);
|
||||
|
||||
// Log validation success
|
||||
if (!validatedConfig.silentStartup) {
|
||||
logger.info('Configuration validated successfully');
|
||||
if (validatedConfig.debugMode) {
|
||||
logger.debug('Current configuration:', validatedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return validatedConfig;
|
||||
} catch (error) {
|
||||
// Log validation errors
|
||||
logger.error('Configuration validation failed:', error);
|
||||
throw new Error('Invalid configuration. Please check your environment variables.');
|
||||
}
|
||||
}
|
||||
|
||||
export const APP_CONFIG: MCPServerConfig = {
|
||||
// Server configuration
|
||||
port: parseInt(process.env.PORT || '3000', 10),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
|
||||
// Execution settings
|
||||
executionTimeout: parseInt(process.env.EXECUTION_TIMEOUT || '30000', 10),
|
||||
streamingEnabled: process.env.STREAMING_ENABLED === 'true',
|
||||
|
||||
// Transport settings
|
||||
useStdioTransport: process.env.USE_STDIO_TRANSPORT === 'true',
|
||||
useHttpTransport: process.env.USE_HTTP_TRANSPORT === 'true',
|
||||
|
||||
// Debug and logging
|
||||
debugMode: process.env.DEBUG_MODE === 'true',
|
||||
debugStdio: process.env.DEBUG_STDIO === 'true',
|
||||
debugHttp: process.env.DEBUG_HTTP === 'true',
|
||||
silentStartup: process.env.SILENT_STARTUP === 'true',
|
||||
|
||||
// CORS settings
|
||||
corsOrigin: process.env.CORS_ORIGIN || '*',
|
||||
};
|
||||
|
||||
export const APP_CONFIG = loadConfig();
|
||||
export type { MCPServerConfigType };
|
||||
export default APP_CONFIG;
|
||||
33
src/index.ts
33
src/index.ts
@@ -5,12 +5,16 @@
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import { MCPServer } from './mcp/MCPServer.js';
|
||||
import { loggingMiddleware, timeoutMiddleware } from './mcp/middleware/index.js';
|
||||
import { StdioTransport } from './mcp/transports/stdio.transport.js';
|
||||
import { HttpTransport } from './mcp/transports/http.transport.js';
|
||||
import { APP_CONFIG } from './config.js';
|
||||
import { logger } from "./utils/logger.js";
|
||||
import { openApiConfig } from './openapi.js';
|
||||
import { apiLimiter, authLimiter } from './middleware/rate-limit.middleware.js';
|
||||
import { SecurityMiddleware } from './security/enhanced-middleware.js';
|
||||
|
||||
// Home Assistant tools
|
||||
import { LightsControlTool } from './tools/homeassistant/lights.tool.js';
|
||||
@@ -111,10 +115,37 @@ async function main() {
|
||||
if (APP_CONFIG.useHttpTransport) {
|
||||
logger.info('Using HTTP transport on port ' + APP_CONFIG.port);
|
||||
const app = express();
|
||||
|
||||
// Apply enhanced security middleware
|
||||
app.use(SecurityMiddleware.applySecurityHeaders);
|
||||
|
||||
// CORS configuration
|
||||
app.use(cors({
|
||||
origin: APP_CONFIG.corsOrigin
|
||||
origin: APP_CONFIG.corsOrigin,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
maxAge: 86400 // 24 hours
|
||||
}));
|
||||
|
||||
// Apply rate limiting to all routes
|
||||
app.use('/api', apiLimiter);
|
||||
app.use('/auth', authLimiter);
|
||||
|
||||
// Swagger UI setup
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiConfig, {
|
||||
explorer: true,
|
||||
customCss: '.swagger-ui .topbar { display: none }',
|
||||
customSiteTitle: 'Home Assistant MCP API Documentation'
|
||||
}));
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
const httpTransport = new HttpTransport({
|
||||
port: APP_CONFIG.port,
|
||||
corsOrigin: APP_CONFIG.corsOrigin,
|
||||
|
||||
26
src/middleware/rate-limit.middleware.ts
Normal file
26
src/middleware/rate-limit.middleware.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { APP_CONFIG } from '../config.js';
|
||||
|
||||
// Create a limiter for API endpoints
|
||||
export const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: APP_CONFIG.rateLimit?.maxRequests || 100, // Limit each IP to 100 requests per windowMs
|
||||
message: {
|
||||
status: 'error',
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
},
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
});
|
||||
|
||||
// Create a stricter limiter for authentication endpoints
|
||||
export const authLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: APP_CONFIG.rateLimit?.maxAuthRequests || 5, // Limit each IP to 5 login requests per hour
|
||||
message: {
|
||||
status: 'error',
|
||||
message: 'Too many login attempts from this IP, please try again later.'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
284
src/openapi.ts
Normal file
284
src/openapi.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import type { OpenAPIV3 } from 'openapi-types'
|
||||
|
||||
export const openApiConfig: OpenAPIV3.Document = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Home Assistant MCP API',
|
||||
description: `
|
||||
# Home Assistant Model Context Protocol API
|
||||
|
||||
The Model Context Protocol (MCP) provides a standardized interface for AI tools to interact with Home Assistant.
|
||||
This API documentation covers all available endpoints and features of the MCP server.
|
||||
|
||||
## Features
|
||||
- Tool Management
|
||||
- Real-time Communication
|
||||
- Health Monitoring
|
||||
- Rate Limiting
|
||||
- Authentication
|
||||
- Server-Sent Events (SSE)
|
||||
`,
|
||||
version: '1.0.0',
|
||||
contact: {
|
||||
name: 'Home Assistant MCP',
|
||||
url: 'https://github.com/your-repo/homeassistant-mcp'
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3000',
|
||||
description: 'Local development server'
|
||||
}
|
||||
],
|
||||
paths: {
|
||||
'/health': {
|
||||
get: {
|
||||
tags: ['Health'],
|
||||
summary: 'Health check endpoint',
|
||||
description: 'Returns the current health status and version of the server',
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Server is healthy',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/HealthCheck'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/tools': {
|
||||
get: {
|
||||
tags: ['Tools'],
|
||||
summary: 'List available tools',
|
||||
description: 'Returns a list of all registered tools and their capabilities',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'List of available tools',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/Tool'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'401': {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/mcp/execute': {
|
||||
post: {
|
||||
tags: ['MCP'],
|
||||
summary: 'Execute a tool command',
|
||||
description: 'Executes a command using a registered tool',
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ExecuteRequest'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Command executed successfully',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/ExecuteResponse'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'400': {
|
||||
description: 'Invalid request',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'401': {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/mcp/stream': {
|
||||
get: {
|
||||
tags: ['SSE'],
|
||||
summary: 'Stream events',
|
||||
description: 'Opens a Server-Sent Events connection for real-time updates',
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'SSE stream established',
|
||||
content: {
|
||||
'text/event-stream': {
|
||||
schema: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'401': {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'Error code'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Error message'
|
||||
}
|
||||
},
|
||||
required: ['code', 'message']
|
||||
},
|
||||
HealthCheck: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['ok', 'error'],
|
||||
description: 'Current health status'
|
||||
},
|
||||
version: {
|
||||
type: 'string',
|
||||
description: 'Server version'
|
||||
}
|
||||
},
|
||||
required: ['status', 'version']
|
||||
},
|
||||
Tool: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Tool name'
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Tool description'
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
description: 'Tool parameters schema'
|
||||
},
|
||||
returns: {
|
||||
type: 'object',
|
||||
description: 'Tool return value schema'
|
||||
}
|
||||
},
|
||||
required: ['name', 'description']
|
||||
},
|
||||
ExecuteRequest: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tool: {
|
||||
type: 'string',
|
||||
description: 'Name of the tool to execute'
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'Tool parameters'
|
||||
}
|
||||
},
|
||||
required: ['tool']
|
||||
},
|
||||
ExecuteResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'Tool execution result'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
description: 'Error message if execution failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'JWT token for authentication'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: [
|
||||
{
|
||||
name: 'Health',
|
||||
description: 'Health check endpoints for monitoring server status'
|
||||
},
|
||||
{
|
||||
name: 'MCP',
|
||||
description: 'Model Context Protocol endpoints for tool execution'
|
||||
},
|
||||
{
|
||||
name: 'Tools',
|
||||
description: 'Tool management endpoints for listing and configuring tools'
|
||||
},
|
||||
{
|
||||
name: 'SSE',
|
||||
description: 'Server-Sent Events endpoints for real-time updates'
|
||||
}
|
||||
],
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
]
|
||||
}
|
||||
79
src/schemas/config.schema.ts
Normal file
79
src/schemas/config.schema.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const RateLimitSchema = z.object({
|
||||
maxRequests: z.number().int().min(1).default(100),
|
||||
maxAuthRequests: z.number().int().min(1).default(5),
|
||||
});
|
||||
|
||||
export const MCPServerConfigSchema = z.object({
|
||||
// Server configuration
|
||||
port: z.number().int().min(1).max(65535).default(3000),
|
||||
environment: z.enum(['development', 'test', 'production']).default('development'),
|
||||
|
||||
// Execution settings
|
||||
executionTimeout: z.number().int().min(1000).max(300000).default(30000),
|
||||
streamingEnabled: z.boolean().default(false),
|
||||
|
||||
// Transport settings
|
||||
useStdioTransport: z.boolean().default(false),
|
||||
useHttpTransport: z.boolean().default(true),
|
||||
|
||||
// Debug and logging
|
||||
debugMode: z.boolean().default(false),
|
||||
debugStdio: z.boolean().default(false),
|
||||
debugHttp: z.boolean().default(false),
|
||||
silentStartup: z.boolean().default(false),
|
||||
|
||||
// CORS settings
|
||||
corsOrigin: z.string().default('*'),
|
||||
|
||||
// Rate limiting
|
||||
rateLimit: RateLimitSchema.default({
|
||||
maxRequests: 100,
|
||||
maxAuthRequests: 5,
|
||||
}),
|
||||
|
||||
// Speech features
|
||||
speech: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
wakeWord: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
threshold: z.number().min(0).max(1).default(0.05),
|
||||
}),
|
||||
asr: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
model: z.enum(['base', 'small', 'medium', 'large']).default('base'),
|
||||
engine: z.enum(['faster_whisper', 'whisper']).default('faster_whisper'),
|
||||
beamSize: z.number().int().min(1).max(10).default(5),
|
||||
computeType: z.enum(['float32', 'float16', 'int8']).default('float32'),
|
||||
language: z.string().default('en'),
|
||||
}),
|
||||
audio: z.object({
|
||||
minSpeechDuration: z.number().min(0.1).max(10).default(1.0),
|
||||
silenceDuration: z.number().min(0.1).max(5).default(0.5),
|
||||
sampleRate: z.number().int().min(8000).max(48000).default(16000),
|
||||
channels: z.number().int().min(1).max(2).default(1),
|
||||
chunkSize: z.number().int().min(256).max(4096).default(1024),
|
||||
}),
|
||||
}).default({
|
||||
enabled: false,
|
||||
wakeWord: { enabled: false, threshold: 0.05 },
|
||||
asr: {
|
||||
enabled: false,
|
||||
model: 'base',
|
||||
engine: 'faster_whisper',
|
||||
beamSize: 5,
|
||||
computeType: 'float32',
|
||||
language: 'en',
|
||||
},
|
||||
audio: {
|
||||
minSpeechDuration: 1.0,
|
||||
silenceDuration: 0.5,
|
||||
sampleRate: 16000,
|
||||
channels: 1,
|
||||
chunkSize: 1024,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export type MCPServerConfigType = z.infer<typeof MCPServerConfigSchema>;
|
||||
135
src/security/__tests__/enhanced-middleware.test.ts
Normal file
135
src/security/__tests__/enhanced-middleware.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { expect, test, describe, beforeEach, afterEach } from 'bun:test';
|
||||
import { SecurityMiddleware } from '../enhanced-middleware';
|
||||
|
||||
describe('Enhanced Security Middleware', () => {
|
||||
describe('Security Headers', () => {
|
||||
test('applies security headers correctly', () => {
|
||||
const request = new Request('http://localhost');
|
||||
SecurityMiddleware.applySecurityHeaders(request);
|
||||
|
||||
expect(request.headers.get('content-security-policy')).toBeDefined();
|
||||
expect(request.headers.get('x-frame-options')).toBe('DENY');
|
||||
expect(request.headers.get('strict-transport-security')).toBeDefined();
|
||||
expect(request.headers.get('x-xss-protection')).toBe('1; mode=block');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Validation', () => {
|
||||
test('validates request size', async () => {
|
||||
const largeBody = 'x'.repeat(2 * 1024 * 1024); // 2MB
|
||||
const request = new Request('http://localhost', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'content-length': largeBody.length.toString()
|
||||
},
|
||||
body: JSON.stringify({ data: largeBody })
|
||||
});
|
||||
|
||||
await expect(SecurityMiddleware.validateRequest(request)).rejects.toThrow('Request body too large');
|
||||
});
|
||||
|
||||
test('validates URL length', async () => {
|
||||
const longUrl = 'http://localhost/' + 'x'.repeat(3000);
|
||||
const request = new Request(longUrl);
|
||||
|
||||
await expect(SecurityMiddleware.validateRequest(request)).rejects.toThrow('URL too long');
|
||||
});
|
||||
|
||||
test('validates and sanitizes POST request body', async () => {
|
||||
const request = new Request('http://localhost', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: '<script>alert("xss")</script>Hello',
|
||||
age: 25
|
||||
})
|
||||
});
|
||||
|
||||
await SecurityMiddleware.validateRequest(request);
|
||||
const body = await request.json();
|
||||
expect(body.name).not.toContain('<script>');
|
||||
expect(body.age).toBe(25);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Sanitization', () => {
|
||||
test('sanitizes string input', () => {
|
||||
const input = '<script>alert("xss")</script>Hello<img src="x" onerror="alert(1)">';
|
||||
const sanitized = SecurityMiddleware.sanitizeInput(input);
|
||||
expect(sanitized).toBe('Hello');
|
||||
});
|
||||
|
||||
test('sanitizes nested object input', () => {
|
||||
const input = {
|
||||
name: '<script>alert("xss")</script>John',
|
||||
details: {
|
||||
bio: '<img src="x" onerror="alert(1)">Web Developer'
|
||||
}
|
||||
};
|
||||
const sanitized = SecurityMiddleware.sanitizeInput(input) as any;
|
||||
expect(sanitized.name).toBe('John');
|
||||
expect(sanitized.details.bio).toBe('Web Developer');
|
||||
});
|
||||
|
||||
test('sanitizes array input', () => {
|
||||
const input = [
|
||||
'<script>alert(1)</script>Hello',
|
||||
'<img src="x" onerror="alert(1)">World'
|
||||
];
|
||||
const sanitized = SecurityMiddleware.sanitizeInput(input) as string[];
|
||||
expect(sanitized[0]).toBe('Hello');
|
||||
expect(sanitized[1]).toBe('World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting', () => {
|
||||
beforeEach(() => {
|
||||
// Reset rate limit stores before each test
|
||||
(SecurityMiddleware as any).rateLimitStore.clear();
|
||||
(SecurityMiddleware as any).authLimitStore.clear();
|
||||
});
|
||||
|
||||
test('enforces regular rate limits', () => {
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
// Should allow up to 100 requests
|
||||
for (let i = 0; i < 100; i++) {
|
||||
expect(() => SecurityMiddleware.checkRateLimit(ip, false)).not.toThrow();
|
||||
}
|
||||
|
||||
// Should block the 101st request
|
||||
expect(() => SecurityMiddleware.checkRateLimit(ip, false)).toThrow('Too many requests');
|
||||
});
|
||||
|
||||
test('enforces stricter auth rate limits', () => {
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
// Should allow up to 5 auth requests
|
||||
for (let i = 0; i < 5; i++) {
|
||||
expect(() => SecurityMiddleware.checkRateLimit(ip, true)).not.toThrow();
|
||||
}
|
||||
|
||||
// Should block the 6th auth request
|
||||
expect(() => SecurityMiddleware.checkRateLimit(ip, true)).toThrow('Too many authentication requests');
|
||||
});
|
||||
|
||||
test('resets rate limits after window expires', async () => {
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
// Make max requests
|
||||
for (let i = 0; i < 100; i++) {
|
||||
SecurityMiddleware.checkRateLimit(ip, false);
|
||||
}
|
||||
|
||||
// Wait for rate limit window to expire
|
||||
const store = (SecurityMiddleware as any).rateLimitStore.get(ip);
|
||||
store.resetTime = Date.now() - 1000; // Set reset time to the past
|
||||
|
||||
// Should allow requests again
|
||||
expect(() => SecurityMiddleware.checkRateLimit(ip, false)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
189
src/security/enhanced-middleware.ts
Normal file
189
src/security/enhanced-middleware.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import express, { Request, Response, NextFunction, Router } from 'express';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
// Custom error type with status code
|
||||
class SecurityError extends Error {
|
||||
constructor(public message: string, public statusCode: number) {
|
||||
super(message);
|
||||
this.name = 'SecurityError';
|
||||
}
|
||||
}
|
||||
|
||||
// Security configuration
|
||||
const SECURITY_CONFIG = {
|
||||
FRAME_OPTIONS: 'DENY',
|
||||
XSS_PROTECTION: '1; mode=block',
|
||||
REFERRER_POLICY: 'strict-origin-when-cross-origin',
|
||||
HSTS_MAX_AGE: 31536000, // 1 year in seconds
|
||||
CSP: {
|
||||
'default-src': ["'self'"],
|
||||
'script-src': ["'self'", "'unsafe-inline'"],
|
||||
'style-src': ["'self'", "'unsafe-inline'"],
|
||||
'img-src': ["'self'", 'data:', 'https:'],
|
||||
'font-src': ["'self'"],
|
||||
'connect-src': ["'self'"],
|
||||
'frame-ancestors': ["'none'"],
|
||||
'form-action': ["'self'"]
|
||||
},
|
||||
// Request validation config
|
||||
MAX_URL_LENGTH: 2048,
|
||||
MAX_BODY_SIZE: '1mb',
|
||||
// Rate limiting config
|
||||
RATE_LIMIT: {
|
||||
WINDOW_MS: 15 * 60 * 1000, // 15 minutes
|
||||
MAX_REQUESTS: 50,
|
||||
MESSAGE: 'Too many requests from this IP, please try again later.'
|
||||
},
|
||||
AUTH_RATE_LIMIT: {
|
||||
WINDOW_MS: 15 * 60 * 1000,
|
||||
MAX_REQUESTS: 3,
|
||||
MESSAGE: 'Too many authentication attempts from this IP, please try again later.'
|
||||
}
|
||||
};
|
||||
|
||||
export class SecurityMiddleware {
|
||||
private static rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||
private static authLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||
|
||||
private static validateRequest(req: Request): void {
|
||||
// Check URL length
|
||||
if (req.originalUrl.length > SECURITY_CONFIG.MAX_URL_LENGTH) {
|
||||
throw new SecurityError('URL too long', 413);
|
||||
}
|
||||
|
||||
// Check content type for POST requests
|
||||
if (req.method === 'POST' && req.headers['content-type'] !== 'application/json') {
|
||||
throw new SecurityError('Content-Type must be application/json', 415);
|
||||
}
|
||||
}
|
||||
|
||||
private static sanitizeInput(input: unknown): unknown {
|
||||
if (typeof input === 'string') {
|
||||
return sanitizeHtml(input, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {}
|
||||
});
|
||||
} else if (Array.isArray(input)) {
|
||||
return input.map(item => SecurityMiddleware.sanitizeInput(item));
|
||||
} else if (input && typeof input === 'object') {
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
sanitized[key] = SecurityMiddleware.sanitizeInput(value);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
private static applySecurityHeaders(res: Response): void {
|
||||
// Remove X-Powered-By header
|
||||
res.removeHeader('X-Powered-By');
|
||||
|
||||
// Set security headers
|
||||
res.setHeader('X-Frame-Options', SECURITY_CONFIG.FRAME_OPTIONS);
|
||||
res.setHeader('X-XSS-Protection', SECURITY_CONFIG.XSS_PROTECTION);
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('Referrer-Policy', SECURITY_CONFIG.REFERRER_POLICY);
|
||||
res.setHeader('Strict-Transport-Security', `max-age=${SECURITY_CONFIG.HSTS_MAX_AGE}; includeSubDomains; preload`);
|
||||
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
|
||||
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
||||
res.setHeader('Origin-Agent-Cluster', '?1');
|
||||
|
||||
// Set Content-Security-Policy
|
||||
const cspDirectives = Object.entries(SECURITY_CONFIG.CSP)
|
||||
.map(([key, values]) => `${key} ${values.join(' ')}`)
|
||||
.join('; ');
|
||||
res.setHeader('Content-Security-Policy', cspDirectives);
|
||||
}
|
||||
|
||||
private static checkRateLimit(req: Request): void {
|
||||
const ip = req.ip || req.socket.remoteAddress || 'unknown';
|
||||
const now = Date.now();
|
||||
const isAuth = req.path.startsWith('/auth');
|
||||
const store = isAuth ? SecurityMiddleware.authLimitStore : SecurityMiddleware.rateLimitStore;
|
||||
const config = isAuth ? SECURITY_CONFIG.AUTH_RATE_LIMIT : SECURITY_CONFIG.RATE_LIMIT;
|
||||
|
||||
let record = store.get(ip);
|
||||
if (!record || now > record.resetTime) {
|
||||
record = { count: 1, resetTime: now + config.WINDOW_MS };
|
||||
} else {
|
||||
record.count++;
|
||||
if (record.count > config.MAX_REQUESTS) {
|
||||
throw new SecurityError(
|
||||
isAuth ? 'Too many authentication requests' : 'Too many requests',
|
||||
429
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
store.set(ip, record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Express router with all security middleware
|
||||
*/
|
||||
public static createRouter(): Router {
|
||||
const router = express.Router();
|
||||
|
||||
// Body parser middleware with size limit
|
||||
router.use(express.json({
|
||||
limit: SECURITY_CONFIG.MAX_BODY_SIZE,
|
||||
type: 'application/json'
|
||||
}));
|
||||
|
||||
// Error handler for body-parser errors
|
||||
router.use((err: Error, _req: Request, res: Response, next: NextFunction) => {
|
||||
if (err instanceof SyntaxError && 'type' in err && err.type === 'entity.too.large') {
|
||||
res.status(413).json({
|
||||
error: true,
|
||||
message: 'Request body too large'
|
||||
});
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Main security middleware
|
||||
router.use((req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
// Apply security headers
|
||||
SecurityMiddleware.applySecurityHeaders(res);
|
||||
|
||||
// Check rate limits
|
||||
SecurityMiddleware.checkRateLimit(req);
|
||||
|
||||
// Validate request
|
||||
SecurityMiddleware.validateRequest(req);
|
||||
|
||||
// Sanitize input
|
||||
if (req.body) {
|
||||
req.body = SecurityMiddleware.sanitizeInput(req.body);
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof SecurityError) {
|
||||
res.status(error.statusCode).json({
|
||||
error: true,
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: true,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
// For testing purposes
|
||||
public static clearRateLimits(): void {
|
||||
SecurityMiddleware.rateLimitStore.clear();
|
||||
SecurityMiddleware.authLimitStore.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user