$server */ public static function resolve(array $server, Settings $settings): string { $remote = self::validIp((string)($server['REMOTE_ADDR'] ?? '')); if ($settings->clientIpMode() === 'direct') { return $remote; } if (!self::ipInList($remote, $settings->trustedProxies())) { throw new RuntimeException('REMOTE_ADDR is not a trusted proxy'); } $xff = (string)($server['HTTP_X_FORWARDED_FOR'] ?? ''); foreach (explode(',', $xff) as $candidate) { $candidate = trim($candidate); if (filter_var($candidate, FILTER_VALIDATE_IP)) { return $candidate; } } throw new RuntimeException('Missing valid forwarded client IP'); } private static function validIp(string $ip): string { $ip = trim($ip); if (!filter_var($ip, FILTER_VALIDATE_IP)) { throw new InvalidArgumentException('Invalid client IP'); } return $ip; } /** * @param list $allowed */ private static function ipInList(string $ip, array $allowed): bool { foreach ($allowed as $entry) { if ($entry === $ip) { return true; } if (str_contains($entry, '/') && self::cidrContains($entry, $ip)) { return true; } } return false; } private static function cidrContains(string $cidr, string $ip): bool { [$network, $prefix] = array_pad(explode('/', $cidr, 2), 2, ''); if (!filter_var($network, FILTER_VALIDATE_IP) || !preg_match('/^[0-9]+$/', $prefix)) { return false; } $networkBin = inet_pton($network); $ipBin = inet_pton($ip); if ($networkBin === false || $ipBin === false || strlen($networkBin) !== strlen($ipBin)) { return false; } $bits = (int)$prefix; $bytes = intdiv($bits, 8); $remainder = $bits % 8; if ($bytes > 0 && substr($networkBin, 0, $bytes) !== substr($ipBin, 0, $bytes)) { return false; } if ($remainder === 0) { return true; } $mask = chr((0xff << (8 - $remainder)) & 0xff); return (ord($networkBin[$bytes]) & ord($mask)) === (ord($ipBin[$bytes]) & ord($mask)); } }