259 lines
8.4 KiB
PHP
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);
|
|
}
|