535 lines
17 KiB
JavaScript
535 lines
17 KiB
JavaScript
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 };
|
|
|