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