UNPKG

n8n-nodes-customssh

Version:

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

434 lines (433 loc) 24.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SshCommandExecutor = void 0; const LoggingUtils_1 = require("../utils/LoggingUtils"); const PromptHandler_1 = require("./PromptHandler"); const SshConnectionManager_1 = require("./SshConnectionManager"); /** * Service for executing SSH commands */ class SshCommandExecutor { /** * Execute a single SSH command on a remote device */ static async executeSshCommand(host, port, username, password, command, ciphers, options) { let lastError = null; let attemptedCiphers = []; // Try with selected cipher(s) and retry logic for (let attempt = 0; attempt < options.retryCount; attempt++) { // If fallback ciphers is disabled, only use the first cipher const effectiveCiphers = options.fallbackCiphers ? ciphers : [ciphers[0]]; for (const cipher of effectiveCiphers) { if (attemptedCiphers.includes(cipher)) continue; attemptedCiphers.push(cipher); try { LoggingUtils_1.LoggingUtils.log(`Attempt ${attempt + 1}, trying cipher: ${cipher}`, options.verboseLogging || false); const result = await this.connectAndExecuteCommand(host, port, username, password, command, cipher, options); return { success: true, output: result.output, exitCode: result.exitCode, cipher: cipher, }; } catch (error) { lastError = error; LoggingUtils_1.LoggingUtils.error(`Connection failed with cipher ${cipher}: ${error.message}`, options.verboseLogging || false); // If it's just a cipher issue, continue trying others if (error.message.includes('no matching cipher found') || error.message.includes('handshake failed')) { continue; } // For other errors, wait before retry await new Promise((resolve) => setTimeout(resolve, options.retryDelay)); break; } } } throw new Error(`Failed to connect after ${options.retryCount} attempts with ciphers [${attemptedCiphers.join(', ')}]. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`); } /** * Execute multiple commands on a remote device */ static async executeMultipleSshCommands(host, port, username, password, commands, ciphers, options) { let lastError = null; let attemptedCiphers = []; // Try with selected cipher(s) and retry logic for (let attempt = 0; attempt < options.retryCount; attempt++) { // If fallback ciphers is disabled, only use the first cipher const effectiveCiphers = options.fallbackCiphers ? ciphers : [ciphers[0]]; for (const cipher of effectiveCiphers) { if (attemptedCiphers.includes(cipher)) continue; attemptedCiphers.push(cipher); try { LoggingUtils_1.LoggingUtils.log(`Attempt ${attempt + 1}, trying cipher: ${cipher}`, options.verboseLogging || false); const results = await this.connectAndExecuteMultipleCommands(host, port, username, password, commands, cipher, options); return { success: true, results, cipher: cipher, }; } catch (error) { lastError = error; LoggingUtils_1.LoggingUtils.error(`Connection failed with cipher ${cipher}: ${error.message}`, options.verboseLogging || false); // If it's just a cipher issue, continue trying others if (error.message.includes('no matching cipher found') || error.message.includes('handshake failed')) { continue; } // For other errors, wait before retry await new Promise((resolve) => setTimeout(resolve, options.retryDelay)); break; } } } throw new Error(`Failed to connect after ${options.retryCount} attempts with ciphers [${attemptedCiphers.join(', ')}]. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`); } /** * Connect and execute a single command */ static async connectAndExecuteCommand(host, port, username, password, command, cipher, options) { let client = null; let stream = null; try { // Connect to the device client = await SshConnectionManager_1.SshConnectionManager.createConnection(host, port, username, password, cipher, options); // Create shell stream = await SshConnectionManager_1.SshConnectionManager.createShell(client, options); // Execute command let dataBuffer = ''; let output = ''; // Set up data handler if (stream) { stream.on('data', (data) => { const text = data.toString('utf8'); LoggingUtils_1.LoggingUtils.log(`Received: ${JSON.stringify(text)}`, options.verboseLogging || false); dataBuffer += text; output += text; }); } // Handle initial commands if specified if (options.initialCommand && stream) { LoggingUtils_1.LoggingUtils.log(`Sending initial command: ${options.initialCommand}`, options.verboseLogging || false); stream.write(`${options.initialCommand}${options.lineEnding || '\r\n'}`); // Wait for response to initial command await PromptHandler_1.PromptHandler.waitForPrompt(stream, dataBuffer, options.commandPromptRegex, options.promptTimeout, (data) => { dataBuffer += data.toString('utf8'); output += data.toString('utf8'); }, options); // Clear output buffer after initial command dataBuffer = ''; output = ''; } // Handle pagination disabling command for Aruba OS if (options.deviceType === 'aruba-os' && stream) { LoggingUtils_1.LoggingUtils.log(`Sending pagination disabling command for Aruba OS`, options.verboseLogging || false); stream.write(`no page${options.lineEnding || '\r\n'}`); // Wait for response await PromptHandler_1.PromptHandler.waitForPrompt(stream, dataBuffer, options.commandPromptRegex, options.promptTimeout, (data) => { dataBuffer += data.toString('utf8'); output += data.toString('utf8'); }, options); // Clear buffer after pagination command dataBuffer = ''; output = ''; } else if (options.deviceType === 'aruba-ap' && stream) { // Aruba APs don't support pagination disable commands // Just send a blank line to stabilize the connection LoggingUtils_1.LoggingUtils.log(`Stabilizing Aruba AP connection`, options.verboseLogging || false); stream.write(`${options.lineEnding || '\r\n'}`); // Wait a bit longer for APs to respond await new Promise((resolve) => setTimeout(resolve, 1000)); // Clear any buffer data dataBuffer = ''; output = ''; } // Handle privilege mode (enable) if requested if (options.enablePrivilegeMode && stream) { await this.handlePrivilegeEscalation(stream, dataBuffer, password, options); // Clear buffer after enable command dataBuffer = ''; output = ''; } // Send command with the configured line ending if (stream) { LoggingUtils_1.LoggingUtils.log(`Sending command: ${command}`, options.verboseLogging || false); // For Aruba switches, we need extra handling if (options.deviceType === 'aruba') { // Send an empty command followed by CR to ensure we're at a fresh prompt stream.write('\r\n'); await new Promise((resolve) => setTimeout(resolve, 500)); } else if (options.deviceType === 'aruba-os') { // For Aruba OS, ensure we're at a clean prompt stream.write('\r\n'); await new Promise((resolve) => setTimeout(resolve, 800)); } else if (options.deviceType === 'aruba-ap') { // For Aruba APs, ensure we're at a clean prompt stream.write('\r\n'); await new Promise((resolve) => setTimeout(resolve, 800)); // Reduced from 1200ms } stream.write(`${command}${options.lineEnding || '\r\n'}`); // Reset the last data time for stable output detection if (options.stableOutputDetection) { options.lastDataTime = Date.now(); } // Add MAC address table detection const isMacAddressTable = command.includes('mac-address') || command.includes('MAC Address'); // Adjust timeout for MAC address commands and Aruba APs let commandTimeout = options.promptTimeout; if (isMacAddressTable && options.deviceType === 'aruba-os') { commandTimeout = Math.max(commandTimeout, 30000); // Longer timeout for MAC address tables LoggingUtils_1.LoggingUtils.log(`MAC address table command detected, using extended timeout: ${commandTimeout}ms`, options.verboseLogging || false); } else if (options.deviceType === 'aruba-ap') { // Aruba APs need longer timeouts due to character-by-character output commandTimeout = Math.max(commandTimeout, 20000); // Reduced from 25000ms LoggingUtils_1.LoggingUtils.log(`Aruba AP detected, using extended timeout: ${commandTimeout}ms`, options.verboseLogging || false); } // Wait for command prompt to appear, indicating command completion await PromptHandler_1.PromptHandler.waitForPrompt(stream, dataBuffer, options.commandPromptRegex, isMacAddressTable ? commandTimeout : options.promptTimeout, (data) => { dataBuffer += data.toString('utf8'); output += data.toString('utf8'); }, options); } // Clean output output = PromptHandler_1.PromptHandler.cleanCommandOutput(output, command, options.commandPromptRegex, options); return { output, exitCode: null }; } finally { // Clean up resources if (stream) { stream.end(`exit${options.lineEnding || '\r\n'}`); } if (client) { client.end(); } } } /** * Connect and execute multiple commands - improved version */ static async connectAndExecuteMultipleCommands(host, port, username, password, commands, cipher, options) { let client = null; let stream = null; try { // Connect to the device client = await SshConnectionManager_1.SshConnectionManager.createConnection(host, port, username, password, cipher, options); // Create shell with options stream = await SshConnectionManager_1.SshConnectionManager.createShell(client, options); // Execute commands sequentially const results = []; let dataBuffer = ''; // Handle initial setup commands if needed if (options.initialCommand && stream) { LoggingUtils_1.LoggingUtils.log(`Sending initial command: ${options.initialCommand}`, options.verboseLogging || false); stream.write(`${options.initialCommand}${options.lineEnding || '\r\n'}`); // Wait for prompt after initial command await PromptHandler_1.PromptHandler.waitForPrompt(stream, dataBuffer, options.commandPromptRegex, options.promptTimeout, (data) => { dataBuffer += data.toString('utf8'); }, options); // Clear buffer dataBuffer = ''; } // Handle pagination disabling command for Aruba OS if (options.deviceType === 'aruba-os' && stream) { LoggingUtils_1.LoggingUtils.log(`Sending pagination disabling command for Aruba OS`, options.verboseLogging || false); stream.write(`no page${options.lineEnding || '\r\n'}`); // Wait for response await PromptHandler_1.PromptHandler.waitForPrompt(stream, dataBuffer, options.commandPromptRegex, options.promptTimeout, (data) => { dataBuffer += data.toString('utf8'); }, options); // Clear buffer dataBuffer = ''; } else if (options.deviceType === 'aruba-ap' && stream) { // Aruba APs don't support pagination disable commands // Just send a blank line to stabilize the connection and clear any banner/MOTD LoggingUtils_1.LoggingUtils.log(`Stabilizing Aruba AP connection (multiple commands)`, options.verboseLogging || false); // Send multiple blank lines to clear any banner/MOTD messages stream.write(`${options.lineEnding || '\r\n'}`); await new Promise((resolve) => setTimeout(resolve, 500)); stream.write(`${options.lineEnding || '\r\n'}`); await new Promise((resolve) => setTimeout(resolve, 500)); // Wait longer for APs to settle and clear buffer completely await new Promise((resolve) => setTimeout(resolve, 1000)); // Clear buffer of any banner/MOTD content dataBuffer = ''; } // Execute privilege mode if needed if (options.enablePrivilegeMode && stream) { await this.handlePrivilegeEscalation(stream, dataBuffer, password, options); // Clear buffer dataBuffer = ''; } // Special device-specific handling before command execution if (options.deviceType === 'aruba' && stream) { // Send an empty command followed by CR to ensure we're at a fresh prompt stream.write('\r\n'); await new Promise((resolve) => setTimeout(resolve, 500)); // Clear out any buffer data from the empty command dataBuffer = ''; } else if (options.deviceType === 'aruba-os' && stream) { // For Aruba OS, ensure we're at a clean prompt stream.write('\r\n'); await new Promise((resolve) => setTimeout(resolve, 800)); // Clear buffer dataBuffer = ''; } else if (options.deviceType === 'aruba-ap' && stream) { // For Aruba APs, ensure we're at a clean prompt and clear any banner content stream.write('\r\n'); await new Promise((resolve) => setTimeout(resolve, 800)); stream.write('\r\n'); await new Promise((resolve) => setTimeout(resolve, 800)); // Clear buffer of any banner/MOTD content dataBuffer = ''; } // Set up common data handler for all command output const streamDataHandler = (data) => { const text = data.toString('utf8'); if (options.verboseLogging) { LoggingUtils_1.LoggingUtils.log(`Received full data: ${JSON.stringify(text)}`, true); } // Update last data time for stable output detection if (options.stableOutputDetection) { options.lastDataTime = Date.now(); } }; // Install the global handler if (stream) { stream.on('data', streamDataHandler); } // Process each command for (let cmdIndex = 0; cmdIndex < commands.length; cmdIndex++) { const commandObj = commands[cmdIndex]; // Reset buffer for each command dataBuffer = ''; let commandOutput = ''; // Add MAC address table detection const isMacAddressTable = commandObj.command.includes('mac-address') || commandObj.command.includes('MAC Address'); // Adjust timeout for MAC address commands and Aruba APs let commandTimeout = options.promptTimeout; if (isMacAddressTable && options.deviceType === 'aruba-os') { commandTimeout = Math.max(commandTimeout, 30000); // Longer timeout for MAC address tables LoggingUtils_1.LoggingUtils.log(`MAC address table command detected, using extended timeout: ${commandTimeout}ms`, options.verboseLogging || false); } else if (options.deviceType === 'aruba-ap') { // Aruba APs need longer timeouts due to character-by-character output commandTimeout = Math.max(commandTimeout, 20000); // Reduced from 25000ms LoggingUtils_1.LoggingUtils.log(`Aruba AP detected, using extended timeout: ${commandTimeout}ms`, options.verboseLogging || false); } LoggingUtils_1.LoggingUtils.log(`Executing command ${cmdIndex + 1}/${commands.length}: ${commandObj.command}`, options.verboseLogging || false); if (stream) { // Set up data handler specifically to capture this command's output const commandDataHandler = (data) => { const text = data.toString('utf8'); dataBuffer += text; commandOutput += text; // For Aruba APs, also update stable output detection timing if (options.deviceType === 'aruba-ap' && options.stableOutputDetection) { options.lastDataTime = Date.now(); } }; // Add command-specific handler stream.on('data', commandDataHandler); // Send the command stream.write(`${commandObj.command}${options.lineEnding || '\r\n'}`); try { // Wait for command prompt to appear, indicating command completion await PromptHandler_1.PromptHandler.waitForPrompt(stream, dataBuffer, options.commandPromptRegex, isMacAddressTable ? commandTimeout : options.promptTimeout, () => { }, // We're handling data in the commandDataHandler options); // Clean output by removing the command echo and prompt // This is critical - we need to extract just the command response const cleanOutput = PromptHandler_1.PromptHandler.cleanCommandOutput(commandOutput, commandObj.command, options.commandPromptRegex, options); LoggingUtils_1.LoggingUtils.log(`Command completed. Raw output length: ${commandOutput.length}, Clean output length: ${cleanOutput.length}`, options.verboseLogging || false); // Save result results.push({ command: commandObj.command, output: cleanOutput, exitCode: null, }); // Remove the specific data handler when done with this command stream.removeListener('data', commandDataHandler); // Send a line break and wait before next command if (cmdIndex < commands.length - 1) { LoggingUtils_1.LoggingUtils.log(`Waiting ${commandObj.waitTime}ms before next command`, options.verboseLogging || false); await new Promise((resolve) => setTimeout(resolve, commandObj.waitTime)); // Clear the buffer between commands dataBuffer = ''; } } catch (error) { LoggingUtils_1.LoggingUtils.error(`Error executing command "${commandObj.command}": ${error.message}`, true); // Remove the handler even on error stream.removeListener('data', commandDataHandler); // Save the partial result with error results.push({ command: commandObj.command, output: `ERROR: ${error.message}\n\nPartial output:\n${commandOutput}`, exitCode: 1, }); // Wait before next command if (cmdIndex < commands.length - 1) { await new Promise((resolve) => setTimeout(resolve, commandObj.waitTime)); dataBuffer = ''; } } } } // Remove global handler if (stream) { stream.removeListener('data', streamDataHandler); } return results; } finally { // Clean up resources if (stream) { stream.end(`exit${options.lineEnding || '\r\n'}`); } if (client) { client.end(); } } } /** * Handle privilege escalation with enable command */ static async handlePrivilegeEscalation(stream, dataBuffer, password, options) { LoggingUtils_1.LoggingUtils.log(`Sending enable command for privilege escalation`, options.verboseLogging || false); stream.write(`enable${options.lineEnding || '\r\n'}`); // Wait for password prompt await new Promise((resolve, reject) => { const passwordTimeout = setTimeout(() => { reject(new Error('Timeout waiting for enable password prompt')); }, options.promptTimeout); const passwordHandler = (data) => { const text = data.toString('utf8'); dataBuffer += text; if (/[Pp]assword:/.test(dataBuffer)) { clearTimeout(passwordTimeout); stream.removeListener('data', passwordHandler); resolve(); } }; stream.on('data', passwordHandler); }); // Send enable password const enablePassword = options.privilegeModePassword || password; stream.write(`${enablePassword}${options.lineEnding || '\r\n'}`); // Wait for prompt after enable await PromptHandler_1.PromptHandler.waitForPrompt(stream, dataBuffer, options.commandPromptRegex, options.promptTimeout, (data) => { dataBuffer += data.toString('utf8'); }, options); } } exports.SshCommandExecutor = SshCommandExecutor;