const dns = require('dns').promises; const { exec, spawn } = require('child_process'); const { promisify } = require('util'); const net = require('net'); const os = require('os'); const { logError, logDebug } = require('../../../infrastructure/logger'); const { trackRequest, trackRequestWithTiming } = require('../../../maintenance/metrics'); const { createErrorResponse } = require('../../../infrastructure/error_handler'); const execAsync = promisify(exec); const state = require('../../../infrastructure/state'); async function handleDiagnosticsRoutes(req, res) { const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname; const method = req.method; // POST /api/diagnostics/dns-lookup if (method === 'POST' && urlPath === '/api/diagnostics/dns-lookup') { try { let body = ''; for await (const chunk of req) { body += chunk.toString(); } const data = JSON.parse(body); const { domain, type = 'A' } = data; if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'domain is required' })); trackRequest(urlPath, false); return true; } const startTime = Date.now(); let results = []; try { switch (type.toUpperCase()) { case 'A': results = await dns.resolve4(domain); break; case 'AAAA': results = await dns.resolve6(domain); break; case 'MX': results = await dns.resolveMx(domain); break; case 'TXT': results = await dns.resolveTxt(domain); break; case 'NS': results = await dns.resolveNs(domain); break; case 'CNAME': results = await dns.resolveCname(domain); break; case 'SRV': results = await dns.resolveSrv(domain); break; case 'PTR': results = await dns.resolvePtr(domain); break; case 'SOA': results = await dns.resolveSoa(domain); break; default: throw new Error(`Unsupported DNS record type: ${type}`); } const responseTime = Date.now() - startTime; trackRequestWithTiming(urlPath, true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, domain, type, results: Array.isArray(results) ? results : [results], responseTime })); } catch (dnsErr) { const responseTime = Date.now() - startTime; trackRequest(urlPath, false); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, domain, type, error: dnsErr.message, results: [], responseTime })); } } catch (err) { logError('Admin', `DNS lookup failed: ${err.message}`); trackRequest(urlPath, false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // POST /api/diagnostics/ping if (method === 'POST' && urlPath === '/api/diagnostics/ping') { try { let body = ''; for await (const chunk of req) { body += chunk.toString(); } const data = JSON.parse(body); const { target, count = 4, stream = false } = data; if (!target) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'target is required' })); trackRequest(urlPath, false); return true; } // Streaming mode if (stream) { const startTime = Date.now(); const platform = os.platform(); const pingArgs = platform === 'win32' ? ['-n', count.toString(), target] : ['-c', count.toString(), target]; const pingProcess = spawn('ping', pingArgs); // Set up streaming response res.writeHead(200, { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); let output = ''; let errorOutput = ''; pingProcess.stdout.on('data', (data) => { const text = data.toString(); output += text; // Send each line as it arrives const lines = text.split('\n').filter(line => line.trim()); for (const line of lines) { res.write(JSON.stringify({ type: 'output', data: line, timestamp: Date.now() }) + '\n'); } }); pingProcess.stderr.on('data', (data) => { const text = data.toString(); errorOutput += text; res.write(JSON.stringify({ type: 'error', data: text, timestamp: Date.now() }) + '\n'); }); pingProcess.on('close', (code) => { const responseTime = Date.now() - startTime; const success = code === 0; trackRequestWithTiming(urlPath, success, responseTime); res.write(JSON.stringify({ type: 'complete', success, exitCode: code, output, error: errorOutput || null, responseTime }) + '\n'); res.end(); }); pingProcess.on('error', (err) => { res.write(JSON.stringify({ type: 'error', error: err.message, timestamp: Date.now() }) + '\n'); res.end(); trackRequest(urlPath, false); }); return true; } // Non-streaming mode (backward compatibility) const startTime = Date.now(); const platform = os.platform(); const pingCmd = platform === 'win32' ? `ping -n ${count} ${target}` : `ping -c ${count} ${target}`; try { const { stdout, stderr } = await execAsync(pingCmd, { timeout: 30000 }); const responseTime = Date.now() - startTime; trackRequestWithTiming(urlPath, true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, target, count, output: stdout, error: stderr || null, responseTime })); } catch (execErr) { const responseTime = Date.now() - startTime; trackRequest(urlPath, false); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, target, count, output: execErr.stdout || '', error: execErr.stderr || execErr.message, responseTime })); } } catch (err) { logError('Admin', `Ping failed: ${err.message}`); trackRequest(urlPath, false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // POST /api/diagnostics/traceroute if (method === 'POST' && urlPath === '/api/diagnostics/traceroute') { try { let body = ''; for await (const chunk of req) { body += chunk.toString(); } const data = JSON.parse(body); const { target, stream = false } = data; if (!target) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'target is required' })); trackRequest(urlPath, false); return true; } // Streaming mode if (stream) { const startTime = Date.now(); const platform = os.platform(); const tracerouteCmd = platform === 'win32' ? 'tracert' : 'traceroute'; const tracerouteArgs = platform === 'win32' ? [target] : [target]; const tracerouteProcess = spawn(tracerouteCmd, tracerouteArgs); // Set up streaming response res.writeHead(200, { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); let output = ''; let errorOutput = ''; tracerouteProcess.stdout.on('data', (data) => { const text = data.toString(); output += text; // Send each line as it arrives const lines = text.split('\n').filter(line => line.trim()); for (const line of lines) { res.write(JSON.stringify({ type: 'output', data: line, timestamp: Date.now() }) + '\n'); } }); tracerouteProcess.stderr.on('data', (data) => { const text = data.toString(); errorOutput += text; res.write(JSON.stringify({ type: 'error', data: text, timestamp: Date.now() }) + '\n'); }); tracerouteProcess.on('close', (code) => { const responseTime = Date.now() - startTime; const success = code === 0; trackRequestWithTiming(urlPath, success, responseTime); res.write(JSON.stringify({ type: 'complete', success, exitCode: code, output, error: errorOutput || null, responseTime }) + '\n'); res.end(); }); tracerouteProcess.on('error', (err) => { res.write(JSON.stringify({ type: 'error', error: err.message, timestamp: Date.now() }) + '\n'); res.end(); trackRequest(urlPath, false); }); return true; } // Non-streaming mode (backward compatibility) const startTime = Date.now(); const platform = os.platform(); const tracerouteCmd = platform === 'win32' ? `tracert ${target}` : `traceroute ${target}`; try { const { stdout, stderr } = await execAsync(tracerouteCmd, { timeout: 60000 }); const responseTime = Date.now() - startTime; trackRequestWithTiming(urlPath, true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, target, output: stdout, error: stderr || null, responseTime })); } catch (execErr) { const responseTime = Date.now() - startTime; trackRequest(urlPath, false); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, target, output: execErr.stdout || '', error: execErr.stderr || execErr.message, responseTime })); } } catch (err) { logError('Admin', `Traceroute failed: ${err.message}`); trackRequest(urlPath, false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // POST /api/diagnostics/connection-test if (method === 'POST' && urlPath === '/api/diagnostics/connection-test') { try { let body = ''; for await (const chunk of req) { body += chunk.toString(); } const data = JSON.parse(body); const { domain, port } = data; if (!domain || !port) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'domain and port are required' })); trackRequest(urlPath, false); return true; } const startTime = Date.now(); // First resolve domain to IP let ip; try { const addresses = await dns.resolve4(domain); ip = addresses[0]; } catch (dnsErr) { trackRequest(urlPath, false); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, domain, port, error: `DNS resolution failed: ${dnsErr.message}`, responseTime: Date.now() - startTime })); return true; } // Test TCP connection const testConnection = () => { return new Promise((resolve) => { const socket = new net.Socket(); const timeout = 5000; let connected = false; socket.setTimeout(timeout); socket.on('connect', () => { connected = true; socket.destroy(); resolve({ success: true, latency: Date.now() - startTime }); }); socket.on('timeout', () => { socket.destroy(); resolve({ success: false, error: 'Connection timeout' }); }); socket.on('error', (err) => { resolve({ success: false, error: err.message }); }); socket.connect(port, ip); }); }; const result = await testConnection(); const responseTime = Date.now() - startTime; trackRequestWithTiming(urlPath, result.success, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ...result, domain, ip, port, responseTime })); } catch (err) { logError('Admin', `Connection test failed: ${err.message}`); trackRequest(urlPath, false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // GET /api/diagnostics/bandwidth if (method === 'GET' && urlPath === '/api/diagnostics/bandwidth') { try { const startTime = Date.now(); const networkInterfaces = os.networkInterfaces(); const stats = {}; for (const [name, addresses] of Object.entries(networkInterfaces)) { if (!addresses) continue; let totalBytes = 0; let totalPackets = 0; for (const addr of addresses) { if (addr.family === 'IPv4' || addr.family === 'IPv6') { // Note: Node.js doesn't provide real-time bandwidth stats // This is a placeholder structure stats[name] = { name, addresses: addresses.map(a => ({ address: a.address, netmask: a.netmask, family: a.family, mac: a.mac || 'N/A', internal: a.internal })), // These would need system-specific tools to get real values bytesReceived: 0, bytesSent: 0, packetsReceived: 0, packetsSent: 0 }; } } } const responseTime = Date.now() - startTime; trackRequestWithTiming(urlPath, true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ interfaces: stats, note: 'Bandwidth statistics require system-specific tools. Interface information only.', responseTime })); } catch (err) { logError('Admin', `Bandwidth stats failed: ${err.message}`); trackRequest(urlPath, false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } // GET /api/diagnostics/invites if (method === 'GET' && urlPath === '/api/diagnostics/invites') { try { if (!state.diagnoseInviteIssues) { res.writeHead(503, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invite diagnostics not available - system still initializing' })); trackRequest(urlPath, false); return true; } const diagnostics = state.diagnoseInviteIssues(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(diagnostics)); trackRequest(urlPath, true); return true; } catch (err) { logError('DiagnosticsRoute', `Invite diagnostics error: ${err.message}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); trackRequest(urlPath, false); return true; } } return false; } module.exports = { handleDiagnosticsRoutes };