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.

360 lines (323 loc) • 10.2 kB
const axios = require('axios'); const https = require('https'); const fs = require('fs'); const path = require('path'); const logger = require('./logger'); class WHMService { constructor(config) { if (!config.server || !config.apiToken) { throw new Error('WHM server and API token are required'); } this.baseURL = `https://${config.server}:${config.port || '2087'}/json-api/`; this.username = config.username || 'root'; this.apiToken = config.apiToken; // TLS settings: verify on by default; allow explicit opt-out or custom CA const verifyTLS = typeof config.verifyTLS === 'boolean' ? config.verifyTLS : true; let httpsAgentOptions = { rejectUnauthorized: verifyTLS }; if (config.caPath && verifyTLS) { try { const caFullPath = path.isAbsolute(config.caPath) ? config.caPath : path.join(process.cwd(), config.caPath); httpsAgentOptions.ca = fs.readFileSync(caFullPath); } catch (e) { logger.warn(`Failed to load custom CA from ${config.caPath}: ${e.message}`); } } // Create axios instance with configuration this.api = axios.create({ baseURL: this.baseURL, headers: { Authorization: `whm ${this.username}:${this.apiToken}` }, httpsAgent: new https.Agent(httpsAgentOptions), timeout: config.timeout || 30000 }); // Add response interceptor for logging this.api.interceptors.response.use( response => response, error => { logger.error(`WHM API Error: ${error.message}`); if (error.response) { logger.error(`Response: ${JSON.stringify(error.response.data)}`); } return Promise.reject(error); } ); // Basic retry with exponential backoff for transient errors this.maxRetries = typeof config.retries === 'number' ? config.retries : 3; this.baseDelayMs = typeof config.retryDelayMs === 'number' ? config.retryDelayMs : 300; } async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } isRetryable(error) { if (!error || error.__noRetry) return false; if (!error.response) return true; // network error/timeouts const status = error.response.status; return status >= 500 || status === 429; // server errors and rate limit } async withRetry(fn) { let attempt = 0; while (true) { try { return await fn(); } catch (err) { attempt++; if (attempt > this.maxRetries || !this.isRetryable(err)) { throw this._handleApiError(err); } const delayMs = this.baseDelayMs * Math.pow(2, attempt - 1); logger.warn(`Retrying WHM request (attempt ${attempt}/${this.maxRetries}) after ${delayMs}ms: ${err.message}`); await this.delay(delayMs); } } } async get(endpoint, params = {}) { return this.withRetry(async () => { const fullParams = { ...params, 'api.version': 1 }; const queryString = new URLSearchParams(fullParams).toString(); const url = `${endpoint}?${queryString}`; const response = await this.api.get(url); return response.data; }); } async post(endpoint, data = {}) { return this.withRetry(async () => { // Convert data to URL-encoded format const params = new URLSearchParams(); params.append('api.version', '1'); for (const [key, value] of Object.entries(data)) { params.append(key, value); } const response = await this.api.post(endpoint, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); return response.data; }); } // Generic call helper for full WHM API access async callApi({ method = 'get', endpoint, params = {} }) { const m = (method || 'get').toLowerCase(); if (!endpoint || typeof endpoint !== 'string') { throw new Error('endpoint is required'); } if (m === 'get') { return this.get(endpoint, params); } if (m === 'post') { return this.post(endpoint, params); } throw new Error(`Unsupported method: ${method}`); } _handleApiError(error) { if (error.response) { const { status, data } = error.response; const message = data?.message || data?.error || error.message || 'WHM API Error'; const enhancedError = new Error(message); enhancedError.status = status; enhancedError.data = data; return enhancedError; } return error; } // Account Management APIs async listAccounts() { return this.get('listaccts'); } async createAccount(accountData) { const data = { username: accountData.username, domain: accountData.domain, password: accountData.password, contactemail: accountData.email, plan: accountData.package || 'default' }; return this.post('createacct', data); } async getAccountSummary(username) { return this.get('accountsummary', { user: username }); } async suspendAccount(username, reason) { return this.post('suspendacct', { user: username, reason }); } async unsuspendAccount(username) { return this.post('unsuspendacct', { user: username }); } async terminateAccount(username) { return this.post('removeacct', { user: username }); } // Server Management APIs async getServerStatus() { try { const loadavg = await this.get('loadavg'); const systemstats = await this.get('systemstats'); return { status: "active", load: loadavg.data || [0, 0, 0], uptime: systemstats.data?.uptime || "Unknown", memory: systemstats.data?.memory || { total: "Unknown", used: "Unknown", free: "Unknown" }, timestamp: new Date().toISOString() }; } catch (error) { // Fallback response if detailed stats unavailable return { status: "active", load: [0, 0, 0], uptime: "Unknown", memory: { total: "Unknown", used: "Unknown", free: "Unknown" }, timestamp: new Date().toISOString(), note: "Detailed stats unavailable" }; } } async getServerLoad() { try { const result = await this.get('loadavg'); return { loadavg: result.data || [0, 0, 0], timestamp: new Date().toISOString() }; } catch (error) { return { loadavg: [0, 0, 0], timestamp: new Date().toISOString(), error: "Load data unavailable" }; } } async getServiceStatus() { try { const result = await this.get('servicestatus'); return { services: result.data?.services || [ { name: "httpd", status: "unknown" }, { name: "mysql", status: "unknown" }, { name: "exim", status: "unknown" } ], timestamp: new Date().toISOString() }; } catch (error) { return { services: [ { name: "httpd", status: "unknown" }, { name: "mysql", status: "unknown" }, { name: "exim", status: "unknown" } ], timestamp: new Date().toISOString(), error: "Service status unavailable" }; } } async restartService(service) { try { const result = await this.post('restartservice', { service }); return { success: true, message: `Service ${service} restart initiated`, timestamp: new Date().toISOString(), details: result }; } catch (error) { return { success: false, message: `Failed to restart ${service}: ${error.message}`, timestamp: new Date().toISOString() }; } } async checkForUpdates() { try { const result = await this.get('getupdates'); return { updates_available: result.data?.updates?.length || 0, updates: result.data?.updates || [], timestamp: new Date().toISOString() }; } catch (error) { return { updates_available: 0, updates: [], timestamp: new Date().toISOString(), error: "Update check unavailable" }; } } async startUpdate() { try { const result = await this.post('upcp'); return { started: true, message: "Update process initiated", timestamp: new Date().toISOString(), details: result }; } catch (error) { return { started: false, message: `Update failed: ${error.message}`, timestamp: new Date().toISOString() }; } } // Domain Management APIs async listDomains(username) { try { const result = await this.get('listsubdomains', { user: username }); return { domains: result.data || [], user: username, timestamp: new Date().toISOString() }; } catch (error) { return { domains: [], user: username, timestamp: new Date().toISOString(), error: "Domain list unavailable" }; } } async addDomain(username, domain) { try { const result = await this.post('addondomain', { user: username, newdomain: domain, subdomain: domain.split('.')[0] }); return { success: true, message: `Domain ${domain} added to account ${username}`, timestamp: new Date().toISOString(), details: result }; } catch (error) { return { success: false, message: `Failed to add domain: ${error.message}`, timestamp: new Date().toISOString() }; } } async deleteDomain(username, domain) { try { const result = await this.post('deladdondomain', { user: username, domain: domain }); return { success: true, message: `Domain ${domain} removed from account ${username}`, timestamp: new Date().toISOString(), details: result }; } catch (error) { return { success: false, message: `Failed to remove domain: ${error.message}`, timestamp: new Date().toISOString() }; } } } module.exports = WHMService;