UNPKG

n8n-nodes-netdevices

Version:
815 lines 33 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseConnection = void 0; const ssh2_1 = require("ssh2"); const events_1 = require("events"); let Logger; try { const { LoggerProxy } = require('n8n-workflow'); Logger = LoggerProxy; } catch (error) { Logger = { debug: console.log, info: console.log, warn: console.warn, error: console.error }; } class BaseConnection extends events_1.EventEmitter { constructor(credentials) { super(); this.isConnected = false; this.currentChannel = null; this.basePrompt = ''; this.enabledPrompt = ''; this.configPrompt = ''; this.timeout = 10000; this.encoding = 'utf8'; this.newline = '\n'; this.returnChar = '\r'; this.fastMode = false; this.commandTimeout = 8000; this.reuseConnection = false; this.connectionPooling = false; this.lastActivity = Date.now(); this.credentials = credentials; this.client = new ssh2_1.Client(); this.timeout = credentials.timeout || 10000; this.fastMode = credentials.fastMode || false; this.commandTimeout = credentials.commandTimeout || 8000; this.reuseConnection = credentials.reuseConnection || false; this.connectionPooling = credentials.connectionPooling || false; this.setupEventHandlers(); this.setupConnectionPooling(); } setupEventHandlers() { this.client.on('ready', () => { this.isConnected = true; this.emit('ready'); }); this.client.on('error', (error) => { this.isConnected = false; this.emit('error', error); }); this.client.on('end', () => { this.isConnected = false; this.emit('end'); }); this.client.on('close', () => { this.isConnected = false; this.emit('close'); }); } async connect() { Logger.debug('Starting SSH connection process', { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, authMethod: this.credentials.authMethod, deviceType: this.credentials.deviceType, timeout: this.timeout, fastMode: this.fastMode, hasPrivateKey: !!this.credentials.privateKey, hasPassphrase: !!this.credentials.passphrase, hasPassword: !!this.credentials.password }); if (this.connectionPooling || this.reuseConnection) { const connectionKey = this.getConnectionKey(); const existingConnection = BaseConnection.connectionPool.get(connectionKey); if (existingConnection && existingConnection.isAlive()) { Logger.debug('Reusing existing connection from pool', { connectionKey }); this.client = existingConnection.client; this.currentChannel = existingConnection.currentChannel; this.isConnected = true; this.basePrompt = existingConnection.basePrompt; this.lastActivity = Date.now(); return; } } this.validateCredentials(); const algorithmConfigs = this.getOptimizedAlgorithms(); Logger.debug('Trying SSH connection with algorithm configurations', { algorithmCount: algorithmConfigs.length, authMethod: this.credentials.authMethod }); for (let i = 0; i < algorithmConfigs.length; i++) { try { Logger.debug(`Attempting connection with algorithm config ${i + 1}/${algorithmConfigs.length}`, { algorithms: algorithmConfigs[i] }); await this.tryConnect(algorithmConfigs[i]); Logger.info('SSH connection established successfully', { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, authMethod: this.credentials.authMethod, algorithmIndex: i + 1 }); if (this.connectionPooling) { const connectionKey = this.getConnectionKey(); BaseConnection.connectionPool.set(connectionKey, this); Logger.debug('Added connection to pool', { connectionKey }); } return; } catch (error) { Logger.warn(`Connection attempt ${i + 1}/${algorithmConfigs.length} failed`, { error: error instanceof Error ? error.message : String(error), host: this.credentials.host, port: this.credentials.port, authMethod: this.credentials.authMethod }); if (i === algorithmConfigs.length - 1) { Logger.error('All connection attempts failed', { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, authMethod: this.credentials.authMethod, finalError: error instanceof Error ? error.message : String(error) }); throw error; } } } } validateCredentials() { Logger.debug('Validating credentials', { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, authMethod: this.credentials.authMethod }); if (!this.credentials.host) { throw new Error('Host is required for SSH connection'); } if (!this.credentials.username) { throw new Error('Username is required for SSH connection'); } if (this.credentials.authMethod === 'privateKey') { if (!this.credentials.privateKey) { throw new Error('SSH private key is required for private key authentication'); } if (!this.credentials.privateKey.includes('-----BEGIN') || !this.credentials.privateKey.includes('-----END')) { Logger.warn('Private key may not be in correct format', { keyLength: this.credentials.privateKey.length, hasBeginMarker: this.credentials.privateKey.includes('-----BEGIN'), hasEndMarker: this.credentials.privateKey.includes('-----END') }); } Logger.debug('Private key validation passed', { keyLength: this.credentials.privateKey.length, hasPassphrase: !!this.credentials.passphrase, passphraseLength: this.credentials.passphrase ? this.credentials.passphrase.length : 0 }); } else { if (!this.credentials.password) { throw new Error('Password is required for password authentication'); } Logger.debug('Password authentication validation passed', { passwordLength: this.credentials.password.length }); } } async tryConnect(algorithms) { return new Promise((resolve, reject) => { const connectionTimeout = this.fastMode ? Math.min(this.timeout, 8000) : this.timeout; Logger.debug('Preparing SSH connection configuration', { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, connectionTimeout, authMethod: this.credentials.authMethod, algorithms }); const connectConfig = { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, readyTimeout: connectionTimeout, keepaliveInterval: this.credentials.keepAlive ? (this.fastMode ? 60000 : 30000) : undefined, algorithms: algorithms, hostHash: 'md5', debug: process.env.SSH_DEBUG === 'true' ? (msg) => Logger.debug('SSH2 Debug: ' + msg) : undefined }; if (this.credentials.authMethod === 'privateKey') { if (!this.credentials.privateKey) { Logger.error('SSH private key is missing for private key authentication'); reject(new Error('SSH private key is required for private key authentication')); return; } connectConfig.privateKey = this.credentials.privateKey; if (this.credentials.passphrase && this.credentials.passphrase.trim() !== '') { connectConfig.passphrase = this.credentials.passphrase; Logger.debug('Using passphrase for private key', { passphraseLength: this.credentials.passphrase.length }); } else { Logger.debug('No passphrase provided for private key'); } connectConfig.tryKeyboard = false; Logger.debug('Configured SSH private key authentication', { keyLength: this.credentials.privateKey.length, hasPassphrase: !!connectConfig.passphrase, tryKeyboard: connectConfig.tryKeyboard }); } else { if (!this.credentials.password) { Logger.error('Password is missing for password authentication'); reject(new Error('Password is required for password authentication')); return; } connectConfig.password = this.credentials.password; Logger.debug('Configured SSH password authentication', { passwordLength: this.credentials.password.length }); } const timeoutId = setTimeout(() => { Logger.error('SSH connection timeout reached', { host: this.credentials.host, port: this.credentials.port, timeout: connectionTimeout, authMethod: this.credentials.authMethod }); this.client.removeAllListeners(); reject(new Error(`Connection timeout after ${connectionTimeout}ms`)); }, connectionTimeout); this.client.once('ready', () => { Logger.info('SSH connection ready', { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, authMethod: this.credentials.authMethod }); clearTimeout(timeoutId); this.lastActivity = Date.now(); this.isConnected = true; this.sessionPreparation() .then(() => { Logger.debug('Session preparation completed successfully'); resolve(); }) .catch((error) => { Logger.error('Session preparation failed', { error: error instanceof Error ? error.message : String(error), host: this.credentials.host }); reject(error); }); }); this.client.once('error', (error) => { Logger.error('SSH connection error', { error: error.message, host: this.credentials.host, port: this.credentials.port, username: this.credentials.username, authMethod: this.credentials.authMethod, level: error.level || 'unknown' }); clearTimeout(timeoutId); let enhancedError = error; if (error.message.includes('All configured authentication methods failed')) { enhancedError = new Error(`Authentication failed: ${error.message}. Please check your ${this.credentials.authMethod === 'privateKey' ? 'SSH private key and passphrase' : 'password'}.`); } else if (error.message.includes('connect ECONNREFUSED')) { enhancedError = new Error(`Connection refused: Cannot connect to ${this.credentials.host}:${this.credentials.port}. Please check if SSH service is running and the host/port are correct.`); } else if (error.message.includes('connect ETIMEDOUT')) { enhancedError = new Error(`Connection timed out: Cannot reach ${this.credentials.host}:${this.credentials.port}. Please check network connectivity and firewall settings.`); } else if (error.message.includes('getaddrinfo ENOTFOUND')) { enhancedError = new Error(`Host not found: Cannot resolve hostname ${this.credentials.host}. Please check the hostname or IP address.`); } reject(enhancedError); }); this.client.once('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => { Logger.debug('Received keyboard-interactive authentication request', { name, instructions, promptCount: prompts.length, authMethod: this.credentials.authMethod }); if (this.credentials.authMethod === 'password' && this.credentials.password) { Logger.debug('Responding to keyboard-interactive with password'); finish([this.credentials.password]); } else { Logger.warn('Rejecting keyboard-interactive authentication for key-based auth'); finish([]); } }); this.client.once('banner', (message) => { Logger.debug('Received SSH banner', { banner: message.trim(), host: this.credentials.host }); }); try { Logger.debug('Initiating SSH connection', { host: this.credentials.host, port: this.credentials.port, username: this.credentials.username }); this.client.connect(connectConfig); } catch (error) { Logger.error('Failed to initiate SSH connection', { error: error instanceof Error ? error.message : String(error), host: this.credentials.host, port: this.credentials.port }); clearTimeout(timeoutId); reject(error); } }); } async disconnect() { return new Promise((resolve) => { if (this.connectionPooling && this.isAlive()) { this.lastActivity = Date.now(); resolve(); return; } if (this.currentChannel) { this.currentChannel.end(); this.currentChannel = null; } if (this.client) { const disconnectTimeout = setTimeout(() => { this.isConnected = false; resolve(); }, this.fastMode ? 2000 : 5000); this.client.once('close', () => { clearTimeout(disconnectTimeout); this.isConnected = false; if (this.connectionPooling) { const connectionKey = this.getConnectionKey(); BaseConnection.connectionPool.delete(connectionKey); } resolve(); }); this.client.end(); } else { resolve(); } }); } async sessionPreparation() { await this.createShellChannel(); if (this.fastMode) { await this.setBasePrompt(); } else { await Promise.all([ this.setBasePrompt(), this.disablePaging(), this.setTerminalWidth(), ]); } } async createShellChannel() { return new Promise((resolve, reject) => { this.client.shell((err, channel) => { if (err) { reject(err); return; } this.currentChannel = channel; this.currentChannel.setEncoding(this.encoding); const waitTime = this.fastMode ? 200 : 500; setTimeout(() => { resolve(); }, waitTime); }); }); } async setBasePrompt() { await this.writeChannel(this.returnChar); const output = await this.readChannel(); const lines = output.trim().split('\n'); const lastLine = lines[lines.length - 1]; this.basePrompt = lastLine.replace(/[>#$%]\s*$/, ''); this.enabledPrompt = this.basePrompt + '#'; this.configPrompt = this.basePrompt + '(config)#'; } async disablePaging() { } async setTerminalWidth() { } async writeChannel(data) { return new Promise((resolve) => { if (this.currentChannel) { this.currentChannel.write(data); global.setTimeout(resolve, 50); } else { resolve(); } }); } async readChannel(timeout = 3000) { return new Promise((resolve) => { let buffer = ''; let timeoutId; const onData = (data) => { buffer += data; }; const cleanup = () => { if (this.currentChannel) { this.currentChannel.removeListener('data', onData); } if (timeoutId) { global.clearTimeout(timeoutId); } }; timeoutId = global.setTimeout(() => { cleanup(); resolve(buffer); }, timeout); if (this.currentChannel) { this.currentChannel.on('data', onData); } else { cleanup(); resolve(''); } }); } async readUntilPrompt(expectedPrompt, timeout = 10000) { return new Promise((resolve, reject) => { let buffer = ''; let timeoutId; const prompt = expectedPrompt || this.basePrompt; const actualTimeout = this.fastMode ? Math.min(timeout, 5000) : timeout; const promptPatterns = [ prompt, prompt + '#', prompt + '>', prompt + '$', prompt + '%' ]; const linuxPromptPatterns = [ /\$\s*$/, /#\s*$/, />\s*$/, /\]\s*\$\s*$/, /\]\s*#\s*$/, /~\s*\$\s*$/, /~\s*#\s*$/, /@.*:\s*\$\s*$/, /@.*:\s*#\s*$/, /@.*:\s*~\s*\$\s*$/, /@.*:\s*~\s*#\s*$/, ]; const onData = (data) => { buffer += data; const hasPrompt = promptPatterns.some(p => buffer.includes(p)); if (hasPrompt) { cleanup(); resolve(buffer); return; } const hasLinuxPrompt = linuxPromptPatterns.some(pattern => pattern.test(buffer)); if (hasLinuxPrompt) { cleanup(); resolve(buffer); return; } if (this.fastMode) { const lines = buffer.split('\n'); const lastLine = lines[lines.length - 1]; if (lastLine.match(/[>#$%]\s*$/)) { cleanup(); resolve(buffer); return; } } const lines = buffer.split('\n'); if (lines.length > 1) { const lastLine = lines[lines.length - 1]; if (lastLine.length > 0 && (lastLine.includes(prompt) || linuxPromptPatterns.some(pattern => pattern.test(lastLine)))) { cleanup(); resolve(buffer); return; } } }; const onError = (error) => { cleanup(); reject(error); }; const cleanup = () => { if (this.currentChannel) { this.currentChannel.removeListener('data', onData); this.currentChannel.removeListener('error', onError); } if (timeoutId) { clearTimeout(timeoutId); } }; timeoutId = setTimeout(() => { cleanup(); reject(new Error(`Timeout waiting for prompt after ${actualTimeout}ms. Buffer: ${buffer.slice(-200)}`)); }, actualTimeout); if (this.currentChannel) { this.currentChannel.on('data', onData); this.currentChannel.on('error', onError); } else { cleanup(); reject(new Error('No active channel available')); } }); } async sendCommand(command) { try { if (!this.isConnected || !this.currentChannel) { throw new Error('Not connected to device'); } this.lastActivity = Date.now(); await this.writeChannel(command + this.newline); const timeout = this.fastMode ? Math.min(this.commandTimeout, 5000) : this.commandTimeout; const output = await this.readUntilPrompt(undefined, timeout); const cleanOutput = this.sanitizeOutput(output, command); if (this.fastMode && command.startsWith('show')) { return { command, output: cleanOutput, success: true }; } const errorPatterns = [ /invalid command/i, /command not found/i, /syntax error/i, /unknown command/i, /access denied/i, /permission denied/i, /authentication failed/i, /connection lost/i, /timeout/i, /error:/i, /failed/i ]; const hasError = errorPatterns.some(pattern => pattern.test(cleanOutput)); if (hasError) { return { command, output: cleanOutput, success: false, error: 'Command execution returned an error' }; } return { command, output: cleanOutput, success: true }; } catch (error) { return { command, output: '', success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async sendConfig(configCommands) { try { if (!this.isConnected || !this.currentChannel) { throw new Error('Not connected to device'); } await this.enterConfigMode(); let allOutput = ''; let hasError = false; let errorMessage = ''; for (const command of configCommands) { try { await this.writeChannel(command + this.newline); const output = await this.readUntilPrompt(undefined, this.timeout); allOutput += output; const errorPatterns = [ /invalid command/i, /syntax error/i, /unknown command/i, /access denied/i, /permission denied/i, /error:/i, /failed/i, /incomplete command/i, /ambiguous command/i ]; if (errorPatterns.some(pattern => pattern.test(output))) { hasError = true; errorMessage = `Error in command: ${command}`; break; } } catch (cmdError) { hasError = true; errorMessage = `Failed to execute command: ${command} - ${cmdError}`; break; } } try { await this.exitConfigMode(); } catch (exitError) { hasError = true; errorMessage = errorMessage || `Failed to exit configuration mode: ${exitError}`; } const cleanOutput = this.sanitizeOutput(allOutput, configCommands.join('; ')); return { command: configCommands.join('; '), output: cleanOutput, success: !hasError, error: hasError ? errorMessage : undefined }; } catch (error) { try { await this.exitConfigMode(); } catch (exitError) { } return { command: configCommands.join('; '), output: '', success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async enterConfigMode() { await this.writeChannel('configure terminal' + this.newline); await this.readUntilPrompt(); } async exitConfigMode() { await this.writeChannel('exit' + this.newline); await this.readUntilPrompt(); } sanitizeOutput(output, command) { const escapedCommand = command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let cleanOutput = output.replace(new RegExp(escapedCommand, 'g'), ''); const escapedBasePrompt = this.basePrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedEnabledPrompt = this.enabledPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedConfigPrompt = this.configPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); cleanOutput = cleanOutput.replace(new RegExp(escapedBasePrompt + '[>#$%]', 'g'), ''); cleanOutput = cleanOutput.replace(new RegExp(escapedEnabledPrompt, 'g'), ''); cleanOutput = cleanOutput.replace(new RegExp(escapedConfigPrompt, 'g'), ''); cleanOutput = cleanOutput.replace(/^\s+|\s+$/g, ''); cleanOutput = cleanOutput.replace(/\r\n/g, '\n'); cleanOutput = cleanOutput.replace(/\r/g, '\n'); return cleanOutput; } async getCurrentConfig() { return await this.sendCommand('show configuration'); } async saveConfig() { return await this.sendCommand('save configuration'); } async rebootDevice() { return await this.sendCommand('reboot'); } isAlive() { return this.isConnected && this.currentChannel && !this.currentChannel.destroyed; } async healthCheck() { try { if (!this.isAlive()) { return false; } await this.writeChannel(this.returnChar); const response = await this.readChannel(5000); return response.length > 0; } catch (error) { return false; } } getDeviceType() { return this.credentials.deviceType; } getHost() { return this.credentials.host; } getConnectionInfo() { return { host: this.credentials.host, port: this.credentials.port, deviceType: this.credentials.deviceType, connected: this.isAlive() }; } setupConnectionPooling() { if (this.connectionPooling && !BaseConnection.poolCleanupInterval) { BaseConnection.poolCleanupInterval = setInterval(() => { this.cleanupConnectionPool(); }, 300000); } } cleanupConnectionPool() { const now = Date.now(); const maxIdleTime = 600000; for (const [key, connection] of BaseConnection.connectionPool.entries()) { if (now - connection.lastActivity > maxIdleTime) { connection.disconnect(); BaseConnection.connectionPool.delete(key); } } } getConnectionKey() { return `${this.credentials.host}:${this.credentials.port}:${this.credentials.username}`; } 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.authMethod === 'privateKey' ? keyBasedAlgorithms : passwordBasedAlgorithms; return [ primaryAlgorithms, { serverHostKey: ['ssh-rsa'], cipher: ['aes128-cbc'], hmac: ['hmac-sha1'], kex: ['diffie-hellman-group1-sha1'] } ]; } } static forceCleanupConnectionPool() { for (const [key, connection] of BaseConnection.connectionPool.entries()) { connection.disconnect(); BaseConnection.connectionPool.delete(key); } if (BaseConnection.poolCleanupInterval) { clearInterval(BaseConnection.poolCleanupInterval); BaseConnection.poolCleanupInterval = null; } } static getConnectionPoolStatus() { return { totalConnections: BaseConnection.connectionPool.size, connections: Array.from(BaseConnection.connectionPool.keys()) }; } } exports.BaseConnection = BaseConnection; BaseConnection.connectionPool = new Map(); BaseConnection.poolCleanupInterval = null; //# sourceMappingURL=base-connection.js.map