ip2nginx/update.php

232 lines
7.1 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();
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);
}