UNPKG

n8n-nodes-customssh

Version:

n8n community node for advanced SSH connections with configurable ciphers and network device support

492 lines (491 loc) 29.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PromptHandler = void 0; const LoggingUtils_1 = require("../utils/LoggingUtils"); /** * Handler for SSH prompt detection and processing */ class PromptHandler { /** * Wait for a specific prompt pattern in the output */ static async waitForPrompt(stream, dataBuffer, promptRegex, timeout, dataCallback, options) { // Create local constants with fallbacks for undefined values const verboseLogging = options.verboseLogging || false; const debugBuffer = options.debugBuffer || false; const allowEmptyPrompt = options.allowEmptyPrompt || false; const isArubaDevice = options.deviceType === 'aruba' || false; const isArubaOsDevice = options.deviceType === 'aruba-os' || false; const isArubaApDevice = options.deviceType === 'aruba-ap' || false; // Track the data received for Aruba OS MAC table detection and Aruba AP handling let lastDataReceived = Date.now(); let commandName = ''; if (dataBuffer.includes('show mac-address')) { commandName = 'show mac-address'; } // Add special detection for MAC address table output const isMacAddressTable = commandName === 'show mac-address' || dataBuffer.includes('MAC Address') || /[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}\s+\w+\s+\d+/.test(dataBuffer); return new Promise((resolve, reject) => { // Debug function to analyze buffer contents const analyzeBuffer = (buffer) => { var _a; if (debugBuffer) { LoggingUtils_1.LoggingUtils.log(`Buffer length: ${buffer.length}`, true); if (buffer.length > 0) { // Log the buffer as a string and as hex to see non-printable characters LoggingUtils_1.LoggingUtils.log(`Buffer content: "${buffer}"`, true); // Show buffer as hex to check for invisible characters const hexView = ((_a = Buffer.from(buffer) .toString('hex') .match(/.{1,2}/g)) === null || _a === void 0 ? void 0 : _a.join(' ')) || ''; LoggingUtils_1.LoggingUtils.log(`Buffer hex: ${hexView}`, true); } else { LoggingUtils_1.LoggingUtils.log(`Buffer is empty`, true); } } }; // Try to detect common prompt patterns or handle special cases const checkForPrompt = (buffer) => { var _a, _b, _c; // Special case: Allow empty buffer as a prompt if the option is enabled if (buffer.length === 0 && allowEmptyPrompt) { LoggingUtils_1.LoggingUtils.log(`Empty buffer accepted as prompt`, verboseLogging); return true; } // Remove ANSI escape sequences for more reliable matching // This is particularly important for Aruba switches that send many control codes const cleanBuffer = buffer.replace(/\x1B\[\??\d*(?:[;\d]*)?[A-Za-z]/g, ''); // Special case for MAC address table output in Aruba OS if (isArubaOsDevice && isMacAddressTable) { // Check if we've had a pause in data reception - this often indicates the command is done const now = Date.now(); const timeSinceLastData = now - lastDataReceived; // Check for end of MAC address table output patterns const macTableEndPattern = /[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}\s+\w+\s+\d+\s*\r?\n\s*$/; // If we see a MAC address followed by a blank line or we've had no data for 2.5 seconds if ((macTableEndPattern.test(cleanBuffer) && timeSinceLastData > 1500) || timeSinceLastData > 2500) { LoggingUtils_1.LoggingUtils.log(`MAC address table output complete - detected end of output after ${timeSinceLastData}ms pause`, verboseLogging); return true; } } // Check for pagination prompts in Aruba OS switches if (isArubaOsDevice && ((_a = options.deviceSpecific) === null || _a === void 0 ? void 0 : _a.handlePagination)) { const paginationPrompt = options.deviceSpecific.paginationPrompt || '--MORE--'; if (buffer.includes(paginationPrompt) || cleanBuffer.includes(paginationPrompt)) { LoggingUtils_1.LoggingUtils.log(`Pagination prompt detected: "${paginationPrompt}"`, verboseLogging); // Send space to continue const continueKey = options.deviceSpecific.paginationContinue || ' '; stream.write(continueKey); // Update lastDataReceived to prevent timeout during pagination lastDataReceived = Date.now(); // Don't resolve yet, as we need to continue collecting data return false; } } // Specific handling for Aruba OS switches if (isArubaOsDevice) { // Check specifically for 'ofer-3-sw1#' which appears in your logs if (cleanBuffer.includes('ofer-3-sw1#')) { LoggingUtils_1.LoggingUtils.log(`Specific hostname prompt detected: ofer-3-sw1#`, verboseLogging); return true; } // Look for common Aruba OS prompt patterns at the end of lines const lines = cleanBuffer.split('\n'); const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : ''; // Check for various prompt patterns that could appear in Aruba OS if (lastLine === '>' || lastLine.endsWith('>') || lastLine === '#' || lastLine.endsWith('#') || lastLine.includes('(config)#') || lastLine.includes('(config-if)#') || lastLine.includes('(config-vlan)#')) { LoggingUtils_1.LoggingUtils.log(`Aruba OS prompt detected in last line: "${lastLine}"`, verboseLogging); return true; } // Check if we have multiple prompt repetitions (common in Aruba OS) if (cleanBuffer.includes('ofer-3-sw1# \nofer-3-sw1#') || cleanBuffer.includes('ofer-3-sw1#\nofer-3-sw1#')) { LoggingUtils_1.LoggingUtils.log(`Repeated prompt pattern detected`, verboseLogging); return true; } // Look for known Aruba OS prompts from device-specific settings if ((_b = options.deviceSpecific) === null || _b === void 0 ? void 0 : _b.knownPrompts) { const knownPrompts = options.deviceSpecific.knownPrompts; for (const knownPrompt of knownPrompts) { if (cleanBuffer.includes(knownPrompt)) { LoggingUtils_1.LoggingUtils.log(`Known Aruba OS prompt detected: ${knownPrompt}`, verboseLogging); return true; } } } } // For Aruba APs specifically if (isArubaApDevice) { // Aruba APs often send character-by-character output and end with patterns like "hallway-2-655-ap# " const lines = cleanBuffer.split('\n'); const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : ''; // Check for AP-specific prompt patterns - but ensure we have command content first const hasCommandContent = cleanBuffer.includes('\n') && cleanBuffer.length > 50; if (hasCommandContent && (/\S+-ap#\s*$/.test(cleanBuffer) || // Pattern like "hallway-2-655-ap# " /\S+-ap#\s*$/.test(lastLine) || lastLine.endsWith('-ap#') || /ap#\s*$/.test(lastLine) || /instant#\s*$/.test(lastLine))) { LoggingUtils_1.LoggingUtils.log(`Aruba AP prompt detected: "${lastLine}" with ${cleanBuffer.length} chars`, verboseLogging); return true; } // For character-by-character output, check if we've had no data for a while and buffer ends with # // But ensure we have some command output first (not just prompts) const now = Date.now(); const timeSinceLastData = now - lastDataReceived; const hasCommandOutput = cleanBuffer.length > 50; // Ensure we have substantial output before considering prompt if (timeSinceLastData > 1000 && hasCommandOutput && (cleanBuffer.endsWith('# ') || cleanBuffer.endsWith('#'))) { LoggingUtils_1.LoggingUtils.log(`Aruba AP prompt detected after ${timeSinceLastData}ms pause with ${cleanBuffer.length} chars`, verboseLogging); return true; } // Look for known AP prompts from device-specific settings if ((_c = options.deviceSpecific) === null || _c === void 0 ? void 0 : _c.knownPrompts) { const knownPrompts = options.deviceSpecific.knownPrompts; for (const knownPrompt of knownPrompts) { if (cleanBuffer.includes(knownPrompt) || lastLine.includes(knownPrompt)) { LoggingUtils_1.LoggingUtils.log(`Known Aruba AP prompt detected: ${knownPrompt}`, verboseLogging); return true; } } } } // For Aruba CX specifically if (isArubaDevice) { // Match patterns like "hostname# " at the end or "hostname# \r\n" const arubaPromptPattern = /\S+#\s+(?:\r\n)?$/; if (arubaPromptPattern.test(cleanBuffer)) { LoggingUtils_1.LoggingUtils.log(`Aruba prompt detected with specific pattern`, verboseLogging); return true; } // For Aruba, also check if the prompt appears anywhere in the last line const lines = cleanBuffer.split('\n'); const lastLine = lines.length > 0 ? lines[lines.length - 1] : ''; if (lastLine.includes('# ')) { LoggingUtils_1.LoggingUtils.log(`Aruba prompt detected in last line: ${lastLine}`, verboseLogging); return true; } } // Regular case: Check against provided regex on both original and cleaned buffer if (promptRegex.test(buffer) || promptRegex.test(cleanBuffer)) { LoggingUtils_1.LoggingUtils.log(`Prompt detected with specified pattern: ${promptRegex}`, verboseLogging); return true; } // Fallback patterns for different device types - check both buffers const fallbackPatterns = [ /\S+#\s*(?:\r\n)?$/, // Hostname followed by # and optional space, may end with \r\n /\S+[#>$]\s*$/, // Any text ending with #, >, or $ /\w+-\w+-\w+#\s*/, // Pattern like 'ofer-3-sw1# ' /#\s*$/, // Just a # at the end of the line />\s*$/, // Just a > at the end of the line /\$\s*$/, // Just a $ at the end of the line ]; for (const pattern of fallbackPatterns) { if (pattern.test(buffer) || pattern.test(cleanBuffer)) { LoggingUtils_1.LoggingUtils.log(`Prompt detected with fallback pattern: ${pattern}`, verboseLogging); return true; } } // For MAC address table commands, check for end-of-table pattern if (isMacAddressTable) { // Check if we have a large amount of MAC address entries const macAddressCount = (cleanBuffer.match(/:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}/g) || []).length; // MAC tables can be very long - if we have a significant number of entries // and a pause in data received, consider the command complete if (macAddressCount > 15 && Date.now() - lastDataReceived > 2000) { LoggingUtils_1.LoggingUtils.log(`MAC address table seems complete with ${macAddressCount} entries and no new data`, verboseLogging); return true; } } // Check if we have stable output (no new data for a while) if (options.stableOutputDetection && options.lastDataTime) { const now = Date.now(); const elapsed = now - options.lastDataTime; // For Aruba devices, use longer stable times due to slower character output let stableTime = 2000; if (isArubaOsDevice) { stableTime = 3600; } else if (isArubaApDevice) { stableTime = 2000; // Reduced from 3000ms for better performance } if (elapsed > stableTime) { LoggingUtils_1.LoggingUtils.log(`Stable output detected after ${elapsed}ms of no new data`, verboseLogging); return true; } } return false; }; let lastDataTime = Date.now(); const dataHandler = (data) => { const text = data.toString('utf8'); dataCallback(data); // Update last data time for stable output detection lastDataTime = Date.now(); lastDataReceived = Date.now(); if (options.stableOutputDetection) { options.lastDataTime = lastDataTime; } if (verboseLogging) { LoggingUtils_1.LoggingUtils.log(`Received: ${JSON.stringify(text)}`, true); } // Check if prompt is detected with updated function if (checkForPrompt(dataBuffer)) { analyzeBuffer(dataBuffer); cleanup(); resolve(); } }; // Set up the data handler stream.on('data', dataHandler); // Set timeout with better debugging const timeoutId = setTimeout(() => { analyzeBuffer(dataBuffer); // Check for stable output const now = Date.now(); const elapsed = now - lastDataTime; // For Aruba devices, use longer stable times due to slower character output let stableTime = 2000; if (isArubaOsDevice) { stableTime = 3600; } else if (isArubaApDevice) { stableTime = 2000; // Reduced from 3000ms for better performance } if (elapsed > stableTime) { LoggingUtils_1.LoggingUtils.log(`No data received for ${elapsed}ms, considering command complete`, verboseLogging); cleanup(); resolve(); return; } // Enhanced Aruba OS detection on timeout if (isArubaOsDevice) { // Clean buffer and check for specific patterns const cleanBuffer = dataBuffer.replace(/\x1B\[\??\d*(?:[;\d]*)?[A-Za-z]/g, ''); // Check for known Aruba OS patterns if (cleanBuffer.includes('ofer-3-sw1#')) { LoggingUtils_1.LoggingUtils.log(`Found hostname 'ofer-3-sw1#' in cleaned buffer - accepting as complete`, verboseLogging); cleanup(); resolve(); return; } // Check last line for prompt characters const lines = cleanBuffer.split('\n'); const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : ''; if (lastLine === '>' || lastLine.endsWith('>') || lastLine === '#' || lastLine.endsWith('#')) { LoggingUtils_1.LoggingUtils.log(`Found Aruba prompt character in last line: "${lastLine}" - accepting as complete`, verboseLogging); cleanup(); resolve(); return; } // If we have any content and timing out, consider it good enough for Aruba OS if (cleanBuffer.length > 0 && elapsed > 2000) { LoggingUtils_1.LoggingUtils.log(`Timeout with content for Aruba OS - accepting as complete after ${elapsed}ms`, verboseLogging); cleanup(); resolve(); return; } } // Timing out, send a carriage return as a last resort to stimulate output LoggingUtils_1.LoggingUtils.log(`Sending CR as last resort to stimulate output`, verboseLogging); stream.write('\r\n'); // Give it a moment to respond before giving up setTimeout(() => { analyzeBuffer(dataBuffer); cleanup(); reject(new Error(`Timeout waiting for prompt match. Buffer remains unrecognized.`)); }, 1500); }, timeout); // Clean up function to remove listeners const cleanup = () => { clearTimeout(timeoutId); stream.removeListener('data', dataHandler); }; // Check if prompt is already in the buffer with updated function // For Aruba APs, don't immediately assume prompt is ready - wait for actual command execution if (!isArubaApDevice && checkForPrompt(dataBuffer)) { analyzeBuffer(dataBuffer); LoggingUtils_1.LoggingUtils.log(`Prompt already in buffer`, verboseLogging); cleanup(); resolve(); } else if (isArubaApDevice && dataBuffer.length > 100 && checkForPrompt(dataBuffer)) { // For Aruba APs, only consider prompt ready if we have substantial content analyzeBuffer(dataBuffer); LoggingUtils_1.LoggingUtils.log(`Aruba AP prompt already in buffer with ${dataBuffer.length} chars`, verboseLogging); cleanup(); resolve(); } }); } /** * Clean SSH command output */ static cleanCommandOutput(rawOutput, command, commandPromptRegex, options) { // If the output is empty already, return it if (!rawOutput || !rawOutput.trim()) { return ''; } try { // Log the raw input for debugging const verboseLogging = (options === null || options === void 0 ? void 0 : options.verboseLogging) || false; const isArubaOsDevice = (options === null || options === void 0 ? void 0 : options.deviceType) === 'aruba-os' || false; const isArubaDevice = (options === null || options === void 0 ? void 0 : options.deviceType) === 'aruba' || false; const isArubaApDevice = (options === null || options === void 0 ? void 0 : options.deviceType) === 'aruba-ap' || false; // Detect if this is MAC address table output for special handling const isMacAddressTable = command === 'show mac-address' || command.includes('mac-address') || /[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}\s+\w+\s+\d+/.test(rawOutput); if (verboseLogging) { const debugOutput = rawOutput.length > 100 ? `${rawOutput.substring(0, 50)}...${rawOutput.substring(rawOutput.length - 50)}` : rawOutput; LoggingUtils_1.LoggingUtils.log(`Cleaning output: ${JSON.stringify(debugOutput)}`, true); } // First, remove ANSI escape sequences that are common in Aruba output let cleanOutput = rawOutput; // More aggressive ANSI sequence removal - this is key for Aruba OS cleanOutput = cleanOutput.replace(/\x1B\[\??\d*(?:[;\d]*)?[A-Za-z]/g, ''); // Remove additional terminal control sequences cleanOutput = cleanOutput.replace(/\x1B\[[0-9;]*[HfABCDEFGJKST]/g, ''); cleanOutput = cleanOutput.replace(/\x1B\[[0-9]*[ABCDEFGHJKLMPQRST]/g, ''); cleanOutput = cleanOutput.replace(/\x1B\=[0-9]*[a-z]/g, ''); // For Aruba specific cleanup if (isArubaDevice || isArubaOsDevice || isArubaApDevice) { // Remove ESC character and its related sequences cleanOutput = cleanOutput.replace(/\x1B./g, ''); // Remove cursor positioning and other special commands cleanOutput = cleanOutput.replace(/\x1B\[\d+;\d+[Hf]/g, ''); cleanOutput = cleanOutput.replace(/\x1B\[\d*[JK]/g, ''); // Clean specific sequences seen in the debug output cleanOutput = cleanOutput.replace(/\x1B\[[?]\d+[hlm]/g, ''); // Remove \u001b sequences that appear in the debug output cleanOutput = cleanOutput.replace(/\u001b/g, ''); // For Aruba APs, remove banner/MOTD content specifically if (isArubaApDevice) { // Remove the tech-support banner message cleanOutput = cleanOutput.replace(/show tech-support and show tech-support supplemental are the two most useful outputs to collect for any kind of troubleshooting session\.\s*/g, ''); // Remove excessive blank lines at the beginning that come from banner cleanOutput = cleanOutput.replace(/^[\r\n\s]*/, ''); // Remove any lines that are just prompts without command output const lines = cleanOutput.split('\n'); const filteredLines = lines.filter((line) => { const trimmedLine = line.trim(); // Keep lines that have actual content, not just prompts or blank lines return (trimmedLine && !trimmedLine.match(/^[a-zA-Z0-9-]+#\s*$/) && !trimmedLine.match(/^\s*$/) && trimmedLine !== ''); }); cleanOutput = filteredLines.join('\n'); } } // Special handling for MAC address table output if (isMacAddressTable) { // Make sure we remove the Status and Counters header that often appears cleanOutput = cleanOutput.replace(/Status and Counters - Port Address Table\s*\r?\n/g, ''); // Clean any row header lines cleanOutput = cleanOutput.replace(/Port\s+Address\s+MAC Address\s+VLAN\s*/g, ''); // Remove --MORE-- pagination markers from the output cleanOutput = cleanOutput.replace(/--MORE--\s*\r?\n?/g, ''); // Make the MAC address table data more readable (one entry per line) cleanOutput = cleanOutput.replace(/\r/g, ''); // If we're just getting MAC addresses, we can return already if (verboseLogging) { LoggingUtils_1.LoggingUtils.log(`MAC address table cleaned with ${cleanOutput.length} bytes`, true); } return cleanOutput.trim(); } // Escape special regex characters in the command const escapedCommand = command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Aruba OS specific cleaning if (isArubaOsDevice) { // Remove the command echo and any repeated command const commandPattern = new RegExp(`^.*?${escapedCommand}.*?(\\r?\\n|$)`, 'm'); const commandMatch = commandPattern.exec(cleanOutput); if (commandMatch) { // Only remove the first match (the echo of the sent command) cleanOutput = cleanOutput.substring(commandMatch[0].length); } // Aruba OS often includes pagination markers, remove them cleanOutput = cleanOutput.replace(/--MORE--\s*\r?\n?/g, ''); cleanOutput = cleanOutput.replace(/--MORE--.*\r?\s+\r?\n/g, '\n'); // Clean login banners and other pre-command outputs cleanOutput = cleanOutput.replace(/Your previous successful login.*\r?\n/g, ''); // Remove hostname repeats (common in Aruba OS output) cleanOutput = cleanOutput.replace(/ofer-3-sw1#\s*\r?\n+ofer-3-sw1#/g, 'ofer-3-sw1#'); // Specific cleanup for the patterns seen in your log cleanOutput = cleanOutput.replace(/ofer-3-sw1#\s*\r?\n+/g, ''); // Find and remove the trailing prompt const promptMatches = cleanOutput.match(/\S+[#>]\s*$/); if (promptMatches) { cleanOutput = cleanOutput.substring(0, promptMatches.index); } // Remove extra blank lines cleanOutput = cleanOutput.replace(/\r?\n\s*\r?\n/g, '\n'); } else { // Regular command echo removal for other devices const commandPattern = new RegExp(`.*?${escapedCommand}.*?(\\r?\\n|$)`, 'm'); const commandMatch = commandPattern.exec(cleanOutput); if (commandMatch) { // Only remove the first match (the echo of the sent command) cleanOutput = cleanOutput.substring(commandMatch[0].length); } } // Try to find the host prompt at the end const promptPattern = commandPromptRegex; const promptMatch = promptPattern.exec(cleanOutput); // If we found the prompt at the end, remove it if (promptMatch && promptMatch.index > cleanOutput.length - 30) { cleanOutput = cleanOutput.substring(0, promptMatch.index); } // Also remove any trailing prompts that match common patterns const trailingPromptPatterns = [ /\S+#\s*$/, // hostname# /\S+>\s*$/, // hostname> /\S+\$\s*$/, // hostname$ /\w+-\w+-\w+#\s*$/, // Specific pattern like ofer-3-sw1# /\(config\)[#>]\s*$/, // Config mode /\(config-if\)[#>]\s*$/, // Interface config mode /\(config-vlan\)[#>]\s*$/, // VLAN config mode /#\s*$/, // Just a # />\s*$/, // Just a > ]; for (const pattern of trailingPromptPatterns) { const match = pattern.exec(cleanOutput); if (match && match.index > cleanOutput.length - 30) { cleanOutput = cleanOutput.substring(0, match.index); } } // Remove extra line breaks at the beginning and end cleanOutput = cleanOutput.replace(/^\s*\r?\n+/, ''); cleanOutput = cleanOutput.replace(/\r?\n+\s*$/, ''); if (verboseLogging) { LoggingUtils_1.LoggingUtils.log(`Output length after cleaning: ${cleanOutput.length}`, true); } return cleanOutput; } catch (error) { // If any error occurs during cleaning, return the raw output console.error('Error cleaning command output:', error); return rawOutput; } } } exports.PromptHandler = PromptHandler;