2025-05-21_1

This commit is contained in:
Alexander Schiemann 2025-05-21 13:35:18 +02:00
parent d5c4d93b54
commit 5c0511db27
4 changed files with 330 additions and 176 deletions

215
README.md
View File

@ -1,33 +1,67 @@
# ip2nginx # ip2nginx
**ip2nginx** is a lightweight and secure system for dynamically updating NGINX reverse proxy configurations based on IP address changes reported by remote systems (e.g., pfSense). It is particularly useful in scenarios where IP addresses change dynamically and need to be synchronized with a web servers reverse proxy configuration. [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.com/donate/?hosted_button_id=JNFS79EFEM7C6)
<!-- TOC -->
## 📚 Table of Contents
- [💡 Project Overview](#-project-overview)
- [⚙️ Features](#-features)
- [📁 Project Structure](#-project-structure)
- [🚀 Update Process: update.php](#-update-process-updatephp)
- [🚀 Update Process: updater.php](#-update-process-updaterphp)
- [✅ Example Entry in meta.json](#-example-entry-in-metajson)
- [✅ Example Entry in token.json](#-example-entry-in-tokenjson)
- [🛡 Security](#-security)
- [🔄 Usage Example from pfSense](#-usage-example-from-pfsense)
- [📅 Cron Setup with run.sh](#-cron-setup-with-runsh)
- [✅ Dependencies](#-dependencies)
- [📜 License](#-license)
- [🤝 Author](#-author)
<!-- /TOC -->
**ip2nginx** is a lightweight and secure system for dynamically updating NGINX reverse proxy configurations based on public IP address changes, typically reported by edge devices like **pfSense**. It ensures that NGINX always routes traffic through the correct IP, even in dynamic environments.
**Current Version:** `0.0.1`
---
## ⚙️ Features ## ⚙️ Features
- Accepts remote updates via update.php (must be present on the server). - Accepts remote updates via `update.php` using **token-authenticated** requests.
- Logs all changes to log.json. - Supports both `POST` and `GET`, though **POST is preferred** to avoid token caching.
- Access is controlled using tokens stored in token.json. - Updates only the `proxy_pass` line in the relevant `location` block of `nginx.conf`.
- Protects against abuse by blocking IPs after repeated failed attempts, using blocklist.json and failures.json. - Automatically marks entries in `meta.json` as `"changed": 1` when input changes.
- The core function is to locate a specific location block inside the domains nginx.conf and update only the proxy_pass value — without overwriting the entire file. - Logs all changes to `log.json` with timestamps.
- Automatically reloads NGINX using nginx -t && systemctl reload nginx. - Automatically reloads NGINX: `nginx -t && systemctl reload nginx` (requires root).
- Built-in abuse protection: failed requests tracked and blocked.
- `.htaccess` ensures that only `update.php` is externally accessible.
---
## 📁 Project Structure ## 📁 Project Structure
``` ```
ip2nginx/ ip2nginx/
├── .htaccess # Restricts access, routes all requests to updater ├── index.php # Shared configuration and fallback error handler
├── updater.php # Applies proxy_pass updates to nginx.conf ├── update.php # Receives incoming remote IP update requests
├── update.php # Processes and applies external IP update requests ├── updater.php # CLI-only: applies changes to nginx.conf if marked
├── run.sh # CLI wrapper script for triggering update ├── run.sh # Wrapper script for cron automation
├── index.php # Default entry point and fallback error handling ├── check_env.php # Environment validator and bootstrapper
├── data/ # Data storage directory ├── .htaccess # Blocks unauthorized access, routes traffic
│ ├── meta.json # Central metadata file for all domains ├── data/
│ ├── token.json # Authorized tokens for each client │ ├── meta.json # Stores current configuration state per domain
│ ├── log.json # Log of updates and timestamps │ ├── token.json # Stores allowed tokens (auth)
│ ├── blocklist.json # IPs blocked after repeated failed attempts │ ├── log.json # Stores audit log of changes
│ └── failures.json # Failed authentication tracking │ ├── blocklist.json # Temporarily blocked IPs (48h ban)
│ └── failures.json # Tracks failed attempts per IP
``` ```
## ✅ Example Entry in `token.json` ---
## ✅ Example: `token.json`
```json ```json
{ {
@ -36,8 +70,9 @@ ip2nginx/
} }
``` ```
---
## ✅ Example Entry in `meta.json` ## ✅ Example: `meta.json`
```json ```json
{ {
@ -53,84 +88,73 @@ ip2nginx/
} }
``` ```
## 🚀 Remote update Process: `update.php` ---
The API supports both GET and POST requests, though POST is recommended to prevent token caching by proxies or browsers. ## 🌐 Remote Update API: `update.php`
Supports **POST** (preferred) and **GET** methods.
| Parameter | Required | Description | | Parameter | Required | Description |
|------------|----------|-----------------------------------------------------------------------------| |------------|----------|-----------------------------------------------------------------------------|
| `name` | ✅ | Identifier for the configuration entry (e.g., `domain1.to.com`) | | `name` | ✅ | Identifier (e.g. `domain1.to.com`) |
| `token` | ✅ | Secret token assigned to the client for authentication | | `token` | ✅ | Secret token assigned for this name |
| `ip` | ❌ | New public IP address (auto-detected if omitted) | | `ip` | ❌ | New public IP (default: auto-detected from request) |
| `domain` | ❌ | Target domain name to connect to (e.g., `domain.from.com`) | | `domain` | ❌ | Backend domain to proxy to (default: same as IP) |
| | | (default: IP address) | | `port` | ❌ | Port number (default: 443 for https, 80 for http) |
| `port` | ❌ | Proxy port (default: `443` for HTTPS, `80` for HTTP) | | `protocol` | ❌ | One of `http` or `https` (default: `http`) |
| `protocol` | ❌ | Protocol to use: `http` or `https` (default: `http`) | | `location` | ❌ | NGINX location block path to update (default: `/`) |
| `location` | ❌ | Specific `location` block in NGINX to update (default: `/`) |
If the domain, IP, port, or protocol differ from the previously saved values, the entry in `meta.json` is automatically marked with `"changed": 1`. Any change in `ip`, `domain`, `port`, or `protocol` triggers `"changed": 1` in `meta.json`.
If **any parameter is received via GET**, then `ip` and `domain` will be overridden with the **clients real IP** for security.
## 🚀 Local update Process: `updater.php` ---
This script is meant to be run via CLI (e.g. cronjob). It will: ## 🧩 Update Process: `updater.php`
To apply updates made via `update.php`:
1. Load all entries from `meta.json` 1. Load all entries from `meta.json`
2. Process only those marked as `"changed": 1` 2. Check for entries marked `"changed": 1`
3. Locate the virtual host config: `/var/www/vhosts/system/<domain>/conf/nginx.conf` 3. Find `/var/www/vhosts/system/<domain>/conf/nginx.conf`
4. Replace only the `proxy_pass` inside the matching `location` block 4. Modify the appropriate `location` blocks `proxy_pass` directive only
5. Validate and reload Nginx (`sudo nginx -t && sudo systemctl reload nginx`) 5. Validate and reload NGINX
6. Reset the `"changed"` flag in `meta.json` 6. Reset `"changed": 0`
## ⏱ Cronjob Setup: Running `run.sh` Every 5 Minutes ---
To automatically apply updates to `nginx.conf` when new data is received via `update.php`, you should schedule the `run.sh` script to run regularly. This ensures that any entries marked as `"changed": 1` in `meta.json` are processed and deployed without manual intervention. ## 🛠 `check_env.php`: Environment Setup
### 🔧 Recommended Cron Configuration This CLI script validates:
To run the update script every 5 or 10 minutes as **root** (required for writing to NGINX config files and reloading the server): - Config files and permissions
- JSON structure of each config file
- Auto-creates missing files (with defaults)
- Token file includes example if missing
1. Open the root users crontab: ---
## ⏱ Cron Setup: `run.sh`
To automate updates, add `run.sh` to your crontab as root:
```sh ```sh
sudo crontab -e sudo crontab -e
``` ```
2. Add the following line: Then add:
```cron ```cron
*/5 * * * * /path/to/ip2nginx/run.sh */5 * * * * /path/to/ip2nginx/run.sh
``` ```
Replace `/path/to/ip2nginx/` with the actual path where your `run.sh` script is located. This ensures automatic application of proxy changes to NGINX config and reloads.
### ✅ Why It Must Run as Root ---
The updater modifies system-level files, typically located in: ## 🔄 pfSense Shell Example
``` Add the following script to `pfSense` via `System > Advanced > Cron`:
/var/www/vhosts/system/<domain>/conf/nginx.conf
```
These files usually require **root** privileges for writing. Additionally, reloading NGINX via:
```sh
sudo nginx -t && sudo systemctl reload nginx
```
also requires elevated permissions. If `run.sh` is not run as root, the updates will silently fail or be skipped due to insufficient access rights.
## 🛡 Security
- `.htaccess` blocks direct access to internal logic.
- Only HTTPS connections should be used.
- Only `update.php` is directly accessible. All other access is routed through `index.php`.
- All access is name and token-authenticated
- IP addresses are blocked for 48 hours after 3 failed attempts
- API responses are intentionally generic to prevent brute-force probing
## 🔄 Usage Example from pfSense
You can create a cron task in pfSense or use a shell script to send updated IP info to the server:
```sh ```sh
#!/bin/sh #!/bin/sh
@ -140,31 +164,34 @@ NAME="domain1.to.com"
DOMAIN="domain.from.com" DOMAIN="domain.from.com"
TOKEN="YOUR_SECRET_TOKEN" TOKEN="YOUR_SECRET_TOKEN"
curl -s -X POST "$SERVER/update.php" \ curl -s -X POST "$SERVER/update.php" -d "name=$NAME" -d "domain=$DOMAIN" -d "protocol=https" -d "port=443" -d "token=$TOKEN"
-d "name=$NAME" \
-d "domain=$DOMAIN" \
-d "protocol=https" \
-d "port=443" \
-d "token=$TOKEN"
``` ```
OR
curl -s -X POST "https://your-server.com/ip2nginx/update.php" "name=domain1.to.com" -d "token=SECRET_TOKEN_8v73jDKsdLzAq9DkeUz1" -d "protocol=https"
Add this script to pfSense via `System > Advanced > Cron` and run it every 5 or 10 minutes.
## ✅ Dependencies
- PHP 7.4+
- `nginx` installed with permissions to reload via `systemctl`
- `curl` for shell scripts (on client side)
- Token-based access configuration in `token.json`
## 📜 License
MIT or similar — free to use, modify, and deploy.
--- ---
Created by **SAFE-CAP / https://safe-cap.com / Alexander Schiemann** ## 🔒 Security Highlights
- `.htaccess` denies access to all files except `update.php`.
- Only HTTPS connections should be used.
- All tokens are stored securely and verified per name.
- After 3 failed attempts, IP is banned for 48 hours.
- Generic error messages avoid leaking details to attackers.
---
## ✅ Requirements
- PHP 7.4 or newer
- NGINX with reload access (`sudo systemctl reload nginx`)
- `curl` on the client side
- Token definitions in `token.json`
---
## 📜 License
MIT (or similar): Open-source, free for use and modification.
---
**Maintained by [SAFE-CAP / Alexander Schiemann](https://safe-cap.com)**

View File

@ -2,7 +2,7 @@
/** /**
* Initializes core constants used across the ip2nginx project. * Initializes core constants used across the ip2nginx project.
* *
* This script is **only** intended to be included by other scripts. * This script is **only** intended to be included by other scripts.
* It prevents direct execution and ensures all key configuration constants are defined. * It prevents direct execution and ensures all key configuration constants are defined.
*/ */
@ -126,6 +126,53 @@ function getQuery(string $name, mixed $def): mixed {
return isEmpty($val) ? $def : $val; return isEmpty($val) ? $def : $val;
} }
/**
* Retrieves multiple request parameters from POST or GET with fallback to default.
* Key matching is case-insensitive.
*
* @param array $params Array of parameters in the format:
* [
* 'KeyName' => defaultValue,
* ...
* ]
* @return array Associative array:
* [
* 'KeyName' => [
* 'value' => mixed,
* 'source' => 'post' | 'get' | 'default'
* ],
* ...
* ]
*/
function getQueryMulti(array $params): array {
$result = [];
// Lowercase copies of GET and POST for case-insensitive matching
$post = array_change_key_case($_POST, CASE_LOWER);
$get = array_change_key_case($_GET, CASE_LOWER);
foreach ($params as $key => $default) {
$keyLower = strtolower($key);
$value = $default;
$source = 'default';
if (isset($post[$keyLower]) && !isEmpty($post[$keyLower])) {
$value = $post[$keyLower];
$source = 'post';
} elseif (isset($get[$keyLower]) && !isEmpty($get[$keyLower])) {
$value = $get[$keyLower];
$source = 'get';
}
$result[$key] = [
'value' => $value,
'source' => $source
];
}
return $result;
}
/** /**
* Checks if a given value is considered "empty". * Checks if a given value is considered "empty".
* - Returns true for null, empty strings, empty arrays, 0, "0", etc. (like PHP's `empty()`) * - Returns true for null, empty strings, empty arrays, 0, "0", etc. (like PHP's `empty()`)
@ -230,5 +277,3 @@ function saveJson(string $filename, ?array $val): bool {
return $success; return $success;
} }

View File

@ -10,55 +10,91 @@ include('index.php');
*/ */
main(); main();
/**
* Main entry point for processing update requests.
*
* This function:
* - Validates environment and access permissions.
* - Retrieves input parameters from POST/GET using getQueryMulti().
* - Applies security logic: if any input came from GET, assumes a potential attack and resets IP/domain.
* - Validates required fields and token.
* - Triggers metadata update and logging if valid.
*/
function main(): void { function main(): void {
// Ensure the data directory exists and is writable // Ensure the data directory exists and is writable
if (!ensureDirectoryExists(DATA_DIR)) { if (!ensureDirectoryExists(DATA_DIR)) {
error_deny(); error_deny();
} }
// Get client IP address // Get the real client IP address
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// Deny access if the client IP is currently blocked // Deny access if the client IP is blocked
if (isBlockedIP($clientIp)) { if (isBlockedIP($clientIp)) {
error_deny(); error_deny();
} }
// Retrieve parameters from GET or POST // Retrieve all expected input parameters using case-insensitive matching
$name = getQuery('name', null); $query = getQueryMulti([
$domain = getQuery('domain', $clientIp); 'name' => null,
$ip = getQuery('ip', $clientIp); 'token' => null,
$token = getQuery('token', null); 'ip' => $clientIp,
$port = getQuery('port', 80); 'domain' => $clientIp, // fallback to IP if domain not given
$protocol = getQuery('protocol', 'http'); 'protocol' => 'http',
'port' => null,
]);
// Load the authorized tokens // Check if any parameter was received via GET (possible tampering)
$tokens = loadTokens(); $usedGET = false;
foreach ($query as $key => $meta) {
if ($meta['source'] === 'get') {
$usedGET = true;
break;
}
}
// Validate required input and IP address format // Extract values
$name = $query['name']['value'];
$domain = $query['domain']['value'];
$ip = $query['ip']['value'];
$token = $query['token']['value'];
$port = $query['port']['value'];
$protocol = $query['protocol']['value'];
// If GET was used, override domain and IP with server-detected IP for safety
if ($usedGET) {
$domain = $clientIp;
$ip = $clientIp;
}
// Load token list
$tokens = loadTokens();
// Basic validation
if (isEmpty($name) || isEmpty($token) || !filter_var($ip, FILTER_VALIDATE_IP)) { if (isEmpty($name) || isEmpty($token) || !filter_var($ip, FILTER_VALIDATE_IP)) {
error_deny(); error_deny();
} }
// Check if token is valid for the given name // Token mismatch
if (!array_key_exists($name, $tokens) || $tokens[$name] !== $token) { if (!array_key_exists($name, $tokens) || $tokens[$name] !== $token) {
recordFailure($clientIp); recordFailure($clientIp);
error_deny(); error_deny();
} }
// Ensure the protocol is either http or https // Validate protocol
if (!in_array($protocol, ['http', 'https'])) { if (!in_array($protocol, ['http', 'https'])) {
error_deny(); error_deny();
} }
// If port is not set, use default based on protocol // Normalize port if empty or invalid
if ($port === null || $port === '') { if (isEmpty($port)) {
$port = ($protocol === 'https') ? '443' : '80'; $port = ($protocol === 'https') ? 443 : 80;
} elseif (!preg_match('/^\d{2,5}$/', $port)) { } elseif (!preg_match('/^\d{2,5}$/', (string)$port)) {
error_deny(); error_deny();
} }
$port = intval($port);
// Update metadata and log the change // All good: update metadata and log
updateMeta($name, $domain, $ip, $port, $protocol); updateMeta($name, $domain, $ip, $port, $protocol);
} }
@ -75,25 +111,27 @@ function loadTokens(): array {
} }
/** /**
* Updates the meta.json file with new connection data for a given name (domain), * Updates the meta.json file with new connection data for the given entry name
* and logs the update if changes are detected. * (typically a domain alias). If any relevant fields (IP, port, protocol, domain)
* differ from previously stored values, the metadata entry is updated, flagged with
* `"changed": 1`, timestamped, and logged.
* *
* If any field (IP, port, protocol, domain) differs from the stored data, * The `location` field is preserved from existing data if it's set and non-empty,
* the entry is updated and flagged with "changed": 1. The location field is * otherwise it defaults to DEF_LOCATION.
* preserved if it already exists and is not empty.
* *
* @param string $name The identifier name (usually domain alias) * @param string $name Logical identifier of the entry (e.g., domain alias)
* @param string $domain The target domain for proxy_pass * @param string $domain Target domain for use in proxy_pass
* @param string $ip The updated IP address * @param string $ip New or updated public IP address
* @param int $port The proxy port to use * @param int $port Port number for proxy (typically 80 or 443)
* @param string $protocol Protocol to use ('http' or 'https') * @param string $protocol Protocol to use for proxy (either 'http' or 'https')
* @return void * @return bool True if the meta file was changed and saved; false otherwise
*/ */
function updateMeta(string $name, string $domain, string $ip, int $port, string $protocol): void { function updateMeta(string $name, string $domain, string $ip, int $port, string $protocol): bool {
$meta = loadJson(METAFILE); $meta = loadJson(METAFILE); // Load current meta.json contents
$resp = "No changes for $name\n"; $resp = "No changes for $name\n"; // Default response if no changes
$result = false; // Flag to track if update occurred
// New data coming in from client // New metadata values from the client
$incoming = [ $incoming = [
'domain' => $domain, 'domain' => $domain,
'ip' => $ip, 'ip' => $ip,
@ -101,37 +139,33 @@ function updateMeta(string $name, string $domain, string $ip, int $port, string
'protocol' => $protocol, 'protocol' => $protocol,
]; ];
// Existing data for this entry, if any // Previously stored values for this entry
$existing = $meta[$name] ?? []; $existing = $meta[$name] ?? [];
// If any changes detected — update // Check if any field has changed
if (array_diff_assoc($incoming, $existing)) { if (array_diff_assoc($incoming, $existing)) {
$incoming['location'] = DEF_LOCATION; // Default location $incoming['location'] = DEF_LOCATION; // Default location path
$incoming['time'] = date('c'); // Timestamp $incoming['time'] = date('c'); // Timestamp in ISO 8601 format
$incoming['changed'] = 1; // Flag as changed
// Preserve existing location if defined and not empty // Preserve existing location if set and not empty
if (array_key_exists('location', $existing)) { if (array_key_exists('location', $existing) && !isEmpty($existing['location'])) {
if (!isEmpty($existing['location'])) { $incoming['location'] = $existing['location'];
$incoming['location'] = $existing['location'];
}
} }
// Save updated meta $incoming['changed'] = 1; // Mark for processing in updater
$meta[$name] = $incoming; logChange($name, $incoming); // Log the update
saveJson (METAFILE, $meta); $meta[$name] = $incoming; // Update in-memory data
$result = saveJson(METAFILE, $meta); // Write back to meta.json
// Log the change $resp = "Updated: [$protocol://$ip:$port] for $name\n";
logChange ($name, $incoming);
$resp = "Updated: [$protocol://$ip:$port] for $name\n";
} }
// Always respond with 200 OK and a message http_response_code(200); // Always return success
http_response_code(200);
echo $resp; echo $resp;
}
return $result;
}
/** /**
* Checks whether a given IP address is currently blocked, * Checks whether a given IP address is currently blocked,
@ -148,7 +182,6 @@ function isBlockedIP(string $ip): bool {
return isset($blocklist[$ip]) && time() < $blocklist[$ip]; return isset($blocklist[$ip]) && time() < $blocklist[$ip];
} }
/** /**
* Ensures that the specified directory (or its parent) exists. * Ensures that the specified directory (or its parent) exists.
* *
@ -172,7 +205,6 @@ function ensureDirectoryExists(string $path, bool $create = true): bool {
return true; return true;
} }
/** /**
* Appends a structured log entry to the log file. * Appends a structured log entry to the log file.
* *
@ -224,4 +256,3 @@ function recordFailure(string $ip): void {
// Save updated failure log // Save updated failure log
saveJson (FAILURESFILE, $failures); saveJson (FAILURESFILE, $failures);
} }

View File

@ -61,7 +61,6 @@ function main(): void {
echo "All applicable domains processed.\n"; echo "All applicable domains processed.\n";
} }
/** /**
* Generic processor for a location block in NGINX config lines. * Generic processor for a location block in NGINX config lines.
* Applies a callback to each line inside the matched location block. * Applies a callback to each line inside the matched location block.
@ -107,58 +106,108 @@ function processLocationBlock(array $lines, string $path, ?string $modifier, cal
return $lines; 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)
function removeLocationBlock(array &$lines, string $path, ?string $modifier = null): array { // Process the location block using a callback to mark lines for deletion
$result = [];
$skipBlock = false;
$buffer = [];
$lines = processLocationBlock($lines, $path, $modifier, function(array &$lines, int $i, string $phase) use (&$skipBlock, &$buffer) { $lines = processLocationBlock($lines, $path, $modifier, function(array &$lines, int $i, string $phase) use (&$skipBlock, &$buffer) {
if ($phase === 'start') { if ($phase === 'start') {
$skipBlock = true; $skipBlock = true; // Begin removing lines at block start
} }
if ($skipBlock) { if ($skipBlock) {
$lines[$i] = null; // Mark for removal $lines[$i] = null; // Mark current line for removal
} }
if ($phase === 'end') { if ($phase === 'end') {
$skipBlock = false; $skipBlock = false; // Stop skipping lines at block end
} }
}); });
// Filter out null lines // Remove all lines marked as null (deleted) and reindex
foreach ($lines as $line) { foreach ($lines as $line) {
if (!isEmpty($line)) $result[] = $line; if (!isEmpty($line)) {
$result[] = $line;
}
} }
return $result; return $result;
} }
/** /**
* Updates one or more directives (e.g. proxy_pass) inside a given location block. * 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 Array of nginx configuration lines. * @param array $lines NGINX config lines as an array
* @param string $locationPath The location path to match (e.g. "/api/"). * @param string $locationPath The location path to match (e.g. "/api/")
* @param string|null $modifier Optional modifier (e.g. "~", "^~"). * @param string|null $modifier Modifier for location (e.g. "~", "^~") or null for plain
* @param array $replacements List of replacements: [['name' => 'proxy_pass', 'val' => 'http://...'], ...] * @param array $replacements Array of directive replacements in the format:
* @return array Modified nginx config lines. * [
* [
* '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 { 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) { $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') { if ($phase === 'inside') {
foreach ($replacements as $rep) { foreach ($replacements as $rep) {
// Validate required fields
if (!isset($rep['name'], $rep['val'])) continue; if (!isset($rep['name'], $rep['val'])) continue;
$name = preg_quote($rep['name'], '/'); $name = preg_quote($rep['name'], '/'); // Escape directive name for regex
$val = $rep['val']; $val = $rep['val'];
if (preg_match("/$name\\s+.+;/", $lines[$i])) {
$lines[$i] = preg_replace("/$name\\s+.+;/", "$name $val;", $lines[$i]); // 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. * Processes a single domain configuration from meta.json.
* *
@ -216,7 +265,9 @@ function processDomain(string $name, array $meta): bool {
// Apply transformation inside matching location block // Apply transformation inside matching location block
$lines = updateLocationBlock($lines, $meta['location'], null, [['name' => 'proxy_pass', 'val' => $proxyTarget]]); $lines = updateLocationBlock($lines, $meta['location'], null, [['name' => 'proxy_pass', 'val' => $proxyTarget]]);
// remove from nginx php обработчик так как он приводит к обработке локально, а не на удаленном сервере
// 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.*'); $lines = removeLocationBlock($lines, '~', '.*\.php.*');
$modified = implode("\n", $lines); $modified = implode("\n", $lines);