90 lines
2.8 KiB
JavaScript
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
|
|
};
|