diff --git a/src/Network/IPRange.php b/src/Network/IPRange.php new file mode 100644 --- /dev/null +++ b/src/Network/IPRange.php @@ -0,0 +1,67 @@ + self::from("127.0.0.0/8"), + "IPv6" => self::from("::1/128"), + ]; + } + +} diff --git a/src/Network/IPv4Range.php b/src/Network/IPv4Range.php new file mode 100644 --- /dev/null +++ b/src/Network/IPv4Range.php @@ -0,0 +1,107 @@ +setBase($base); + $this->setNetworkBits($networkBits); + } + + /// + /// Getters and setters + /// + + /** + * @return string + */ + public function getBase () : string { + return $this->base; + } + + /** + * @param string $base + */ + public function setBase (string $base) : void { + if (!IP::isIPv4($base)) { + throw new InvalidArgumentException; + } + + $this->base = $base; + } + + /** + * @return int + */ + public function getNetworkBits () : int { + return $this->networkBits; + } + + /** + * @param int $networkBits + */ + public function setNetworkBits (int $networkBits) : void { + if ($networkBits < 0 || $networkBits > 32) { + throw new InvalidArgumentException; + } + + $this->networkBits = $networkBits; + } + + /// + /// Helper methods + /// + + public function getFirst () : string { + return $this->base; + } + + public function getLast () : string { + return long2ip(ip2long($this->base) + 2 ** $this->count() - 1); + } + + public function contains (string $ip) : bool { + if (!IP::isIP($ip)) { + throw new InvalidArgumentException; + } + + if (!IP::isIPv4($ip)) { + return false; + } + + $ipAsLong = ip2long($ip); + $baseAsLong = ip2long($this->base); + + return $ipAsLong >= $baseAsLong + && $ipAsLong <= $baseAsLong + $this->count() - 1; + + return false; + } + + /// + /// Countable interface + /// + + public function count () : int { + return 32 - $this->networkBits; + } + +} diff --git a/src/Network/IPv6.php b/src/Network/IPv6.php new file mode 100644 --- /dev/null +++ b/src/Network/IPv6.php @@ -0,0 +1,98 @@ +ip = $ip; + } + + public static function from (string $ip) : self { + $ipv6 = new self($ip); + + if (!$ipv6->isValid()) { + throw new InvalidArgumentException; + } + + $ipv6->normalize(); + + return $ipv6; + } + + public static function fromBinaryBits (array $bits) : self { + $fullBits = $bits + array_fill(0, 128, 0); + $hextets = []; + + for ($i = 0 ; $i < 8 ; $i++) { + // Read 16 bits + $slice = implode("", array_slice($fullBits, $i * 16, 16)); + $hextets[] = base_convert($slice, 2, 16); + } + + return self::from(implode(":", $hextets)); + } + + /// + /// Helper methods + /// + + public function isValid () : bool { + return IP::isIPv6($this->ip); + } + + public function increment (int $increment = 1) : self { + if ($increment === 0) { + return $this; + } + + if ($increment < 0) { + throw new InvalidArgumentException("This method doesn't support decrementation."); + } + + $ipAsNumericBinary = inet_pton($this->ip); + + // See https://gist.github.com/little-apps/88bbd23576008a84e0b6 + $i = strlen($ipAsNumericBinary) - 1; + $remainder = $increment; + + while ($remainder > 0 && $i >= 0) { + $sum = ord($ipAsNumericBinary[$i]) + $remainder; + $remainder = $sum / 256; + $ipAsNumericBinary[$i] = chr($sum % 256); + + --$i; + } + + $this->ip = inet_ntop($ipAsNumericBinary); + return $this; + } + + public function normalize () : self { + $this->ip = inet_ntop(inet_pton($this->ip)); + return $this; + } + + public function isNormalized() : bool { + return $this->ip === inet_ntop(inet_pton($this->ip)); + } + + /// + /// Magic methods + /// + + public function __toString () : string { + return $this->ip; + } +} diff --git a/src/Network/IPv6Range.php b/src/Network/IPv6Range.php new file mode 100644 --- /dev/null +++ b/src/Network/IPv6Range.php @@ -0,0 +1,119 @@ +setBase($base); + $this->setNetworkBits($networkBits); + } + + /// + /// Getters and setters + /// + + /** + * @return string + */ + public function getBase () : string { + return $this->base; + } + + /** + * @param string $base + */ + public function setBase (string $base) : void { + if (!IP::isIPv6($base)) { + throw new InvalidArgumentException; + } + + $this->base = $base; + } + + /** + * @return int + */ + public function getNetworkBits () : int { + return $this->networkBits; + } + + /** + * @param int $networkBits + */ + public function setNetworkBits (int $networkBits) : void { + if ($networkBits < 0 || $networkBits > 128) { + throw new InvalidArgumentException; + } + + $this->networkBits = $networkBits; + } + + /// + /// Helper methods + /// + + public function getFirst () : string { + return $this->base; + } + + public function getLast () : string { + if ($this->count() === 0) { + return $this->base; + } + + $base = inet_pton($this->getFirst()); + $mask = inet_pton($this->getInversedMask()); + return inet_ntop($base | $mask); + } + + private function getInversedMask () : string { + $bits = array_fill(0, $this->networkBits, 0) + array_fill(0, 128, 1); + + return (string)IPv6::fromBinaryBits($bits); + } + + public function contains (string $ip) : bool { + if (!IP::isIP($ip)) { + throw new InvalidArgumentException; + } + + if (IP::isIPv4($ip)) { + $ip = "::ffff:" . $ip; // IPv4-mapped IPv6 address + } + + $baseAsNumericBinary = inet_pton($this->getFirst()); + $lastAsNumericBinary = inet_pton($this->getLast()); + $ipAsNumericBinary = inet_pton($ip); + + return strlen($ipAsNumericBinary) == strlen($baseAsNumericBinary) + && $ipAsNumericBinary >= $baseAsNumericBinary + && $ipAsNumericBinary <= $lastAsNumericBinary; + } + + /// + /// Countable interface + /// + + public function count () : int { + return 128 - $this->networkBits; + } + +} diff --git a/tests/Network/IPv4RangeTest.php b/tests/Network/IPv4RangeTest.php new file mode 100644 --- /dev/null +++ b/tests/Network/IPv4RangeTest.php @@ -0,0 +1,47 @@ +range = IPRange::from("216.66.0.0/18"); + } + + /// + /// Tests + /// + + public function testGetBase () : void { + $this->assertEquals("216.66.0.0", $this->range->getBase()); + } + + public function testGetNetworkBits () : void { + $this->assertEquals(18, $this->range->getNetworkBits()); + } + + public function testCount () : void { + $this->assertEquals(14, $this->range->count()); // 14 + 18 = 32 bits + } + + public function testGetFirst () : void { + $this->assertEquals("216.66.0.0", $this->range->getFirst()); + } + + public function testGetLast () : void { + $this->assertEquals("216.66.63.255", $this->range->getLast()); + } + +} diff --git a/tests/Network/IPv6RangeTest.php b/tests/Network/IPv6RangeTest.php new file mode 100644 --- /dev/null +++ b/tests/Network/IPv6RangeTest.php @@ -0,0 +1,55 @@ +range = IPRange::from("2001:400::/23"); + } + + /// + /// Tests + /// + + public function testGetBase () : void { + $this->assertEquals("2001:400::", $this->range->getBase()); + } + + public function testGetNetworkBits () : void { + $this->assertEquals(23, $this->range->getNetworkBits()); + } + + public function testCount () : void { + $this->assertEquals(105, $this->range->count()); // 23 + 105 = 128 bits + } + + public function testGetFirst () : void { + $this->assertEquals("2001:400::", $this->range->getFirst()); + } + + public function testGetLast () : void { + $this->assertEquals("2001:5ff:ffff:ffff:ffff:ffff:ffff:ffff", $this->range->getLast()); + } + + public function testContains () : void { + $this->assertTrue($this->range->contains("2001:431::af")); + } + + public function testContainsWorksWithIPv4MappedIPv6Address () : void { + $this->assertTrue(IPRange::from("::ffff:0.0.0.0/96")->contains("1.2.3.4")); + } + +}