const { execSync, execFileSync } = require('child_process'); // Parse ALLOWED_SERVICES from the environment, defaulting to the fallback list in docs const ALLOWED_SERVICES = process.env.ALLOWED_SERVICES ? process.env.ALLOWED_SERVICES.split(',').map(s => s.trim()) : ['nginx', 'postgresql', 'mariadb', 'redis', 'memcached', 'sshd', 'nftables']; /** * Executes an rc-service command for the specified service * @param {string} serviceName Name of the service * @param {string} action action to perform: 'start', 'stop', 'restart', 'status' * @returns {object} { stdout, stderr, code } */ const runRcService = (serviceName, action) => { // Final safeguard inside the child_process wrapper, even though the router uses middleware. if (!ALLOWED_SERVICES.includes(serviceName)) { throw new Error('Service not in whitelist'); } // Define valid actions to prevent command injection const VALID_ACTIONS = ['start', 'stop', 'restart', 'status']; if (!VALID_ACTIONS.includes(action)) { throw new Error('Invalid service action'); } try { // execFileSync bypasses the shell entirely and is safer. const output = execFileSync('sudo', ['rc-service', serviceName, action], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); return { success: true, service: serviceName, action: action, output: output.trim() }; } catch (err) { return { success: false, service: serviceName, action: action, error: err.message, code: err.status || 1, stderr: err.stderr ? err.stderr.toString().trim() : '', stdout: err.stdout ? err.stdout.toString().trim() : '' }; } }; /** * Retrieves the status of all allowed services. * In Alpine's rc-service, status exits with 0 if running, and non-zero if stopped/failed. */ const getServicesStatus = () => { const statuses = {}; for (const service of ALLOWED_SERVICES) { try { // execFileSync returns 0 (completes) if service is running, throws if not execFileSync('sudo', ['rc-service', service, 'status'], { encoding: 'utf-8', stdio: ['ignore', 'ignore', 'ignore'] }); statuses[service] = 'started'; } catch (e) { // Exited with non-zero statuses[service] = 'stopped'; } } return { success: true, services: statuses }; }; /** * Execute start, stop, or restart action for a specific service. */ const executeServiceAction = (serviceName, action) => { return runRcService(serviceName, action); }; module.exports = { ALLOWED_SERVICES, getServicesStatus, executeServiceAction };