Initial commit of ip2nginx project (v0.0.1)
This commit is contained in:
commit
b04c92ce49
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
._*
|
||||
.vscode
|
||||
**/_*
|
||||
13
.htaccess
Normal file
13
.htaccess
Normal file
@ -0,0 +1,13 @@
|
||||
# Disable directory listing
|
||||
Options -Indexes
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Allow direct access to update.php
|
||||
RewriteCond %{REQUEST_URI} ^/update\.php$ [NC]
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Route all other requests through index.php
|
||||
RewriteRule ^.*$ index.php [L,QSA]
|
||||
</IfModule>
|
||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 SAFE-CAP
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
170
README.md
Normal file
170
README.md
Normal file
@ -0,0 +1,170 @@
|
||||
# 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 server’s reverse proxy configuration.
|
||||
|
||||
## ⚙️ Features
|
||||
|
||||
- Accepts remote updates via update.php (must be present on the server).
|
||||
- Logs all changes to log.json.
|
||||
- Access is controlled using tokens stored in token.json.
|
||||
- Protects against abuse by blocking IPs after repeated failed attempts, using blocklist.json and failures.json.
|
||||
- The core function is to locate a specific location block inside the domain’s nginx.conf and update only the proxy_pass value — without overwriting the entire file.
|
||||
- Automatically reloads NGINX using nginx -t && systemctl reload nginx.
|
||||
|
||||
## 📁 Project Structure
|
||||
```
|
||||
ip2nginx/
|
||||
├── .htaccess # Restricts access, routes all requests to updater
|
||||
├── updater.php # Applies proxy_pass updates to nginx.conf
|
||||
├── update.php # Processes and applies external IP update requests
|
||||
├── run.sh # CLI wrapper script for triggering update
|
||||
├── index.php # Default entry point and fallback error handling
|
||||
├── data/ # Data storage directory
|
||||
│ ├── meta.json # Central metadata file for all domains
|
||||
│ ├── token.json # Authorized tokens for each client
|
||||
│ ├── log.json # Log of updates and timestamps
|
||||
│ ├── blocklist.json # IPs blocked after repeated failed attempts
|
||||
│ └── failures.json # Failed authentication tracking
|
||||
```
|
||||
|
||||
## ✅ Example Entry in `token.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"domain1.to.com": "SECRET_TOKEN_8v73jDKsdLzAq9DkeUz1",
|
||||
"domain2.to.com": "SECRET_TOKEN_3im83jUj28mjo2mI23un"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## ✅ Example Entry in `meta.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"domain1.to.com": {
|
||||
"domain": "domain.from.com",
|
||||
"ip": "192.0.2.4",
|
||||
"port": "443",
|
||||
"protocol": "https",
|
||||
"location": "/",
|
||||
"time": "2025-05-16T09:00:00+00:00",
|
||||
"changed": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 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.
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|------------|----------|-----------------------------------------------------------------------------|
|
||||
| `name` | ✅ | Identifier for the configuration entry (e.g., `domain1.to.com`) |
|
||||
| `token` | ✅ | Secret token assigned to the client for authentication |
|
||||
| `ip` | ❌ | New public IP address (auto-detected if omitted) |
|
||||
| `domain` | ❌ | Target domain name to connect to (e.g., `domain.from.com`) |
|
||||
| | | (default: IP address) |
|
||||
| `port` | ❌ | Proxy port (default: `443` for HTTPS, `80` for HTTP) |
|
||||
| `protocol` | ❌ | Protocol to use: `http` or `https` (default: `http`) |
|
||||
| `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`.
|
||||
|
||||
|
||||
## 🚀 Local update Process: `updater.php`
|
||||
|
||||
This script is meant to be run via CLI (e.g. cronjob). It will:
|
||||
|
||||
1. Load all entries from `meta.json`
|
||||
2. Process only those marked as `"changed": 1`
|
||||
3. Locate the virtual host config: `/var/www/vhosts/system/<domain>/conf/nginx.conf`
|
||||
4. Replace only the `proxy_pass` inside the matching `location` block
|
||||
5. Validate and reload Nginx (`sudo nginx -t && sudo systemctl reload nginx`)
|
||||
6. Reset the `"changed"` flag in `meta.json`
|
||||
|
||||
## ⏱ 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.
|
||||
|
||||
### 🔧 Recommended Cron Configuration
|
||||
|
||||
To run the update script every 5 or 10 minutes as **root** (required for writing to NGINX config files and reloading the server):
|
||||
|
||||
1. Open the root user’s crontab:
|
||||
|
||||
```sh
|
||||
sudo crontab -e
|
||||
```
|
||||
|
||||
2. Add the following line:
|
||||
|
||||
```cron
|
||||
*/5 * * * * /path/to/ip2nginx/run.sh
|
||||
```
|
||||
|
||||
Replace `/path/to/ip2nginx/` with the actual path where your `run.sh` script is located.
|
||||
|
||||
### ✅ Why It Must Run as Root
|
||||
|
||||
The updater modifies system-level files, typically located in:
|
||||
|
||||
```
|
||||
/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
|
||||
#!/bin/sh
|
||||
|
||||
SERVER="https://your-server.com/ip2nginx"
|
||||
NAME="domain1.to.com"
|
||||
DOMAIN="domain.from.com"
|
||||
TOKEN="YOUR_SECRET_TOKEN"
|
||||
|
||||
curl -s -X POST "$SERVER/update.php" \
|
||||
-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**
|
||||
91
check_env.php
Normal file
91
check_env.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?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
|
||||
}
|
||||
|
||||
/**
|
||||
* This script checks environment integrity:
|
||||
* - Verifies required constants
|
||||
* - Validates directory and file permissions
|
||||
* - Creates required files if missing
|
||||
* - Verifies JSON syntax
|
||||
*/
|
||||
|
||||
echo "== ip2nginx Environment Check ==\n";
|
||||
|
||||
// List of required files with their default content and persistence flag
|
||||
$requiredFiles = [
|
||||
TOKENFILE => ['default' => DEF_EXAMPLE, 'keep' => true],
|
||||
METAFILE => ['default' => [], 'keep' => false],
|
||||
LOGFILE => ['default' => [], 'keep' => false],
|
||||
BLOCKLIST_FILE => ['default' => [], 'keep' => false],
|
||||
FAILURESFILE => ['default' => [], 'keep' => false],
|
||||
];
|
||||
|
||||
// Check if DATA_DIR exists and is writable
|
||||
echo "- DATA_DIR: " . DATA_DIR . "\n";
|
||||
if (!is_dir(DATA_DIR)) {
|
||||
echo " ❌ DATA_DIR does not exist\n";
|
||||
} elseif (!is_writable(DATA_DIR)) {
|
||||
echo " ⚠️ DATA_DIR is not writable\n";
|
||||
} else {
|
||||
echo " ✅ DATA_DIR OK\n";
|
||||
}
|
||||
|
||||
// Check and optionally create required files
|
||||
foreach ($requiredFiles as $file => $opts) {
|
||||
echo "- Checking: $file\n";
|
||||
if (!file_exists($file)) {
|
||||
echo " ⚠️ File not found. Creating...\n";
|
||||
saveJson($file, $opts['default']);
|
||||
} else {
|
||||
$opts['keep'] = true;
|
||||
}
|
||||
|
||||
if (!file_exists($file)) {
|
||||
echo " ❌ Failed to create\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_readable($file)) {
|
||||
echo " ⚠️ Not readable\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_writable($file)) {
|
||||
echo " ⚠️ Not writable\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate JSON syntax
|
||||
loadJson($file);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo " ❌ Invalid JSON: " . json_last_error_msg() . "\n";
|
||||
} else {
|
||||
echo " ✅ JSON valid\n";
|
||||
}
|
||||
|
||||
if (!$opts['keep']) {
|
||||
echo " ℹ️ Will be auto-deleted\n";
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
// Check NGINX conf directory
|
||||
echo "- NGINX base: " . VHOST_BASE . "\n";
|
||||
if (!is_dir(VHOST_BASE)) {
|
||||
echo " ❌ VHOST_BASE directory missing\n";
|
||||
} elseif (!is_readable(VHOST_BASE)) {
|
||||
echo " ⚠️ VHOST_BASE not readable\n";
|
||||
} else {
|
||||
echo " ✅ VHOST_BASE OK\n";
|
||||
}
|
||||
|
||||
echo "Check complete.\n";
|
||||
228
index.php
Normal file
228
index.php
Normal file
@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Initializes core constants used across the ip2nginx project.
|
||||
*
|
||||
* This script is **only** intended to be included by other scripts.
|
||||
* It prevents direct execution and ensures all key configuration constants are defined.
|
||||
*/
|
||||
|
||||
// If the script is included and the FILE_IS_INCLUDED flag is defined,
|
||||
// proceed to define the required constants.
|
||||
if (defined('FILE_IS_INCLUDED')) {
|
||||
|
||||
// === Core data paths ===
|
||||
|
||||
// Define DATA_DIR if not already defined (used to store data files)
|
||||
if (!defined('DATA_DIR')) {
|
||||
define('DATA_DIR', __DIR__ . '/data'); // Directory where data files are stored
|
||||
}
|
||||
|
||||
// Define METAFILE if not already defined (path to meta.json file)
|
||||
if (!defined('METAFILE')) {
|
||||
define('METAFILE', DATA_DIR . '/meta.json'); // Central file for domain metadata
|
||||
}
|
||||
|
||||
// === Nginx configuration settings ===
|
||||
|
||||
// Define VHOST_BASE if not already defined
|
||||
if (!defined('VHOST_BASE')) {
|
||||
define('VHOST_BASE', '/var/www/vhosts/system'); // Base path to vhost configs (Plesk)
|
||||
}
|
||||
|
||||
// Define NGINX_CONF_NAME if not already defined
|
||||
if (!defined('NGINX_CONF_NAME')) {
|
||||
define('NGINX_CONF_NAME', 'nginx.conf'); // Target file to modify inside each vhost
|
||||
}
|
||||
|
||||
// === Additional configuration ===
|
||||
|
||||
// Define blocklist file
|
||||
if (!defined('BLOCKLIST_FILE')) {
|
||||
define('BLOCKLIST_FILE', DATA_DIR . '/blocklist.json'); // File storing blocked IPs
|
||||
}
|
||||
|
||||
// Define authentication failure tracking file
|
||||
if (!defined('FAILURESFILE')) {
|
||||
define('FAILURESFILE', DATA_DIR . '/failures.json'); // Tracks failed login attempts
|
||||
}
|
||||
|
||||
// Define general log file
|
||||
if (!defined('LOGFILE')) {
|
||||
define('LOGFILE', DATA_DIR . '/log.json'); // Change log
|
||||
}
|
||||
|
||||
// Define API token file
|
||||
if (!defined('TOKENFILE')) {
|
||||
define('TOKENFILE', DATA_DIR . '/token.json'); // Token registry
|
||||
}
|
||||
|
||||
// Define default location block used if none is set
|
||||
if (!defined('DEF_LOCATION')) {
|
||||
define('DEF_LOCATION', '/'); // Default nginx location path
|
||||
}
|
||||
|
||||
// Define default example block for meta.json
|
||||
if (!defined('DEF_EXAMPLE')) {
|
||||
define('DEF_EXAMPLE', ["example.com" => "SECRET_EXAMPLE"]);
|
||||
}
|
||||
|
||||
// === Security settings ===
|
||||
|
||||
// Block duration in seconds (48 hours by default)
|
||||
if (!defined('BLOCK_DURATION_SECONDS')) {
|
||||
define('BLOCK_DURATION_SECONDS', 48 * 3600); // Duration of IP block after failures
|
||||
}
|
||||
|
||||
} else {
|
||||
// If the file was accessed directly, deny access
|
||||
error_deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a standard 403 Forbidden HTTP response with an HTML message
|
||||
* and terminates the script.
|
||||
*/
|
||||
function error_deny() : void {
|
||||
// Set HTTP response status code to 403
|
||||
http_response_code(403);
|
||||
|
||||
// Explicitly send HTTP headers for 403 Forbidden and HTML content type
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
|
||||
// Output a simple HTML page explaining the forbidden access
|
||||
echo "<HTML>\n";
|
||||
echo "<HEAD>\n";
|
||||
echo "<TITLE>403 Forbidden</TITLE>\n";
|
||||
echo "</HEAD>\n";
|
||||
echo "<BODY>\n";
|
||||
echo "<H1>Forbidden</H1>\n";
|
||||
echo "You do not have permission to access this document.\n";
|
||||
echo "<P>\n";
|
||||
echo "<HR>\n";
|
||||
echo "<ADDRESS>\n";
|
||||
echo "Web Server at " . $_SERVER['SERVER_NAME'] . "\n";
|
||||
echo "</ADDRESS>\n";
|
||||
echo "</BODY>\n";
|
||||
echo "</HTML>\n";
|
||||
|
||||
// Exit the script with status code 403
|
||||
exit(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a parameter from the request (POST takes precedence over GET).
|
||||
* If the parameter is not found in either, returns the provided default value.
|
||||
*
|
||||
* @param string $name The name of the parameter to retrieve
|
||||
* @param mixed $def The default value to return if parameter is not found
|
||||
* @return mixed The value of the parameter or the default value
|
||||
*/
|
||||
function getQuery(string $name, mixed $def): mixed {
|
||||
$val = $_POST[$name] ?? null;
|
||||
|
||||
if ($val === null) {
|
||||
$val = $_GET[$name] ?? $def;
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given value is considered "empty".
|
||||
* - Returns true for null, empty strings, empty arrays, 0, "0", etc. (like PHP's `empty()`)
|
||||
* - Additionally, for strings, also trims whitespace before checking emptiness
|
||||
*
|
||||
* @param mixed $val The value to evaluate
|
||||
* @return bool True if the value is empty or whitespace-only, false otherwise
|
||||
*/
|
||||
function isEmpty(mixed $val): bool {
|
||||
// Handle typical empty values (null, "", 0, [], false, etc.)
|
||||
if (empty($val)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's a string, trim and check again
|
||||
if (is_string($val)) {
|
||||
if (trim($val) === '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and parses a JSON file into an associative array.
|
||||
*
|
||||
* Checks if the file exists and is readable before attempting to decode it.
|
||||
* If the JSON is invalid, an error message is printed.
|
||||
* If decoding fails or the result is not an array, an empty array is returned.
|
||||
*
|
||||
* @param string $filename Path to the JSON file
|
||||
* @return array Parsed associative array or empty array on error
|
||||
*/
|
||||
function loadJson(string $filename): array {
|
||||
$result = [];
|
||||
|
||||
// Attempt to read and decode the file if it exists and is readable
|
||||
if (file_exists($filename) && is_readable($filename)) {
|
||||
$result = json_decode(file_get_contents($filename), true);
|
||||
|
||||
// Log an error if JSON is invalid
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
echo "Invalid JSON in $filename: " . json_last_error_msg() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the result is an array
|
||||
if (!is_array($result)) {
|
||||
$result = [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a PHP array to a JSON file in pretty format.
|
||||
*
|
||||
* - Ensures the target file is writable if it already exists.
|
||||
* - Automatically replaces null or empty input with an empty array.
|
||||
* - Returns true on success, false on any failure.
|
||||
*
|
||||
* @param string $filename The path to the JSON file to write
|
||||
* @param array|null $val The data to save (null or empty → [])
|
||||
* @return bool True if saved successfully, false otherwise
|
||||
*/
|
||||
function saveJson(string $filename, ?array $val): bool {
|
||||
// If file exists, check for write permissions
|
||||
if (file_exists($filename)) {
|
||||
if (!is_writable($filename)) {
|
||||
echo "No access to write in $filename\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to empty array if null or empty
|
||||
if (isEmpty($val)) {
|
||||
$val = [];
|
||||
}
|
||||
|
||||
// Encode to JSON
|
||||
$encoded = json_encode($val, JSON_PRETTY_PRINT);
|
||||
if ($encoded === false) {
|
||||
echo "Failed to encode $filename: " . json_last_error_msg() . "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save to file
|
||||
if (file_put_contents($filename, $encoded) === false) {
|
||||
echo "Failed to write to $filename\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
7
run.sh
Normal file
7
run.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Resolve absolute path to the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Run the PHP updater script
|
||||
/usr/bin/env php "$SCRIPT_DIR/updater.php"
|
||||
231
update.php
Normal file
231
update.php
Normal file
@ -0,0 +1,231 @@
|
||||
<?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');
|
||||
|
||||
/**
|
||||
* Main logic handler
|
||||
*/
|
||||
main();
|
||||
|
||||
function main(): void {
|
||||
// Ensure the data directory exists and is writable
|
||||
if (!ensureDirectoryExists(DATA_DIR)) {
|
||||
error_deny();
|
||||
}
|
||||
|
||||
// Get client IP address
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
|
||||
// Deny access if the client IP is currently blocked
|
||||
if (isBlockedIP($clientIp)) {
|
||||
error_deny();
|
||||
}
|
||||
|
||||
// Retrieve parameters from GET or POST
|
||||
$name = getQuery('name', null);
|
||||
$domain = getQuery('domain', null);
|
||||
$token = getQuery('token', null);
|
||||
$ip = getQuery('ip', $clientIp);
|
||||
$port = getQuery('port', 80);
|
||||
$protocol = getQuery('protocol', 'http');
|
||||
|
||||
// Load the authorized tokens
|
||||
$tokens = loadTokens();
|
||||
|
||||
// Validate required input and IP address format
|
||||
if (isEmpty($name) || isEmpty($token) || !filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
error_deny();
|
||||
}
|
||||
|
||||
// Check if token is valid for the given name
|
||||
if (!array_key_exists($name, $tokens) || $tokens[$name] !== $token) {
|
||||
recordFailure($clientIp);
|
||||
error_deny();
|
||||
}
|
||||
|
||||
// Ensure the protocol is either http or https
|
||||
if (!in_array($protocol, ['http', 'https'])) {
|
||||
error_deny();
|
||||
}
|
||||
|
||||
// If port is not set, use default based on protocol
|
||||
if ($port === null || $port === '') {
|
||||
$port = ($protocol === 'https') ? '443' : '80';
|
||||
} elseif (!preg_match('/^\d{2,5}$/', $port)) {
|
||||
error_deny();
|
||||
}
|
||||
|
||||
if ($domain === null || $domain === '') {
|
||||
$domain = $ip;
|
||||
}
|
||||
|
||||
// Update metadata and log the change
|
||||
updateMeta($name, $domain, $ip, $port, $protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads token list from TOKENFILE or creates an example if file does not exist
|
||||
*/
|
||||
function loadTokens(): array {
|
||||
if (!file_exists(TOKENFILE)) {
|
||||
saveJson(TOKENFILE, DEF_EXAMPLE);
|
||||
echo "Token file created at " . TOKENFILE . ". Please edit it and rerun.\n";
|
||||
exit (1);
|
||||
}
|
||||
return loadJson(TOKENFILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the meta.json file with new connection data for a given name (domain),
|
||||
* and logs the update if changes are detected.
|
||||
*
|
||||
* If any field (IP, port, protocol, domain) differs from the stored data,
|
||||
* the entry is updated and flagged with "changed": 1. The location field is
|
||||
* preserved if it already exists and is not empty.
|
||||
*
|
||||
* @param string $name The identifier name (usually domain alias)
|
||||
* @param string $domain The target domain for proxy_pass
|
||||
* @param string $ip The updated IP address
|
||||
* @param int $port The proxy port to use
|
||||
* @param string $protocol Protocol to use ('http' or 'https')
|
||||
* @return void
|
||||
*/
|
||||
function updateMeta(string $name, string $domain, string $ip, int $port, string $protocol): void {
|
||||
$meta = loadJson(METAFILE);
|
||||
$resp = "No changes for $name\n";
|
||||
|
||||
// New data coming in from client
|
||||
$incoming = [
|
||||
'domain' => $domain,
|
||||
'ip' => $ip,
|
||||
'port' => $port,
|
||||
'protocol' => $protocol,
|
||||
];
|
||||
|
||||
// Existing data for this entry, if any
|
||||
$existing = $meta[$name] ?? [];
|
||||
|
||||
// If any changes detected — update
|
||||
if (array_diff_assoc($incoming, $existing)) {
|
||||
$incoming['location'] = DEF_LOCATION; // Default location
|
||||
$incoming['time'] = date('c'); // Timestamp
|
||||
$incoming['changed'] = 1; // Flag as changed
|
||||
|
||||
// Preserve existing location if defined and not empty
|
||||
if (array_key_exists('location', $existing)) {
|
||||
if (!isEmpty($existing['location'])) {
|
||||
$incoming['location'] = $existing['location'];
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated meta
|
||||
$meta[$name] = $incoming;
|
||||
|
||||
saveJson (METAFILE, $meta);
|
||||
|
||||
// Log the change
|
||||
logChange ($name, $incoming);
|
||||
$resp = "Updated: [$protocol://$ip:$port] for $name\n";
|
||||
}
|
||||
|
||||
// Always respond with 200 OK and a message
|
||||
http_response_code(200);
|
||||
echo $resp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether a given IP address is currently blocked,
|
||||
* based on a JSON file containing expiration timestamps.
|
||||
*
|
||||
* @param string $ip IP address to check
|
||||
* @return bool True if the IP is currently blocked, false otherwise
|
||||
*/
|
||||
function isBlockedIP(string $ip): bool {
|
||||
// Load blocklist from file, or use an empty list if file is missing
|
||||
$blocklist = loadJson(BLOCKLIST_FILE);
|
||||
|
||||
// Return true if IP exists in blocklist and its expiration time is in the future
|
||||
return isset($blocklist[$ip]) && time() < $blocklist[$ip];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensures that the specified directory (or its parent) exists.
|
||||
*
|
||||
* If the provided path is a file, the function checks the directory it would belong to.
|
||||
* If the directory does not exist and $create is true, it attempts to create it.
|
||||
*
|
||||
* @param string $path The path to check (can be a file or a directory)
|
||||
* @param bool $create Whether to create the directory if it doesn't exist (default: true)
|
||||
* @return bool True if the directory exists or was successfully created, false otherwise
|
||||
*/
|
||||
function ensureDirectoryExists(string $path, bool $create = true): bool {
|
||||
// Determine the directory: if it's not already a dir, get its parent
|
||||
$dir = is_dir($path) ? $path : dirname($path);
|
||||
|
||||
// If the directory doesn't exist and we're allowed to create it
|
||||
if (!is_dir($dir) && $create) {
|
||||
return mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
// Directory exists or was not created intentionally
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends a structured log entry to the log file.
|
||||
*
|
||||
* Each log entry is an associative array containing the provided data,
|
||||
* merged with a `name` identifier. The function preserves existing log
|
||||
* entries by appending to the decoded array before writing back to the file.
|
||||
*
|
||||
* @param string $name Identifier for the log entry (e.g. domain or config name)
|
||||
* @param array $data Additional data to include in the log entry
|
||||
* @return void
|
||||
*/
|
||||
function logChange(string $name, array $data): void {
|
||||
// Load existing log data or start with an empty array
|
||||
$log = loadJson(LOGFILE);
|
||||
|
||||
// Merge the name into the entry and append
|
||||
$log[] = array_merge(['name' => $name], $data);
|
||||
|
||||
// Write updated log back to file
|
||||
saveJson(LOGFILE, $log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a failed authentication attempt for the given IP address.
|
||||
*
|
||||
* Tracks the number of failures in `failures.json`. If an IP reaches
|
||||
* the failure threshold (e.g., 3), it is added to the blocklist with
|
||||
* an expiration timestamp defined by `BLOCK_DURATION_SECONDS`.
|
||||
*
|
||||
* @param string $ip The IP address that failed authentication
|
||||
* @return void
|
||||
*/
|
||||
function recordFailure(string $ip): void {
|
||||
// Load the current failure count from the failures file
|
||||
$failures = loadJson(FAILURESFILE);
|
||||
|
||||
// Increment failure count for the IP
|
||||
$failures[$ip] = ($failures[$ip] ?? 0) + 1;
|
||||
|
||||
// If threshold exceeded, add IP to blocklist and reset failure count
|
||||
if ($failures[$ip] >= 3) {
|
||||
$blocklist = loadJson(BLOCKLIST_FILE);
|
||||
$blocklist[$ip] = time() + BLOCK_DURATION_SECONDS;
|
||||
|
||||
saveJson (BLOCKLIST_FILE, $blocklist);
|
||||
unset ($failures[$ip]); // Reset after blocking
|
||||
}
|
||||
|
||||
// Save updated failure log
|
||||
saveJson (FAILURESFILE, $failures);
|
||||
}
|
||||
|
||||
174
updater.php
Normal file
174
updater.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user