Files
Website-Stress-Test/proxy-server.js

352 lines
13 KiB
JavaScript

// ===================================
// CORS PROXY SERVER
// ===================================
// This proxy server allows the stress testing tool to test
// production websites without CORS restrictions.
const http = require('http');
const https = require('https');
const url = require('url');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
const fs = require('fs');
const path = require('path');
const PORT = process.env.PORT || 3000;
// Configuration
const CONFIG = {
// Maximum request timeout (30 seconds)
timeout: 30000,
// Allowed origins (restrict to your stress testing tool's domain)
// Use '*' for development, specific domain for production
allowedOrigins: '*',
// Maximum concurrent connections
maxConnections: 10000, // Increased for cluster
// User agents for rotation
userAgents: [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
]
};
// Global agents for connection pooling
const httpAgent = new http.Agent({ keepAlive: true, maxSockets: Infinity });
const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: Infinity });
// Get random user agent
function getRandomUserAgent() {
return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)];
}
const { exec } = require('child_process');
// Helper to get git info
const getGitInfo = () => {
return new Promise((resolve) => {
exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => {
if (err) {
resolve({ commit: 'Unknown', date: 'Unknown' });
return;
}
const parts = stdout.trim().split('\n');
resolve({
commit: parts[0] || 'Unknown',
date: parts[1] || 'Unknown'
});
});
});
};
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
console.log(`Spawning ${numCPUs} workers...`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died. Respawning...`);
cluster.fork();
});
// Master process only listens for SIGINT to gracefully shut down workers
process.on('SIGINT', () => {
console.log('\n\n🛑 Shutting down proxy server (master)...');
for (const id in cluster.workers) {
cluster.workers[id].kill();
}
process.exit(0);
});
} else {
// Create the proxy server
const server = http.createServer((req, res) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
handleCORS(res);
res.writeHead(200);
res.end();
return;
}
// Health check
if (req.url === '/health' || req.url === '//health') {
handleCORS(res);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', worker: process.pid }));
return;
}
// Handle Git Info request
// Nginx proxy_pass might result in double slashes (//git-info)
if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') {
handleCORS(res);
getGitInfo().then(info => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(info));
});
return;
}
// Serve static files for the UI
if (req.method === 'GET') {
let requestPath = req.url.split('?')[0];
let filePath = '.' + requestPath;
if (requestPath === '/') filePath = './index.html';
// Basic security: don't allow accessing files outside the directory or sensitive files
const resolvedPath = path.resolve(filePath);
const rootPath = path.resolve('.');
if (!resolvedPath.startsWith(rootPath) || filePath.includes('..')) {
res.writeHead(403);
res.end('Forbidden');
return;
}
fs.access(filePath, fs.constants.F_OK, (err) => {
if (!err) {
const extname = path.extname(filePath).toLowerCase();
let contentType = 'text/html';
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.json': 'application/json'
};
contentType = mimeTypes[extname] || 'text/plain';
fs.readFile(filePath, (error, content) => {
if (!error) {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
} else {
res.writeHead(500);
res.end('Server Error');
}
});
} else if (req.url === '/health' || req.url === '//health' || (req.url === '/git-info' || req.url === '//git-info')) {
// Handled by other logic (keep going)
} else if (req.url === '/') {
// Fallback for root if index.html doesn't exist? (Unlikely)
} else {
// Not a static file, maybe fall through to POST check
}
});
// Special handling for health and git-info which are GET but not files
if (req.url.includes('/health') || req.url.includes('/git-info')) {
// Let it fall through to those handlers
} else {
return;
}
}
// Only allow POST requests to the proxy
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
return;
}
// Parse request body
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const proxyRequest = JSON.parse(body);
handleProxyRequest(proxyRequest, res);
} catch (error) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Invalid JSON',
message: error.message
}));
}
});
});
// Handle the actual proxy request
function handleProxyRequest(proxyRequest, clientRes) {
const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest;
// Validate target URL
if (!targetUrl) {
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({ error: 'targetUrl is required' }));
return;
}
let parsedUrl;
try {
parsedUrl = new URL(targetUrl);
} catch (error) {
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({ error: 'Invalid URL' }));
return;
}
// Determine if we need http or https and which agent to use
const isHttps = parsedUrl.protocol === 'https:';
const protocol = isHttps ? https : http;
const agent = isHttps ? httpsAgent : httpAgent;
// Prepare request options with random user agent
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + parsedUrl.search,
method: method,
agent: agent, // Use the global agent for connection pooling
headers: {
...headers,
'User-Agent': getRandomUserAgent(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: CONFIG.timeout
};
const startTime = Date.now();
// Make the request to the target server
const proxyReq = protocol.request(options, (proxyRes) => {
const responseTime = Date.now() - startTime;
// Collect response data
let responseData = '';
let responseSize = 0;
const maxBodySize = 500000; // 500KB limit for crawler
proxyRes.on('data', chunk => {
responseSize += chunk.length;
// Only collect body if under size limit (for crawler)
if (responseSize < maxBodySize) {
responseData += chunk.toString();
}
});
proxyRes.on('end', () => {
// Send response back to client with CORS headers
handleCORS(clientRes);
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
success: true,
statusCode: proxyRes.statusCode,
statusMessage: proxyRes.statusMessage,
responseTime: responseTime,
headers: proxyRes.headers,
body: responseData, // Full body for crawler link extraction
bodySize: responseSize,
proxyWorker: process.pid // Add worker ID for debugging
}));
});
});
// Handle request errors
proxyReq.on('error', (error) => {
const responseTime = Date.now() - startTime;
handleCORS(clientRes);
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
success: false,
error: error.message,
responseTime: responseTime,
statusCode: 0
}));
});
// Handle timeout
proxyReq.on('timeout', () => {
proxyReq.destroy();
const responseTime = Date.now() - startTime;
handleCORS(clientRes);
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
success: false,
error: 'Request timeout',
responseTime: responseTime,
statusCode: 0
}));
});
// Send request body if present
if (body && method !== 'GET' && method !== 'HEAD') {
proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body));
}
proxyReq.end();
}
// Add CORS headers to response
function handleCORS(res) {
res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins);
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
// Start the server
server.listen(PORT, () => {
console.log(`Worker ${process.pid} running on http://localhost:${PORT}`);
});
// Handle server errors
server.on('error', (error) => {
console.error(`❌ Worker ${process.pid} server error:`, error.message);
process.exit(1);
});
// Graceful shutdown for workers
process.on('SIGINT', () => {
console.log(`\n\n🛑 Worker ${process.pid} shutting down...`);
server.close(() => {
console.log(`✅ Worker ${process.pid} closed`);
process.exit(0);
});
});
}