UNPKG

ssh-bridge-ai

Version:

AI-Powered SSH Tool with Bulletproof Connections & Enterprise Sandbox Security + Cursor-like Confirmation - Enable AI assistants to securely SSH into your servers with persistent sessions, keepalive, automatic recovery, sandbox command testing, and user c

279 lines (237 loc) 7.88 kB
const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const yaml = require('js-yaml'); const toml = require('@iarna/toml'); const { ValidationUtils } = require('./utils/validation'); const { ErrorHandler, FileSystemError, SecurityError } = require('./utils/errors'); const logger = require('./utils/logger'); const { FILESYSTEM } = require('./utils/constants'); class SSHCredentialsManager { constructor() { this.supportedFormats = ['json', 'yaml', 'yml', 'toml', 'config']; } /** * Parse SSH config format (like ~/.ssh/config) */ parseSSHConfig(content) { const hosts = {}; const lines = content.split('\n'); let currentHost = null; for (const line of lines) { const trimmed = line.trim(); // Skip comments and empty lines if (!trimmed || trimmed.startsWith('#')) continue; // Host directive if (trimmed.toLowerCase().startsWith('host ')) { currentHost = trimmed.split(/\s+/).slice(1).join(' '); hosts[currentHost] = {}; continue; } // Configuration for current host if (currentHost && trimmed.includes(' ')) { const [key, ...valueParts] = trimmed.split(/\s+/); const value = valueParts.join(' '); switch (key.toLowerCase()) { case 'hostname': hosts[currentHost].hostname = value; break; case 'user': hosts[currentHost].username = value; break; case 'port': hosts[currentHost].port = parseInt(value); break; case 'identityfile': hosts[currentHost].privateKey = value.replace(/^~/, os.homedir()); break; case 'passwordauthentication': hosts[currentHost].passwordAuth = value.toLowerCase() === 'yes'; break; // Store other options as-is default: hosts[currentHost][key.toLowerCase()] = value; } } } return hosts; } /** * Parse credentials file based on format */ async parseCredentialsFile(filePath) { try { // SECURITY: Validate file path before reading if (!ValidationUtils.validateFilePath(filePath)) { throw new SecurityError('Invalid file path detected'); } // SECURITY: Check file size limit const stats = await fs.stat(filePath); if (stats.size > FILESYSTEM.MAX_FILE_SIZE) { throw new FileSystemError('File is too large to process'); } const content = await fs.readFile(filePath, 'utf8'); const ext = path.extname(filePath).toLowerCase().slice(1); switch (ext) { case 'json': return JSON.parse(content); case 'yaml': case 'yml': return yaml.load(content); case 'toml': return toml.parse(content); case 'config': case '': // No extension, assume SSH config format return this.parseSSHConfig(content); default: throw new Error(`Unsupported file format: ${ext}`); } } catch (error) { logger.error(`Failed to parse credentials file ${filePath}`, { error: error.message }); throw new FileSystemError(`Failed to parse credentials file: ${error.message}`); } } /** * Normalize credentials to standard format */ normalizeCredentials(credentials) { const normalized = {}; for (const [hostKey, config] of Object.entries(credentials)) { // Handle different formats let normalizedConfig = {}; if (typeof config === 'string') { // Simple format: "host": "user@hostname:port" const parsed = this.parseConnectionString(config); normalizedConfig = parsed; } else if (typeof config === 'object') { // Detailed format normalizedConfig = { hostname: config.hostname || config.host || hostKey, username: config.username || config.user, port: config.port || 22, privateKey: config.privateKey || config.identityFile || config.key, password: config.password, passwordAuth: config.passwordAuth || config.passwordAuthentication, ...config // Include other SSH options }; } // Expand ~ in key paths if (normalizedConfig.privateKey && normalizedConfig.privateKey.startsWith('~')) { normalizedConfig.privateKey = normalizedConfig.privateKey.replace(/^~/, os.homedir()); } normalized[hostKey] = normalizedConfig; } return normalized; } /** * Parse connection string format "user@host:port" */ parseConnectionString(connectionString) { const parts = connectionString.split('@'); if (parts.length !== 2) { throw new Error(`Invalid connection string format: ${connectionString}`); } const username = parts[0]; const hostPort = parts[1].split(':'); const hostname = hostPort[0]; const port = hostPort[1] ? parseInt(hostPort[1]) : 22; return { username, hostname, port }; } /** * Find credentials for a specific connection */ findCredentials(credentials, connection) { const parsed = this.parseConnectionString(connection); // Try exact match first for (const [hostKey, config] of Object.entries(credentials)) { if (hostKey === connection) { return config; } } // Try matching by hostname and username for (const [hostKey, config] of Object.entries(credentials)) { if (config.hostname === parsed.hostname && config.username === parsed.username) { return config; } } // Try matching by hostname only for (const [hostKey, config] of Object.entries(credentials)) { if (config.hostname === parsed.hostname) { return config; } } return null; } /** * Load credentials from file */ async loadCredentials(filePath) { try { const rawCredentials = await this.parseCredentialsFile(filePath); return this.normalizeCredentials(rawCredentials); } catch (error) { throw new Error(`Failed to load credentials: ${error.message}`); } } /** * Create example configuration files */ generateExampleConfigs() { const examples = { 'sshbridge.json': JSON.stringify({ "production-server": { "hostname": "prod.example.com", "username": "deploy", "port": 22, "privateKey": "~/.ssh/id_rsa" }, "staging-server": { "hostname": "staging.example.com", "username": "developer", "password": "your-password", "passwordAuth": true }, "simple-format": "user@hostname:2222" }, null, 2), 'sshbridge.yaml': `# SSH Bridge Configuration production-server: hostname: prod.example.com username: deploy port: 22 privateKey: ~/.ssh/id_rsa staging-server: hostname: staging.example.com username: developer password: your-password passwordAuth: true # Simple format also supported simple-format: user@hostname:2222 `, 'sshbridge.toml': `# SSH Bridge Configuration [production-server] hostname = "prod.example.com" username = "deploy" port = 22 privateKey = "~/.ssh/id_rsa" [staging-server] hostname = "staging.example.com" username = "developer" password = "your-password" passwordAuth = true `, 'sshbridge.config': `# SSH Config format (like ~/.ssh/config) Host production-server HostName prod.example.com User deploy Port 22 IdentityFile ~/.ssh/id_rsa Host staging-server HostName staging.example.com User developer PasswordAuthentication yes ` }; return examples; } } module.exports = { SSHCredentialsManager };