proxy-server.js•10.6 kB
/**
* HTTP/HTTPS Proxy Server
* Core proxy server implementation with traffic capture
*/
import http from 'http';
import https from 'https';
import { URL } from 'url';
import net from 'net';
export class ProxyServer {
constructor(targetManager, trafficAnalyzer = null) {
this.targetManager = targetManager;
this.trafficAnalyzer = trafficAnalyzer;
this.server = null;
this.port = null;
this.host = null;
this.running = false;
// Performance metrics
this.metrics = {
requestCount: 0,
proxyCount: 0,
directCount: 0,
errorCount: 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
*/
async start(port = 8080, host = 'localhost') {
if (this.running) {
throw new Error('Proxy server is already running');
}
this.port = port;
this.host = host;
this.metrics.startTime = new Date();
this.server = http.createServer();
// Handle HTTP requests
this.server.on('request', (req, res) => {
this._handleHttpRequest(req, res);
});
// Handle HTTPS CONNECT method for tunneling
this.server.on('connect', (req, clientSocket, head) => {
this._handleHttpsConnect(req, clientSocket, head);
});
// Serve PAC file
this.server.on('request', (req, res) => {
if (req.url === '/proxy.pac') {
this._servePacFile(req, res);
return;
}
});
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}`);
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 {
const url = new URL(clientReq.url);
const shouldProxy = this._shouldProxyRequest(url.hostname);
if (!shouldProxy) {
this.metrics.directCount++;
this._sendDirectResponse(clientRes, url.href);
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
* @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++;
clientSocket.write('HTTP/1.1 200 Connection established\r\n\r\n');
// Create direct tunnel
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);
});
serverSocket.on('error', (error) => {
console.error('HTTPS tunnel error:', error.message);
clientSocket.end();
});
return;
}
this.metrics.proxyCount++;
this._proxyHttpsConnect(req, clientSocket, head, hostname, port, startTime);
} catch (error) {
this.metrics.errorCount++;
console.error('HTTPS CONNECT error:', error.message);
clientSocket.end();
}
}
/**
* Proxy HTTP request through our server
* @private
*/
async _proxyHttpRequest(clientReq, clientRes, url, startTime) {
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);
// Capture traffic if analyzer is available
if (this.trafficAnalyzer && this._shouldCaptureTraffic(url.hostname)) {
this._captureTraffic(clientReq, proxyRes, url, responseTime);
}
// 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);
}
/**
* Proxy HTTPS CONNECT tunnel
* @private
*/
_proxyHttpsConnect(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);
// Set up bidirectional piping
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('HTTPS proxy error:', error.message);
clientSocket.end();
});
clientSocket.on('error', (error) => {
console.error('Client socket error:', error.message);
serverSocket.end();
});
}
/**
* Serve PAC file
* @private
*/
_servePacFile(req, res) {
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);
}
/**
* Check if request should be proxied
* @private
*/
_shouldProxyRequest(hostname) {
return this.targetManager.shouldProxy(hostname);
}
/**
* Check if traffic should be captured
* @private
*/
_shouldCaptureTraffic(hostname) {
const target = this.targetManager.findTarget(hostname);
return target && (target.captureHeaders || target.captureBody);
}
/**
* Capture HTTP traffic
* @private
*/
_captureTraffic(clientReq, proxyRes, url, responseTime) {
const target = this.targetManager.findTarget(url.hostname);
if (!target) return;
const entry = {
timestamp: new Date().toISOString(),
url: url.href,
method: clientReq.method,
domain: url.hostname,
statusCode: proxyRes.statusCode,
responseTime,
headers: target.captureHeaders ? {
request: { ...clientReq.headers },
response: { ...proxyRes.headers }
} : undefined
};
// Capture body if enabled (requires buffering)
if (target.captureBody) {
// Note: Body capture would require more complex implementation
// to buffer and forward simultaneously
entry.bodyCaptured = false;
entry.note = 'Body capture requires buffering implementation';
}
this.trafficAnalyzer.addEntry(entry);
}
/**
* 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
};
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(),
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,
bytesTransferred: 0,
averageResponseTime: 0,
startTime,
lastRequestTime: null
};
}
}