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
JavaScript
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