fix traceroute module

This commit is contained in:
2025-03-29 18:40:08 +01:00
parent 7f11612aa6
commit 1c6802995f
+42 -43
View File
@@ -12,6 +12,14 @@ const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
const router = express.Router(); const router = express.Router();
// Helper function to safely create an error message string
function getErrorMessage(err, defaultMessage = 'An unknown error occurred') {
if (typeof err === 'string') return err;
if (err && typeof err.message === 'string' && err.message.trim() !== '') return err.message;
return defaultMessage;
}
// Route handler for / (relative to /api/traceroute) // Route handler for / (relative to /api/traceroute)
router.get('/', (req, res) => { router.get('/', (req, res) => {
const targetIpRaw = req.query.targetIp; const targetIpRaw = req.query.targetIp;
@@ -22,7 +30,6 @@ router.get('/', (req, res) => {
if (!isValidIp(targetIp)) { if (!isValidIp(targetIp)) {
logger.warn({ requestIp, targetIp }, 'Invalid target IP for traceroute'); logger.warn({ requestIp, targetIp }, 'Invalid target IP for traceroute');
// Send JSON error for consistency, even though it's an SSE endpoint initially
return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' }); return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' });
} }
if (isPrivateIp(targetIp)) { if (isPrivateIp(targetIp)) {
@@ -30,12 +37,10 @@ router.get('/', (req, res) => {
return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' }); return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' });
} }
// Start Sentry transaction for the stream
const transaction = Sentry.startTransaction({ const transaction = Sentry.startTransaction({
op: "traceroute.stream", op: "traceroute.stream",
name: `/api/traceroute?targetIp=${targetIp}`, // Use sanitized targetIp name: `/api/traceroute?targetIp=${targetIp}`,
}); });
// Set scope for this request to associate errors/events with the transaction
Sentry.configureScope(scope => { Sentry.configureScope(scope => {
scope.setSpan(transaction); scope.setSpan(transaction);
scope.setContext("request", { ip: requestIp, targetIp }); scope.setContext("request", { ip: requestIp, targetIp });
@@ -43,35 +48,35 @@ router.get('/', (req, res) => {
try { try {
logger.info({ requestIp, targetIp }, `Starting traceroute stream...`); logger.info({ requestIp, targetIp }, `Starting traceroute stream...`);
// Set SSE headers
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Important for Nginx buffering res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders(); // Send headers immediately res.flushHeaders();
// Traceroute command arguments (using -n to avoid DNS lookups within traceroute itself)
const args = ['-n', targetIp]; const args = ['-n', targetIp];
const command = 'traceroute'; const command = 'traceroute';
const proc = spawn(command, args); const proc = spawn(command, args);
logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Spawned traceroute process'); logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Spawned traceroute process');
let buffer = ''; // Buffer for incomplete lines let buffer = '';
// Helper function to send SSE events safely
const sendEvent = (event, data) => { const sendEvent = (event, data) => {
try { try {
// Check if the connection is still writable before sending
if (!res.writableEnded) { if (!res.writableEnded) {
// Ensure error events always have a string in data.error
if (event === 'error' && (!data || typeof data.error !== 'string')) {
const safeErrorMessage = getErrorMessage(data?.error, 'Traceroute encountered an unspecified error.');
logger.warn({ requestIp, targetIp, originalData: data }, `Corrected invalid error event data. Sending: ${safeErrorMessage}`);
data = { error: safeErrorMessage };
}
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
} else { } else {
logger.warn({ requestIp, targetIp, event }, "Attempted to write to closed SSE stream."); logger.warn({ requestIp, targetIp, event }, "Attempted to write to closed SSE stream.");
} }
} catch (e) { } catch (e) {
// Catch errors during write (e.g., client disconnected)
logger.error({ requestIp, targetIp, event, error: e.message }, "Error writing to SSE stream (client likely disconnected)"); logger.error({ requestIp, targetIp, event, error: e.message }, "Error writing to SSE stream (client likely disconnected)");
Sentry.captureException(e, { level: 'warning', extra: { requestIp, targetIp, event } }); Sentry.captureException(e, { level: 'warning', extra: { requestIp, targetIp, event } });
// Clean up: kill process, end response, finish transaction
if (proc && !proc.killed) proc.kill(); if (proc && !proc.killed) proc.kill();
if (!res.writableEnded) res.end(); if (!res.writableEnded) res.end();
transaction.setStatus('internal_error'); transaction.setStatus('internal_error');
@@ -79,45 +84,40 @@ router.get('/', (req, res) => {
} }
}; };
// Handle stdout data (traceroute output)
proc.stdout.on('data', (data) => { proc.stdout.on('data', (data) => {
buffer += data.toString(); buffer += data.toString();
let lines = buffer.split('\n'); let lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep the last potentially incomplete line buffer = lines.pop() || '';
lines.forEach(line => { lines.forEach(line => {
const parsed = parseTracerouteLine(line); const parsed = parseTracerouteLine(line);
if (parsed) { if (parsed) {
logger.debug({ requestIp, targetIp, hop: parsed.hop, ip: parsed.ip }, 'Sending hop data'); logger.debug({ requestIp, targetIp, hop: parsed.hop, ip: parsed.ip }, 'Sending hop data');
sendEvent('hop', parsed); sendEvent('hop', parsed);
} else if (line.trim()) { } else if (line.trim()) {
// Send non-hop lines as info messages (e.g., header)
logger.debug({ requestIp, targetIp, message: line.trim() }, 'Sending info data'); logger.debug({ requestIp, targetIp, message: line.trim() }, 'Sending info data');
sendEvent('info', { message: line.trim() }); sendEvent('info', { message: line.trim() });
} }
}); });
}); });
// Handle stderr data
proc.stderr.on('data', (data) => { proc.stderr.on('data', (data) => {
const errorMsg = data.toString().trim(); const errorMsg = getErrorMessage(data.toString().trim(), 'Traceroute produced unknown stderr output.');
logger.warn({ requestIp, targetIp, stderr: errorMsg }, 'Traceroute stderr output'); logger.warn({ requestIp, targetIp, stderr: errorMsg }, 'Traceroute stderr output');
Sentry.captureMessage('Traceroute stderr output', { level: 'warning', extra: { requestIp, targetIp, stderr: errorMsg } }); Sentry.captureMessage('Traceroute stderr output', { level: 'warning', extra: { requestIp, targetIp, stderr: errorMsg } });
sendEvent('error', { error: errorMsg }); // Send stderr as an error event sendEvent('error', { error: errorMsg }); // errorMsg is now guaranteed to be a string
}); });
// Handle process errors (e.g., command not found)
proc.on('error', (err) => { proc.on('error', (err) => {
logger.error({ requestIp, targetIp, error: err.message }, `Failed to start traceroute command`); const errorMsg = getErrorMessage(err, 'Failed to start traceroute command due to an unknown error.');
Sentry.captureException(err, { extra: { requestIp, targetIp } }); logger.error({ requestIp, targetIp, error: errorMsg }, `Failed to start traceroute command`);
sendEvent('error', { error: `Failed to start traceroute: ${err.message}` }); Sentry.captureException(err, { extra: { requestIp, targetIp } }); // Send original error to Sentry
if (!res.writableEnded) res.end(); // Ensure response is ended sendEvent('error', { error: `Failed to start traceroute: ${errorMsg}` }); // Send safe message to client
if (!res.writableEnded) res.end();
transaction.setStatus('internal_error'); transaction.setStatus('internal_error');
transaction.finish(); transaction.finish();
}); });
// Handle process close event
proc.on('close', (code) => { proc.on('close', (code) => {
// Process any remaining data in the buffer
if (buffer) { if (buffer) {
const parsed = parseTracerouteLine(buffer); const parsed = parseTracerouteLine(buffer);
if (parsed) sendEvent('hop', parsed); if (parsed) sendEvent('hop', parsed);
@@ -125,43 +125,42 @@ router.get('/', (req, res) => {
} }
if (code !== 0) { if (code !== 0) {
logger.error({ requestIp, targetIp, exitCode: code }, `Traceroute command finished with error code ${code}`); const errorMsg = `Traceroute command failed with exit code ${code}`;
logger.error({ requestIp, targetIp, exitCode: code }, errorMsg);
Sentry.captureMessage('Traceroute command failed', { level: 'error', extra: { requestIp, targetIp, exitCode: code } }); Sentry.captureMessage('Traceroute command failed', { level: 'error', extra: { requestIp, targetIp, exitCode: code } });
sendEvent('error', { error: `Traceroute command failed with exit code ${code}` }); sendEvent('error', { error: errorMsg }); // Send specific error message
transaction.setStatus('unknown_error'); // Or more specific if possible transaction.setStatus('unknown_error');
} else { } else {
logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`); logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`);
transaction.setStatus('ok'); transaction.setStatus('ok');
} }
sendEvent('end', { exitCode: code }); // Signal the end of the stream sendEvent('end', { exitCode: code });
if (!res.writableEnded) res.end(); // Ensure response is ended if (!res.writableEnded) res.end();
transaction.finish(); // Finish Sentry transaction transaction.finish();
}); });
// Handle client disconnection
req.on('close', () => { req.on('close', () => {
logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.'); logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.');
if (proc && !proc.killed) proc.kill(); // Kill the traceroute process if (proc && !proc.killed) proc.kill();
if (!res.writableEnded) res.end(); // Ensure response is ended if (!res.writableEnded) res.end();
transaction.setStatus('cancelled'); // Mark transaction as cancelled transaction.setStatus('cancelled');
transaction.finish(); transaction.finish();
}); });
} catch (error) { } catch (error) {
// Catch errors during initial setup (before headers sent) const errorMsg = getErrorMessage(error, 'Failed to initiate traceroute due to an internal server error.');
logger.error({ requestIp, targetIp, error: error.message, stack: error.stack }, 'Error setting up traceroute stream'); logger.error({ requestIp, targetIp, error: errorMsg, stack: error.stack }, 'Error setting up traceroute stream');
Sentry.captureException(error, { extra: { requestIp, targetIp } }); Sentry.captureException(error, { extra: { requestIp, targetIp } }); // Send original error to Sentry
transaction.setStatus('internal_error'); transaction.setStatus('internal_error');
transaction.finish(); transaction.finish();
// If headers haven't been sent, send a standard JSON error
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${error.message}` }); res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${errorMsg}` });
} else { } else {
// If headers were sent, try to send an error event via SSE (best effort)
try { try {
if (!res.writableEnded) { if (!res.writableEnded) {
sendEvent('error', { error: `Internal server error during setup: ${error.message}` }); // Use the safe sendEvent function here as well
sendEvent('error', { error: `Internal server error during setup: ${errorMsg}` });
res.end(); res.end();
} }
} catch (e) { logger.error({ requestIp, targetIp, error: e.message }, "Error writing final setup error to SSE stream"); } } catch (e) { logger.error({ requestIp, targetIp, error: e.message }, "Error writing final setup error to SSE stream"); }