ip2nginx/updater.php
2025-05-20 16:24:04 +02:00

231 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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) {
// Process and detect if actual change occurred
if (processDomain($name, $meta)) {
// Reset 'changed' flag after processing
$domains[$name]['changed'] = 0;
$changed = true;
}
}
// 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";
}
/**
* Generic processor for a location block in NGINX config lines.
* Applies a callback to each line inside the matched location block.
*
* @param array $lines NGINX config lines
* @param string $path Location path to match (e.g. "/api/", "\.php$")
* @param string|null $modifier Location modifier (e.g. "~", "^~", "=", or null)
* @param callable $callback Callback: function(array &$lines, int $i): void
* @return array Modified lines
*/
function processLocationBlock(array $lines, string $path, ?string $modifier, callable $callback): array {
$inBlock = false;
$braceLevel = 0;
$escapedPath = preg_quote($path, '/');
if ($modifier !== null) {
$pattern = "/^\\s*location\\s+{$escapedPath}\\s+$modifier\\s*\\{/"; // e.g. location /api/ ^~ {
} else {
$pattern = "/^\\s*location\\s+{$escapedPath}\\s*\\{/"; // e.g. location /api/ {
}
foreach ($lines as $i => $line) {
if (!$inBlock && preg_match($pattern, $line)) {
$inBlock = true;
$braceLevel = substr_count($line, '{') - substr_count($line, '}');
$callback($lines, $i, 'start');
continue;
}
if ($inBlock) {
$braceLevel += substr_count($line, '{');
$braceLevel -= substr_count($line, '}');
$callback($lines, $i, 'inside');
if ($braceLevel <= 0) {
$callback($lines, $i, 'end');
$inBlock = false;
}
}
}
return $lines;
}
function removeLocationBlock(array &$lines, string $path, ?string $modifier = null): array {
$result = [];
$skipBlock = false;
$buffer = [];
$lines = processLocationBlock($lines, $path, $modifier, function(array &$lines, int $i, string $phase) use (&$skipBlock, &$buffer) {
if ($phase === 'start') {
$skipBlock = true;
}
if ($skipBlock) {
$lines[$i] = null; // Mark for removal
}
if ($phase === 'end') {
$skipBlock = false;
}
});
// Filter out null lines
foreach ($lines as $line) {
if (!isEmpty($line)) $result[] = $line;
}
return $result;
}
/**
* Updates one or more directives (e.g. proxy_pass) inside a given location block.
*
* @param array $lines Array of nginx configuration lines.
* @param string $locationPath The location path to match (e.g. "/api/").
* @param string|null $modifier Optional modifier (e.g. "~", "^~").
* @param array $replacements List of replacements: [['name' => 'proxy_pass', 'val' => 'http://...'], ...]
* @return array Modified nginx config lines.
*/
function updateLocationBlock(array $lines, string $locationPath, ?string $modifier, array $replacements): array {
return processLocationBlock($lines, $locationPath, $modifier, function(array &$lines, int $i, string $phase) use ($replacements) {
if ($phase === 'inside') {
foreach ($replacements as $rep) {
if (!isset($rep['name'], $rep['val'])) continue;
$name = preg_quote($rep['name'], '/');
$val = $rep['val'];
if (preg_match("/$name\\s+.+;/", $lines[$i])) {
$lines[$i] = preg_replace("/$name\\s+.+;/", "$name $val;", $lines[$i]);
}
}
}
});
}
/**
* 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);
// 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);
$lines = explode("\n", $original); // Split config into lines
// Build new proxy_pass target
$proxyTarget = $meta['protocol'] . '://' . $meta['domain'] . ':' . $meta['port'];
// Apply transformation inside matching location block
$lines = updateLocationBlock($lines, $meta['location'], null, [['name' => 'proxy_pass', 'val' => $proxyTarget]]);
// remove from nginx php обработчик так как он приводит к обработке локально, а не на удаленном сервере
$lines = removeLocationBlock($lines, '~', '.*\.php.*');
$modified = implode("\n", $lines);
// Write only if content changed
if ($original !== $modified) {
$result = (file_put_contents($systemConfPath, $modified) !== false);
echo "Domain: $name is updated\n";
}
return $result;
}