UNPKG

mcp-cve-intelligence-server-lite-test

Version:

Lite Model Context Protocol server for comprehensive CVE intelligence gathering with multi-source exploit discovery, designed for security professionals and cybersecurity researchers - Alpha Release

330 lines 12.1 kB
import { createContextLogger, createPerformanceTimer } from '../utils/logger.js'; import { randomUUID } from 'crypto'; const logger = createContextLogger('RequestLogger'); const DEFAULT_OPTIONS = { logLevel: 'info', logHeaders: false, logBody: false, logResponse: false, excludePaths: ['/health', '/favicon.ico'], includePerformanceMetrics: true, maxBodySize: 1024, // 1KB default }; export class RequestLoggingMiddleware { options; requestStore = new Map(); constructor(options = {}) { this.options = { ...DEFAULT_OPTIONS, ...options }; } /** * Express middleware for request logging */ middleware() { return (req, res, next) => { const requestId = randomUUID(); const timer = createPerformanceTimer(`request_${requestId}`); // Skip excluded paths if (this.options.excludePaths.includes(req.path)) { return next(); } // Extract client IP (handle proxies) const clientIp = this.getClientIp(req); // Create log entry const logEntry = { requestId, method: req.method, url: req.url, userAgent: req.get('User-Agent'), contentType: req.get('Content-Type'), contentLength: req.get('Content-Length') ? parseInt(req.get('Content-Length') || '0') : undefined, sessionId: req.get('mcp-session-id'), clientIp, timestamp: new Date().toISOString(), }; // Store request for response logging this.requestStore.set(requestId, logEntry); // Log request details this.logRequest(req, logEntry); // Capture response details const originalSend = res.send; const originalJson = res.json; let responseSize = 0; // Override res.send to capture response size res.send = function (body) { if (body) { const bodyStr = typeof body === 'string' ? body : JSON.stringify(body); responseSize = Buffer.byteLength(bodyStr, 'utf8'); } return originalSend.call(this, body); }; // Override res.json to capture response size res.json = function (obj) { if (obj) { responseSize = Buffer.byteLength(JSON.stringify(obj), 'utf8'); } return originalJson.call(this, obj); }; // Log response when finished res.on('finish', () => { const duration = timer.end({ statusCode: res.statusCode, responseSize, }); logEntry.duration = duration; logEntry.statusCode = res.statusCode; logEntry.responseSize = responseSize; this.logResponse(logEntry); this.requestStore.delete(requestId); }); // Log errors res.on('error', (error) => { logEntry.error = error.message; logger.error('Response error', error, { requestId }); this.requestStore.delete(requestId); }); // Add request ID to response headers for debugging res.setHeader('X-Request-ID', requestId); next(); }; } /** * Express error handling middleware for request logging */ errorMiddleware() { return (error, req, res, next) => { const requestId = res.getHeader('X-Request-ID'); if (requestId && this.requestStore.has(requestId)) { const logEntry = this.requestStore.get(requestId); if (logEntry) { logEntry.error = error.message; logEntry.statusCode = res.statusCode || 500; this.logError(logEntry, error); this.requestStore.delete(requestId); } } else { // Log error without request context logger.error('Untracked request error', error, { method: req.method, url: req.url, statusCode: res.statusCode, }); } next(error); }; } /** * Get request metrics for monitoring */ getMetrics() { // This is a basic implementation - in production you might want to use // a more sophisticated metrics collection system return { activeRequests: this.requestStore.size, totalRequests: 0, // Would need to be tracked separately averageResponseTime: 0, // Would need to be calculated from historical data }; } /** * Get performance statistics for monitored requests */ getPerformanceStats() { const requests = Array.from(this.requestStore.values()); const completedRequests = requests.filter(r => r.statusCode !== undefined); if (completedRequests.length === 0) { return { totalRequests: 0, averageResponseTime: 0, errorRate: 0, statusCodeDistribution: {}, }; } const totalTime = completedRequests.reduce((sum, r) => sum + (r.duration || 0), 0); const errorCount = completedRequests.filter(r => r.statusCode && r.statusCode >= 400).length; const statusCodeDistribution = {}; completedRequests.forEach(r => { if (r.statusCode) { statusCodeDistribution[r.statusCode] = (statusCodeDistribution[r.statusCode] || 0) + 1; } }); return { totalRequests: completedRequests.length, averageResponseTime: totalTime / completedRequests.length, errorRate: errorCount / completedRequests.length, statusCodeDistribution, }; } /** * Clear old request entries to prevent memory leaks */ cleanupOldRequests(maxAge = 3600000) { const now = Date.now(); let cleanedCount = 0; for (const [requestId, entry] of this.requestStore.entries()) { const requestAge = now - new Date(entry.timestamp).getTime(); if (requestAge > maxAge) { this.requestStore.delete(requestId); cleanedCount++; } } logger.debug('Cleaned up old request entries', { cleanedCount, remainingCount: this.requestStore.size }); return cleanedCount; } // Private logging methods logRequest(req, logEntry) { const logData = { requestId: logEntry.requestId, method: logEntry.method, url: logEntry.url, clientIp: logEntry.clientIp, userAgent: logEntry.userAgent, contentType: logEntry.contentType, contentLength: logEntry.contentLength, sessionId: logEntry.sessionId, }; // Add headers if enabled if (this.options.logHeaders) { logData.headers = req.headers; } // Add body if enabled and within size limit if (this.options.logBody && req.body) { const bodySize = Buffer.byteLength(JSON.stringify(req.body), 'utf8'); if (bodySize <= this.options.maxBodySize) { logData.body = req.body; } else { logData.bodyTruncated = `Body too large (${bodySize} bytes)`; } } switch (this.options.logLevel) { case 'verbose': logger.debug('Incoming request', logData); break; case 'debug': logger.debug('Request received', { requestId: logEntry.requestId, method: logEntry.method, url: logEntry.url, clientIp: logEntry.clientIp, sessionId: logEntry.sessionId, }); break; case 'info': default: logger.info('HTTP Request', { requestId: logEntry.requestId, method: logEntry.method, url: logEntry.url, clientIp: logEntry.clientIp, }); break; } } logResponse(logEntry) { const isError = logEntry.statusCode && logEntry.statusCode >= 400; const logData = { requestId: logEntry.requestId, method: logEntry.method, url: logEntry.url, statusCode: logEntry.statusCode, duration: logEntry.duration, responseSize: logEntry.responseSize, }; // Add performance metrics if enabled if (this.options.includePerformanceMetrics) { logData.performanceMetrics = { duration: logEntry.duration, responseSize: logEntry.responseSize, requestSize: logEntry.contentLength, }; } if (isError) { logger.warn('HTTP Response (Error)', logData); } else { switch (this.options.logLevel) { case 'verbose': case 'debug': logger.debug('HTTP Response', logData); break; case 'info': default: logger.info('HTTP Response', { requestId: logEntry.requestId, statusCode: logEntry.statusCode, duration: logEntry.duration, }); break; } } } logError(logEntry, error) { logger.error('HTTP Request Error', error, { requestId: logEntry.requestId, method: logEntry.method, url: logEntry.url, statusCode: logEntry.statusCode, duration: logEntry.duration, clientIp: logEntry.clientIp, }); } getClientIp(req) { // Handle various proxy headers const forwarded = req.get('X-Forwarded-For'); if (forwarded) { return forwarded.split(',')[0].trim(); } const realIp = req.get('X-Real-IP'); if (realIp) { return realIp; } const clientIp = req.get('X-Client-IP'); if (clientIp) { return clientIp; } // Fallback to connection remote address return req.connection.remoteAddress || req.socket.remoteAddress || 'unknown'; } } /** * Factory class for creating request logging middleware with observability */ export class RequestLoggingMiddlewareFactory { static createMiddleware(options) { return new RequestLoggingMiddleware(options); } } /** * Factory function to create request logging middleware with default options */ export function createRequestLoggingMiddleware(options) { return RequestLoggingMiddlewareFactory.createMiddleware(options); } /** * Pre-configured middleware for different environments */ export const requestLoggingPresets = { development: () => createRequestLoggingMiddleware({ logLevel: 'debug', logHeaders: true, logBody: true, logResponse: true, includePerformanceMetrics: true, }), production: () => createRequestLoggingMiddleware({ logLevel: 'info', logHeaders: false, logBody: false, logResponse: false, includePerformanceMetrics: true, }), verbose: () => createRequestLoggingMiddleware({ logLevel: 'verbose', logHeaders: true, logBody: true, logResponse: true, includePerformanceMetrics: true, maxBodySize: 4096, // 4KB }), }; //# sourceMappingURL=request-logging-middleware.js.map