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); }