282 lines
10 KiB
PHP
282 lines
10 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) {
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Removes a specific `location` block from an array of NGINX configuration lines.
|
|
*
|
|
* This function uses a helper (`processLocationBlock`) to identify a `location` block
|
|
* by its path and optional modifier (e.g., `~`, `=`, `^~`), and removes the entire block
|
|
* including its contents from the provided config lines.
|
|
*
|
|
* @param array &$lines NGINX config lines.
|
|
* @param string $path The location path to match (e.g., "/api", "\.php$").
|
|
* @param string|null $modifier Optional location modifier (e.g. `~`, `=`, `^~`). If null, matches unmodified location.
|
|
* @return array A new array of config lines with the matched block removed.
|
|
*/
|
|
function removeLocationBlock(array $lines, string $path, ?string $modifier = null): array {
|
|
$result = []; // Final result array for cleaned config lines
|
|
$skipBlock = false; // Flag indicating whether we are inside a block to remove
|
|
$buffer = []; // Temporary buffer for skipped lines (optional, unused here)
|
|
|
|
// Process the location block using a callback to mark lines for deletion
|
|
$lines = processLocationBlock($lines, $path, $modifier, function(array &$lines, int $i, string $phase) use (&$skipBlock, &$buffer) {
|
|
if ($phase === 'start') {
|
|
$skipBlock = true; // Begin removing lines at block start
|
|
}
|
|
if ($skipBlock) {
|
|
$lines[$i] = null; // Mark current line for removal
|
|
}
|
|
if ($phase === 'end') {
|
|
$skipBlock = false; // Stop skipping lines at block end
|
|
}
|
|
});
|
|
|
|
// Remove all lines marked as null (deleted) and reindex
|
|
foreach ($lines as $line) {
|
|
if (!isEmpty($line)) {
|
|
$result[] = $line;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Updates one or more NGINX directives inside a specific location block.
|
|
* Also optionally appends the directive if it's not found in the block.
|
|
*
|
|
* @param array $lines NGINX config lines as an array
|
|
* @param string $locationPath The location path to match (e.g. "/api/")
|
|
* @param string|null $modifier Modifier for location (e.g. "~", "^~") or null for plain
|
|
* @param array $replacements Array of directive replacements in the format:
|
|
* [
|
|
* [
|
|
* 'name' => 'directive_name',
|
|
* 'val' => 'replacement value',
|
|
* 'append' => true|false (optional)
|
|
* ],
|
|
* ...
|
|
* ]
|
|
* @return array Modified lines with replacements and optional appends
|
|
*/
|
|
function updateLocationBlock(array $lines, string $locationPath, ?string $modifier, array $replacements): array {
|
|
$seenDirectives = []; // Tracks which directives were matched and replaced
|
|
|
|
return processLocationBlock($lines, $locationPath, $modifier, function(array &$lines, int $i, string $phase) use ($replacements, &$seenDirectives) {
|
|
// This block runs for each line inside the matching location block
|
|
|
|
if ($phase === 'inside') {
|
|
foreach ($replacements as $rep) {
|
|
// Validate required fields
|
|
if (!isset($rep['name'], $rep['val'])) continue;
|
|
|
|
$name = preg_quote($rep['name'], '/'); // Escape directive name for regex
|
|
$val = $rep['val'];
|
|
|
|
// If line contains the directive, replace its value
|
|
if (preg_match("/^\\s*{$name}\\s+.+;/", $lines[$i])) {
|
|
$lines[$i] = preg_replace("/^\\s*{$name}\\s+.+;/", "{$rep['name']} {$val};", $lines[$i]);
|
|
$seenDirectives[$rep['name']] = true; // Mark directive as seen
|
|
}
|
|
}
|
|
}
|
|
|
|
// At the end of the location block, insert any missing directives marked for appending
|
|
if ($phase === 'end') {
|
|
$inserts = [];
|
|
|
|
foreach ($replacements as $rep) {
|
|
$append = $rep['append'] ?? false;
|
|
if (!isset($rep['name'], $rep['val']) || !$append) continue;
|
|
|
|
// If directive wasn't seen inside the block, add it now
|
|
if (empty($seenDirectives[$rep['name']])) {
|
|
$inserts[] = " {$rep['name']} {$rep['val']};"; // Indentation is preserved
|
|
}
|
|
}
|
|
|
|
// Insert new lines right before the closing brace of the location block
|
|
if (!empty($inserts)) {
|
|
array_splice($lines, $i, 0, $inserts);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 the PHP handler location block from nginx config.
|
|
// This prevents local PHP processing and ensures the request is forwarded to the remote upstream server.
|
|
$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;
|
|
}
|