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
JavaScript
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 };