ip2nginx/update.php
2025-05-21 13:35:18 +02:00

259 lines
8.4 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');
/**
* Main logic handler
*/
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 {
// Ensure the data directory exists and is writable
if (!ensureDirectoryExists(DATA_DIR)) {
error_deny();
}
// Get the real client IP address
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// Deny access if the client IP is blocked
if (isBlockedIP($clientIp)) {
error_deny();
}
// Retrieve all expected input parameters using case-insensitive matching
$query = getQueryMulti([
'name' => null,
'token' => null,
'ip' => $clientIp,
'domain' => $clientIp, // fallback to IP if domain not given
'protocol' => 'http',
'port' => null,
]);
// Check if any parameter was received via GET (possible tampering)
$usedGET = false;
foreach ($query as $key => $meta) {
if ($meta['source'] === 'get') {
$usedGET = true;
break;
}
}
// 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)) {
error_deny();
}
// Token mismatch
if (!array_key_exists($name, $tokens) || $tokens[$name] !== $token) {
recordFailure($clientIp);
error_deny();
}
// Validate protocol
if (!in_array($protocol, ['http', 'https'])) {
error_deny();
}
// Normalize port if empty or invalid
if (isEmpty($port)) {
$port = ($protocol === 'https') ? 443 : 80;
} elseif (!preg_match('/^\d{2,5}$/', (string)$port)) {
error_deny();
}
$port = intval($port);
// All good: update metadata and log
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 the given entry name
* (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.
*
* The `location` field is preserved from existing data if it's set and non-empty,
* otherwise it defaults to DEF_LOCATION.
*
* @param string $name Logical identifier of the entry (e.g., domain alias)
* @param string $domain Target domain for use in proxy_pass
* @param string $ip New or updated public IP address
* @param int $port Port number for proxy (typically 80 or 443)
* @param string $protocol Protocol to use for proxy (either 'http' or 'https')
* @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): bool {
$meta = loadJson(METAFILE); // Load current meta.json contents
$resp = "No changes for $name\n"; // Default response if no changes
$result = false; // Flag to track if update occurred
// New metadata values from the client
$incoming = [
'domain' => $domain,
'ip' => $ip,
'port' => $port,
'protocol' => $protocol,
];
// Previously stored values for this entry
$existing = $meta[$name] ?? [];
// Check if any field has changed
if (array_diff_assoc($incoming, $existing)) {
$incoming['location'] = DEF_LOCATION; // Default location path
$incoming['time'] = date('c'); // Timestamp in ISO 8601 format
// Preserve existing location if set and not empty
if (array_key_exists('location', $existing) && !isEmpty($existing['location'])) {
$incoming['location'] = $existing['location'];
}
$incoming['changed'] = 1; // Mark for processing in updater
logChange($name, $incoming); // Log the update
$meta[$name] = $incoming; // Update in-memory data
$result = saveJson(METAFILE, $meta); // Write back to meta.json
$resp = "Updated: [$protocol://$ip:$port] for $name\n";
}
http_response_code(200); // Always return success
echo $resp;
return $result;
}
/**
* 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);
}