113 lines
3.2 KiB
PHP
113 lines
3.2 KiB
PHP
<?php
|
|
namespace Misuzu\Net;
|
|
|
|
use Exception;
|
|
use InvalidArgumentException;
|
|
use GeoIp2\Exception\AddressNotFoundException;
|
|
|
|
final class IPAddress {
|
|
public const VERSION_UNKNOWN = 0;
|
|
public const VERSION_4 = 4;
|
|
public const VERSION_6 = 6;
|
|
|
|
private const SIZES = [
|
|
self::VERSION_4 => 4,
|
|
self::VERSION_6 => 16,
|
|
];
|
|
|
|
public const DEFAULT_V4 = '127.1';
|
|
public const DEFAULT_V6 = '::1';
|
|
|
|
public static function remote(string $fallback = self::DEFAULT_V6): string {
|
|
return $_SERVER['REMOTE_ADDR'] ?? $fallback;
|
|
}
|
|
|
|
public static function country(string $address, string $fallback = 'XX'): string {
|
|
if(!GeoIP::isAvailable())
|
|
return $fallback;
|
|
|
|
try {
|
|
return GeoIP::resolveCountry($address)->country->isoCode ?? $fallback;
|
|
} catch(AddressNotFoundException $e) {
|
|
return $fallback;
|
|
}
|
|
}
|
|
|
|
public static function rawWidth(int $version): int {
|
|
return isset(self::SIZES[$version]) ? self::SIZES[$version] : 0;
|
|
}
|
|
|
|
public static function detectStringVersion(string $address): int {
|
|
if(filter_var($address, FILTER_VALIDATE_IP) !== false) {
|
|
if(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false)
|
|
return self::VERSION_6;
|
|
|
|
if(filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
|
|
return self::VERSION_4;
|
|
}
|
|
|
|
return self::VERSION_UNKNOWN;
|
|
}
|
|
|
|
public static function detectRawVersion(string $address): int {
|
|
$addressLength = strlen($address);
|
|
|
|
foreach(self::SIZES as $version => $length) {
|
|
if($length === $addressLength)
|
|
return $version;
|
|
}
|
|
|
|
return self::VERSION_UNKNOWN;
|
|
}
|
|
|
|
public static function cidrToRaw(string $cidr): ?array {
|
|
if(strpos($cidr, '/') !== false) {
|
|
[$subnet, $mask] = explode('/', $cidr, 2);
|
|
} else {
|
|
$subnet = $cidr;
|
|
}
|
|
|
|
try {
|
|
$subnet = inet_pton($subnet);
|
|
} catch(Exception $ex) {
|
|
return null;
|
|
}
|
|
|
|
$mask = empty($mask) ? null : (int)$mask;
|
|
|
|
return compact('subnet', 'mask');
|
|
}
|
|
|
|
public static function matchCidr(string $address, string $cidr): bool {
|
|
$address = inet_pton($address);
|
|
$cidr = self::cidrToRaw($cidr);
|
|
return self::matchCidrRaw($address, $cidr['subnet'], $cidr['mask']);
|
|
}
|
|
|
|
public static function matchCidrRaw(string $address, string $subnet, ?int $mask = null): bool {
|
|
$version = self::detectRawVersion($subnet);
|
|
|
|
if($version === self::VERSION_UNKNOWN)
|
|
return false;
|
|
|
|
$bits = self::SIZES[$version] * 8;
|
|
|
|
if(empty($mask))
|
|
$mask = $bits;
|
|
|
|
if($mask < 1 || $mask > $bits || $version !== self::detectRawVersion($subnet))
|
|
return false;
|
|
|
|
for($i = 0; $i < ceil($mask / 8); $i++) {
|
|
$byteMask = (0xFF00 >> max(0, min(8, $mask - ($i * 8)))) & 0xFF;
|
|
$addressByte = ord($address[$i]) & $byteMask;
|
|
$subnetByte = ord($subnet[$i]) & $byteMask;
|
|
|
|
if($addressByte !== $subnetByte)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|