170 lines
4.6 KiB
JavaScript
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
|
|
};
|
|
|