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