Files
p2ns/includes/admin/admin-backend/routes/diagnostics.js
T

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 };