UNPKG

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
"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