proxy-server-ssl.js•32.1 kB
/**
* HTTP/HTTPS Proxy Server with SSL Bumping
* Enhanced proxy server with HTTPS interception capabilities
*/
import http from 'http';
import https from 'https';
import { URL } from 'url';
import net from 'net';
import tls from 'tls';
import fs from 'fs/promises';
import assert from 'assert';
export class ProxyServerWithSSL {
constructor(targetManager, trafficAnalyzer = null, sslManager = null) {
// Contract: targetManager is required and must have necessary methods
assert(targetManager, 'targetManager is required');
assert(typeof targetManager.shouldProxy === 'function', 'targetManager must have shouldProxy method');
assert(typeof targetManager.findTarget === 'function', 'targetManager must have findTarget method');
assert(typeof targetManager.generatePacFile === 'function', 'targetManager must have generatePacFile method');
// Contract: trafficAnalyzer if provided must have addEntry method
if (trafficAnalyzer) {
assert(typeof trafficAnalyzer.addEntry === 'function', 'trafficAnalyzer must have addEntry method');
}
// Contract: sslManager if provided must have required SSL methods
if (sslManager) {
assert(typeof sslManager.generateServerCertificate === 'function', 'sslManager must have generateServerCertificate method');
assert(typeof sslManager.getCACertificate === 'function', 'sslManager must have getCACertificate method');
}
this.targetManager = targetManager;
this.trafficAnalyzer = trafficAnalyzer;
this.sslManager = sslManager;
this.server = null;
this.port = null;
this.host = null;
this.running = false;
this.sslBumpingEnabled = false;
// SSL certificate cache
this.certificateCache = new Map();
// Performance metrics
this.metrics = {
requestCount: 0,
proxyCount: 0,
directCount: 0,
errorCount: 0,
sslBumpingCount: 0,
bytesTransferred: 0,
averageResponseTime: 0,
startTime: null,
lastRequestTime: null
};
}
/**
* Start the proxy server
* @param {number} port - Port to listen on
* @param {string} host - Host to bind to
* @param {Object} options - Server options
*/
async start(port = 8080, host = 'localhost', options = {}) {
// Pre-conditions
assert(typeof port === 'number' && port > 0 && port <= 65535, 'Port must be a valid number between 1 and 65535');
assert(typeof host === 'string' && host.length > 0, 'Host must be a non-empty string');
assert(typeof options === 'object', 'Options must be an object');
if (this.running) {
throw new Error('Proxy server is already running');
}
this.port = port;
this.host = host;
this.sslBumpingEnabled = options.enableSSLBumping || false;
this.metrics.startTime = new Date();
// Contract: SSL Bumping requires sslManager
if (this.sslBumpingEnabled) {
if (!this.sslManager) {
throw new Error('SSL Bumping enabled but sslManager not provided');
}
await this._initializeSSL();
}
this.server = http.createServer();
// Handle HTTP requests (consolidated handler)
this.server.on('request', (req, res) => {
// Serve PAC file and CA certificate first
if (req.url === '/proxy.pac') {
this._servePacFile(req, res);
return;
}
if (req.url === '/ca.crt') {
this._serveCACertificate(req, res);
return;
}
// Handle regular HTTP proxy requests
this._handleHttpRequest(req, res);
});
// Handle HTTPS CONNECT method for tunneling
this.server.on('connect', (req, clientSocket, head) => {
this._handleHttpsConnect(req, clientSocket, head);
});
// Handle WebSocket upgrade requests
this.server.on('upgrade', (req, clientSocket, head) => {
this._handleWebSocketUpgrade(req, clientSocket, head);
});
return new Promise((resolve, reject) => {
this.server.listen(port, host, (error) => {
if (error) {
reject(error);
} else {
this.running = true;
console.log(`🚀 Proxy server started on ${host}:${port}`);
if (this.sslBumpingEnabled) {
console.log(`🔒 SSL Bumping: ENABLED`);
this._logCAInstallationInstructions();
} else {
console.log(`🔒 SSL Bumping: DISABLED (HTTPS tunneled only)`);
}
resolve();
}
});
});
}
/**
* Stop the proxy server
*/
async stop() {
if (!this.running || !this.server) {
return;
}
return new Promise((resolve) => {
this.server.close(() => {
this.running = false;
this.server = null;
console.log('✅ Proxy server stopped');
resolve();
});
});
}
/**
* Handle HTTP requests
* @private
*/
async _handleHttpRequest(clientReq, clientRes) {
const startTime = Date.now();
this.metrics.requestCount++;
this.metrics.lastRequestTime = new Date();
try {
let url;
let shouldProxy = false;
// Check if this is a proxy request with full URL (http://example.com/path)
if (clientReq.url.startsWith('http://') || clientReq.url.startsWith('https://')) {
// Full URL proxy request pattern: GET http://example.com/path HTTP/1.1
url = new URL(clientReq.url);
shouldProxy = this._shouldProxyRequest(url.hostname);
} else {
// Relative path proxy request pattern: GET /path HTTP/1.1
// Extract hostname from Host header
const hostHeader = clientReq.headers.host;
if (!hostHeader) {
throw new Error('Missing Host header for relative path request');
}
// Parse hostname and port from Host header
const [hostname, port] = hostHeader.split(':');
shouldProxy = this._shouldProxyRequest(hostname);
if (shouldProxy) {
// Reconstruct full URL for proxy request
const protocol = clientReq.connection.encrypted ? 'https:' : 'http:';
url = new URL(`${protocol}//${hostHeader}${clientReq.url}`);
}
}
if (!shouldProxy) {
this.metrics.directCount++;
this._sendDirectResponse(clientRes, clientReq.url);
return;
}
this.metrics.proxyCount++;
await this._proxyHttpRequest(clientReq, clientRes, url, startTime);
} catch (error) {
this.metrics.errorCount++;
console.error('Proxy error:', error.message);
this._sendErrorResponse(clientRes, 500, 'Proxy Error');
}
}
/**
* Handle HTTPS CONNECT tunneling with optional SSL bumping
* @private
*/
_handleHttpsConnect(req, clientSocket, head) {
const startTime = Date.now();
this.metrics.requestCount++;
try {
const [hostname, port = 443] = req.url.split(':');
const shouldProxy = this._shouldProxyRequest(hostname);
if (!shouldProxy) {
this.metrics.directCount++;
this._createDirectTunnel(clientSocket, hostname, port);
return;
}
this.metrics.proxyCount++;
if (this.sslBumpingEnabled && this._shouldBumpSSL(hostname)) {
this._performSSLBumping(req, clientSocket, head, hostname, port, startTime);
} else {
this._createProxyTunnel(req, clientSocket, head, hostname, port, startTime);
}
} catch (error) {
this.metrics.errorCount++;
console.error('HTTPS CONNECT error:', error.message);
clientSocket.end();
}
}
/**
* Handle WebSocket upgrade requests
* @private
*/
_handleWebSocketUpgrade(req, clientSocket, head) {
const startTime = Date.now();
this.metrics.requestCount++;
try {
let hostname, port = 80;
// Parse hostname from request
if (req.url.startsWith('ws://') || req.url.startsWith('wss://')) {
const url = new URL(req.url);
hostname = url.hostname;
port = url.port || (url.protocol === 'wss:' ? 443 : 80);
} else {
// Use Host header for relative WebSocket URLs
const hostHeader = req.headers.host;
if (!hostHeader) {
console.error('Missing Host header for WebSocket upgrade');
clientSocket.end();
return;
}
[hostname, port = 80] = hostHeader.split(':');
}
const shouldProxy = this._shouldProxyRequest(hostname);
if (!shouldProxy) {
this.metrics.directCount++;
this._createDirectWebSocketTunnel(clientSocket, hostname, port, req, head);
return;
}
this.metrics.proxyCount++;
this._createProxyWebSocketTunnel(clientSocket, hostname, port, req, head, startTime);
} catch (error) {
this.metrics.errorCount++;
console.error('WebSocket upgrade error:', error.message);
clientSocket.end();
}
}
/**
* Create proxy WebSocket tunnel
* @private
*/
_createProxyWebSocketTunnel(clientSocket, hostname, port, req, head, startTime) {
const targetSocket = net.createConnection(port, hostname, () => {
// Forward the original upgrade request
const requestHeaders = Object.keys(req.headers)
.map(key => `${key}: ${req.headers[key]}`)
.join('\r\n');
const upgradeRequest = [
`${req.method} ${req.url} HTTP/${req.httpVersion}`,
requestHeaders,
'',
''
].join('\r\n');
targetSocket.write(upgradeRequest);
if (head && head.length > 0) {
targetSocket.write(head);
}
});
targetSocket.on('error', (error) => {
console.error(`WebSocket proxy tunnel error to ${hostname}:${port}:`, error.message);
clientSocket.end();
});
// Bidirectional pipe for WebSocket data
clientSocket.pipe(targetSocket);
targetSocket.pipe(clientSocket);
clientSocket.on('error', () => targetSocket.end());
targetSocket.on('error', () => clientSocket.end());
// Log WebSocket connection
console.log(`🔌 WebSocket proxy tunnel: ${hostname}:${port}`);
// Capture WebSocket traffic if enabled
if (this._shouldCaptureTraffic(hostname)) {
this._captureWebSocketTraffic(req, hostname, startTime);
}
}
/**
* Create direct WebSocket tunnel (bypass proxy)
* @private
*/
_createDirectWebSocketTunnel(clientSocket, hostname, port, req, head) {
const targetSocket = net.createConnection(port, hostname, () => {
// Forward the original upgrade request
const requestHeaders = Object.keys(req.headers)
.map(key => `${key}: ${req.headers[key]}`)
.join('\r\n');
const upgradeRequest = [
`${req.method} ${req.url} HTTP/${req.httpVersion}`,
requestHeaders,
'',
''
].join('\r\n');
targetSocket.write(upgradeRequest);
if (head && head.length > 0) {
targetSocket.write(head);
}
});
targetSocket.on('error', (error) => {
console.error(`WebSocket direct tunnel error to ${hostname}:${port}:`, error.message);
clientSocket.end();
});
// Bidirectional pipe for WebSocket data
clientSocket.pipe(targetSocket);
targetSocket.pipe(clientSocket);
clientSocket.on('error', () => targetSocket.end());
targetSocket.on('error', () => clientSocket.end());
console.log(`🔌 WebSocket direct tunnel: ${hostname}:${port}`);
}
/**
* Capture WebSocket traffic
* @private
*/
_captureWebSocketTraffic(req, hostname, startTime) {
const responseTime = Date.now() - startTime;
const trafficEntry = {
id: `ws-${Date.now()}-${Math.random().toString(36).substring(7)}`,
timestamp: new Date().toISOString(),
method: 'WEBSOCKET',
url: `ws://${hostname}${req.url}`,
hostname: hostname,
responseTime: responseTime,
headers: this._shouldCaptureTraffic(hostname) ? req.headers : null,
userAgent: req.headers['user-agent'] || 'Unknown',
remoteAddress: req.connection.remoteAddress
};
this.trafficLog.push(trafficEntry);
console.log(`📊 WebSocket traffic captured: ${hostname}`);
}
/**
* Perform SSL bumping (man-in-the-middle)
* @private
*/
async _performSSLBumping(req, clientSocket, head, hostname, port, startTime) {
try {
this.metrics.sslBumpingCount++;
// Get or generate certificate for this domain
const { key, cert } = await this._getCertificateForDomain(hostname);
// Send 200 Connection Established to client
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
// Create TLS server for client connection
const tlsOptions = {
key,
cert,
// Allow legacy certificates for testing
secureProtocol: 'TLSv1_2_method'
};
const tlsServer = tls.createSecureContext(tlsOptions);
const tlsSocket = new tls.TLSSocket(clientSocket, {
isServer: true,
secureContext: tlsServer
});
tlsSocket.on('secure', () => {
console.log(`🔓 SSL bumping established for ${hostname}`);
// Create HTTP server to handle decrypted requests
this._handleDecryptedHTTPS(tlsSocket, hostname, port, startTime);
});
tlsSocket.on('error', (error) => {
console.error(`SSL bumping error for ${hostname}:`, error.message);
clientSocket.end();
});
} catch (error) {
console.error('SSL bumping setup failed:', error.message);
// Fallback to normal tunneling
this._createProxyTunnel(req, clientSocket, head, hostname, port, startTime);
}
}
/**
* Handle decrypted HTTPS traffic
* @private
*/
_handleDecryptedHTTPS(tlsSocket, hostname, port, startTime) {
let buffer = '';
tlsSocket.on('data', async (data) => {
buffer += data.toString();
// Check if we have a complete HTTP request
const headerEnd = buffer.indexOf('\r\n\r\n');
if (headerEnd === -1) return; // Wait for more data
const headerPart = buffer.substring(0, headerEnd);
const bodyPart = buffer.substring(headerEnd + 4);
// Parse HTTP request
const lines = headerPart.split('\r\n');
const requestLine = lines[0];
const [method, path, version] = requestLine.split(' ');
// Build headers object
const headers = {};
for (let i = 1; i < lines.length; i++) {
const [key, value] = lines[i].split(': ');
if (key && value) {
headers[key.toLowerCase()] = value;
}
}
// Ensure Host header
if (!headers.host) {
headers.host = hostname;
}
// Create URL for this request
const url = new URL(`https://${hostname}:${port}${path}`);
try {
// Forward to actual server
await this._forwardDecryptedRequest(
tlsSocket, method, url, headers, bodyPart, startTime
);
} catch (error) {
console.error('Error forwarding decrypted request:', error.message);
this._sendErrorToTLSSocket(tlsSocket, 502, 'Bad Gateway');
}
// Reset buffer for next request
buffer = '';
});
tlsSocket.on('error', (error) => {
console.error('TLS socket error:', error.message);
});
}
/**
* Forward decrypted HTTPS request to actual server
* @private
*/
async _forwardDecryptedRequest(tlsSocket, method, url, headers, body, startTime) {
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method,
headers: { ...headers }
};
// Remove proxy-specific headers
delete options.headers['proxy-connection'];
delete options.headers['proxy-authorization'];
const proxyReq = https.request(options, (proxyRes) => {
const responseTime = Date.now() - startTime;
this._updateMetrics(responseTime);
// Capture decrypted traffic
if (this.trafficAnalyzer && this._shouldCaptureTraffic(url.hostname)) {
this._captureDecryptedTraffic(method, url, headers, proxyRes, responseTime, body);
}
// Build HTTP response
let responseHeaders = '';
responseHeaders += `HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}\r\n`;
for (const [key, value] of Object.entries(proxyRes.headers)) {
responseHeaders += `${key}: ${value}\r\n`;
}
responseHeaders += '\r\n';
// Send response headers
tlsSocket.write(responseHeaders);
// Forward response body
proxyRes.on('data', (chunk) => {
tlsSocket.write(chunk);
});
proxyRes.on('end', () => {
// Response complete - keep connection alive for next request
this.metrics.bytesTransferred += parseInt(proxyRes.headers['content-length'] || '0');
});
});
proxyReq.on('error', (error) => {
console.error('Decrypted proxy request error:', error.message);
this._sendErrorToTLSSocket(tlsSocket, 502, 'Bad Gateway');
});
// Send request body if present
if (body) {
proxyReq.write(body);
}
proxyReq.end();
}
/**
* Create direct tunnel (no proxying)
* @private
*/
_createDirectTunnel(clientSocket, hostname, port) {
const serverSocket = net.connect(port, hostname, () => {
clientSocket.write('HTTP/1.1 200 Connection established\r\n\r\n');
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});
serverSocket.on('error', (error) => {
console.error('Direct tunnel error:', error.message);
clientSocket.end();
});
}
/**
* Create proxy tunnel (no SSL bumping)
* @private
*/
_createProxyTunnel(req, clientSocket, head, hostname, port, startTime) {
const serverSocket = net.connect(port, hostname, () => {
clientSocket.write('HTTP/1.1 200 Connection established\r\n\r\n');
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
// Log the connection if traffic analyzer is available
if (this.trafficAnalyzer && this._shouldCaptureTraffic(hostname)) {
const responseTime = Date.now() - startTime;
this._captureHttpsConnect(req, hostname, port, responseTime);
this._updateMetrics(responseTime);
}
});
serverSocket.on('error', (error) => {
console.error('Proxy tunnel error:', error.message);
clientSocket.end();
});
}
/**
* Get or generate certificate for domain
* @private
*/
async _getCertificateForDomain(hostname) {
// Check cache first
if (this.certificateCache.has(hostname)) {
return this.certificateCache.get(hostname);
}
try {
// Generate certificate for this domain
const certInfo = await this.sslManager.generateServerCertificate(hostname, [
hostname,
`*.${hostname}` // Include wildcard
]);
// Read certificate and key
const cert = await fs.readFile(certInfo.certPath, 'utf-8');
const key = await fs.readFile(certInfo.keyPath, 'utf-8');
const result = { cert, key };
// Cache for future use
this.certificateCache.set(hostname, result);
console.log(`📜 Generated SSL certificate for ${hostname}`);
return result;
} catch (error) {
console.error(`Failed to generate certificate for ${hostname}:`, error.message);
throw error;
}
}
/**
* Initialize SSL components
* @private
*/
async _initializeSSL() {
if (!this.sslManager) {
throw new Error('SSL Manager not provided but SSL bumping is enabled');
}
const status = await this.sslManager.initialize();
if (!status.caExists) {
console.log('🔧 Creating Certificate Authority for SSL bumping...');
await this.sslManager.createCA('default', {
description: 'Web Proxy MCP Server SSL Bumping CA'
});
}
console.log(`🔒 SSL Manager initialized with CA: ${status.caName}`);
}
/**
* Check if SSL bumping should be performed for this hostname
* @private
*/
_shouldBumpSSL(hostname) {
// Only bump SSL for monitored domains that have traffic capture enabled
const target = this.targetManager.findTarget(hostname);
return target && (target.captureHeaders || target.captureBody);
}
/**
* Serve CA certificate for download
* @private
*/
async _serveCACertificate(req, res) {
try {
// Check if headers already sent
if (res.headersSent) {
console.warn('Headers already sent for CA certificate request');
return;
}
if (!this.sslManager) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('SSL Manager not available');
return;
}
const caCert = await this.sslManager.getCACertificate();
res.writeHead(200, {
'Content-Type': 'application/x-x509-ca-cert',
'Content-Disposition': `attachment; filename="${this.sslManager.currentCA}.crt"`,
'Cache-Control': 'no-cache'
});
res.end(caCert.certContent);
} catch (error) {
console.error('Error serving CA certificate:', error);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Error serving CA certificate: ${error.message}`);
}
}
}
/**
* Log CA installation instructions
* @private
*/
async _logCAInstallationInstructions() {
if (!this.sslManager) return;
try {
const caCert = await this.sslManager.getCACertificate();
console.log('\n' + '='.repeat(80));
console.log('🔒 SSL BUMPING ACTIVE - CA CERTIFICATE INSTALLATION REQUIRED');
console.log('='.repeat(80));
console.log(`\n📥 Download CA Certificate:`);
console.log(` curl -o proxy-ca.crt http://${this.host}:${this.port}/ca.crt`);
console.log(` OR browse to: http://${this.host}:${this.port}/ca.crt`);
console.log(`\n📁 CA Certificate Location:`);
console.log(` ${caCert.certPath}`);
console.log('\n' + caCert.installationInstructions);
console.log('\n' + '='.repeat(80));
} catch (error) {
console.error('Failed to display CA installation instructions:', error.message);
}
}
/**
* Capture decrypted HTTPS traffic
* @private
*/
_captureDecryptedTraffic(method, url, requestHeaders, proxyRes, responseTime, requestBody) {
const target = this.targetManager.findTarget(url.hostname);
if (!target) return;
const entry = {
timestamp: new Date().toISOString(),
url: url.href,
method,
domain: url.hostname,
statusCode: proxyRes.statusCode,
responseTime,
sslBumped: true,
headers: target.captureHeaders ? {
request: { ...requestHeaders },
response: { ...proxyRes.headers }
} : undefined,
requestBody: target.captureBody && requestBody ? requestBody : undefined
};
// Note: Response body capture would require additional buffering
if (target.captureBody) {
entry.bodyCaptured = false;
entry.note = 'Response body capture requires additional implementation';
}
this.trafficAnalyzer.addEntry(entry);
}
/**
* Send error response to TLS socket
* @private
*/
_sendErrorToTLSSocket(tlsSocket, statusCode, message) {
const response = `HTTP/1.1 ${statusCode} ${message}\r\nContent-Type: text/plain\r\n\r\n${message}`;
tlsSocket.write(response);
}
// ... (继续原有的方法)
/**
* Proxy HTTP request through our server
* @private
*/
async _proxyHttpRequest(clientReq, clientRes, url, startTime) {
// Pre-conditions
assert(clientReq, 'clientReq is required');
assert(clientRes, 'clientRes is required');
assert(url, 'url is required');
assert(typeof url.hostname === 'string', 'url.hostname must be a string');
assert(typeof startTime === 'number', 'startTime must be a number');
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method: clientReq.method,
headers: { ...clientReq.headers }
};
// Remove proxy-specific headers
delete options.headers['proxy-connection'];
delete options.headers['proxy-authorization'];
const httpModule = url.protocol === 'https:' ? https : http;
const proxyReq = httpModule.request(options, (proxyRes) => {
const responseTime = Date.now() - startTime;
this._updateMetrics(responseTime);
// Contract: Ensure traffic capture is attempted for monitored targets
const shouldCapture = this._shouldCaptureTraffic(url.hostname);
console.log(`🔍 Traffic capture check for ${url.hostname}: shouldCapture=${shouldCapture}, hasAnalyzer=${!!this.trafficAnalyzer}`);
if (this.trafficAnalyzer && shouldCapture) {
try {
this._captureTraffic(clientReq, proxyRes, url, responseTime);
console.log(`📊 Traffic capture attempted for ${url.hostname}`);
} catch (error) {
console.error(`Failed to capture traffic for ${url.hostname}:`, error.message);
}
} else {
console.log(`❌ Traffic capture skipped for ${url.hostname}: analyzer=${!!this.trafficAnalyzer}, shouldCapture=${shouldCapture}`);
}
// Forward response headers
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
// Forward response body
proxyRes.pipe(clientRes);
proxyRes.on('end', () => {
this.metrics.bytesTransferred += parseInt(proxyRes.headers['content-length'] || '0');
});
});
proxyReq.on('error', (error) => {
console.error('Proxy request error:', error.message);
this._sendErrorResponse(clientRes, 502, 'Bad Gateway');
});
// Forward request body
clientReq.pipe(proxyReq);
}
/**
* Serve PAC file
* @private
*/
_servePacFile(req, res) {
try {
// Check if headers already sent
if (res.headersSent) {
console.warn('Headers already sent for PAC file request');
return;
}
const pacContent = this.targetManager.generatePacFile(this.host, this.port);
res.writeHead(200, {
'Content-Type': 'application/x-ns-proxy-autoconfig',
'Cache-Control': 'no-cache'
});
res.end(pacContent);
} catch (error) {
console.error('Error serving PAC file:', error);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error serving PAC file');
}
}
}
/**
* Check if request should be proxied
* @private
*/
_shouldProxyRequest(hostname) {
return this.targetManager.shouldProxy(hostname);
}
/**
* Check if traffic should be captured
* @private
*/
_shouldCaptureTraffic(hostname) {
// Pre-conditions
assert(typeof hostname === 'string', 'hostname must be a string');
assert(hostname.length > 0, 'hostname cannot be empty');
const target = this.targetManager.findTarget(hostname);
console.log(`🔍 Traffic capture check for ${hostname}: shouldCapture=${target ? (target.captureHeaders || target.captureBody) : false}, hasAnalyzer=${!!this.trafficAnalyzer}`);
const result = target ? (target.captureHeaders || target.captureBody) : false;
// Post-condition: result must be boolean
assert(typeof result === 'boolean', 'shouldCaptureTraffic must return boolean');
if (!result) {
console.log(`❌ Traffic capture skipped for ${hostname}: analyzer=${!!this.trafficAnalyzer}, shouldCapture=${result}`);
}
return result;
}
/**
* Capture HTTP traffic
* @private
*/
_captureTraffic(clientReq, proxyRes, url, responseTime) {
// Pre-conditions
assert(clientReq, 'clientReq is required');
assert(proxyRes, 'proxyRes is required');
assert(url, 'url is required');
assert(typeof url.hostname === 'string', 'url.hostname must be a string');
assert(typeof responseTime === 'number', 'responseTime must be a number');
// Contract: trafficAnalyzer must be available for capture
if (!this.trafficAnalyzer) {
console.warn('Traffic capture skipped: trafficAnalyzer not available');
return;
}
const target = this.targetManager.findTarget(url.hostname);
if (!target) {
console.debug(`Traffic capture skipped: ${url.hostname} not a monitored target`);
return;
}
const entry = {
timestamp: new Date().toISOString(),
url: url.href,
method: clientReq.method,
domain: url.hostname,
statusCode: proxyRes.statusCode,
responseTime,
sslBumped: false,
headers: target.captureHeaders ? {
request: { ...clientReq.headers },
response: { ...proxyRes.headers }
} : undefined
};
// Capture body if enabled (requires buffering)
if (target.captureBody) {
entry.bodyCaptured = false;
entry.note = 'Body capture requires buffering implementation';
}
try {
this.trafficAnalyzer.addEntry(entry);
console.log(`📊 Traffic captured: ${clientReq.method} ${url.href} -> ${proxyRes.statusCode}`);
} catch (error) {
console.error('Failed to add traffic entry:', error.message);
throw new Error(`Traffic capture failed: ${error.message}`);
}
// Post-condition: Verify entry was added
assert(this.trafficAnalyzer.entries && this.trafficAnalyzer.entries.length > 0, 'Traffic entry should have been added');
}
/**
* Capture HTTPS CONNECT information
* @private
*/
_captureHttpsConnect(req, hostname, port, responseTime) {
const entry = {
timestamp: new Date().toISOString(),
url: `https://${hostname}:${port}`,
method: 'CONNECT',
domain: hostname,
statusCode: 200,
responseTime,
tunneled: true,
sslBumped: false
};
this.trafficAnalyzer.addEntry(entry);
}
/**
* Send direct response for non-proxied requests
* @private
*/
_sendDirectResponse(res, url) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Direct access: ${url}\nNot routed through proxy (not a monitored domain)`);
}
/**
* Send error response
* @private
*/
_sendErrorResponse(res, statusCode, message) {
res.writeHead(statusCode, { 'Content-Type': 'text/plain' });
res.end(`Proxy Error: ${message}`);
}
/**
* Update performance metrics
* @private
*/
_updateMetrics(responseTime) {
const count = this.metrics.proxyCount + this.metrics.directCount;
this.metrics.averageResponseTime =
(this.metrics.averageResponseTime * (count - 1) + responseTime) / count;
}
/**
* Get server status
* @returns {Object} Server status
*/
getStatus() {
return {
running: this.running,
address: this.getAddress(),
sslBumpingEnabled: this.sslBumpingEnabled,
caName: this.sslManager ? this.sslManager.currentCA : null,
uptime: this.metrics.startTime ?
Math.floor((Date.now() - this.metrics.startTime.getTime()) / 1000) : 0,
metrics: { ...this.metrics }
};
}
/**
* Get server address
* @returns {string} Server address
*/
getAddress() {
return this.running ? `http://${this.host}:${this.port}` : null;
}
/**
* Check if server is running
* @returns {boolean} Running status
*/
isRunning() {
return this.running;
}
/**
* Get performance metrics
* @returns {Object} Performance metrics
*/
getMetrics() {
return { ...this.metrics };
}
/**
* Reset performance metrics
*/
resetMetrics() {
const startTime = this.metrics.startTime;
this.metrics = {
requestCount: 0,
proxyCount: 0,
directCount: 0,
errorCount: 0,
sslBumpingCount: 0,
bytesTransferred: 0,
averageResponseTime: 0,
startTime,
lastRequestTime: null
};
}
}