UNPKG

@genxis/whmrockstar

Version:

🎸 GenXis WHMRockStar - AI-powered multi-server WHM/cPanel management via Model Context Protocol (MCP). Enhanced with proper MCP stdio protocol support and multi-server management.

375 lines (324 loc) • 11 kB
const axios = require('axios'); const https = require('https'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { Client } = require('ssh2'); const logger = require('./logger'); class SSHManager { constructor(config) { this.config = config; this.baseURL = `https://${config.serverIp}:${config.port || 2087}`; this.username = config.username || 'root'; this.apiToken = config.apiToken; this.sshClient = null; // SSH key paths const homeDir = process.env.HOME || process.env.USERPROFILE; this.sshDir = path.join(homeDir, '.genxis-whmrockstar', 'ssh'); this.privateKeyPath = path.join(this.sshDir, `${config.serverId || 'server'}_rsa`); this.publicKeyPath = path.join(this.sshDir, `${config.serverId || 'server'}_rsa.pub`); // Configure HTTPS agent for WHM API const httpsAgentOptions = { rejectUnauthorized: config.verifyTLS !== false }; this.api = axios.create({ baseURL: this.baseURL, headers: { Authorization: `whm ${this.username}:${this.apiToken}` }, httpsAgent: new https.Agent(httpsAgentOptions), timeout: 30000 }); } // Generate SSH key pair async generateSSHKeys() { try { // Create SSH directory if it doesn't exist if (!fs.existsSync(this.sshDir)) { fs.mkdirSync(this.sshDir, { recursive: true }); } // Check if keys already exist if (fs.existsSync(this.privateKeyPath) && fs.existsSync(this.publicKeyPath)) { logger.info('SSH keys already exist for this server'); return { privateKey: fs.readFileSync(this.privateKeyPath, 'utf8'), publicKey: fs.readFileSync(this.publicKeyPath, 'utf8') }; } // Generate new key pair using Node.js crypto const { generateKeyPairSync } = require('crypto'); const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); // Convert to OpenSSH format const sshPublicKey = this.convertToOpenSSHFormat(publicKey); // Save keys to files fs.writeFileSync(this.privateKeyPath, privateKey, { mode: 0o600 }); fs.writeFileSync(this.publicKeyPath, sshPublicKey, { mode: 0o644 }); logger.info('Generated new SSH key pair'); return { privateKey, publicKey: sshPublicKey }; } catch (error) { logger.error(`Failed to generate SSH keys: ${error.message}`); throw error; } } // Convert PEM public key to OpenSSH format convertToOpenSSHFormat(pemKey) { // Simple conversion - in production you'd use a proper library const keyData = pemKey .replace(/-----BEGIN PUBLIC KEY-----/, '') .replace(/-----END PUBLIC KEY-----/, '') .replace(/\n/g, ''); return `ssh-rsa ${keyData} genxis-whmrockstar@${this.config.serverIp}`; } // Install SSH key on server via WHM API async installSSHKey() { try { const { publicKey } = await this.generateSSHKeys(); // Use WHM API to add SSH key to root's authorized_keys const params = new URLSearchParams({ 'api.version': '1', 'user': 'root', 'key': publicKey, 'name': `genxis-whmrockstar-${Date.now()}` }); const response = await this.api.post(`/json-api/addsshkey?api.version=1`, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (response.data?.metadata?.result === 1) { logger.info('SSH key installed successfully on server'); return true; } else { throw new Error(response.data?.metadata?.reason || 'Failed to install SSH key'); } } catch (error) { logger.error(`Failed to install SSH key: ${error.message}`); throw error; } } // Connect to server via SSH async connect() { return new Promise((resolve, reject) => { if (this.sshClient && this.sshClient._sock && !this.sshClient._sock.destroyed) { resolve(this.sshClient); return; } // Ensure SSH key exists if (!fs.existsSync(this.privateKeyPath)) { reject(new Error('SSH key not found. Run installSSHKey first.')); return; } const privateKey = fs.readFileSync(this.privateKeyPath, 'utf8'); this.sshClient = new Client(); this.sshClient.on('ready', () => { logger.info('SSH connection established'); resolve(this.sshClient); }); this.sshClient.on('error', (err) => { logger.error(`SSH connection error: ${err.message}`); reject(err); }); this.sshClient.connect({ host: this.config.serverIp, port: this.config.sshPort || 22, username: 'root', privateKey: privateKey, algorithms: { serverHostKey: ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521'] } }); }); } // Execute SSH command async executeCommand(command) { try { const client = await this.connect(); return new Promise((resolve, reject) => { client.exec(command, (err, stream) => { if (err) { reject(err); return; } let output = ''; let errorOutput = ''; stream.on('close', (code, signal) => { if (code !== 0 && errorOutput) { reject(new Error(errorOutput)); } else { resolve({ output, error: errorOutput, code }); } }); stream.on('data', (data) => { output += data.toString(); }); stream.stderr.on('data', (data) => { errorOutput += data.toString(); }); }); }); } catch (error) { logger.error(`SSH command execution failed: ${error.message}`); throw error; } } // Read file via SSH async readFile(filePath) { const result = await this.executeCommand(`cat "${filePath}"`); return result.output; } // Write file via SSH async writeFile(filePath, content) { // Escape content for shell const escapedContent = content.replace(/'/g, "'\\''"); const command = `echo '${escapedContent}' > "${filePath}"`; await this.executeCommand(command); return true; } // List directory via SSH async listDirectory(dirPath) { const result = await this.executeCommand(`ls -la "${dirPath}"`); return this.parseListOutput(result.output); } // Parse ls output parseListOutput(output) { const lines = output.trim().split('\n'); const files = []; // Skip the first line (total) for (let i = 1; i < lines.length; i++) { const parts = lines[i].split(/\s+/); if (parts.length >= 9) { files.push({ permissions: parts[0], links: parts[1], owner: parts[2], group: parts[3], size: parts[4], date: `${parts[5]} ${parts[6]} ${parts[7]}`, name: parts.slice(8).join(' '), type: parts[0].startsWith('d') ? 'directory' : 'file' }); } } return files; } // Delete file via SSH async deleteFile(filePath) { await this.executeCommand(`rm -f "${filePath}"`); return true; } // Create directory via SSH async createDirectory(dirPath) { await this.executeCommand(`mkdir -p "${dirPath}"`); return true; } // Check Apache config async checkApacheConfig() { const result = await this.executeCommand('apachectl configtest 2>&1'); return { valid: result.output.includes('Syntax OK'), output: result.output + result.error }; } // Restart service async restartService(serviceName) { const result = await this.executeCommand(`systemctl restart ${serviceName}`); return { success: result.code === 0, output: result.output }; } // Tail log file async tailLog(logPath, lines = 50) { const result = await this.executeCommand(`tail -n ${lines} "${logPath}"`); return result.output; } // Find configuration issues async findSystemConfigIssues() { const issues = []; try { // Check Apache config const apacheCheck = await this.checkApacheConfig(); if (!apacheCheck.valid) { issues.push({ service: 'Apache', issue: 'Configuration syntax error', details: apacheCheck.output, severity: 'error' }); } // Check disk space const diskResult = await this.executeCommand('df -h | grep -E "^/dev/"'); const diskLines = diskResult.output.split('\n'); for (const line of diskLines) { const parts = line.split(/\s+/); if (parts.length >= 5) { const usage = parseInt(parts[4].replace('%', '')); if (usage > 90) { issues.push({ service: 'Disk', issue: `High disk usage on ${parts[5]}`, details: `${usage}% used`, severity: usage > 95 ? 'error' : 'warning' }); } } } // Check memory const memResult = await this.executeCommand('free -m | grep "^Mem:"'); const memParts = memResult.output.split(/\s+/); if (memParts.length >= 3) { const total = parseInt(memParts[1]); const used = parseInt(memParts[2]); const usagePercent = (used / total) * 100; if (usagePercent > 90) { issues.push({ service: 'Memory', issue: 'High memory usage', details: `${usagePercent.toFixed(1)}% used`, severity: usagePercent > 95 ? 'error' : 'warning' }); } } // Check for recent errors in Apache error log const errorLog = await this.tailLog('/var/log/apache2/error.log', 100); const errorLines = errorLog.split('\n'); const recentErrors = errorLines.filter(line => line.includes('[error]') || line.includes('[crit]') || line.includes('[alert]') ); if (recentErrors.length > 0) { issues.push({ service: 'Apache', issue: 'Recent errors in error log', details: `${recentErrors.length} errors found`, severity: 'warning', samples: recentErrors.slice(0, 3) }); } } catch (error) { logger.error(`Error checking system config: ${error.message}`); } return issues; } // Disconnect SSH disconnect() { if (this.sshClient) { this.sshClient.end(); this.sshClient = null; logger.info('SSH connection closed'); } } } module.exports = SSHManager;