UNPKG

node-red-contrib-network-tools

Version:

Comprehensive network monitoring and discovery tools for Node-RED with Bonjour/mDNS support

348 lines (299 loc) 15.1 kB
module.exports = function(RED) { const ping = require('ping'); function EnhancedPingNode(config) { RED.nodes.createNode(this, config); var node = this; // Store configuration node.ipAddress = config.ipAddress; node.timeout = config.timeout || 5000; node.interval = config.interval || 0; // Continuous ping interval (0 = single ping) node.count = config.count || 1; node.size = config.size || 32; // Packet size node.retries = config.retries || 0; node.name = config.name; var intervalId = null; var pingHistory = []; var maxHistorySize = 100; node.on('input', function(msg) { // Handle control commands first if (msg.command === 'stop' && intervalId) { clearInterval(intervalId); intervalId = null; node.status({fill: "grey", shape: "ring", text: "stopped"}); return; } if (msg.command === 'clear-history') { pingHistory = []; node.status({fill: "blue", shape: "dot", text: "history cleared"}); return; } if (msg.command === 'get-history') { var historyOutput = { payload: pingHistory, command: 'history-data', timestamp: new Date().toISOString() }; node.send([historyOutput, null]); return; } // Stop any existing interval if (intervalId) { clearInterval(intervalId); intervalId = null; } // Get configuration from message or node config // First validate if msg.payload is a valid IP/hostname before using it var targetIP; if (msg.payload && isValidTarget(String(msg.payload))) { targetIP = String(msg.payload); } else if (msg.ip && isValidTarget(String(msg.ip))) { targetIP = String(msg.ip); } else { targetIP = node.ipAddress; } var pingCount = msg.count || node.count; var pingInterval = msg.interval || node.interval; var timeout = msg.timeout || node.timeout; var packetSize = msg.size || node.size; var maxRetries = msg.retries || node.retries; if (!targetIP) { node.error("No IP address provided", msg); return; } // Final validation (should always pass now, but kept for safety) if (!isValidTarget(targetIP)) { node.error("Invalid IP address or hostname format: " + targetIP, msg); return; } // Single ping or continuous ping if (pingInterval > 0) { startContinuousPing(targetIP, pingInterval, timeout, packetSize, maxRetries, msg); } else { performPingSequence(targetIP, pingCount, timeout, packetSize, maxRetries, msg); } }); function isValidTarget(target) { if (!target || typeof target !== 'string') { return false; } // Check if it's a timestamp (all digits, typically 10-13 digits for Unix timestamp) if (/^\d{10,}$/.test(target)) { return false; } var ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; var ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^(?:[0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$/; var hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; // Check for IPv4 or IPv6 first if (ipv4Regex.test(target) || ipv6Regex.test(target)) { return true; } // For hostnames, ensure it's not just a pure number (likely a timestamp) // and must contain at least one letter or dot if (hostnameRegex.test(target)) { // Reject pure numeric strings (timestamps) - must have at least one letter or dot return /[a-zA-Z.]/.test(target) && !(/^\d+$/.test(target)); } return false; } function startContinuousPing(targetIP, interval, timeout, packetSize, maxRetries, originalMsg) { node.status({fill: "blue", shape: "dot", text: "continuous ping " + targetIP}); intervalId = setInterval(() => { performSinglePing(targetIP, timeout, packetSize, maxRetries, originalMsg, true); }, interval); // Send initial ping immediately performSinglePing(targetIP, timeout, packetSize, maxRetries, originalMsg, true); } function performPingSequence(targetIP, count, timeout, packetSize, maxRetries, originalMsg) { var results = []; var completed = 0; node.status({fill: "yellow", shape: "dot", text: `pinging ${targetIP} (${count}x)`}); for (let i = 0; i < count; i++) { setTimeout(() => { performSinglePing(targetIP, timeout, packetSize, maxRetries, originalMsg, false, (result) => { results.push(result); completed++; if (completed === count) { // Send aggregated results sendAggregatedResults(targetIP, results, originalMsg); } }); }, i * 100); // Space out pings by 100ms } } function performSinglePing(targetIP, timeout, packetSize, maxRetries, originalMsg, isContinuous, callback) { var attempt = 0; function attemptPing() { var pingConfig = { timeout: timeout / 1000, extra: process.platform === 'win32' ? ["-n", "1", "-l", packetSize.toString()] : ["-c", "1", "-s", packetSize.toString()] }; ping.promise.probe(targetIP, pingConfig) .then(function(result) { var timestamp = new Date().toISOString(); var pingResult = { host: targetIP, alive: result.alive, time: result.time, min: result.min, max: result.max, avg: result.avg, packetLoss: result.packetLoss, output: result.output, packetSize: packetSize, attempt: attempt + 1, timestamp: timestamp }; // Add to history addToHistory(targetIP, pingResult); // Create statistics for single ping (especially for continuous mode) var statistics = { host: targetIP, totalPings: 1, successfulPings: result.alive ? 1 : 0, failedPings: result.alive ? 0 : 1, successRate: result.alive ? 100 : 0, averageTime: result.alive ? parseFloat(result.time || 0) : 0, minResponseTime: result.alive ? parseFloat(result.time || 0) : 0, maxResponseTime: result.alive ? parseFloat(result.time || 0) : 0, jitter: 0, // Single ping has no jitter timestamp: timestamp }; var outputMsg = { ...originalMsg, payload: { statistics: statistics, pingResult: pingResult, target: targetIP, timestamp: timestamp }, ip: targetIP, timestamp: timestamp, history: isContinuous ? getRecentHistory(targetIP, 10) : undefined }; if (result.alive) { if (!isContinuous) { node.status({fill: "green", shape: "dot", text: `alive (${result.time}ms)`}); } // Only send messages directly for continuous mode if (isContinuous) { node.send([outputMsg, null]); } } else { if (attempt < maxRetries) { attempt++; setTimeout(attemptPing, 500); // Reduced retry delay from 1000ms to 500ms return; } if (!isContinuous) { node.status({fill: "red", shape: "dot", text: "not reachable"}); } // Only send messages directly for continuous mode if (isContinuous) { node.send([null, outputMsg]); } } if (callback) callback(pingResult); }) .catch(function(error) { if (attempt < maxRetries) { attempt++; setTimeout(attemptPing, 500); // Reduced retry delay from 1000ms to 500ms return; }node.error("Ping failed: " + error.message, originalMsg); if (!isContinuous) { node.status({fill: "red", shape: "ring", text: "error"}); } var errorTimestamp = new Date().toISOString(); var errorStatistics = { host: targetIP, totalPings: 1, successfulPings: 0, failedPings: 1, successRate: 0, averageTime: 0, minResponseTime: 0, maxResponseTime: 0, jitter: 0, timestamp: errorTimestamp }; var errorMsg = { ...originalMsg, payload: { statistics: errorStatistics, error: error.message, alive: false, attempt: attempt + 1, timestamp: errorTimestamp }, ip: targetIP, timestamp: errorTimestamp }; // Only send messages directly for continuous mode if (isContinuous) { node.send([null, errorMsg]); } if (callback) callback(errorMsg.payload); }); } attemptPing(); } function sendAggregatedResults(targetIP, results, originalMsg) { var alive = results.filter(r => r.alive); var dead = results.filter(r => !r.alive); var statistics = { host: targetIP, totalPings: results.length, successfulPings: alive.length, failedPings: dead.length, successRate: (alive.length / results.length) * 100, averageTime: alive.length > 0 ? alive.reduce((sum, r) => sum + parseFloat(r.time || 0), 0) / alive.length : 0, minResponseTime: alive.length > 0 ? Math.min(...alive.map(r => parseFloat(r.time || 0))) : 0, maxResponseTime: alive.length > 0 ? Math.max(...alive.map(r => parseFloat(r.time || 0))) : 0, jitter: calculateJitter(alive), results: results, timestamp: new Date().toISOString() }; var outputMsg = { ...originalMsg, payload: { statistics: statistics, target: targetIP, timestamp: statistics.timestamp }, ip: targetIP, timestamp: statistics.timestamp }; if (statistics.successRate > 50) { node.status({fill: "green", shape: "dot", text: `${statistics.successRate.toFixed(1)}% success`}); node.send([outputMsg, null]); } else { node.status({fill: "red", shape: "dot", text: `${statistics.successRate.toFixed(1)}% success`}); node.send([null, outputMsg]); } } function calculateJitter(aliveResults) { if (aliveResults.length < 2) return 0; var times = aliveResults.map(r => parseFloat(r.time || 0)); var deltas = []; for (let i = 1; i < times.length; i++) { deltas.push(Math.abs(times[i] - times[i-1])); } return deltas.length > 0 ? deltas.reduce((sum, delta) => sum + delta, 0) / deltas.length : 0; } function addToHistory(ip, result) { if (!pingHistory[ip]) { pingHistory[ip] = []; } pingHistory[ip].push(result); if (pingHistory[ip].length > maxHistorySize) { pingHistory[ip].shift(); } } function getRecentHistory(ip, count) { if (!pingHistory[ip]) return []; return pingHistory[ip].slice(-count); } node.on('close', function() { if (intervalId) { clearInterval(intervalId); } node.status({}); }); } RED.nodes.registerType("enhanced-ping", EnhancedPingNode); };