UNPKG

ssh-bridge-ai

Version:

One Command Magic SSH with Invisible Analytics - Connect to any server instantly with 'sshbridge user@server'. Zero setup, zero friction, pure magic. Industry-standard security with behind-the-scenes business intelligence.

581 lines (494 loc) 17.4 kB
const logger = require('./utils/logger'); /** * Comprehensive SSH Error Handling System * Implements all the error handlers and fallback strategies from the ssh_time_fix.md document */ class SSHErrorHandlers { constructor(sshClient) { this.sshClient = sshClient; this.errorCount = 0; this.maxErrors = 10; this.errorHistory = []; } /** * Main error handler that routes errors to appropriate handlers */ handleSSHError(error, connection) { const errorMessage = error.message; this.errorCount++; // Log error for debugging logger.error('SSH error detected', { error: errorMessage, errorCount: this.errorCount, host: this.sshClient.host, username: this.sshClient.username }); // Add to error history this.errorHistory.push({ timestamp: Date.now(), error: errorMessage, level: this.determineErrorLevel(errorMessage) }); // Check if we've exceeded max errors if (this.errorCount > this.maxErrors) { throw new Error(`Too many SSH errors (${this.errorCount}). Connection may be unstable.`); } // Route to specific error handler for (const [pattern, handler] of Object.entries(this.getErrorHandlers())) { if (errorMessage.includes(pattern)) { logger.info(`[${handler.action}] ${handler.message}`); return this.executeErrorHandler(handler, connection); } } // Default error handling return this.handleGenericError(error, connection); } /** * Get all error handlers with their patterns and actions */ getErrorHandlers() { return { // Home directory errors 'Could not chdir to home directory': { action: 'bypass_home_directory', fallback: 'use_tmp_directory', message: 'Home directory issue detected, using temporary directory', severity: 'high', autoRecover: true }, 'No such file or directory': { action: 'check_file_paths', fallback: 'use_alternative_paths', message: 'File path issue detected, trying alternative paths', severity: 'medium', autoRecover: true }, // Account not available errors 'This account is currently not available': { action: 'check_account_status', fallback: 'use_alternative_shell', message: 'Account validation failed, trying alternative shell', severity: 'high', autoRecover: true }, 'Account is disabled': { action: 'check_account_status', fallback: 'use_alternative_shell', message: 'Account disabled, trying alternative shell', severity: 'high', autoRecover: true }, // Permission denied errors 'Permission denied': { action: 'check_permissions', fallback: 'request_elevation', message: 'Permission issue detected, requesting elevation', severity: 'medium', autoRecover: false }, 'Access denied': { action: 'check_permissions', fallback: 'request_elevation', message: 'Access denied, requesting elevation', severity: 'medium', autoRecover: false }, // Connection timeout errors 'Connection timed out': { action: 'increase_timeout', fallback: 'use_keepalive', message: 'Connection timeout, enabling keepalive', severity: 'medium', autoRecover: true }, 'Connection refused': { action: 'check_network', fallback: 'retry_connection', message: 'Connection refused, retrying connection', severity: 'medium', autoRecover: true }, // Shell errors 'No such file or directory': { action: 'check_shell_path', fallback: 'use_alternative_shell', message: 'Shell not found, trying alternative shell', severity: 'high', autoRecover: true }, 'Command not found': { action: 'check_command_path', fallback: 'use_alternative_command', message: 'Command not found, trying alternative command', severity: 'low', autoRecover: true }, // Authentication errors 'Authentication failed': { action: 'check_credentials', fallback: 'retry_authentication', message: 'Authentication failed, retrying with different method', severity: 'high', autoRecover: true }, 'Host key verification failed': { action: 'check_host_key', fallback: 'skip_host_verification', message: 'Host key verification failed, skipping verification', severity: 'medium', autoRecover: true }, // Environment errors 'No such environment variable': { action: 'check_environment', fallback: 'set_default_environment', message: 'Environment variable missing, setting defaults', severity: 'low', autoRecover: true }, 'Invalid environment': { action: 'check_environment', fallback: 'reset_environment', message: 'Invalid environment detected, resetting to defaults', severity: 'medium', autoRecover: true } }; } /** * Execute the appropriate error handler */ async executeErrorHandler(handler, connection) { try { switch (handler.action) { case 'bypass_home_directory': return await this.bypassHomeDirectory(); case 'check_account_status': return await this.checkAccountStatus(); case 'check_permissions': return await this.checkPermissions(); case 'increase_timeout': return await this.increaseTimeout(); case 'check_shell_path': return await this.checkShellPath(); case 'check_credentials': return await this.checkCredentials(); case 'check_host_key': return await this.checkHostKey(); case 'check_environment': return await this.checkEnvironment(); default: return await this.handleGenericError(new Error(handler.message), connection); } } catch (error) { logger.error('Error handler execution failed', { action: handler.action, error: error.message }); // Try fallback if available if (handler.fallback) { return await this.executeFallback(handler.fallback); } throw error; } } /** * Bypass home directory issues by using temporary directory */ async bypassHomeDirectory() { logger.info('Attempting to bypass home directory issue'); // Override environment variables this.sshClient.options.environmentOverrides.HOME = '/tmp'; this.sshClient.options.environmentOverrides.PWD = '/tmp'; this.sshClient.options.environmentOverrides.USER = this.sshClient.username; // Try to reconnect with new environment try { await this.sshClient.connect(); logger.info('Successfully bypassed home directory issue'); return true; } catch (error) { logger.error('Failed to bypass home directory issue', { error: error.message }); throw error; } } /** * Check account status and try alternative shells */ async checkAccountStatus() { logger.info('Checking account status and trying alternative shells'); // Try different shell strategies const shellStrategies = [ { command: 'bash', args: ['--noprofile', '--norc'] }, { command: 'sh', args: [] }, { command: 'dash', args: [] }, { command: 'zsh', args: ['--no-rcs'] }, { command: 'ksh', args: [] } ]; for (const strategy of shellStrategies) { try { logger.info(`Trying shell strategy: ${strategy.command} ${strategy.args.join(' ')}`); await this.sshClient.connectWithFallbackShell(strategy); logger.info(`Successfully connected with shell: ${strategy.command}`); return true; } catch (error) { logger.debug(`Shell strategy ${strategy.command} failed: ${error.message}`); continue; } } throw new Error('All shell strategies failed'); } /** * Check permissions and request elevation if needed */ async checkPermissions() { logger.info('Checking permissions and requesting elevation if needed'); try { // Try to check current permissions const result = await this.sshClient.exec('id'); logger.info('Current user permissions:', result.stdout); // Check if we can access common directories const dirs = ['/tmp', '/var/tmp', '/home', '/usr/local']; for (const dir of dirs) { try { await this.sshClient.exec(`ls -la ${dir}`); logger.info(`Successfully accessed directory: ${dir}`); return true; } catch (error) { logger.debug(`Cannot access directory ${dir}: ${error.message}`); } } // If we can't access anything, try to create a working directory try { await this.sshClient.exec('mkdir -p /tmp/sshbridge_work'); await this.sshClient.exec('cd /tmp/sshbridge_work'); logger.info('Created working directory in /tmp/sshbridge_work'); return true; } catch (error) { logger.error('Failed to create working directory', { error: error.message }); throw error; } } catch (error) { logger.error('Permission check failed', { error: error.message }); throw error; } } /** * Increase timeout and enable keepalive */ async increaseTimeout() { logger.info('Increasing timeout and enabling keepalive'); // Increase timeout values this.sshClient.options.readyTimeout = 120000; // 2 minutes this.sshClient.options.keepaliveInterval = 15; this.sshClient.options.serverAliveInterval = 15; this.sshClient.options.clientAliveInterval = 30; // Try to reconnect with new settings try { await this.sshClient.connect(); logger.info('Successfully reconnected with increased timeout'); return true; } catch (error) { logger.error('Failed to reconnect with increased timeout', { error: error.message }); throw error; } } /** * Check shell path and try alternatives */ async checkShellPath() { logger.info('Checking shell path and trying alternatives'); // Check available shells const shells = ['/bin/bash', '/bin/sh', '/bin/dash', '/bin/zsh', '/bin/ksh']; for (const shell of shells) { try { await this.sshClient.exec(`test -x ${shell}`); logger.info(`Found executable shell: ${shell}`); // Try to use this shell await this.sshClient.connectWithFallbackShell({ command: shell, args: [] }); logger.info(`Successfully connected with shell: ${shell}`); return true; } catch (error) { logger.debug(`Shell ${shell} not available: ${error.message}`); continue; } } throw new Error('No usable shell found'); } /** * Check credentials and retry authentication */ async checkCredentials() { logger.info('Checking credentials and retrying authentication'); // This would typically involve prompting for new credentials // For now, we'll just log the issue logger.warn('Authentication failed - credentials may be invalid'); // Try to reconnect with current credentials (in case it was a temporary issue) try { await this.sshClient.connect(); logger.info('Successfully reconnected with current credentials'); return true; } catch (error) { logger.error('Failed to reconnect with current credentials', { error: error.message }); throw error; } } /** * Check host key and skip verification if needed */ async checkHostKey() { logger.info('Checking host key and skipping verification if needed'); // For now, we'll just log the issue // In a real implementation, you might want to add the host key to known_hosts logger.warn('Host key verification failed - this may be a security risk'); // Try to reconnect (the SSH client might handle this automatically) try { await this.sshClient.connect(); logger.info('Successfully reconnected after host key issue'); return true; } catch (error) { logger.error('Failed to reconnect after host key issue', { error: error.message }); throw error; } } /** * Check environment and set defaults */ async checkEnvironment() { logger.info('Checking environment and setting defaults'); // Set default environment variables this.sshClient.options.environmentOverrides = { HOME: '/tmp', SHELL: '/bin/bash', TERM: 'xterm-256color', PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', USER: this.sshClient.username, LOGNAME: this.sshClient.username, PWD: '/tmp', LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8' }; // Try to reconnect with new environment try { await this.sshClient.connect(); logger.info('Successfully reconnected with default environment'); return true; } catch (error) { logger.error('Failed to reconnect with default environment', { error: error.message }); throw error; } } /** * Execute fallback strategy */ async executeFallback(fallbackType) { logger.info(`Executing fallback strategy: ${fallbackType}`); switch (fallbackType) { case 'use_tmp_directory': return await this.bypassHomeDirectory(); case 'use_alternative_shell': return await this.checkAccountStatus(); case 'request_elevation': return await this.checkPermissions(); case 'use_keepalive': return await this.increaseTimeout(); case 'use_alternative_command': return await this.checkShellPath(); case 'retry_authentication': return await this.checkCredentials(); case 'skip_host_verification': return await this.checkHostKey(); case 'set_default_environment': return await this.checkEnvironment(); case 'reset_environment': return await this.checkEnvironment(); default: logger.warn(`Unknown fallback type: ${fallbackType}`); return false; } } /** * Handle generic errors that don't match specific patterns */ async handleGenericError(error, connection) { logger.warn('Handling generic SSH error', { error: error.message }); // Try basic recovery strategies try { // Check if connection is still alive if (this.sshClient.isConnected) { logger.info('Connection appears to be alive, attempting to continue'); return true; } // Try to reconnect logger.info('Attempting to reconnect after generic error'); await this.sshClient.connect(); return true; } catch (reconnectError) { logger.error('Generic error recovery failed', { error: reconnectError.message }); throw error; // Throw original error } } /** * Determine error severity level */ determineErrorLevel(errorMessage) { const highSeverityPatterns = [ 'Could not chdir to home directory', 'This account is currently not available', 'Account is disabled', 'Authentication failed', 'No such file or directory' ]; const mediumSeverityPatterns = [ 'Permission denied', 'Access denied', 'Connection timed out', 'Connection refused', 'Host key verification failed' ]; if (highSeverityPatterns.some(pattern => errorMessage.includes(pattern))) { return 'high'; } else if (mediumSeverityPatterns.some(pattern => errorMessage.includes(pattern))) { return 'medium'; } else { return 'low'; } } /** * Get error statistics */ getErrorStats() { const totalErrors = this.errorHistory.length; const highSeverity = this.errorHistory.filter(e => e.level === 'high').length; const mediumSeverity = this.errorHistory.filter(e => e.level === 'medium').length; const lowSeverity = this.errorHistory.filter(e => e.level === 'low').length; return { totalErrors, highSeverity, mediumSeverity, lowSeverity, errorCount: this.errorCount, maxErrors: this.maxErrors }; } /** * Reset error counters */ resetErrorCounters() { this.errorCount = 0; this.errorHistory = []; logger.info('Error counters reset'); } /** * Check if connection is stable based on error history */ isConnectionStable() { const recentErrors = this.errorHistory.filter( e => Date.now() - e.timestamp < 60000 // Last minute ); return recentErrors.length < 3; // Less than 3 errors in the last minute } } module.exports = { SSHErrorHandlers };