91 lines
2.9 KiB
PHP
91 lines
2.9 KiB
PHP
|
|
<?php
|
||
|
|
if (!defined('ABSPATH')) exit;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* ITK Whitelist
|
||
|
|
*
|
||
|
|
* Loads config/whitelist.conf and provides a fast static check.
|
||
|
|
* Any IP/CIDR or UA substring match bypasses all ITK restrictions.
|
||
|
|
*/
|
||
|
|
class ITK_Whitelist {
|
||
|
|
|
||
|
|
private static ?bool $result = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns true if the current request is whitelisted.
|
||
|
|
* Result is cached for the lifetime of the request.
|
||
|
|
*/
|
||
|
|
public static function allowed(): bool {
|
||
|
|
if (self::$result !== null) return self::$result;
|
||
|
|
self::$result = self::check();
|
||
|
|
return self::$result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function check(): bool {
|
||
|
|
$ip = self::get_ip();
|
||
|
|
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
|
||
|
|
|
||
|
|
$entries = self::load();
|
||
|
|
|
||
|
|
foreach ($entries as $entry) {
|
||
|
|
if (str_starts_with($entry, 'ua:')) {
|
||
|
|
$needle = substr($entry, 3);
|
||
|
|
if ($needle !== '' && stripos($ua, $needle) !== false) return true;
|
||
|
|
} elseif (str_contains($entry, '/')) {
|
||
|
|
if (self::ip_in_cidr($ip, $entry)) return true;
|
||
|
|
} else {
|
||
|
|
if ($ip === $entry) return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function load(): array {
|
||
|
|
$transient = 'itk_whitelist';
|
||
|
|
$cached = get_transient($transient);
|
||
|
|
if ($cached !== false) return $cached;
|
||
|
|
|
||
|
|
$file = ITK_PATH . 'config/whitelist.conf';
|
||
|
|
$entries = [];
|
||
|
|
|
||
|
|
if (file_exists($file)) {
|
||
|
|
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||
|
|
$line = trim($line);
|
||
|
|
if ($line === '' || $line[0] === '#') continue;
|
||
|
|
$entries[] = $line;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
set_transient($transient, $entries, 300); // 5-minute cache
|
||
|
|
return $entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function invalidate_cache(): void {
|
||
|
|
delete_transient('itk_whitelist');
|
||
|
|
self::$result = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function get_ip(): string {
|
||
|
|
// Prefer X-Forwarded-For first header (same logic as bot blocker)
|
||
|
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||
|
|
return trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
|
||
|
|
}
|
||
|
|
return $_SERVER['REMOTE_ADDR'] ?? '';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function ip_in_cidr(string $ip, string $cidr): bool {
|
||
|
|
[$subnet, $bits] = explode('/', $cidr, 2);
|
||
|
|
$bits = (int)$bits;
|
||
|
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
|
||
|
|
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||
|
|
$ip_long = ip2long($ip);
|
||
|
|
$sub_long = ip2long($subnet);
|
||
|
|
if ($ip_long === false || $sub_long === false) return false;
|
||
|
|
$mask = $bits === 0 ? 0 : (~0 << (32 - $bits));
|
||
|
|
return ($ip_long & $mask) === ($sub_long & $mask);
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|