const dns = require('dns').promises; const { spawn } = require('child_process'); 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 { validateIP, validateDomain, sanitizeInput, validatePort } = require('../../../infrastructure/validation'); const state = require('../../../infrastructure/state'); const MIN_DIAGNOSTIC_COUNT = 1; const MAX_DIAGNOSTIC_COUNT = 10; const PING_TIMEOUT_MS = 30000; const TRACEROUTE_TIMEOUT_MS = 60000; const MAX_DIAGNOSTIC_TARGET_LENGTH = 253; function isValidHostname(target) { if (!target || typeof target !== 'string') return false; // Accept simple hostnames/FQDNs used in local network diagnostics. return /^(?=.{1,253}$)(?!-)[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.?$/.test(target); } function validateDiagnosticTarget(target) { const sanitized = sanitizeInput(target); if (!sanitized || sanitized.length > MAX_DIAGNOSTIC_TARGET_LENGTH) { return null; } if (validateIP(sanitized) || validateDomain(sanitized) || isValidHostname(sanitized)) { return sanitized; } return null; } function normalizeCount(count) { const parsed = Number.parseInt(count, 10); if (Number.isNaN(parsed)) return 4; return Math.min(MAX_DIAGNOSTIC_COUNT, Math.max(MIN_DIAGNOSTIC_COUNT, parsed)); } function runCommand(command, args, timeoutMs) { return new Promise((resolve, reject) => { const child = spawn(command, args, { timeout: timeoutMs }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('error', reject); child.on('close', (code, signal) => { resolve({ code, signal, stdout, stderr }); }); }); } 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; const safeTarget = validateDiagnosticTarget(target); if (!safeTarget) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'A valid diagnostic target is required' })); trackRequest(urlPath, false); return true; } const safeCount = normalizeCount(count); // Streaming mode if (stream) { const startTime = Date.now(); const platform = os.platform(); const pingArgs = platform === 'win32' ? ['-n', safeCount.toString(), safeTarget] : ['-c', safeCount.toString(), safeTarget]; 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 pingArgs = platform === 'win32' ? ['-n', safeCount.toString(), safeTarget] : ['-c', safeCount.toString(), safeTarget]; try { const { stdout, stderr, code } = await runCommand('ping', pingArgs, PING_TIMEOUT_MS); const responseTime = Date.now() - startTime; const success = code === 0; trackRequestWithTiming(urlPath, success, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success, target: safeTarget, count: safeCount, 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: safeTarget, count: safeCount, output: '', error: 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; const safeTarget = validateDiagnosticTarget(target); if (!safeTarget) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'A valid diagnostic 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 = [safeTarget]; 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' : 'traceroute'; const tracerouteArgs = [safeTarget]; try { const { stdout, stderr, code } = await runCommand(tracerouteCmd, tracerouteArgs, TRACEROUTE_TIMEOUT_MS); const responseTime = Date.now() - startTime; const success = code === 0; trackRequestWithTiming(urlPath, success, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success, target: safeTarget, 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: safeTarget, output: '', error: 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(); const safeDomain = validateDiagnosticTarget(domain); const safePort = Number.parseInt(port, 10); if (!safeDomain || !validatePort(safePort)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Valid domain and port are required' })); trackRequest(urlPath, false); return true; } // First resolve domain to IP let ip; try { const addresses = await dns.resolve4(safeDomain); ip = addresses[0]; } catch (dnsErr) { trackRequest(urlPath, false); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, domain: safeDomain, port: safePort, 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(safePort, 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: safeDomain, ip, port: safePort, 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 };