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