<?php
/**
* CIDR class helps validate an ip address against a CIDR ip range.
*
* This class has not been optimized, but works comparing an ip address
* with a wild card address or a CIDR ip range.
*
* @package CIDR
* @version 1.0
* @author Frank Forte
* @copyright Copyright (c) 2015 Frank Forte
* @license BSD-3 License https://opensource.org/licenses/BSD-3-Clause
*/
class CIDR{
/**
* compare an IPv4 or IPv6 address with a CIDR address or range
*
* @param string $address a valid IPv6 address
* @param string $subnet a valid IPv6 subnet[/mask]
* @return boolean whether $address is within the ip range made up of the subnet and mask
*/
public static function match($ip, $cidr){
// make sure we compare ip addresses as case insensitive
$ip = strtolower($ip);
$cidr = strtolower($cidr);
// comparing exact ip?
if($ip == $cidr){ return true; }
if(strpos($cidr,'/') !== false){
list($subnet, $mask) = explode('/', $cidr);
} else {
$subnet = $cidr;
$mask = '';
}
// validate ips and shorten them
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ipVersion = 'v4';
// shorten ip
$ip = preg_replace( '/^(.*\.|.*:)?0+([1-9])/','$1$2',$ip );
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ipVersion = 'v6';
// shorten ip
$ip = self::IPv6_compress($ip);
} else {
return false; // invalid ip
}
// shorten cidr and subnet
if(strpos($subnet,':') === false)
{
$subnet = preg_replace( '/^(.*\.|.*:)?0+([1-9])/','$1$2',$subnet );
}
else
{
$pos = strpos($subnet,'*');
if($pos !== false)
{
$subnet = substr($subnet,0,$pos);
$i = 0;
$subnet = explode(':',$subnet);
$size = $j = sizeof($subnet);
while($j < 8){
$subnet[] = '0';
$j++;
$i++;
}
$subnet = implode(':',$subnet);
if(substr($subnet,-1) == ':'){
$subnet .= '0';
}
}
$subnet = self::IPv6_compress($subnet);
if($pos !== false)
{
$subnet = explode(':',$subnet);
$j = sizeof($subnet);
while($j > $size)
{
array_pop($subnet);
$j--;
}
$subnet = implode(':',$subnet).'*';
}
}
// shortened cidr
$cidr = ($mask ? $subnet.'/'.$mask : $subnet);
// if $cidr is ipv6, convert $ip to ipv6 for easier comparison
if(strpos($subnet,':') !== false && $ipVersion == 'v4') {
$v6bits = array('0000','0000','0000','0000','0000','0000',$ip);
$ip4parts = explode('.', $v6bits[count($v6bits)-1]);
$ip6trans = sprintf("%02x%02x:%02x%02x", $ip4parts[0], $ip4parts[1], $ip4parts[2], $ip4parts[3]);
$v6bits[count($v6bits)-1] = $ip6trans;
$ip = implode(':', $v6bits);
// shorten ip
$ip = self::IPv6_compress($ip);
$ipVersion = 'v6';
}
if($ip == $cidr)
{
return true;
}
// wildcard matching (easier since we already shortened or "canonicalized" ip and cidr above)
$pos = strpos($cidr,'*');
if($pos !== false)
{
if(substr($ip,0,$pos) == substr($cidr,0,$pos))
{
return true;
}
else
{
return false;
}
}
switch ($ipVersion) {
case 'v4':
return self::IPv4Match($ip, $subnet, $mask);
break;
case 'v6':
return self::IPv6Match($ip, $subnet, $mask);
break;
}
}
/**
* Check IPv6 address is within an IP range
*
* @param string $address a valid IPv6 address
* @param string $subnet a valid IPv6 subnet
* @param string $mask a valid IPv6 subnet mask
* @return boolean whether $address is within the ip range made up of the subnet and mask
*/
private static function IPv6Match($ip, $subnet, $mask)
{
$subnet = inet_pton($subnet);
$ip = inet_pton($ip);
// thanks to MW on http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet
$binMask = str_repeat("f", $mask / 4);
switch ($mask % 4) {
case 0:
break;
case 1:
$binMask .= "8";
break;
case 2:
$binMask .= "c";
break;
case 3:
$binMask .= "e";
break;
}
$binMask = str_pad($binMask, 32, '0');
$binMask = pack("H*", $binMask);
return ($ip & $binMask) == $subnet;
}
/**
* Check IPv4 address is within an IP range
*
* @param string $address a valid IPv4 address
* @param string $subnet a valid IPv4 subnet
* @param string $mask a valid IPv4 subnet mask
* @return boolean whether $address is within the ip rage made up of the subnet and mask
*/
private static function IPv4Match($address, $subnet, $mask)
{
// credit goes to Sam on http://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php5
if ((ip2long($address) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
return true;
}
return false;
}
/**
* Compress an IPv6 Address
*
* @param string $ip a valid IPv6 address or CIDR
* @return string IPv6 ip address or CIDR in short form notation
*/
public static function IPv6_compress($ip)
{
$bits = explode('/',$ip); // in case this is a CIDR range
// want to expand and re-compress in case we have "::" in different spots.
$bits[0] = self::IPv6_expand($bits[0]);
$bits[0] = inet_ntop(inet_pton($bits[0]));
return strtolower(implode('/',$bits));
}
/**
* Expand an IPv6 Address
*
* @param string $ip a valid IPv6 address
* @return string IPv6 ip address in long form notation
*/
public static function IPv6_expand($ip){
$bits = explode('/',$ip); // in case this is a CIDR range
// add missing components
if (strpos($bits[0], '::') !== false) {
$part = explode('::', $bits[0]);
$part[0] = explode(':', $part[0]);
$part[1] = explode(':', $part[1]);
$missing = array();
for ($i = 0; $i < (8 - (count($part[0]) + count($part[1]))); $i++)
array_push($missing, '0000');
$missing = array_merge($part[0], $missing);
$part = array_merge($missing, $part[1]);
} else {
$part = explode(":", $bits[0]);
}
// Pad components to 4 characters
foreach ($part as &$p) {
while (strlen($p) < 4) $p = '0' . $p;
}
unset($p);
$bits[0] = implode(':', $part);
// if it is the incorrect length, something went wrong.
if (strlen($bits[0]) != 39) {
return false;
}
return strtolower(implode('/',$bits));
}
}
?>
|