n8n-nodes-netdevices
Version:
n8n node to interact with network devices (Cisco, Juniper, Palo Alto PAN-OS, Ciena SAOS, Linux, etc.)
690 lines • 31.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JumpHostConnection = void 0;
const ssh2_1 = require("ssh2");
const base_connection_1 = require("./base-connection");
let Logger;
try {
Logger = require('n8n-workflow').LoggerProxy;
}
catch (error) {
Logger = {
debug: (...args) => console.log('[DEBUG]', ...args),
info: (...args) => console.log('[INFO]', ...args),
warn: (...args) => console.warn('[WARN]', ...args),
error: (...args) => console.error('[ERROR]', ...args)
};
}
class JumpHostConnection extends base_connection_1.BaseConnection {
constructor(credentials) {
super(credentials);
this.jumpHostConnected = false;
this.jumpHostClient = new ssh2_1.Client();
this.setupJumpHostEventHandlers();
this.setupTargetConnectionEventHandlers();
}
setupJumpHostEventHandlers() {
this.jumpHostClient.on('error', (error) => {
Logger.error('Jump host connection error', {
jumpHost: this.credentials.jumpHostHost,
error: error.message
});
});
this.jumpHostClient.on('end', () => {
Logger.info('Jump host connection ended', {
jumpHost: this.credentials.jumpHostHost
});
this.jumpHostConnected = false;
});
this.jumpHostClient.on('close', () => {
Logger.info('Jump host connection closed', {
jumpHost: this.credentials.jumpHostHost
});
this.jumpHostConnected = false;
});
}
setupTargetConnectionEventHandlers() {
this.client.removeAllListeners();
this.client.on('ready', () => {
Logger.debug('Target connection ready through jump host', {
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
this.isConnected = true;
this.lastActivity = Date.now();
this.emit('ready');
});
this.client.on('error', (error) => {
Logger.error('Target connection error through jump host', {
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost,
error: error.message
});
this.isConnected = false;
this.emit('error', error);
});
this.client.on('end', () => {
Logger.debug('Target connection ended through jump host', {
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
this.isConnected = false;
this.emit('end');
});
this.client.on('close', () => {
Logger.debug('Target connection closed through jump host', {
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
this.isConnected = false;
this.emit('close');
});
}
async connect() {
Logger.debug('Starting jump host connection process', {
jumpHost: this.credentials.jumpHostHost,
target: this.credentials.host,
jumpHostAuthMethod: this.credentials.jumpHostAuthMethod,
targetAuthMethod: this.credentials.authMethod
});
try {
await this.connectToJumpHost();
await this.createOutboundTunnel();
await this.connectThroughTunnel();
Logger.info('Jump host connection established successfully', {
jumpHost: this.credentials.jumpHostHost,
target: this.credentials.host
});
}
catch (error) {
Logger.error('Jump host connection failed', {
jumpHost: this.credentials.jumpHostHost,
target: this.credentials.host,
error: error instanceof Error ? error.message : String(error)
});
await this.cleanup();
throw error;
}
}
async connectToJumpHost() {
return new Promise((resolve, reject) => {
Logger.debug('Preparing jump host SSH connection configuration', {
jumpHost: this.credentials.jumpHostHost,
port: this.credentials.jumpHostPort,
username: this.credentials.jumpHostUsername,
connectionTimeout: this.timeout,
authMethod: this.credentials.jumpHostAuthMethod,
hasPrivateKey: !!this.credentials.jumpHostPrivateKey,
hasPassphrase: !!this.credentials.jumpHostPassphrase,
hasPassword: !!this.credentials.jumpHostPassword
});
const algorithms = this.getOptimizedAlgorithms();
Logger.debug('Trying jump host SSH connection with algorithm configurations', {
algorithmCount: algorithms.length,
authMethod: this.credentials.jumpHostAuthMethod
});
let lastError = null;
let algorithmIndex = 0;
const tryNextAlgorithm = () => {
if (algorithmIndex >= algorithms.length) {
Logger.error('All jump host SSH algorithm configurations failed', {
jumpHost: this.credentials.jumpHostHost,
authMethod: this.credentials.jumpHostAuthMethod,
lastError: lastError === null || lastError === void 0 ? void 0 : lastError.message
});
reject(lastError || new Error('All SSH algorithm configurations failed'));
return;
}
const currentAlgorithms = algorithms[algorithmIndex];
Logger.debug('Attempting jump host connection with algorithm config', {
algorithmIndex: algorithmIndex + 1,
total: algorithms.length,
algorithms: currentAlgorithms
});
this.tryJumpHostConnectWithConfig(currentAlgorithms)
.then(() => {
Logger.info('Jump host connection established with algorithm config', {
algorithmIndex: algorithmIndex + 1,
jumpHost: this.credentials.jumpHostHost
});
resolve();
})
.catch((error) => {
lastError = error;
algorithmIndex++;
Logger.warn('Jump host connection failed with algorithm config', {
algorithmIndex: algorithmIndex,
jumpHost: this.credentials.jumpHostHost,
error: error.message
});
setTimeout(tryNextAlgorithm, 100);
});
};
tryNextAlgorithm();
});
}
async tryJumpHostConnectWithConfig(algorithms) {
return new Promise((resolve, reject) => {
var _a, _b, _c, _d;
const connectConfig = {
host: this.credentials.jumpHostHost,
port: this.credentials.jumpHostPort,
username: this.credentials.jumpHostUsername,
readyTimeout: this.timeout,
algorithms: algorithms
};
if (this.credentials.jumpHostAuthMethod === 'privateKey') {
if (!this.credentials.jumpHostPrivateKey) {
Logger.error('Jump host SSH private key is missing for private key authentication');
reject(new Error('Jump host SSH private key is required for private key authentication'));
return;
}
try {
(0, base_connection_1.validateSSHPrivateKey)(this.credentials.jumpHostPrivateKey);
let normalizedKey = (0, base_connection_1.formatSSHPrivateKey)(this.credentials.jumpHostPrivateKey);
Logger.debug('Jump host private key validation and formatting successful', {
originalLength: this.credentials.jumpHostPrivateKey.length,
formattedLength: normalizedKey.length,
hasBeginMarker: normalizedKey.includes('-----BEGIN'),
hasEndMarker: normalizedKey.includes('-----END')
});
const keyLines = normalizedKey.split('\n');
Logger.debug('Jump host private key format details', {
totalLines: keyLines.length,
firstLine: (_a = keyLines[0]) === null || _a === void 0 ? void 0 : _a.substring(0, 50),
lastLine: (_b = keyLines[keyLines.length - 1]) === null || _b === void 0 ? void 0 : _b.substring(0, 50),
hasEmptyLines: keyLines.some(line => line.trim() === ''),
lineLengths: keyLines.map(line => line.length).slice(0, 5)
});
if (normalizedKey.includes('-----BEGIN OPENSSH PRIVATE KEY-----')) {
Logger.debug('Detected OpenSSH format key, ensuring proper formatting');
normalizedKey = normalizedKey.replace(/\n\s*\n/g, '\n').trim() + '\n';
}
else if (normalizedKey.includes('-----BEGIN RSA PRIVATE KEY-----')) {
Logger.debug('Detected RSA format key, ensuring proper line wrapping');
const lines = normalizedKey.split('\n');
if (lines.length === 1) {
const keyContent = normalizedKey;
const beginMatch = keyContent.match(/-----BEGIN RSA PRIVATE KEY-----(.*)-----END RSA PRIVATE KEY-----/);
if (beginMatch) {
const content = beginMatch[1].trim().replace(/\s/g, '');
const wrappedContent = ((_c = content.match(/.{1,64}/g)) === null || _c === void 0 ? void 0 : _c.join('\n')) || content;
normalizedKey = `-----BEGIN RSA PRIVATE KEY-----\n${wrappedContent}\n-----END RSA PRIVATE KEY-----`;
Logger.debug('Reformatted single-line RSA key', {
originalLength: keyContent.length,
newLength: normalizedKey.length,
contentLines: wrappedContent.split('\n').length
});
}
}
else {
const header = lines[0];
const footer = lines[lines.length - 1];
const content = lines.slice(1, -1).join('').replace(/\s/g, '');
const wrappedContent = ((_d = content.match(/.{1,64}/g)) === null || _d === void 0 ? void 0 : _d.join('\n')) || content;
normalizedKey = `${header}\n${wrappedContent}\n${footer}`;
}
}
connectConfig.privateKey = normalizedKey;
}
catch (keyError) {
Logger.error('Jump host private key validation failed', {
error: keyError instanceof Error ? keyError.message : String(keyError),
keyLength: this.credentials.jumpHostPrivateKey.length,
hasBeginMarker: this.credentials.jumpHostPrivateKey.includes('-----BEGIN'),
hasEndMarker: this.credentials.jumpHostPrivateKey.includes('-----END')
});
reject(new Error(`Jump host SSH private key validation failed: ${keyError instanceof Error ? keyError.message : String(keyError)}`));
return;
}
if (this.credentials.jumpHostPassphrase && this.credentials.jumpHostPassphrase.trim() !== '') {
connectConfig.passphrase = this.credentials.jumpHostPassphrase;
Logger.debug('Using passphrase for jump host private key', {
passphraseLength: this.credentials.jumpHostPassphrase.length
});
}
else {
Logger.debug('No passphrase provided for jump host private key');
}
connectConfig.tryKeyboard = false;
Logger.debug('Configured jump host SSH private key authentication', {
keyLength: this.credentials.jumpHostPrivateKey.length,
hasPassphrase: !!connectConfig.passphrase,
tryKeyboard: connectConfig.tryKeyboard
});
}
else {
if (!this.credentials.jumpHostPassword) {
Logger.error('Jump host password is missing for password authentication');
reject(new Error('Jump host password is required for password authentication'));
return;
}
connectConfig.password = this.credentials.jumpHostPassword;
Logger.debug('Configured jump host SSH password authentication', {
passwordLength: this.credentials.jumpHostPassword.length
});
}
Logger.debug('Connecting to jump host', {
jumpHost: this.credentials.jumpHostHost,
port: this.credentials.jumpHostPort,
username: this.credentials.jumpHostUsername,
authMethod: this.credentials.jumpHostAuthMethod
});
try {
this.jumpHostClient.connect(connectConfig);
}
catch (error) {
Logger.error('Failed to initiate jump host SSH connection', {
error: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : undefined,
jumpHost: this.credentials.jumpHostHost,
port: this.credentials.jumpHostPort,
authMethod: this.credentials.jumpHostAuthMethod,
connectConfigKeys: Object.keys(connectConfig).filter(key => key !== 'privateKey' && key !== 'password' && key !== 'passphrase')
});
reject(error);
return;
}
this.jumpHostClient.once('ready', () => {
Logger.info('Jump host connection established', {
jumpHost: this.credentials.jumpHostHost,
username: this.credentials.jumpHostUsername
});
this.jumpHostConnected = true;
resolve();
});
this.jumpHostClient.once('error', (error) => {
Logger.error('Jump host connection failed', {
jumpHost: this.credentials.jumpHostHost,
error: error.message,
errorStack: error.stack,
authMethod: this.credentials.jumpHostAuthMethod,
errorLevel: error.level,
errorDescription: error.description
});
reject(error);
});
});
}
async createOutboundTunnel() {
return new Promise((resolve, reject) => {
Logger.debug('Creating outbound tunnel', {
jumpHost: this.credentials.jumpHostHost,
target: `${this.credentials.host}:${this.credentials.port}`
});
this.jumpHostClient.forwardOut('127.0.0.1', 0, this.credentials.host, this.credentials.port, (err, stream) => {
if (err) {
Logger.error('Tunnel creation failed', {
jumpHost: this.credentials.jumpHostHost,
target: `${this.credentials.host}:${this.credentials.port}`,
error: err.message
});
reject(err);
return;
}
this.tunnelStream = stream;
Logger.info('Outbound tunnel created successfully', {
jumpHost: this.credentials.jumpHostHost,
target: `${this.credentials.host}:${this.credentials.port}`
});
resolve();
});
});
}
async connectThroughTunnel() {
return new Promise((resolve, reject) => {
Logger.debug('Connecting to target through tunnel', {
target: this.credentials.host,
username: this.credentials.username,
authMethod: this.credentials.authMethod
});
const connectConfig = {
sock: this.tunnelStream,
username: this.credentials.username,
readyTimeout: this.timeout,
algorithms: this.getOptimizedAlgorithms()[0]
};
if (this.credentials.authMethod === 'privateKey') {
connectConfig.privateKey = this.credentials.privateKey;
if (this.credentials.passphrase) {
connectConfig.passphrase = this.credentials.passphrase;
}
connectConfig.tryKeyboard = false;
}
else {
connectConfig.password = this.credentials.password;
}
const timeoutId = setTimeout(() => {
Logger.error('Target connection timeout through tunnel', {
target: this.credentials.host,
timeout: this.timeout
});
reject(new Error(`Target connection timeout after ${this.timeout}ms`));
}, this.timeout);
this.client.once('ready', () => {
clearTimeout(timeoutId);
Logger.info('Target device connection established through jump host', {
target: this.credentials.host,
username: this.credentials.username
});
this.isConnected = true;
this.lastActivity = Date.now();
Logger.debug('Connection state after tunnel connection', {
isConnected: this.isConnected,
jumpHostConnected: this.jumpHostConnected,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
resolve();
});
this.client.once('error', (error) => {
clearTimeout(timeoutId);
Logger.error('Target device connection through jump host failed', {
target: this.credentials.host,
error: error.message,
errorStack: error.stack
});
reject(error);
});
try {
this.client.connect(connectConfig);
}
catch (error) {
clearTimeout(timeoutId);
Logger.error('Failed to initiate target connection through tunnel', {
error: error instanceof Error ? error.message : String(error),
target: this.credentials.host
});
reject(error);
}
});
}
isConnectedAndReady() {
const basicConnected = this.isConnected && this.jumpHostConnected;
const tunnelExists = !!this.tunnelStream;
const tunnelDestroyed = this.tunnelStream ? this.tunnelStream.destroyed : false;
Logger.debug('Jump host connection status check', {
isConnected: this.isConnected,
jumpHostConnected: this.jumpHostConnected,
hasTunnel: tunnelExists,
tunnelDestroyed: tunnelDestroyed,
tunnelReadable: this.tunnelStream ? this.tunnelStream.readable : 'no-tunnel',
tunnelWritable: this.tunnelStream ? this.tunnelStream.writable : 'no-tunnel',
basicConnected: basicConnected,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost,
deviceType: this.credentials.deviceType
});
if (this.isLinuxDevice()) {
return basicConnected;
}
else {
return basicConnected && tunnelExists && !tunnelDestroyed;
}
}
isLinuxDevice() {
return this.credentials.deviceType.toLowerCase() === 'linux';
}
async sendCommand(command) {
const basicConnected = this.isConnected && this.jumpHostConnected;
const tunnelExists = !!this.tunnelStream;
const tunnelDestroyed = this.tunnelStream ? this.tunnelStream.destroyed : false;
if (!basicConnected) {
const error = `Not connected to device. Connected states: target=${this.isConnected}, jumpHost=${this.jumpHostConnected}`;
Logger.error('Jump host sendCommand failed - connection check', {
isConnected: this.isConnected,
jumpHostConnected: this.jumpHostConnected,
tunnelExists: tunnelExists,
tunnelDestroyed: tunnelDestroyed,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost,
deviceType: this.credentials.deviceType
});
throw new Error(error);
}
if (tunnelExists && tunnelDestroyed) {
Logger.warn('Tunnel stream was destroyed, attempting to re-establish connection', {
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost,
deviceType: this.credentials.deviceType
});
throw new Error('Tunnel connection was lost, please retry');
}
Logger.debug('Executing command through jump host', {
command,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost,
deviceType: this.credentials.deviceType,
isLinux: this.isLinuxDevice(),
connectionStatus: this.isConnectedAndReady(),
basicConnected: basicConnected,
tunnelExists: tunnelExists,
tunnelDestroyed: tunnelDestroyed
});
if (this.isLinuxDevice()) {
return await this.sendLinuxCommand(command);
}
try {
return await super.sendCommand(command);
}
catch (commandError) {
Logger.error('Command execution failed through jump host', {
command,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost,
deviceType: this.credentials.deviceType,
error: commandError instanceof Error ? commandError.message : String(commandError),
connectionState: {
isConnected: this.isConnected,
jumpHostConnected: this.jumpHostConnected,
tunnelExists: tunnelExists,
tunnelDestroyed: tunnelDestroyed
}
});
throw new Error(`Jump host command failed: ${commandError instanceof Error ? commandError.message : String(commandError)}`);
}
}
async sendLinuxCommand(command) {
return new Promise((resolve, reject) => {
if (!this.client) {
const err = new Error('SSH client not available for command execution');
Logger.error('sendLinuxCommand: ' + err.message, {
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
return reject(err);
}
Logger.debug('Executing Linux command via client.exec() through jump host', {
command,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
let output = '';
let errorOutput = '';
let streamClosed = false;
let timeoutId;
const cleanup = () => {
if (timeoutId)
clearTimeout(timeoutId);
};
this.client.exec(command, (err, stream) => {
if (err) {
Logger.error('sendLinuxCommand: Failed to execute command through jump host', {
command,
error: err.message,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
cleanup();
return reject(err);
}
timeoutId = setTimeout(() => {
const msg = `sendLinuxCommand: Command timeout after ${this.commandTimeout}ms`;
Logger.error(msg, {
command,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
cleanup();
stream.close();
reject(new Error(msg));
}, this.commandTimeout);
stream.on('data', (data) => {
const chunk = data.toString('utf8');
output += chunk;
Logger.debug('sendLinuxCommand: stdout data received through jump host', {
command,
length: chunk.length,
target: this.credentials.host
});
});
stream.stderr.on('data', (data) => {
const chunk = data.toString('utf8');
errorOutput += chunk;
Logger.warn('sendLinuxCommand: stderr data received through jump host', {
command,
length: chunk.length,
target: this.credentials.host
});
});
stream.on('close', (code, signal) => {
if (streamClosed)
return;
streamClosed = true;
Logger.debug('sendLinuxCommand: stream closed through jump host', {
command,
code,
signal,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
cleanup();
this.lastActivity = Date.now();
if (errorOutput && !output) {
resolve({
command,
output: this.stripAnsi(errorOutput).trim(),
success: false,
error: `Command failed with exit code ${code || 'N/A'}`
});
}
else {
const fullOutput = errorOutput ? `${output}\n--- STDERR ---\n${errorOutput}` : output;
resolve({
command,
output: this.stripAnsi(fullOutput).trim(),
success: code === 0,
error: code !== 0 ? `Command failed with exit code ${code || 'N/A'}` : undefined
});
}
});
stream.on('error', (streamErr) => {
Logger.error('sendLinuxCommand: stream error through jump host', {
command,
error: streamErr.message,
target: this.credentials.host,
jumpHost: this.credentials.jumpHostHost
});
cleanup();
reject(streamErr);
});
});
});
}
stripAnsi(str) {
return str.replace(/[\u001b\u009b][[()#;?]*.{0,2}(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
}
async disconnect() {
Logger.debug('Disconnecting jump host connection', {
jumpHost: this.credentials.jumpHostHost,
target: this.credentials.host,
isConnected: this.isConnected,
jumpHostConnected: this.jumpHostConnected
});
await this.cleanup();
await super.disconnect();
}
async cleanup() {
Logger.debug('Cleaning up jump host resources', {
jumpHost: this.credentials.jumpHostHost,
target: this.credentials.host
});
if (this.tunnelStream) {
this.tunnelStream.end();
this.tunnelStream = null;
}
if (this.jumpHostClient && this.jumpHostConnected) {
this.jumpHostClient.end();
this.jumpHostConnected = false;
}
}
getConnectionInfo() {
const baseInfo = super.getConnectionInfo();
return {
...baseInfo,
jumpHost: this.credentials.jumpHostHost
};
}
getOptimizedAlgorithms() {
if (this.fastMode) {
return [
{
serverHostKey: ['ssh-rsa', 'ecdsa-sha2-nistp256'],
cipher: ['aes128-ctr', 'aes128-cbc'],
hmac: ['hmac-sha1'],
kex: ['diffie-hellman-group14-sha1', 'ecdh-sha2-nistp256']
}
];
}
else {
const keyBasedAlgorithms = {
serverHostKey: [
'ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512',
'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521',
'ssh-ed25519'
],
cipher: [
'aes128-ctr', 'aes192-ctr', 'aes256-ctr',
'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com',
'aes128-cbc', 'aes192-cbc'
],
hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'],
kex: [
'curve25519-sha256', 'curve25519-sha256@libssh.org',
'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512',
'diffie-hellman-group14-sha256', 'ecdh-sha2-nistp256',
'diffie-hellman-group14-sha1'
]
};
const passwordBasedAlgorithms = {
serverHostKey: [
'ssh-rsa', 'rsa-sha2-256', 'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384', 'ssh-ed25519'
],
cipher: [
'aes128-ctr', 'aes192-ctr', 'aes256-ctr',
'aes128-cbc', 'aes192-cbc'
],
hmac: ['hmac-sha2-256', 'hmac-sha1'],
kex: [
'diffie-hellman-group14-sha256', 'ecdh-sha2-nistp256',
'diffie-hellman-group14-sha1'
]
};
const primaryAlgorithms = this.credentials.jumpHostAuthMethod === 'privateKey'
? keyBasedAlgorithms
: passwordBasedAlgorithms;
return [
primaryAlgorithms,
{
serverHostKey: ['ssh-rsa'],
cipher: ['aes128-cbc'],
hmac: ['hmac-sha1'],
kex: ['diffie-hellman-group1-sha1']
}
];
}
}
}
exports.JumpHostConnection = JumpHostConnection;
//# sourceMappingURL=jump-host-connection.js.map