ip2nginx/updater.php

175 lines
5.5 KiB
PHP

<?php
// Define a constant to indicate index.php is included, not accessed directly
define('FILE_IS_INCLUDED', true);
// Include the index.php file, which contains shared configuration and logic
include('index.php');
// Only allow this script to run from the command line interface
if (php_sapi_name() !== 'cli') {
error_deny(); // Deny access if run from a browser or non-CLI environment
}
/**
* Main logic handler
*/
main();
/**
* Main routine that processes domain updates based on `meta.json`.
*
* - Loads domain configuration data.
* - Applies updates for domains where `"changed": 1`.
* - Resets the change flag after successful update.
* - Reloads NGINX only if at least one domain was updated.
* - Saves the updated metadata back to disk.
*
* Exits with error code 1 on failure to save updated data.
*/
function main(): void {
// Load content of meta.json
$domains = loadJson(METAFILE);
// Ensure meta is valid and contains domain entries
if (isEmpty($domains)) {
echo "No valid domains in meta.json\n";
exit(1);
}
// Loop through all domains and apply updates if 'changed' flag is set
$changed = false;
foreach ($domains as $name => $meta) {
if (!isset($meta['changed']) || !$meta['changed']) {
continue;
}
// Process and detect if actual change occurred
$changed |= processDomain($name, $meta);
// Reset 'changed' flag after processing
$domains[$name]['changed'] = 0;
}
// Restart nginx if any updates were made and save new state
if ($changed) {
shell_exec("sudo nginx -t && sudo systemctl reload nginx");
echo "Reloaded nginx configuration.\n";
// Save updated meta.json back to file
if (!saveJson(METAFILE, $domains)) {
exit(1);
}
}
echo "All applicable domains processed.\n";
}
/**
* Modify the proxy_pass value inside a specific location block
*/
function updateProxyPassInLocation(string $nginxConf, string $locationPath, string $newProxyPass): string {
$lines = explode("\n", $nginxConf); // Split config into lines
$inBlock = false; // Flag: inside target location block
$locationStart = false;
$braceLevel = 0; // Tracks nested braces
foreach ($lines as $i => $line) {
$escapedLocation = preg_quote($locationPath, '/'); // Escape location for regex
if (preg_match("/^\s*location\s+$escapedLocation\s*\{\s*$/", $line)) {
$inBlock = true;
$locationStart = true;
$braceLevel = 1;
continue;
}
if ($inBlock) {
if (strpos($line, '{') !== false) $braceLevel++;
if (strpos($line, '}') !== false) $braceLevel--;
// Replace existing proxy_pass directive
if (preg_match("/proxy_pass\\s+.+;/", $line)) {
$lines[$i] = preg_replace("/proxy_pass\\s+.+;/", "proxy_pass $newProxyPass;", $line);
}
// Exit block if all braces are closed
if ($braceLevel === 0) {
$inBlock = false;
}
}
}
return implode("\n", $lines); // Return modified config
}
/**
* Processes a single domain configuration from meta.json.
*
* - Verifies all required fields are present and non-empty.
* - Constructs the expected proxy_pass value from protocol, domain, and port.
* - Replaces only the proxy_pass directive within the specified location block.
* - If the config changes, it is saved and NGINX will be marked for reload.
* - Even if no actual config differences are found, a restart can still be triggered
* by manually setting `"changed": 1` in meta.json.
*
* @param string $name The key in meta.json (usually target vhost name)
* @param array $meta The domain's metadata including IP, protocol, port, location, etc.
* @return bool True if a reload should be performed, false otherwise
*/
function processDomain(string $name, array $meta): bool {
$requiredFields = ['domain', 'ip', 'protocol', 'port', 'location']; // Required keys
$missing = [];
$result = false;
// Collect any missing or empty required fields
foreach ($requiredFields as $field) {
if (isEmpty($meta[$field])) {
$missing[] = $field;
}
}
// Skip this domain if required fields are missing
if (!isEmpty($missing)) {
echo "Skipping '$name': missing or empty field(s): " . implode(', ', $missing) . "\n";
return false;
}
// Allow forced reloads even without content change by setting "changed": 1 in meta.json
$result = boolval($meta['changed'] ?? 1);
if (!$result) {
echo "No changes for $name\n";
return false;
}
// Full path to nginx config file for this domain
$systemConfPath = VHOST_BASE . '/' . $name . '/conf/' . NGINX_CONF_NAME;
// Check file existence and permissions
if (!file_exists($systemConfPath)) {
echo "System config not found for $name\n";
return false;
}
if (!is_readable($systemConfPath) || !is_writable($systemConfPath)) {
echo "Permission denied for $systemConfPath\n";
return false;
}
// Read current nginx config
$original = file_get_contents($systemConfPath);
// Build new proxy_pass target
$proxyTarget = $meta['protocol'] . '://' . $meta['domain'] . ':' . $meta['port'];
// Apply transformation inside matching location block
$modified = updateProxyPassInLocation($original, $meta['location'], $proxyTarget);
// Write only if content changed
if ($original !== $modified) {
$result = (file_put_contents($systemConfPath, $modified) !== false);
echo "Updated proxy_pass for $name\n";
} else {
echo "No changes for $name\n";
}
return $result;
}