mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-04-19 14:13:44 +02:00
seperate server.js
This commit is contained in:
115
backend/routes/dnsLookup.js
Normal file
115
backend/routes/dnsLookup.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// backend/routes/dnsLookup.js
|
||||
const express = require('express');
|
||||
const Sentry = require("@sentry/node");
|
||||
const dns = require('dns').promises;
|
||||
const pino = require('pino');
|
||||
|
||||
// Import utilities
|
||||
const { isValidDomain } = require('../utils');
|
||||
|
||||
// Logger for this module
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Supported DNS record types
|
||||
const VALID_DNS_TYPES = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'CNAME', 'SOA', 'SRV', 'PTR', 'ANY'];
|
||||
|
||||
// Route handler for / (relative to /api/dns-lookup)
|
||||
router.get('/', async (req, res, next) => {
|
||||
const domainRaw = req.query.domain;
|
||||
const domain = typeof domainRaw === 'string' ? domainRaw.trim() : domainRaw;
|
||||
const typeRaw = req.query.type;
|
||||
// Default to 'ANY' if type is missing or invalid, convert valid types to uppercase
|
||||
let type = typeof typeRaw === 'string' ? typeRaw.trim().toUpperCase() : 'ANY';
|
||||
if (!VALID_DNS_TYPES.includes(type)) {
|
||||
logger.warn({ requestIp: req.ip, domain, requestedType: typeRaw }, 'Invalid record type requested, defaulting to ANY');
|
||||
type = 'ANY'; // Default to 'ANY' for invalid types
|
||||
}
|
||||
|
||||
const requestIp = req.ip || req.socket.remoteAddress;
|
||||
|
||||
logger.info({ requestIp, domain, type }, 'DNS lookup request received');
|
||||
|
||||
if (!isValidDomain(domain)) {
|
||||
logger.warn({ requestIp, domain }, 'Invalid domain for DNS lookup');
|
||||
return res.status(400).json({ success: false, error: 'Invalid domain name provided.' });
|
||||
}
|
||||
|
||||
// Note: No isPrivateIp check here as DNS lookups for internal domains might be valid use cases.
|
||||
|
||||
try {
|
||||
let records;
|
||||
if (type === 'ANY') {
|
||||
// Define types to query for 'ANY' - exclude PTR as it requires an IP
|
||||
const typesToQuery = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'CNAME', 'SOA', 'SRV'];
|
||||
const promises = typesToQuery.map(t =>
|
||||
dns.resolve(domain, t)
|
||||
.then(result => ({ type: t, records: result })) // Wrap result with type
|
||||
.catch(err => ({ type: t, error: err })) // Wrap error with type
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
records = {};
|
||||
results.forEach(result => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const data = result.value;
|
||||
if (data.error) {
|
||||
// Log DNS resolution errors for specific types as warnings/debug
|
||||
if (data.error.code !== 'ENOTFOUND' && data.error.code !== 'ENODATA') {
|
||||
logger.warn({ requestIp, domain, type: data.type, error: data.error.message, code: data.error.code }, `DNS lookup failed for type ${data.type}`);
|
||||
} else {
|
||||
logger.debug({ requestIp, domain, type: data.type, code: data.error.code }, `No record found for type ${data.type}`);
|
||||
}
|
||||
// Optionally include error details in response (or just omit the type)
|
||||
// records[data.type] = { error: `Lookup failed (${data.error.code || 'Unknown'})` };
|
||||
} else if (data.records && data.records.length > 0) {
|
||||
// Only add if records exist
|
||||
records[data.type] = data.records;
|
||||
}
|
||||
} else {
|
||||
// Handle unexpected errors from Promise.allSettled (should be rare)
|
||||
logger.error({ requestIp, domain, type: 'ANY', error: result.reason?.message }, 'Unexpected error during Promise.allSettled for ANY DNS lookup');
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(records).length === 0) {
|
||||
// If no records found for any type
|
||||
logger.info({ requestIp, domain, type }, 'DNS lookup for ANY type yielded no records.');
|
||||
// Send success: true, but with an empty records object or a note
|
||||
// return res.json({ success: true, domain, type, records: {}, note: 'No records found for queried types.' });
|
||||
}
|
||||
|
||||
} else {
|
||||
// Handle specific type query
|
||||
try {
|
||||
records = await dns.resolve(domain, type);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOTFOUND' || error.code === 'ENODATA') {
|
||||
logger.info({ requestIp, domain, type, code: error.code }, `DNS lookup failed (No record) for type ${type}`);
|
||||
// Return success: true, but indicate no records found
|
||||
return res.json({ success: true, domain, type, records: [], note: `No ${type} records found.` });
|
||||
} else {
|
||||
// Rethrow other errors to be caught by the outer catch block
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info({ requestIp, domain, type }, 'DNS lookup successful');
|
||||
// For specific type, records will be an array. For ANY, it's an object.
|
||||
res.json({ success: true, domain, type, records });
|
||||
|
||||
} catch (error) {
|
||||
// Catches errors from specific type lookups (not ENOTFOUND/ENODATA) or unexpected errors
|
||||
logger.error({ requestIp, domain, type, error: error.message, code: error.code }, 'DNS lookup failed');
|
||||
Sentry.captureException(error, { extra: { requestIp, domain, type } });
|
||||
// Send appropriate status code based on error if possible, otherwise 500
|
||||
const statusCode = error.code === 'ESERVFAIL' ? 502 : 500;
|
||||
res.status(statusCode).json({ success: false, error: `DNS lookup failed: ${error.message} (Code: ${error.code || 'Unknown'})` });
|
||||
// next(error); // Optional: Pass to Sentry error handler
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
116
backend/routes/ipinfo.js
Normal file
116
backend/routes/ipinfo.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// backend/routes/ipinfo.js
|
||||
const express = require('express');
|
||||
const Sentry = require("@sentry/node");
|
||||
const dns = require('dns').promises;
|
||||
const pino = require('pino'); // Assuming logger is needed, or pass it down
|
||||
|
||||
// Import utilities and MaxMind reader access
|
||||
const { isValidIp, getCleanIp } = require('../utils');
|
||||
const { getMaxMindReaders } = require('../maxmind');
|
||||
|
||||
// Create a logger instance for this route module
|
||||
// Ideally, the main logger instance should be passed down or configured globally
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Route handler for / (relative to where this router is mounted, e.g., /api/ipinfo)
|
||||
router.get('/', async (req, res, next) => {
|
||||
const requestIp = req.ip || req.socket.remoteAddress;
|
||||
logger.info({ ip: requestIp, method: req.method, url: req.originalUrl }, 'ipinfo request received');
|
||||
const clientIp = getCleanIp(requestIp);
|
||||
logger.debug({ rawIp: requestIp, cleanedIp: clientIp }, 'IP cleaning result');
|
||||
|
||||
if (!clientIp || !isValidIp(clientIp)) {
|
||||
if (clientIp === '127.0.0.1' || clientIp === '::1') {
|
||||
logger.info({ ip: clientIp }, 'Responding with localhost info');
|
||||
return res.json({
|
||||
ip: clientIp,
|
||||
geo: { note: 'Localhost IP, no Geo data available.' },
|
||||
asn: { note: 'Localhost IP, no ASN data available.' },
|
||||
rdns: ['localhost'],
|
||||
});
|
||||
}
|
||||
logger.error({ rawIp: requestIp, cleanedIp: clientIp }, 'Could not determine a valid client IP');
|
||||
Sentry.captureMessage('Could not determine a valid client IP', {
|
||||
level: 'error',
|
||||
extra: { rawIp: requestIp, cleanedIp: clientIp }
|
||||
});
|
||||
// Use 400 for client error (invalid IP derived)
|
||||
return res.status(400).json({ error: 'Could not determine a valid client IP address.', rawIp: requestIp, cleanedIp: clientIp });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get initialized MaxMind readers
|
||||
const { cityReader, asnReader } = getMaxMindReaders();
|
||||
|
||||
let geo = null;
|
||||
try {
|
||||
const geoData = cityReader.city(clientIp);
|
||||
geo = {
|
||||
city: geoData.city?.names?.en,
|
||||
region: geoData.subdivisions?.[0]?.isoCode,
|
||||
country: geoData.country?.isoCode,
|
||||
countryName: geoData.country?.names?.en,
|
||||
postalCode: geoData.postal?.code,
|
||||
latitude: geoData.location?.latitude,
|
||||
longitude: geoData.location?.longitude,
|
||||
timezone: geoData.location?.timeZone,
|
||||
};
|
||||
// Remove null/undefined values
|
||||
geo = Object.fromEntries(Object.entries(geo).filter(([_, v]) => v != null));
|
||||
logger.debug({ ip: clientIp, geo }, 'GeoIP lookup successful');
|
||||
} catch (e) {
|
||||
// Log as warning, as this is expected for private IPs or IPs not in DB
|
||||
logger.warn({ ip: clientIp, error: e.message }, `MaxMind City lookup failed`);
|
||||
geo = { error: 'GeoIP lookup failed (IP not found in database or private range).' };
|
||||
}
|
||||
|
||||
let asn = null;
|
||||
try {
|
||||
const asnData = asnReader.asn(clientIp);
|
||||
asn = {
|
||||
number: asnData.autonomousSystemNumber,
|
||||
organization: asnData.autonomousSystemOrganization,
|
||||
};
|
||||
asn = Object.fromEntries(Object.entries(asn).filter(([_, v]) => v != null));
|
||||
logger.debug({ ip: clientIp, asn }, 'ASN lookup successful');
|
||||
} catch (e) {
|
||||
logger.warn({ ip: clientIp, error: e.message }, `MaxMind ASN lookup failed`);
|
||||
asn = { error: 'ASN lookup failed (IP not found in database or private range).' };
|
||||
}
|
||||
|
||||
let rdns = null;
|
||||
try {
|
||||
const hostnames = await dns.reverse(clientIp);
|
||||
rdns = hostnames;
|
||||
logger.debug({ ip: clientIp, rdns }, 'rDNS lookup successful');
|
||||
} catch (e) {
|
||||
// Log non-existence as debug, other errors as warn
|
||||
if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') {
|
||||
logger.warn({ ip: clientIp, error: e.message, code: e.code }, `rDNS lookup error`);
|
||||
} else {
|
||||
logger.debug({ ip: clientIp, code: e.code }, 'rDNS lookup failed (No record)');
|
||||
}
|
||||
// Provide a structured error in the response
|
||||
rdns = { error: `rDNS lookup failed (${e.code || 'Unknown error'})` };
|
||||
}
|
||||
|
||||
res.json({
|
||||
ip: clientIp,
|
||||
// Only include geo/asn if they don't contain an error and have data
|
||||
geo: geo.error ? geo : (Object.keys(geo).length > 0 ? geo : null),
|
||||
asn: asn.error ? asn : (Object.keys(asn).length > 0 ? asn : null),
|
||||
rdns // rdns will contain either the array of hostnames or the error object
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Catch unexpected errors during processing (e.g., issues with getMaxMindReaders)
|
||||
logger.error({ ip: clientIp, error: error.message, stack: error.stack }, 'Error processing ipinfo');
|
||||
Sentry.captureException(error, { extra: { ip: clientIp } });
|
||||
// Pass the error to the Sentry error handler middleware
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
104
backend/routes/lookup.js
Normal file
104
backend/routes/lookup.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// backend/routes/lookup.js
|
||||
const express = require('express');
|
||||
const Sentry = require("@sentry/node");
|
||||
const dns = require('dns').promises;
|
||||
const pino = require('pino');
|
||||
|
||||
// Import utilities and MaxMind reader access
|
||||
const { isValidIp, isPrivateIp } = require('../utils');
|
||||
const { getMaxMindReaders } = require('../maxmind');
|
||||
|
||||
// Logger for this module
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Route handler for / (relative to /api/lookup)
|
||||
router.get('/', async (req, res, next) => {
|
||||
const targetIpRaw = req.query.targetIp;
|
||||
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
|
||||
const requestIp = req.ip || req.socket.remoteAddress; // IP of the client making the request
|
||||
|
||||
logger.info({ requestIp, targetIp }, 'Lookup request received');
|
||||
|
||||
if (!isValidIp(targetIp)) {
|
||||
logger.warn({ requestIp, targetIp }, 'Invalid target IP for lookup');
|
||||
return res.status(400).json({ success: false, error: 'Invalid IP address provided for lookup.' });
|
||||
}
|
||||
if (isPrivateIp(targetIp)) {
|
||||
logger.warn({ requestIp, targetIp }, 'Attempt to lookup private IP blocked');
|
||||
return res.status(403).json({ success: false, error: 'Lookup for private or local IP addresses is not supported.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get initialized MaxMind readers
|
||||
const { cityReader, asnReader } = getMaxMindReaders();
|
||||
|
||||
// Perform lookups in parallel
|
||||
const geoPromise = cityReader.city(targetIp)
|
||||
.then(geoData => {
|
||||
let geo = {
|
||||
city: geoData.city?.names?.en, region: geoData.subdivisions?.[0]?.isoCode,
|
||||
country: geoData.country?.isoCode, countryName: geoData.country?.names?.en,
|
||||
postalCode: geoData.postal?.code, latitude: geoData.location?.latitude,
|
||||
longitude: geoData.location?.longitude, timezone: geoData.location?.timeZone,
|
||||
};
|
||||
geo = Object.fromEntries(Object.entries(geo).filter(([_, v]) => v != null));
|
||||
logger.debug({ targetIp, geo }, 'GeoIP lookup successful for lookup');
|
||||
return Object.keys(geo).length > 0 ? geo : null; // Return null if empty
|
||||
})
|
||||
.catch(e => {
|
||||
logger.warn({ targetIp, error: e.message }, `MaxMind City lookup failed for lookup`);
|
||||
return { error: 'GeoIP lookup failed (IP not found in database or private range).' };
|
||||
});
|
||||
|
||||
const asnPromise = asnReader.asn(targetIp)
|
||||
.then(asnData => {
|
||||
let asn = { number: asnData.autonomousSystemNumber, organization: asnData.autonomousSystemOrganization };
|
||||
asn = Object.fromEntries(Object.entries(asn).filter(([_, v]) => v != null));
|
||||
logger.debug({ targetIp, asn }, 'ASN lookup successful for lookup');
|
||||
return Object.keys(asn).length > 0 ? asn : null; // Return null if empty
|
||||
})
|
||||
.catch(e => {
|
||||
logger.warn({ targetIp, error: e.message }, `MaxMind ASN lookup failed for lookup`);
|
||||
return { error: 'ASN lookup failed (IP not found in database or private range).' };
|
||||
});
|
||||
|
||||
const rdnsPromise = dns.reverse(targetIp)
|
||||
.then(hostnames => {
|
||||
logger.debug({ targetIp, rdns: hostnames }, 'rDNS lookup successful for lookup');
|
||||
return hostnames; // Returns array of hostnames
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') {
|
||||
logger.warn({ targetIp, error: e.message, code: e.code }, `rDNS lookup error for lookup`);
|
||||
} else {
|
||||
logger.debug({ targetIp, code: e.code }, 'rDNS lookup failed (No record) for lookup');
|
||||
}
|
||||
return { error: `rDNS lookup failed (${e.code || 'Unknown error'})` };
|
||||
});
|
||||
|
||||
// Wait for all promises to settle
|
||||
const [geoResult, asnResult, rdnsResult] = await Promise.all([
|
||||
geoPromise,
|
||||
asnPromise,
|
||||
rdnsPromise
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true, // Indicate overall success of the request processing
|
||||
ip: targetIp,
|
||||
geo: geoResult, // Will be the geo object, null, or error object
|
||||
asn: asnResult, // Will be the asn object, null, or error object
|
||||
rdns: rdnsResult // Will be the hostname array or error object
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Catch unexpected errors (e.g., issue with getMaxMindReaders or Promise.all)
|
||||
logger.error({ targetIp, requestIp, error: error.message, stack: error.stack }, 'Error processing lookup');
|
||||
Sentry.captureException(error, { extra: { targetIp, requestIp } });
|
||||
next(error); // Pass to the main error handler
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
82
backend/routes/ping.js
Normal file
82
backend/routes/ping.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// backend/routes/ping.js
|
||||
const express = require('express');
|
||||
const Sentry = require("@sentry/node");
|
||||
const pino = require('pino');
|
||||
|
||||
// Import utilities
|
||||
const { isValidIp, isPrivateIp, executeCommand, parsePingOutput } = require('../utils');
|
||||
|
||||
// Logger for this module
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Route handler for / (relative to /api/ping)
|
||||
router.get('/', async (req, res, next) => {
|
||||
const targetIpRaw = req.query.targetIp;
|
||||
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
|
||||
const requestIp = req.ip || req.socket.remoteAddress;
|
||||
|
||||
logger.info({ requestIp, targetIp }, 'Ping request received');
|
||||
|
||||
if (!isValidIp(targetIp)) {
|
||||
logger.warn({ requestIp, targetIp }, 'Invalid target IP for ping');
|
||||
return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' });
|
||||
}
|
||||
if (isPrivateIp(targetIp)) {
|
||||
logger.warn({ requestIp, targetIp }, 'Attempt to ping private IP blocked');
|
||||
return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const pingCount = process.env.PING_COUNT || '4';
|
||||
let countArg = parseInt(pingCount, 10); // Use let as it might be reassigned
|
||||
// Validate countArg to prevent potential issues
|
||||
if (isNaN(countArg) || countArg <= 0 || countArg > 10) { // Limit count for safety
|
||||
logger.warn({ requestIp, targetIp, requestedCount: pingCount }, 'Invalid or excessive ping count requested, using default.');
|
||||
countArg = 4; // Default to 4 if invalid
|
||||
}
|
||||
|
||||
const args = ['-c', `${countArg}`, targetIp];
|
||||
const command = 'ping';
|
||||
|
||||
logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Executing ping');
|
||||
const output = await executeCommand(command, args);
|
||||
const parsedResult = parsePingOutput(output);
|
||||
|
||||
if (parsedResult.error) {
|
||||
logger.warn({ requestIp, targetIp, error: parsedResult.error, rawOutput: parsedResult.rawOutput }, 'Ping command executed but resulted in an error state');
|
||||
// Send 200 OK but indicate failure in the response body
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
error: parsedResult.error,
|
||||
rawOutput: parsedResult.rawOutput,
|
||||
stats: parsedResult.stats // Include stats even if there's an error message
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({ requestIp, targetIp, stats: parsedResult.stats }, 'Ping successful');
|
||||
res.json({ success: true, ...parsedResult });
|
||||
|
||||
} catch (error) {
|
||||
// This catch block handles errors from executeCommand (e.g., command not found, non-zero exit code)
|
||||
logger.error({ requestIp, targetIp, error: error.message, stderr: error.stderr }, 'Ping command failed execution');
|
||||
Sentry.captureException(error, { extra: { requestIp, targetIp, stderr: error.stderr } });
|
||||
|
||||
// Attempt to parse the error output (might be stdout or stderr from the error object)
|
||||
const errorOutput = error.stderr || error.stdout || error.message;
|
||||
const parsedError = parsePingOutput(errorOutput);
|
||||
|
||||
// Send 500 Internal Server Error, but include parsed details if available
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
// Prioritize parsed error message, fallback to original error message
|
||||
error: `Ping command failed: ${parsedError.error || error.message}`,
|
||||
rawOutput: parsedError.rawOutput || errorOutput // Include raw output for debugging
|
||||
});
|
||||
// Optionally call next(error) if you want the main Sentry error handler to also catch this
|
||||
// next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
172
backend/routes/traceroute.js
Normal file
172
backend/routes/traceroute.js
Normal file
@@ -0,0 +1,172 @@
|
||||
// backend/routes/traceroute.js
|
||||
const express = require('express');
|
||||
const Sentry = require("@sentry/node");
|
||||
const { spawn } = require('child_process');
|
||||
const pino = require('pino');
|
||||
|
||||
// Import utilities
|
||||
const { isValidIp, isPrivateIp, parseTracerouteLine } = require('../utils');
|
||||
|
||||
// Logger for this module
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Route handler for / (relative to /api/traceroute)
|
||||
router.get('/', (req, res) => {
|
||||
const targetIpRaw = req.query.targetIp;
|
||||
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
|
||||
const requestIp = req.ip || req.socket.remoteAddress;
|
||||
|
||||
logger.info({ requestIp, targetIp }, 'Traceroute stream request received');
|
||||
|
||||
if (!isValidIp(targetIp)) {
|
||||
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.' });
|
||||
}
|
||||
if (isPrivateIp(targetIp)) {
|
||||
logger.warn({ requestIp, targetIp }, 'Attempt to traceroute private IP blocked');
|
||||
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({
|
||||
op: "traceroute.stream",
|
||||
name: `/api/traceroute?targetIp=${targetIp}`, // Use sanitized targetIp
|
||||
});
|
||||
// Set scope for this request to associate errors/events with the transaction
|
||||
Sentry.configureScope(scope => {
|
||||
scope.setSpan(transaction);
|
||||
scope.setContext("request", { ip: requestIp, targetIp });
|
||||
});
|
||||
|
||||
try {
|
||||
logger.info({ requestIp, targetIp }, `Starting traceroute stream...`);
|
||||
// Set SSE headers
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no'); // Important for Nginx buffering
|
||||
res.flushHeaders(); // Send headers immediately
|
||||
|
||||
// Traceroute command arguments (using -n to avoid DNS lookups within traceroute itself)
|
||||
const args = ['-n', targetIp];
|
||||
const command = 'traceroute';
|
||||
const proc = spawn(command, args);
|
||||
logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Spawned traceroute process');
|
||||
|
||||
let buffer = ''; // Buffer for incomplete lines
|
||||
|
||||
// Helper function to send SSE events safely
|
||||
const sendEvent = (event, data) => {
|
||||
try {
|
||||
// Check if the connection is still writable before sending
|
||||
if (!res.writableEnded) {
|
||||
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
} else {
|
||||
logger.warn({ requestIp, targetIp, event }, "Attempted to write to closed SSE stream.");
|
||||
}
|
||||
} 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)");
|
||||
Sentry.captureException(e, { level: 'warning', extra: { requestIp, targetIp, event } });
|
||||
// Clean up: kill process, end response, finish transaction
|
||||
if (proc && !proc.killed) proc.kill();
|
||||
if (!res.writableEnded) res.end();
|
||||
transaction.setStatus('internal_error');
|
||||
transaction.finish();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle stdout data (traceroute output)
|
||||
proc.stdout.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
let lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep the last potentially incomplete line
|
||||
lines.forEach(line => {
|
||||
const parsed = parseTracerouteLine(line);
|
||||
if (parsed) {
|
||||
logger.debug({ requestIp, targetIp, hop: parsed.hop, ip: parsed.ip }, 'Sending hop data');
|
||||
sendEvent('hop', parsed);
|
||||
} else if (line.trim()) {
|
||||
// Send non-hop lines as info messages (e.g., header)
|
||||
logger.debug({ requestIp, targetIp, message: line.trim() }, 'Sending info data');
|
||||
sendEvent('info', { message: line.trim() });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle stderr data
|
||||
proc.stderr.on('data', (data) => {
|
||||
const errorMsg = data.toString().trim();
|
||||
logger.warn({ requestIp, targetIp, stderr: errorMsg }, 'Traceroute stderr output');
|
||||
Sentry.captureMessage('Traceroute stderr output', { level: 'warning', extra: { requestIp, targetIp, stderr: errorMsg } });
|
||||
sendEvent('error', { error: errorMsg }); // Send stderr as an error event
|
||||
});
|
||||
|
||||
// Handle process errors (e.g., command not found)
|
||||
proc.on('error', (err) => {
|
||||
logger.error({ requestIp, targetIp, error: err.message }, `Failed to start traceroute command`);
|
||||
Sentry.captureException(err, { extra: { requestIp, targetIp } });
|
||||
sendEvent('error', { error: `Failed to start traceroute: ${err.message}` });
|
||||
if (!res.writableEnded) res.end(); // Ensure response is ended
|
||||
transaction.setStatus('internal_error');
|
||||
transaction.finish();
|
||||
});
|
||||
|
||||
// Handle process close event
|
||||
proc.on('close', (code) => {
|
||||
// Process any remaining data in the buffer
|
||||
if (buffer) {
|
||||
const parsed = parseTracerouteLine(buffer);
|
||||
if (parsed) sendEvent('hop', parsed);
|
||||
else if (buffer.trim()) sendEvent('info', { message: buffer.trim() });
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
logger.error({ requestIp, targetIp, exitCode: code }, `Traceroute command finished with error code ${code}`);
|
||||
Sentry.captureMessage('Traceroute command failed', { level: 'error', extra: { requestIp, targetIp, exitCode: code } });
|
||||
sendEvent('error', { error: `Traceroute command failed with exit code ${code}` });
|
||||
transaction.setStatus('unknown_error'); // Or more specific if possible
|
||||
} else {
|
||||
logger.info({ requestIp, targetIp }, `Traceroute stream completed successfully.`);
|
||||
transaction.setStatus('ok');
|
||||
}
|
||||
sendEvent('end', { exitCode: code }); // Signal the end of the stream
|
||||
if (!res.writableEnded) res.end(); // Ensure response is ended
|
||||
transaction.finish(); // Finish Sentry transaction
|
||||
});
|
||||
|
||||
// Handle client disconnection
|
||||
req.on('close', () => {
|
||||
logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.');
|
||||
if (proc && !proc.killed) proc.kill(); // Kill the traceroute process
|
||||
if (!res.writableEnded) res.end(); // Ensure response is ended
|
||||
transaction.setStatus('cancelled'); // Mark transaction as cancelled
|
||||
transaction.finish();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Catch errors during initial setup (before headers sent)
|
||||
logger.error({ requestIp, targetIp, error: error.message, stack: error.stack }, 'Error setting up traceroute stream');
|
||||
Sentry.captureException(error, { extra: { requestIp, targetIp } });
|
||||
transaction.setStatus('internal_error');
|
||||
transaction.finish();
|
||||
|
||||
// If headers haven't been sent, send a standard JSON error
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${error.message}` });
|
||||
} else {
|
||||
// If headers were sent, try to send an error event via SSE (best effort)
|
||||
try {
|
||||
if (!res.writableEnded) {
|
||||
sendEvent('error', { error: `Internal server error during setup: ${error.message}` });
|
||||
res.end();
|
||||
}
|
||||
} catch (e) { logger.error({ requestIp, targetIp, error: e.message }, "Error writing final setup error to SSE stream"); }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
20
backend/routes/version.js
Normal file
20
backend/routes/version.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// backend/routes/version.js
|
||||
const express = require('express');
|
||||
const pino = require('pino');
|
||||
|
||||
// Logger for this module
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Route handler for / (relative to /api/version)
|
||||
router.get('/', (req, res) => {
|
||||
// Read commit SHA from environment variable (set during build/deploy)
|
||||
const commitSha = process.env.GIT_COMMIT_SHA || 'unknown';
|
||||
const requestIp = req.ip || req.socket.remoteAddress;
|
||||
|
||||
logger.info({ requestIp, commitSha }, 'Version request received');
|
||||
res.json({ commitSha });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
86
backend/routes/whoisLookup.js
Normal file
86
backend/routes/whoisLookup.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// backend/routes/whoisLookup.js
|
||||
const express = require('express');
|
||||
const Sentry = require("@sentry/node");
|
||||
const whois = require('whois-json');
|
||||
const pino = require('pino');
|
||||
|
||||
// Import utilities
|
||||
const { isValidIp, isValidDomain } = require('../utils');
|
||||
|
||||
// Logger for this module
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Route handler for / (relative to /api/whois-lookup)
|
||||
router.get('/', async (req, res, next) => {
|
||||
const queryRaw = req.query.query;
|
||||
const query = typeof queryRaw === 'string' ? queryRaw.trim() : queryRaw;
|
||||
const requestIp = req.ip || req.socket.remoteAddress;
|
||||
|
||||
logger.info({ requestIp, query }, 'WHOIS lookup request received');
|
||||
|
||||
// Validate if the query is either a valid IP or a valid domain
|
||||
if (!isValidIp(query) && !isValidDomain(query)) {
|
||||
logger.warn({ requestIp, query }, 'Invalid query for WHOIS lookup');
|
||||
return res.status(400).json({ success: false, error: 'Invalid domain name or IP address provided for WHOIS lookup.' });
|
||||
}
|
||||
|
||||
// Note: No isPrivateIp check here, as WHOIS for IPs might be desired regardless of range,
|
||||
// and domain lookups don't involve IP ranges.
|
||||
|
||||
try {
|
||||
// Execute WHOIS lookup with a timeout
|
||||
const result = await whois(query, {
|
||||
timeout: parseInt(process.env.WHOIS_TIMEOUT || '10000', 10), // Configurable timeout (default 10s), ensure integer
|
||||
// follow: 3, // Optional: limit number of redirects followed
|
||||
// verbose: true // Optional: get raw text output as well
|
||||
});
|
||||
|
||||
// Check if the result indicates an error (some servers return structured errors)
|
||||
// This check might need adjustment based on the 'whois-json' library's output for errors.
|
||||
if (result && (result.error || result.Error)) {
|
||||
logger.warn({ requestIp, query, whoisResult: result }, 'WHOIS lookup returned an error structure');
|
||||
return res.status(404).json({ success: false, error: `WHOIS lookup failed: ${result.error || result.Error}`, result });
|
||||
}
|
||||
// Basic check if the result is empty or just contains the query itself (might indicate no data)
|
||||
if (!result || Object.keys(result).length === 0 || (Object.keys(result).length === 1 && (result.domainName === query || result.query === query))) {
|
||||
logger.info({ requestIp, query }, 'WHOIS lookup returned no detailed data.');
|
||||
// Consider 404 Not Found if no data is available
|
||||
return res.status(404).json({ success: false, error: 'No detailed WHOIS information found for the query.', query });
|
||||
}
|
||||
|
||||
|
||||
logger.info({ requestIp, query }, 'WHOIS lookup successful');
|
||||
res.json({ success: true, query, result });
|
||||
|
||||
} catch (error) {
|
||||
logger.error({ requestIp, query, error: error.message }, 'WHOIS lookup failed');
|
||||
Sentry.captureException(error, { extra: { requestIp, query } });
|
||||
|
||||
// Provide more user-friendly error messages based on common errors
|
||||
let errorMessage = error.message;
|
||||
let statusCode = 500; // Default to Internal Server Error
|
||||
|
||||
if (error.message.includes('ETIMEDOUT') || error.message.includes('ESOCKETTIMEDOUT')) {
|
||||
errorMessage = 'WHOIS server timed out.';
|
||||
statusCode = 504; // Gateway Timeout
|
||||
} else if (error.message.includes('ENOTFOUND')) {
|
||||
// This might indicate the domain doesn't exist or the WHOIS server for the TLD couldn't be found
|
||||
errorMessage = 'Domain or IP not found, or the corresponding WHOIS server is unavailable.';
|
||||
statusCode = 404; // Not Found
|
||||
} else if (error.message.includes('ECONNREFUSED')) {
|
||||
errorMessage = 'Connection to WHOIS server refused.';
|
||||
statusCode = 503; // Service Unavailable
|
||||
} else if (error.message.includes('No WHOIS server found for')) {
|
||||
errorMessage = 'Could not find a WHOIS server for the requested domain/TLD.';
|
||||
statusCode = 404; // Not Found (as the server for it isn't known)
|
||||
}
|
||||
// Add more specific error handling if needed based on observed errors
|
||||
|
||||
res.status(statusCode).json({ success: false, error: `WHOIS lookup failed: ${errorMessage}` });
|
||||
// next(error); // Optional: Pass to Sentry error handler
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user