sawa-control-panel/backend/services/rcService.js

90 lines
2.8 KiB
JavaScript

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
};