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