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