Created
June 30, 2010 03:50
-
-
Save gws/458218 to your computer and use it in GitHub Desktop.
Class for dealing with IPv4 and IPv6 addresses, specifically supporting storage in a relational database
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* ---------------------------------------------------------------------------- | |
* "THE BEER-WARE LICENSE" (Revision 42): | |
* gordon.stratton@gmail.com wrote this file. As long as you retain this notice | |
* you can do whatever you want with this stuff. If we meet some day, and you | |
* think this stuff is worth it, you can buy me a beer in return. | |
* ---------------------------------------------------------------------------- | |
*/ | |
/** | |
* Encapsulate an IP address | |
* | |
* This class supports both IPv4 and IPv6 addresses. | |
*/ | |
class Ip | |
{ | |
/** | |
* IP address in packed in_addr representation | |
* | |
* @var string | |
*/ | |
protected $ip; | |
/** | |
* Length of the packed in_addr IP address, in bytes | |
* | |
* @var int | |
*/ | |
protected $numBytes; | |
/** | |
* Version of the IP address | |
* | |
* @var int | |
*/ | |
protected $version; | |
/** | |
* Constructor | |
* | |
* Accepts an IPv4 or an IPv6 address in any valid format. | |
* | |
* @param string $raw Raw IP address | |
*/ | |
public function __construct($raw) | |
{ | |
$packed = @inet_pton($raw); | |
if ($packed === false) { | |
throw new InvalidArgumentException( | |
'Invalid IP address: inet_pton failed to understand it.' | |
); | |
} | |
$this->ip = $packed; | |
$this->numBytes = mb_strlen($packed, '8bit'); | |
if ($this->numBytes !== 4 && $this->numBytes !== 16) { | |
throw new InvalidArgumentException( | |
'Invalid IP address: length in bytes must be 4 or 16.' | |
); | |
} | |
$this->version = $this->numBytes === 4 ? 4 : 6; | |
} | |
/** | |
* Constructs an Ip object from an array of integers | |
* | |
* One likely use for this is to store an IP address in a database in an | |
* address-agnostic fashion. | |
* | |
* Note: The array of integers must be most-significant-integer first | |
* | |
* @return Ip | |
*/ | |
public static function fromIntegerArray(array $integers) | |
{ | |
if (count($integers) !== 4) { | |
throw new InvalidArgumentException( | |
'Wrong number of integers; expected 4' | |
); | |
} | |
list($i4, $i3, $i2, $i1) = $integers; | |
if ($i4 == 0 && $i3 == 0 && $i2 == 0x0000ffff) { | |
$packed = pack('N1', $i1); | |
} else { | |
$packed = pack('N4', $i4, $i3, $i2, $i1); | |
} | |
return new self(inet_ntop($packed)); | |
} | |
/** | |
* Formats the IP address using inet_ntop | |
* | |
* @return string | |
*/ | |
public function format() | |
{ | |
return inet_ntop($this->ip); | |
} | |
/** | |
* Marshals an Ip object to an array of integers | |
* | |
* One likely use for this is to store an IP address in a database in an | |
* address-agnostic fashion. | |
* | |
* Note: The array of integers will be most-significant-integer first | |
* | |
* @return array | |
*/ | |
public function toIntegerArray() | |
{ | |
$i4 = $i3 = $i2 = $i1 = 0; | |
if ($this->numBytes === 4) { | |
$i2 = 0x0000ffff; | |
list(, $i1) = unpack('N1', $this->ip); | |
} elseif ($this->numBytes === 16) { | |
list(, $i4, $i3, $i2, $i1) = unpack('N4', $this->ip); | |
} | |
// The checks and additions below work around the effects described in | |
// the "Caution" message on 32-bit systems here: | |
// | |
// http://php.net/manual/en/function.unpack.php | |
return array( | |
$i4 < 0 ? $i4 + 4294967296 : $i4, | |
$i3 < 0 ? $i3 + 4294967296 : $i3, | |
$i2 < 0 ? $i2 + 4294967296 : $i2, | |
$i1 < 0 ? $i1 + 4294967296 : $i1 | |
); | |
} | |
/** | |
* Tests if an Ip object belongs to a specific network | |
* | |
* Accepts a network base address and a prefix length. | |
* | |
* @param $base Base address for the network to test | |
* @param int $prefixlen Prefix length to test | |
* @return bool | |
*/ | |
public function isInNetwork(Ip $base, $prefixlen) | |
{ | |
$baseVersion = $base->getVersion(); | |
$thisVersion = $this->getVersion(); | |
if ($prefixlen < 0) { | |
throw new \InvalidArgumentException( | |
'Prefix length cannot be negative' | |
); | |
} | |
if ($baseVersion !== $thisVersion) { | |
throw new \InvalidArgumentException( | |
'Address version does not match supplied network base version' | |
); | |
} | |
if ($thisVersion === 4 && $prefixlen > 32) { | |
throw new \InvalidArgumentException( | |
'Address version (4) was not supplied a correct prefix length' | |
); | |
} | |
if ($thisVersion === 6 && $prefixlen > 128) { | |
throw new \InvalidArgumentException( | |
'Address version (6) was not supplied a correct prefix length' | |
); | |
} | |
// First create a 128-bit long binary string regardless of IP version. | |
// IPv4 addresses are left-padded with zeroes so that the algorithm | |
// can be used generically. | |
// | |
// Next, split the 128-bit string into 32-bit chunks, and convert them | |
// to unsigned integers. We can then use this to perform the bitwise | |
// comparisons that we need later. | |
$prefixIntegerArray = array_map( | |
'bindec', | |
str_split( | |
str_pad( | |
str_pad( | |
str_repeat('1', $prefixlen), | |
$thisVersion === 4 ? 32 : 128, | |
'0' | |
), | |
128, | |
'0', | |
STR_PAD_LEFT | |
), | |
32 | |
) | |
); | |
$baseIntegerArray = $base->toIntegerArray(); | |
$thisIntegerArray = $this->toIntegerArray(); | |
// If it's version 4, we just want to compare final array position | |
$start = $thisVersion === 4 ? 3 : 0; | |
for ($i = $start; $i < 4; $i++) { | |
$a = $thisIntegerArray[$i]; | |
$b = $baseIntegerArray[$i]; | |
$p = $prefixIntegerArray[$i]; | |
// If the address ANDed with the prefix mask is not equal to the | |
// base address ANDed with the prefix mask, we can bail. | |
if (($a & $p) !== ($b & $p)) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* Returns the IP version of this object | |
* | |
* @return int | |
*/ | |
public function getVersion() | |
{ | |
return $this->version; | |
} | |
/** | |
* @see format() | |
* @return string | |
*/ | |
public function __toString() | |
{ | |
return $this->format(); | |
} | |
} | |
// Quick tests to show usage | |
$v4 = new Ip('192.0.2.1'); | |
$v6 = new Ip('2001:db8::dead'); | |
var_dump($v4->format()); | |
var_dump($v6->format()); | |
var_dump($v4->getVersion()); | |
var_dump($v6->getVersion()); | |
var_dump($v4->toIntegerArray()); | |
var_dump($v6->toIntegerArray()); | |
var_dump($v4->isInNetwork(new Ip('192.0.2.0'), 24)); | |
var_dump($v4->isInNetwork(new Ip('192.0.3.0'), 24)); | |
var_dump($v6->isInNetwork(new Ip('2001:db8::'), 32)); | |
var_dump($v6->isInNetwork(new Ip('2001:db9::'), 32)); | |
$v4Roundtrip = Ip::fromIntegerArray($v4->toIntegerArray()); | |
$v6Roundtrip = Ip::fromIntegerArray($v6->toIntegerArray()); | |
var_dump($v4Roundtrip->format()); | |
var_dump($v6Roundtrip->format()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment