n8n-nodes-netdevices
Version:
n8n node to interact with network devices
815 lines • 33 kB
JavaScript
"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