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

170 lines
4.6 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const NGINX_DIR = process.env.NGINX_CONF_D || '/etc/nginx/conf.d';
/**
* Validates filename to prevent path traversal.
* @param {string} filename
*/
const validateFilename = (filename) => {
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
throw new Error('Invalid filename');
}
if (filename.includes('..') || filename.includes('/')) {
throw new Error('Invalid filename');
}
};
/**
* Validates filesystem path characters.
* @param {string} pathStr
*/
const validatePath = (pathStr) => {
if (!/^[a-zA-Z0-9/_.-]+$/.test(pathStr)) {
throw new Error('Invalid path characters');
}
};
/**
* Lists all virtual hosts in the Nginx config directory.
* Looks for .conf and .conf.disabled files.
* @returns {Array<object>} [{ filename, serverName, root, enabled }]
*/
const listVHosts = () => {
try {
if (!fs.existsSync(NGINX_DIR)) {
console.warn(`Nginx config directory not found: ${NGINX_DIR}`);
return [];
}
const files = fs.readdirSync(NGINX_DIR);
const vhosts = [];
files.forEach(file => {
const isEnabled = file.endsWith('.conf');
const isDisabled = file.endsWith('.conf.disabled');
if (isEnabled || isDisabled) {
const fullPath = path.join(NGINX_DIR, file);
const content = fs.readFileSync(fullPath, 'utf8');
// Extract server_name and root using regex
const serverNameMatch = content.match(/server_name\s+([^;]+);/);
const rootMatch = content.match(/root\s+([^;]+);/);
vhosts.push({
filename: file,
serverName: serverNameMatch ? serverNameMatch[1].trim() : 'unknown',
root: rootMatch ? rootMatch[1].trim() : 'unknown',
enabled: isEnabled
});
}
});
return vhosts;
} catch (err) {
console.error('Error listing vhosts:', err.message);
throw err;
}
};
/**
* Renames a vhost file to enable or disable it.
* @param {string} filename
* @param {boolean} enable
*/
const toggleVHost = (filename, enable) => {
validateFilename(filename);
const currentPath = path.join(NGINX_DIR, filename);
let newFilename = filename;
if (enable && filename.endsWith('.conf.disabled')) {
newFilename = filename.replace('.conf.disabled', '.conf');
} else if (!enable && filename.endsWith('.conf')) {
newFilename = filename + '.disabled';
} else {
// Already in desired state or unexpected filename
return filename;
}
const newPath = path.join(NGINX_DIR, newFilename);
// Using sudo mv via execFileSync might be safer if permissions are tight,
// but the app should have permission if setup correctly.
// We'll try fs.renameSync first.
fs.renameSync(currentPath, newPath);
return newFilename;
};
/**
* Creates a new virtual host configuration.
* @param {string} domain
* @param {string} documentRoot
* @param {number} port
* @param {string} type - 'static' | 'proxy'
*/
const createVHost = (domain, documentRoot, port, type) => {
validateFilename(domain);
validatePath(documentRoot);
const filename = `${domain}.conf.disabled`;
const fullPath = path.join(NGINX_DIR, filename);
let config = '';
if (type === 'static') {
config = `server {
listen ${port};
server_name ${domain};
root ${documentRoot};
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
`;
} else if (type === 'proxy') {
config = `server {
listen 80;
server_name ${domain};
location / {
proxy_pass http://127.0.0.1:${port};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
`;
}
fs.writeFileSync(fullPath, config);
return filename;
};
/**
* Reloads Nginx using rc-service.
*/
const reloadNginx = () => {
try {
// First test config
execFileSync('sudo', ['nginx', '-t']);
// Then reload
execFileSync('sudo', ['rc-service', 'nginx', 'reload']);
return true;
} catch (err) {
console.error('Nginx reload failed:', err.message);
throw new Error(`Nginx reload failed: ${err.stderr || err.message}`);
}
};
module.exports = {
listVHosts,
toggleVHost,
createVHost,
reloadNginx
};