diff --git a/.gitignore b/.gitignore index d96108628..7cadd463c 100644 --- a/.gitignore +++ b/.gitignore @@ -147,6 +147,9 @@ mexitek/phpcolors/README.md mikey179/vfsstream +mlocati/ip-lib/README.md +mlocati/ip-lib/composer.json + mtdowling/jmespath.php/tests nextcloud/lognormalizer/.gitignore diff --git a/composer.json b/composer.json index 1dbdd4179..7e1ed00ff 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "laravel/serializable-closure": "^1.2", "mexitek/phpcolors": "^1.0", "microsoft/azure-storage-blob": "^1.5", + "mlocati/ip-lib": "^1.18", "nextcloud/lognormalizer": "^1.0", "nikic/php-parser": "^4.2", "opis/closure": "^3.6", diff --git a/composer.lock b/composer.lock index 2be33eee4..9ceed2f23 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "682bfa95cb2df85d82d78f313aebd5fd", + "content-hash": "37d86728a17ad5fe98c1e8086a6c0c51", "packages": [ { "name": "aws/aws-sdk-php", @@ -2238,6 +2238,77 @@ }, "time": "2020-12-28T07:59:51+00:00" }, + { + "name": "mlocati/ip-lib", + "version": "1.18.0", + "source": { + "type": "git", + "url": "https://github.com/mlocati/ip-lib.git", + "reference": "c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2", + "reference": "c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "IPLib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "homepage": "https://github.com/mlocati", + "role": "Author" + } + ], + "description": "Handle IPv4, IPv6 addresses and ranges", + "homepage": "https://github.com/mlocati/ip-lib", + "keywords": [ + "IP", + "address", + "addresses", + "ipv4", + "ipv6", + "manage", + "managing", + "matching", + "network", + "networking", + "range", + "subnet" + ], + "support": { + "issues": "https://github.com/mlocati/ip-lib/issues", + "source": "https://github.com/mlocati/ip-lib/tree/1.18.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/mlocati", + "type": "github" + }, + { + "url": "https://paypal.me/mlocati", + "type": "other" + } + ], + "time": "2022-01-13T18:05:33+00:00" + }, { "name": "mtdowling/jmespath.php", "version": "2.6.1", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index ad99ca644..316638824 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -1463,6 +1463,22 @@ 'ID3Parser\\getID3\\getid3_exception' => $vendorDir . '/christophwurst/id3parser/src/getID3/getid3_exception.php', 'ID3Parser\\getID3\\getid3_handler' => $vendorDir . '/christophwurst/id3parser/src/getID3/getid3_handler.php', 'ID3Parser\\getID3\\getid3_lib' => $vendorDir . '/christophwurst/id3parser/src/getID3/getid3_lib.php', + 'IPLib\\Address\\AddressInterface' => $vendorDir . '/mlocati/ip-lib/src/Address/AddressInterface.php', + 'IPLib\\Address\\AssignedRange' => $vendorDir . '/mlocati/ip-lib/src/Address/AssignedRange.php', + 'IPLib\\Address\\IPv4' => $vendorDir . '/mlocati/ip-lib/src/Address/IPv4.php', + 'IPLib\\Address\\IPv6' => $vendorDir . '/mlocati/ip-lib/src/Address/IPv6.php', + 'IPLib\\Address\\Type' => $vendorDir . '/mlocati/ip-lib/src/Address/Type.php', + 'IPLib\\Factory' => $vendorDir . '/mlocati/ip-lib/src/Factory.php', + 'IPLib\\ParseStringFlag' => $vendorDir . '/mlocati/ip-lib/src/ParseStringFlag.php', + 'IPLib\\Range\\AbstractRange' => $vendorDir . '/mlocati/ip-lib/src/Range/AbstractRange.php', + 'IPLib\\Range\\Pattern' => $vendorDir . '/mlocati/ip-lib/src/Range/Pattern.php', + 'IPLib\\Range\\RangeInterface' => $vendorDir . '/mlocati/ip-lib/src/Range/RangeInterface.php', + 'IPLib\\Range\\Single' => $vendorDir . '/mlocati/ip-lib/src/Range/Single.php', + 'IPLib\\Range\\Subnet' => $vendorDir . '/mlocati/ip-lib/src/Range/Subnet.php', + 'IPLib\\Range\\Type' => $vendorDir . '/mlocati/ip-lib/src/Range/Type.php', + 'IPLib\\Service\\BinaryMath' => $vendorDir . '/mlocati/ip-lib/src/Service/BinaryMath.php', + 'IPLib\\Service\\RangesFromBoundaryCalculator' => $vendorDir . '/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php', + 'IPLib\\Service\\UnsignedIntegerMath' => $vendorDir . '/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php', 'Icewind\\Streams\\CallbackWrapper' => $vendorDir . '/icewind/streams/src/CallbackWrapper.php', 'Icewind\\Streams\\CountWrapper' => $vendorDir . '/icewind/streams/src/CountWrapper.php', 'Icewind\\Streams\\Directory' => $vendorDir . '/icewind/streams/src/Directory.php', diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php index 4b572bd19..59242d02e 100644 --- a/composer/autoload_psr4.php +++ b/composer/autoload_psr4.php @@ -67,6 +67,7 @@ 'JsonSchema\\' => array($vendorDir . '/justinrainbow/json-schema/src/JsonSchema'), 'JmesPath\\' => array($vendorDir . '/mtdowling/jmespath.php/src'), 'Icewind\\Streams\\' => array($vendorDir . '/icewind/streams/src'), + 'IPLib\\' => array($vendorDir . '/mlocati/ip-lib/src'), 'ID3Parser\\' => array($vendorDir . '/christophwurst/id3parser/src'), 'Http\\Promise\\' => array($vendorDir . '/php-http/promise/src'), 'Http\\Client\\' => array($vendorDir . '/php-http/httplug/src'), diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 7efc962f8..353fc3128 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -235,6 +235,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'I' => array ( 'Icewind\\Streams\\' => 16, + 'IPLib\\' => 6, 'ID3Parser\\' => 10, ), 'H' => @@ -535,6 +536,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 array ( 0 => __DIR__ . '/..' . '/icewind/streams/src', ), + 'IPLib\\' => + array ( + 0 => __DIR__ . '/..' . '/mlocati/ip-lib/src', + ), 'ID3Parser\\' => array ( 0 => __DIR__ . '/..' . '/christophwurst/id3parser/src', @@ -2126,6 +2131,22 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'ID3Parser\\getID3\\getid3_exception' => __DIR__ . '/..' . '/christophwurst/id3parser/src/getID3/getid3_exception.php', 'ID3Parser\\getID3\\getid3_handler' => __DIR__ . '/..' . '/christophwurst/id3parser/src/getID3/getid3_handler.php', 'ID3Parser\\getID3\\getid3_lib' => __DIR__ . '/..' . '/christophwurst/id3parser/src/getID3/getid3_lib.php', + 'IPLib\\Address\\AddressInterface' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/AddressInterface.php', + 'IPLib\\Address\\AssignedRange' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/AssignedRange.php', + 'IPLib\\Address\\IPv4' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/IPv4.php', + 'IPLib\\Address\\IPv6' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/IPv6.php', + 'IPLib\\Address\\Type' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/Type.php', + 'IPLib\\Factory' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Factory.php', + 'IPLib\\ParseStringFlag' => __DIR__ . '/..' . '/mlocati/ip-lib/src/ParseStringFlag.php', + 'IPLib\\Range\\AbstractRange' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/AbstractRange.php', + 'IPLib\\Range\\Pattern' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Pattern.php', + 'IPLib\\Range\\RangeInterface' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/RangeInterface.php', + 'IPLib\\Range\\Single' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Single.php', + 'IPLib\\Range\\Subnet' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Subnet.php', + 'IPLib\\Range\\Type' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Type.php', + 'IPLib\\Service\\BinaryMath' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/BinaryMath.php', + 'IPLib\\Service\\RangesFromBoundaryCalculator' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php', + 'IPLib\\Service\\UnsignedIntegerMath' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php', 'Icewind\\Streams\\CallbackWrapper' => __DIR__ . '/..' . '/icewind/streams/src/CallbackWrapper.php', 'Icewind\\Streams\\CountWrapper' => __DIR__ . '/..' . '/icewind/streams/src/CountWrapper.php', 'Icewind\\Streams\\Directory' => __DIR__ . '/..' . '/icewind/streams/src/Directory.php', diff --git a/composer/installed.json b/composer/installed.json index 1def23058..6dac20eb5 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -2331,6 +2331,80 @@ }, "install-path": "../microsoft/azure-storage-common" }, + { + "name": "mlocati/ip-lib", + "version": "1.18.0", + "version_normalized": "1.18.0.0", + "source": { + "type": "git", + "url": "https://github.com/mlocati/ip-lib.git", + "reference": "c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2", + "reference": "c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.5 || ^9.5" + }, + "time": "2022-01-13T18:05:33+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "IPLib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "homepage": "https://github.com/mlocati", + "role": "Author" + } + ], + "description": "Handle IPv4, IPv6 addresses and ranges", + "homepage": "https://github.com/mlocati/ip-lib", + "keywords": [ + "IP", + "address", + "addresses", + "ipv4", + "ipv6", + "manage", + "managing", + "matching", + "network", + "networking", + "range", + "subnet" + ], + "support": { + "issues": "https://github.com/mlocati/ip-lib/issues", + "source": "https://github.com/mlocati/ip-lib/tree/1.18.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/mlocati", + "type": "github" + }, + { + "url": "https://paypal.me/mlocati", + "type": "other" + } + ], + "install-path": "../mlocati/ip-lib" + }, { "name": "mtdowling/jmespath.php", "version": "2.6.1", diff --git a/composer/installed.php b/composer/installed.php index bd2d77256..677e6f991 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'nextcloud/3rdparty', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'd6a35b6d5759c08dd268618951f9e5b1c18aa939', + 'reference' => 'f143482ffb0b8dfdbc08cd848ce2e66f02a5d9b6', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -307,6 +307,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'mlocati/ip-lib' => array( + 'pretty_version' => '1.18.0', + 'version' => '1.18.0.0', + 'reference' => 'c77bd0b1f3e3956c7e9661e75cb1f54ed67d95d2', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mlocati/ip-lib', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'mtdowling/jmespath.php' => array( 'pretty_version' => '2.6.1', 'version' => '2.6.1.0', @@ -319,7 +328,7 @@ 'nextcloud/3rdparty' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'd6a35b6d5759c08dd268618951f9e5b1c18aa939', + 'reference' => 'f143482ffb0b8dfdbc08cd848ce2e66f02a5d9b6', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), diff --git a/mlocati/ip-lib/LICENSE.txt b/mlocati/ip-lib/LICENSE.txt new file mode 100644 index 000000000..5c21e47cf --- /dev/null +++ b/mlocati/ip-lib/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2016 Michele Locati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/mlocati/ip-lib/ip-lib.php b/mlocati/ip-lib/ip-lib.php new file mode 100644 index 000000000..c34d0b49c --- /dev/null +++ b/mlocati/ip-lib/ip-lib.php @@ -0,0 +1,13 @@ +range = $range; + $this->type = $type; + $this->exceptions = $exceptions; + } + + /** + * Get the range definition. + * + * @return \IPLib\Range\RangeInterface + */ + public function getRange() + { + return $this->range; + } + + /** + * Get the range type. + * + * @return int one of the \IPLib\Range\Type::T_ constants + */ + public function getType() + { + return $this->type; + } + + /** + * Get the list of exceptions for this range type. + * + * @return \IPLib\Address\AssignedRange[] + */ + public function getExceptions() + { + return $this->exceptions; + } + + /** + * Get the assigned type for a specific address. + * + * @param \IPLib\Address\AddressInterface $address + * + * @return int|null return NULL of the address is outside this address; a \IPLib\Range\Type::T_ constant otherwise + */ + public function getAddressType(AddressInterface $address) + { + $result = null; + if ($this->range->contains($address)) { + foreach ($this->exceptions as $exception) { + $result = $exception->getAddressType($address); + if ($result !== null) { + break; + } + } + if ($result === null) { + $result = $this->type; + } + } + + return $result; + } + + /** + * Get the assigned type for a specific address range. + * + * @param \IPLib\Range\RangeInterface $range + * + * @return int|false|null return NULL of the range is fully outside this range; false if it's partly crosses this range (or it contains mixed types); a \IPLib\Range\Type::T_ constant otherwise + */ + public function getRangeType(RangeInterface $range) + { + $myStart = $this->range->getComparableStartString(); + $rangeEnd = $range->getComparableEndString(); + if ($myStart > $rangeEnd) { + $result = null; + } else { + $myEnd = $this->range->getComparableEndString(); + $rangeStart = $range->getComparableStartString(); + if ($myEnd < $rangeStart) { + $result = null; + } elseif ($rangeStart < $myStart || $rangeEnd > $myEnd) { + $result = false; + } else { + $result = null; + foreach ($this->exceptions as $exception) { + $result = $exception->getRangeType($range); + if ($result !== null) { + break; + } + } + if ($result === null) { + $result = $this->getType(); + } + } + } + + return $result; + } +} diff --git a/mlocati/ip-lib/src/Address/IPv4.php b/mlocati/ip-lib/src/Address/IPv4.php new file mode 100644 index 000000000..cd2c416f5 --- /dev/null +++ b/mlocati/ip-lib/src/Address/IPv4.php @@ -0,0 +1,515 @@ +address = $address; + $this->bytes = null; + $this->rangeType = null; + $this->comparableString = null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::__toString() + */ + public function __toString() + { + return $this->address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNumberOfBits() + */ + public static function getNumberOfBits() + { + return 32; + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $address the address to parse + * @param bool $mayIncludePort + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Address\IPv4::parseString() + * @since 1.1.0 added the $mayIncludePort argument + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($address, $mayIncludePort = true, $supportNonDecimalIPv4 = false) + { + return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Parse a string and returns an IPv4 instance if the string is valid, or null otherwise. + * + * @param string|mixed $address the address to parse + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($address, $flags = 0) + { + if (!is_string($address)) { + return null; + } + $flags = (int) $flags; + $matches = null; + if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) { + if (preg_match('/^([12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2})\.in-addr\.arpa\.?$/i', $address, $matches)) { + $address = implode('.', array_reverse(explode('.', $matches[1]))); + $flags = $flags & ~(ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED); + } + } + if ($flags & ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED) { + if (strpos($address, '.') === 0) { + return null; + } + $lengthNonHex = '{1,11}'; + $lengthHex = '{1,8}'; + $chunk234Optional = true; + } else { + if (!strpos($address, '.')) { + return null; + } + $lengthNonHex = '{1,3}'; + $lengthHex = '{1,2}'; + $chunk234Optional = false; + } + $rxChunk1 = "0?[0-9]{$lengthNonHex}"; + if ($flags & ParseStringFlag::IPV4_MAYBE_NON_DECIMAL) { + $rxChunk1 = "(?:0[Xx]0*[0-9A-Fa-f]{$lengthHex})|(?:{$rxChunk1})"; + $onlyDecimal = false; + } else { + $onlyDecimal = true; + } + $rxChunk1 = "0*?({$rxChunk1})"; + $rxChunk234 = "\.{$rxChunk1}"; + if ($chunk234Optional) { + $rxChunk234 = "(?:{$rxChunk234})?"; + } + $rx = "{$rxChunk1}{$rxChunk234}{$rxChunk234}{$rxChunk234}"; + if ($flags & ParseStringFlag::MAY_INCLUDE_PORT) { + $rx .= '(?::\d+)?'; + } + if (!preg_match('/^' . $rx . '$/', $address, $matches)) { + return null; + } + $math = new \IPLib\Service\UnsignedIntegerMath(); + $nums = array(); + $maxChunkIndex = count($matches) - 1; + for ($i = 1; $i <= $maxChunkIndex; $i++) { + $numBytes = $i === $maxChunkIndex ? 5 - $i : 1; + $chunkBytes = $math->getBytes($matches[$i], $numBytes, $onlyDecimal); + if ($chunkBytes === null) { + return null; + } + $nums = array_merge($nums, $chunkBytes); + } + + return new static(implode('.', $nums)); + } + + /** + * Parse an array of bytes and returns an IPv4 instance if the array is valid, or null otherwise. + * + * @param int[]|array $bytes + * + * @return static|null + */ + public static function fromBytes(array $bytes) + { + $result = null; + if (count($bytes) === 4) { + $chunks = array_map( + function ($byte) { + return (is_int($byte) && $byte >= 0 && $byte <= 255) ? (string) $byte : false; + }, + $bytes + ); + if (in_array(false, $chunks, true) === false) { + $result = new static(implode('.', $chunks)); + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::toString() + */ + public function toString($long = false) + { + if ($long) { + return $this->getComparableString(); + } + + return $this->address; + } + + /** + * Get the octal representation of this IP address. + * + * @param bool $long + * + * @return string + * + * @since 1.10.0 + * + * @example if $long == false: if the decimal representation is '0.7.8.255': '0.7.010.0377' + * @example if $long == true: if the decimal representation is '0.7.8.255': '0000.0007.0010.0377' + */ + public function toOctal($long = false) + { + $chunks = array(); + foreach ($this->getBytes() as $byte) { + if ($long) { + $chunks[] = sprintf('%04o', $byte); + } else { + $chunks[] = '0' . decoct($byte); + } + } + + return implode('.', $chunks); + } + + /** + * Get the hexadecimal representation of this IP address. + * + * @param bool $long + * + * @return string + * + * @since 1.10.0 + * + * @example if $long == false: if the decimal representation is '0.9.10.255': '0.9.0xa.0xff' + * @example if $long == true: if the decimal representation is '0.9.10.255': '0x00.0x09.0x0a.0xff' + */ + public function toHexadecimal($long = false) + { + $chunks = array(); + foreach ($this->getBytes() as $byte) { + if ($long) { + $chunks[] = sprintf('0x%02x', $byte); + } else { + $chunks[] = '0x' . dechex($byte); + } + } + + return implode('.', $chunks); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBytes() + */ + public function getBytes() + { + if ($this->bytes === null) { + $this->bytes = array_map( + function ($chunk) { + return (int) $chunk; + }, + explode('.', $this->address) + ); + } + + return $this->bytes; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBits() + */ + public function getBits() + { + $parts = array(); + foreach ($this->getBytes() as $byte) { + $parts[] = sprintf('%08b', $byte); + } + + return implode('', $parts); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressType() + */ + public function getAddressType() + { + return Type::T_IPv4; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType() + */ + public static function getDefaultReservedRangeType() + { + return RangeType::T_PUBLIC; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReservedRanges() + */ + public static function getReservedRanges() + { + if (self::$reservedRanges === null) { + $reservedRanges = array(); + foreach (array( + // RFC 5735 + '0.0.0.0/8' => array(RangeType::T_THISNETWORK, array('0.0.0.0/32' => RangeType::T_UNSPECIFIED)), + // RFC 5735 + '10.0.0.0/8' => array(RangeType::T_PRIVATENETWORK), + // RFC 6598 + '100.64.0.0/10' => array(RangeType::T_CGNAT), + // RFC 5735 + '127.0.0.0/8' => array(RangeType::T_LOOPBACK), + // RFC 5735 + '169.254.0.0/16' => array(RangeType::T_LINKLOCAL), + // RFC 5735 + '172.16.0.0/12' => array(RangeType::T_PRIVATENETWORK), + // RFC 5735 + '192.0.0.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '192.0.2.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '192.88.99.0/24' => array(RangeType::T_ANYCASTRELAY), + // RFC 5735 + '192.168.0.0/16' => array(RangeType::T_PRIVATENETWORK), + // RFC 5735 + '198.18.0.0/15' => array(RangeType::T_RESERVED), + // RFC 5735 + '198.51.100.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '203.0.113.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '224.0.0.0/4' => array(RangeType::T_MULTICAST), + // RFC 5735 + '240.0.0.0/4' => array(RangeType::T_RESERVED, array('255.255.255.255/32' => RangeType::T_LIMITEDBROADCAST)), + ) as $range => $data) { + $exceptions = array(); + if (isset($data[1])) { + foreach ($data[1] as $exceptionRange => $exceptionType) { + $exceptions[] = new AssignedRange(Subnet::parseString($exceptionRange), $exceptionType); + } + } + $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions); + } + self::$reservedRanges = $reservedRanges; + } + + return self::$reservedRanges; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getRangeType() + */ + public function getRangeType() + { + if ($this->rangeType === null) { + $rangeType = null; + foreach (static::getReservedRanges() as $reservedRange) { + $rangeType = $reservedRange->getAddressType($this); + if ($rangeType !== null) { + break; + } + } + $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType; + } + + return $this->rangeType; + } + + /** + * Create an IPv6 representation of this address (in 6to4 notation). + * + * @return \IPLib\Address\IPv6 + */ + public function toIPv6() + { + $myBytes = $this->getBytes(); + + return IPv6::parseString('2002:' . sprintf('%02x', $myBytes[0]) . sprintf('%02x', $myBytes[1]) . ':' . sprintf('%02x', $myBytes[2]) . sprintf('%02x', $myBytes[3]) . '::'); + } + + /** + * Create an IPv6 representation of this address (in IPv6 IPv4-mapped notation). + * + * @return \IPLib\Address\IPv6 + * + * @since 1.11.0 + */ + public function toIPv6IPv4Mapped() + { + return IPv6::fromBytes(array_merge(array(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff), $this->getBytes())); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getComparableString() + */ + public function getComparableString() + { + if ($this->comparableString === null) { + $chunks = array(); + foreach ($this->getBytes() as $byte) { + $chunks[] = sprintf('%03d', $byte); + } + $this->comparableString = implode('.', $chunks); + } + + return $this->comparableString; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::matches() + */ + public function matches(RangeInterface $range) + { + return $range->contains($this); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressAtOffset() + */ + public function getAddressAtOffset($n) + { + if (!is_int($n)) { + return null; + } + + $boundary = 256; + $mod = $n; + $bytes = $this->getBytes(); + for ($i = count($bytes) - 1; $i >= 0; $i--) { + $tmp = ($bytes[$i] + $mod) % $boundary; + $mod = (int) floor(($bytes[$i] + $mod) / $boundary); + if ($tmp < 0) { + $tmp += $boundary; + } + + $bytes[$i] = $tmp; + } + + if ($mod !== 0) { + return null; + } + + return static::fromBytes($bytes); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNextAddress() + */ + public function getNextAddress() + { + return $this->getAddressAtOffset(1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getPreviousAddress() + */ + public function getPreviousAddress() + { + return $this->getAddressAtOffset(-1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return implode( + '.', + array_reverse($this->getBytes()) + ) . '.in-addr.arpa'; + } +} diff --git a/mlocati/ip-lib/src/Address/IPv6.php b/mlocati/ip-lib/src/Address/IPv6.php new file mode 100644 index 000000000..e094881b6 --- /dev/null +++ b/mlocati/ip-lib/src/Address/IPv6.php @@ -0,0 +1,608 @@ +longAddress = $longAddress; + $this->shortAddress = null; + $this->bytes = null; + $this->words = null; + $this->rangeType = null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::__toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNumberOfBits() + */ + public static function getNumberOfBits() + { + return 128; + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag + * - if $mayIncludeZoneID is true, use the ParseStringFlag::MAY_INCLUDE_ZONEID flag + * + * @param string|mixed $address + * @param bool $mayIncludePort + * @param bool $mayIncludeZoneID + * + * @return static|null + * + * @see \IPLib\Address\IPv6::parseString() + * @since 1.1.0 added the $mayIncludePort argument + * @since 1.3.0 added the $mayIncludeZoneID argument + */ + public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true) + { + return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($mayIncludeZoneID ? ParseStringFlag::MAY_INCLUDE_ZONEID : 0)); + } + + /** + * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise. + * + * @param string|mixed $address the address to parse + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($address, $flags = 0) + { + if (!is_string($address)) { + return null; + } + $matches = null; + $flags = (int) $flags; + if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) { + if (preg_match('/^([0-9a-f](?:\.[0-9a-f]){31})\.ip6\.arpa\.?/i', $address, $matches)) { + $nibbles = array_reverse(explode('.', $matches[1])); + $quibbles = array(); + foreach (array_chunk($nibbles, 4) as $n) { + $quibbles[] = implode('', $n); + } + $address = implode(':', $quibbles); + } + } + $result = null; + if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) { + if ($flags & ParseStringFlag::MAY_INCLUDE_PORT && $address[0] === '[' && preg_match('/^\[(.+)]:\d+$/', $address, $matches)) { + $address = $matches[1]; + } + if ($flags & ParseStringFlag::MAY_INCLUDE_ZONEID) { + $percentagePos = strpos($address, '%'); + if ($percentagePos > 0) { + $address = substr($address, 0, $percentagePos); + } + } + if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) { + $address6 = static::parseString($matches[1] . '0:0'); + if ($address6 !== null) { + $address4 = IPv4::parseString($matches[2]); + if ($address4 !== null) { + $bytes4 = $address4->getBytes(); + $address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]); + $result = $address6; + } + } + } else { + if (strpos($address, '::') === false) { + $chunks = explode(':', $address); + } else { + $chunks = array(); + $parts = explode('::', $address); + if (count($parts) === 2) { + $before = ($parts[0] === '') ? array() : explode(':', $parts[0]); + $after = ($parts[1] === '') ? array() : explode(':', $parts[1]); + $missing = 8 - count($before) - count($after); + if ($missing >= 0) { + $chunks = $before; + if ($missing !== 0) { + $chunks = array_merge($chunks, array_fill(0, $missing, '0')); + } + $chunks = array_merge($chunks, $after); + } + } + } + if (count($chunks) === 8) { + $nums = array_map( + function ($chunk) { + return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false; + }, + $chunks + ); + if (!in_array(false, $nums, true)) { + $longAddress = implode( + ':', + array_map( + function ($num) { + return sprintf('%04x', $num); + }, + $nums + ) + ); + $result = new static($longAddress); + } + } + } + } + + return $result; + } + + /** + * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise. + * + * @param int[]|array $bytes + * + * @return static|null + */ + public static function fromBytes(array $bytes) + { + $result = null; + if (count($bytes) === 16) { + $address = ''; + for ($i = 0; $i < 16; $i++) { + if ($i !== 0 && $i % 2 === 0) { + $address .= ':'; + } + $byte = $bytes[$i]; + if (is_int($byte) && $byte >= 0 && $byte <= 255) { + $address .= sprintf('%02x', $byte); + } else { + $address = null; + break; + } + } + if ($address !== null) { + $result = new static($address); + } + } + + return $result; + } + + /** + * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise. + * + * @param int[]|array $words + * + * @return static|null + */ + public static function fromWords(array $words) + { + $result = null; + if (count($words) === 8) { + $chunks = array(); + for ($i = 0; $i < 8; $i++) { + $word = $words[$i]; + if (is_int($word) && $word >= 0 && $word <= 0xffff) { + $chunks[] = sprintf('%04x', $word); + } else { + $chunks = null; + break; + } + } + if ($chunks !== null) { + $result = new static(implode(':', $chunks)); + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::toString() + */ + public function toString($long = false) + { + if ($long) { + $result = $this->longAddress; + } else { + if ($this->shortAddress === null) { + if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) { + $lastBytes = array_slice($this->getBytes(), -4); + $this->shortAddress = '::ffff:' . implode('.', $lastBytes); + } else { + $chunks = array_map( + function ($word) { + return dechex($word); + }, + $this->getWords() + ); + $shortAddress = implode(':', $chunks); + $matches = null; + for ($i = 8; $i > 1; $i--) { + $search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)'; + if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) { + $shortAddress = $matches[1] . '::' . $matches[2]; + break; + } + } + $this->shortAddress = $shortAddress; + } + } + $result = $this->shortAddress; + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBytes() + */ + public function getBytes() + { + if ($this->bytes === null) { + $bytes = array(); + foreach ($this->getWords() as $word) { + $bytes[] = $word >> 8; + $bytes[] = $word & 0xff; + } + $this->bytes = $bytes; + } + + return $this->bytes; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBits() + */ + public function getBits() + { + $parts = array(); + foreach ($this->getBytes() as $byte) { + $parts[] = sprintf('%08b', $byte); + } + + return implode('', $parts); + } + + /** + * Get the word list of the IP address. + * + * @return int[] + */ + public function getWords() + { + if ($this->words === null) { + $this->words = array_map( + function ($chunk) { + return hexdec($chunk); + }, + explode(':', $this->longAddress) + ); + } + + return $this->words; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressType() + */ + public function getAddressType() + { + return Type::T_IPv6; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType() + */ + public static function getDefaultReservedRangeType() + { + return RangeType::T_RESERVED; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReservedRanges() + */ + public static function getReservedRanges() + { + if (self::$reservedRanges === null) { + $reservedRanges = array(); + foreach (array( + // RFC 4291 + '::/128' => array(RangeType::T_UNSPECIFIED), + // RFC 4291 + '::1/128' => array(RangeType::T_LOOPBACK), + // RFC 4291 + '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)), + //'2002::/16' => array(RangeType::), + // RFC 4291 + '2000::/3' => array(RangeType::T_PUBLIC), + // RFC 4193 + 'fc00::/7' => array(RangeType::T_PRIVATENETWORK), + // RFC 4291 + 'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST), + // RFC 4291 + 'ff00::/8' => array(RangeType::T_MULTICAST), + // RFC 4291 + //'::/8' => array(RangeType::T_RESERVED), + // RFC 4048 + //'200::/7' => array(RangeType::T_RESERVED), + // RFC 4291 + //'400::/6' => array(RangeType::T_RESERVED), + // RFC 4291 + //'800::/5' => array(RangeType::T_RESERVED), + // RFC 4291 + //'1000::/4' => array(RangeType::T_RESERVED), + // RFC 4291 + //'4000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'6000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'8000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'a000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'c000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'e000::/4' => array(RangeType::T_RESERVED), + // RFC 4291 + //'f000::/5' => array(RangeType::T_RESERVED), + // RFC 4291 + //'f800::/6' => array(RangeType::T_RESERVED), + // RFC 4291 + //'fe00::/9' => array(RangeType::T_RESERVED), + // RFC 3879 + //'fec0::/10' => array(RangeType::T_RESERVED), + ) as $range => $data) { + $exceptions = array(); + if (isset($data[1])) { + foreach ($data[1] as $exceptionRange => $exceptionType) { + $exceptions[] = new AssignedRange(Subnet::parseString($exceptionRange), $exceptionType); + } + } + $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions); + } + self::$reservedRanges = $reservedRanges; + } + + return self::$reservedRanges; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getRangeType() + */ + public function getRangeType() + { + if ($this->rangeType === null) { + $ipv4 = $this->toIPv4(); + if ($ipv4 !== null) { + $this->rangeType = $ipv4->getRangeType(); + } else { + $rangeType = null; + foreach (static::getReservedRanges() as $reservedRange) { + $rangeType = $reservedRange->getAddressType($this); + if ($rangeType !== null) { + break; + } + } + $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType; + } + } + + return $this->rangeType; + } + + /** + * Create an IPv4 representation of this address (if possible, otherwise returns null). + * + * @return \IPLib\Address\IPv4|null + */ + public function toIPv4() + { + if (strpos($this->longAddress, '2002:') === 0) { + // 6to4 + return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4)); + } + if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) { + // IPv4-mapped IPv6 addresses + return IPv4::fromBytes(array_slice($this->getBytes(), -4)); + } + + return null; + } + + /** + * Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax. + * + * @param bool $ipV6Long render the IPv6 part in "long" format? + * @param bool $ipV4Long render the IPv4 part in "long" format? + * + * @return string + * + * @example '::13.1.68.3' + * @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true + * @example '::013.001.068.003' when $ipV4Long is true + * @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true + * + * @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3. + * @since 1.9.0 + */ + public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false) + { + $myBytes = $this->getBytes(); + $ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff)); + $ipv6String = static::fromBytes($ipv6Bytes)->toString($ipV6Long); + $ipv4Bytes = array_slice($myBytes, 12, 4); + $ipv4String = IPv4::fromBytes($ipv4Bytes)->toString($ipV4Long); + + return preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getComparableString() + */ + public function getComparableString() + { + return $this->longAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::matches() + */ + public function matches(RangeInterface $range) + { + return $range->contains($this); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressAtOffset() + */ + public function getAddressAtOffset($n) + { + if (!is_int($n)) { + return null; + } + + $boundary = 0x10000; + $mod = $n; + $words = $this->getWords(); + for ($i = count($words) - 1; $i >= 0; $i--) { + $tmp = ($words[$i] + $mod) % $boundary; + $mod = (int) floor(($words[$i] + $mod) / $boundary); + if ($tmp < 0) { + $tmp += $boundary; + } + + $words[$i] = $tmp; + } + + if ($mod !== 0) { + return null; + } + + return static::fromWords($words); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNextAddress() + */ + public function getNextAddress() + { + return $this->getAddressAtOffset(1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getPreviousAddress() + */ + public function getPreviousAddress() + { + return $this->getAddressAtOffset(-1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return implode( + '.', + array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1)) + ) . '.ip6.arpa'; + } +} diff --git a/mlocati/ip-lib/src/Address/Type.php b/mlocati/ip-lib/src/Address/Type.php new file mode 100644 index 000000000..17488f381 --- /dev/null +++ b/mlocati/ip-lib/src/Address/Type.php @@ -0,0 +1,44 @@ +getNumberOfBits())); + } + $numberOfBits = $from->getNumberOfBits(); + if ($to->getNumberOfBits() !== $numberOfBits) { + return null; + } + $calculator = new RangesFromBoundaryCalculator($numberOfBits); + + return $calculator->getRanges($from, $to); + } + + /** + * @param \IPLib\Address\AddressInterface $from + * @param \IPLib\Address\AddressInterface $to + * + * @return \IPLib\Range\RangeInterface|null + * + * @since 1.2.0 + */ + protected static function rangeFromBoundaryAddresses(AddressInterface $from = null, AddressInterface $to = null) + { + if ($from === null && $to === null) { + $result = null; + } elseif ($to === null) { + $result = Range\Single::fromAddress($from); + } elseif ($from === null) { + $result = Range\Single::fromAddress($to); + } else { + $result = null; + $addressType = $from->getAddressType(); + if ($addressType === $to->getAddressType()) { + $cmp = strcmp($from->getComparableString(), $to->getComparableString()); + if ($cmp === 0) { + $result = Range\Single::fromAddress($from); + } else { + if ($cmp > 0) { + list($from, $to) = array($to, $from); + } + $fromBytes = $from->getBytes(); + $toBytes = $to->getBytes(); + $numBytes = count($fromBytes); + $sameBits = 0; + for ($byteIndex = 0; $byteIndex < $numBytes; $byteIndex++) { + $fromByte = $fromBytes[$byteIndex]; + $toByte = $toBytes[$byteIndex]; + if ($fromByte === $toByte) { + $sameBits += 8; + } else { + $differentBitsInByte = decbin($fromByte ^ $toByte); + $sameBits += 8 - strlen($differentBitsInByte); + break; + } + } + $result = static::parseRangeString($from->toString() . '/' . (string) $sameBits); + } + } + } + + return $result; + } + + /** + * @param string|\IPLib\Address\AddressInterface $from + * @param string|\IPLib\Address\AddressInterface $to + * @param int $flags + * + * @return \IPLib\Address\AddressInterface[]|null[]|false[] + */ + private static function parseBoundaries($from, $to, $flags = 0) + { + $result = array(); + foreach (array('from', 'to') as $param) { + $value = $$param; + if (!($value instanceof AddressInterface)) { + $value = (string) $value; + if ($value === '') { + $value = null; + } else { + $value = static::parseAddressString($value, $flags); + if ($value === null) { + $value = false; + } + } + } + $result[] = $value; + } + if ($result[0] && $result[1] && strcmp($result[0]->getComparableString(), $result[1]->getComparableString()) > 0) { + $result = array($result[1], $result[0]); + } + + return $result; + } +} diff --git a/mlocati/ip-lib/src/ParseStringFlag.php b/mlocati/ip-lib/src/ParseStringFlag.php new file mode 100644 index 000000000..860256e75 --- /dev/null +++ b/mlocati/ip-lib/src/ParseStringFlag.php @@ -0,0 +1,79 @@ + 5.0.0.1 + * @example 5.256 => 5.0.1.0 + * @example 5.0.256 => 5.0.1.0 + * @example 123456789 => 7.91.205.21 + */ + const IPV4_MAYBE_NON_DECIMAL = 4; + + /** + * Use this flag if IPv4 subnet ranges may be in compact form. + * + * @example 127/24 => 127.0.0.0/24 + * @example 10/8 => 10.0.0.0/8 + * @example 10/24 => 10.0.0.0/24 + * @example 10.10.10/24 => 10.10.10.0/24 + * + * @var int + */ + const IPV4SUBNET_MAYBE_COMPACT = 8; + + /** + * Use this flag if IPv4 addresses may be in non quad-dotted notation. + * This notation is accepted by the implementation of inet_aton and inet_addr of the libc implementation of GNU, Windows and Mac (but not Musl), but not by inet_pton and ip2long. + * + * @var int + * + * @example 5.1 => 5.0.0.1 + * @example 5.256 => 5.0.1.0 + * @example 5.0.256 => 5.0.1.0 + * @example 123456789 => 7.91.205.21 + * + * @see https://man7.org/linux/man-pages/man3/inet_addr.3.html#DESCRIPTION + * @see https://www.freebsd.org/cgi/man.cgi?query=inet_net&sektion=3&apropos=0&manpath=FreeBSD+12.2-RELEASE+and+Ports#end + * @see http://git.musl-libc.org/cgit/musl/tree/src/network/inet_aton.c?h=v1.2.2 + */ + const IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED = 16; + + /** + * Use this flag if you want to accept parsing IPv4/IPv6 addresses in Reverse DNS Lookup Address format. + * + * @var int + * + * @since 1.18.0 + * + * @example 140.13.12.10.in-addr.arpa => 10.12.13.140 + * @example b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa => 4321:0:1:2:3:4:567:89ab + */ + const ADDRESS_MAYBE_RDNS = 32; +} diff --git a/mlocati/ip-lib/src/Range/AbstractRange.php b/mlocati/ip-lib/src/Range/AbstractRange.php new file mode 100644 index 000000000..122043506 --- /dev/null +++ b/mlocati/ip-lib/src/Range/AbstractRange.php @@ -0,0 +1,125 @@ +rangeType === null) { + $addressType = $this->getAddressType(); + if ($addressType === AddressType::T_IPv6 && Subnet::get6to4()->containsRange($this)) { + $this->rangeType = Factory::getRangeFromBoundaries($this->fromAddress->toIPv4(), $this->toAddress->toIPv4())->getRangeType(); + } else { + switch ($addressType) { + case AddressType::T_IPv4: + $defaultType = IPv4::getDefaultReservedRangeType(); + $reservedRanges = IPv4::getReservedRanges(); + break; + case AddressType::T_IPv6: + $defaultType = IPv6::getDefaultReservedRangeType(); + $reservedRanges = IPv6::getReservedRanges(); + break; + default: + throw new \Exception('@todo'); // @codeCoverageIgnore + } + $rangeType = null; + foreach ($reservedRanges as $reservedRange) { + $rangeType = $reservedRange->getRangeType($this); + if ($rangeType !== null) { + break; + } + } + $this->rangeType = $rangeType === null ? $defaultType : $rangeType; + } + } + + return $this->rangeType === false ? null : $this->rangeType; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressAtOffset() + */ + public function getAddressAtOffset($n) + { + if (!is_int($n)) { + return null; + } + + $address = null; + if ($n >= 0) { + $start = Factory::parseAddressString($this->getComparableStartString()); + $address = $start->getAddressAtOffset($n); + } else { + $end = Factory::parseAddressString($this->getComparableEndString()); + $address = $end->getAddressAtOffset($n + 1); + } + + if ($address === null) { + return null; + } + + return $this->contains($address) ? $address : null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::contains() + */ + public function contains(AddressInterface $address) + { + $result = false; + if ($address->getAddressType() === $this->getAddressType()) { + $cmp = $address->getComparableString(); + $from = $this->getComparableStartString(); + if ($cmp >= $from) { + $to = $this->getComparableEndString(); + if ($cmp <= $to) { + $result = true; + } + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::containsRange() + */ + public function containsRange(RangeInterface $range) + { + $result = false; + if ($range->getAddressType() === $this->getAddressType()) { + $myStart = $this->getComparableStartString(); + $itsStart = $range->getComparableStartString(); + if ($itsStart >= $myStart) { + $myEnd = $this->getComparableEndString(); + $itsEnd = $range->getComparableEndString(); + if ($itsEnd <= $myEnd) { + $result = true; + } + } + } + + return $result; + } +} diff --git a/mlocati/ip-lib/src/Range/Pattern.php b/mlocati/ip-lib/src/Range/Pattern.php new file mode 100644 index 000000000..1e180eadb --- /dev/null +++ b/mlocati/ip-lib/src/Range/Pattern.php @@ -0,0 +1,322 @@ +fromAddress = $fromAddress; + $this->toAddress = $toAddress; + $this->asterisksCount = $asterisksCount; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::__toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $range + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Range\Pattern::parseString() + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($range, $supportNonDecimalIPv4 = false) + { + return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Try get the range instance starting from its string representation. + * + * @param string|mixed $range + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @since 1.17.0 + * @see \IPLib\ParseStringFlag + */ + public static function parseString($range, $flags = 0) + { + if (!is_string($range) || strpos($range, '*') === false) { + return null; + } + if ($range === '*.*.*.*') { + return new static(IPv4::parseString('0.0.0.0'), IPv4::parseString('255.255.255.255'), 4); + } + if ($range === '*:*:*:*:*:*:*:*') { + return new static(IPv6::parseString('::'), IPv6::parseString('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), 8); + } + $matches = null; + if (strpos($range, '.') !== false && preg_match('/^[^*]+((?:\.\*)+)$/', $range, $matches)) { + $asterisksCount = strlen($matches[1]) >> 1; + if ($asterisksCount > 0) { + $missingDots = 3 - substr_count($range, '.'); + if ($missingDots > 0) { + $range .= str_repeat('.*', $missingDots); + $asterisksCount += $missingDots; + } + } + $fromAddress = IPv4::parseString(str_replace('*', '0', $range), $flags); + if ($fromAddress === null) { + return null; + } + $fixedBytes = array_slice($fromAddress->getBytes(), 0, -$asterisksCount); + $otherBytes = array_fill(0, $asterisksCount, 255); + $toAddress = IPv4::fromBytes(array_merge($fixedBytes, $otherBytes)); + + return new static($fromAddress, $toAddress, $asterisksCount); + } + if (strpos($range, ':') !== false && preg_match('/^[^*]+((?::\*)+)$/', $range, $matches)) { + $asterisksCount = strlen($matches[1]) >> 1; + $fromAddress = IPv6::parseString(str_replace('*', '0', $range)); + if ($fromAddress === null) { + return null; + } + $fixedWords = array_slice($fromAddress->getWords(), 0, -$asterisksCount); + $otherWords = array_fill(0, $asterisksCount, 0xffff); + $toAddress = IPv6::fromWords(array_merge($fixedWords, $otherWords)); + + return new static($fromAddress, $toAddress, $asterisksCount); + } + + return null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::toString() + */ + public function toString($long = false) + { + if ($this->asterisksCount === 0) { + return $this->fromAddress->toString($long); + } + switch (true) { + case $this->fromAddress instanceof \IPLib\Address\IPv4: + $chunks = explode('.', $this->fromAddress->toString()); + $chunks = array_slice($chunks, 0, -$this->asterisksCount); + $chunks = array_pad($chunks, 4, '*'); + $result = implode('.', $chunks); + break; + case $this->fromAddress instanceof \IPLib\Address\IPv6: + if ($long) { + $chunks = explode(':', $this->fromAddress->toString(true)); + $chunks = array_slice($chunks, 0, -$this->asterisksCount); + $chunks = array_pad($chunks, 8, '*'); + $result = implode(':', $chunks); + } elseif ($this->asterisksCount === 8) { + $result = '*:*:*:*:*:*:*:*'; + } else { + $bytes = $this->toAddress->getBytes(); + $bytes = array_slice($bytes, 0, -$this->asterisksCount * 2); + $bytes = array_pad($bytes, 16, 1); + $address = IPv6::fromBytes($bytes); + $before = substr($address->toString(false), 0, -strlen(':101') * $this->asterisksCount); + $result = $before . str_repeat(':*', $this->asterisksCount); + } + break; + default: + throw new \Exception('@todo'); // @codeCoverageIgnore + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressType() + */ + public function getAddressType() + { + return $this->fromAddress->getAddressType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getStartAddress() + */ + public function getStartAddress() + { + return $this->fromAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getEndAddress() + */ + public function getEndAddress() + { + return $this->toAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableStartString() + */ + public function getComparableStartString() + { + return $this->fromAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableEndString() + */ + public function getComparableEndString() + { + return $this->toAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asSubnet() + * @since 1.8.0 + */ + public function asSubnet() + { + return new Subnet($this->getStartAddress(), $this->getEndAddress(), $this->getNetworkPrefix()); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asPattern() + */ + public function asPattern() + { + return $this; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSubnetMask() + */ + public function getSubnetMask() + { + if ($this->getAddressType() !== AddressType::T_IPv4) { + return null; + } + switch ($this->asterisksCount) { + case 0: + $bytes = array(255, 255, 255, 255); + break; + case 4: + $bytes = array(0, 0, 0, 0); + break; + default: + $bytes = array_pad(array_fill(0, 4 - $this->asterisksCount, 255), 4, 0); + break; + } + + return IPv4::fromBytes($bytes); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return $this->asterisksCount === 0 ? array($this->getStartAddress()->getReverseDNSLookupName()) : $this->asSubnet()->getReverseDNSLookupName(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSize() + */ + public function getSize() + { + $fromAddress = $this->fromAddress; + $maxPrefix = $fromAddress::getNumberOfBits(); + $prefix = $this->getNetworkPrefix(); + + return pow(2, ($maxPrefix - $prefix)); + } + + /** + * @return float|int + */ + private function getNetworkPrefix() + { + switch ($this->getAddressType()) { + case AddressType::T_IPv4: + return 8 * (4 - $this->asterisksCount); + case AddressType::T_IPv6: + return 16 * (8 - $this->asterisksCount); + } + } +} diff --git a/mlocati/ip-lib/src/Range/RangeInterface.php b/mlocati/ip-lib/src/Range/RangeInterface.php new file mode 100644 index 000000000..4d4d83471 --- /dev/null +++ b/mlocati/ip-lib/src/Range/RangeInterface.php @@ -0,0 +1,160 @@ +address = $address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::__toString() + */ + public function __toString() + { + return $this->address->__toString(); + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $range + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Range\Single::parseString() + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($range, $supportNonDecimalIPv4 = false) + { + return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Try get the range instance starting from its string representation. + * + * @param string|mixed $range + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($range, $flags = 0) + { + $result = null; + $flags = (int) $flags; + $address = Factory::parseAddressString($range, $flags); + if ($address !== null) { + $result = new static($address); + } + + return $result; + } + + /** + * Create the range instance starting from an address instance. + * + * @param \IPLib\Address\AddressInterface $address + * + * @return static + * + * @since 1.2.0 + */ + public static function fromAddress(AddressInterface $address) + { + return new static($address); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::toString() + */ + public function toString($long = false) + { + return $this->address->toString($long); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressType() + */ + public function getAddressType() + { + return $this->address->getAddressType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getRangeType() + */ + public function getRangeType() + { + return $this->address->getRangeType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::contains() + */ + public function contains(AddressInterface $address) + { + $result = false; + if ($address->getAddressType() === $this->getAddressType()) { + if ($address->toString(false) === $this->address->toString(false)) { + $result = true; + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getStartAddress() + */ + public function getStartAddress() + { + return $this->address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getEndAddress() + */ + public function getEndAddress() + { + return $this->address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableStartString() + */ + public function getComparableStartString() + { + return $this->address->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableEndString() + */ + public function getComparableEndString() + { + return $this->address->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asSubnet() + */ + public function asSubnet() + { + $networkPrefixes = array( + AddressType::T_IPv4 => 32, + AddressType::T_IPv6 => 128, + ); + + return new Subnet($this->address, $this->address, $networkPrefixes[$this->address->getAddressType()]); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asPattern() + */ + public function asPattern() + { + return new Pattern($this->address, $this->address, 0); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSubnetMask() + */ + public function getSubnetMask() + { + if ($this->getAddressType() !== AddressType::T_IPv4) { + return null; + } + + return IPv4::fromBytes(array(255, 255, 255, 255)); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return array($this->getStartAddress()->getReverseDNSLookupName()); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSize() + */ + public function getSize() + { + return 1; + } +} diff --git a/mlocati/ip-lib/src/Range/Subnet.php b/mlocati/ip-lib/src/Range/Subnet.php new file mode 100644 index 000000000..ea3976229 --- /dev/null +++ b/mlocati/ip-lib/src/Range/Subnet.php @@ -0,0 +1,354 @@ +fromAddress = $fromAddress; + $this->toAddress = $toAddress; + $this->networkPrefix = $networkPrefix; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::__toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $range + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Range\Subnet::parseString() + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($range, $supportNonDecimalIPv4 = false) + { + return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Try get the range instance starting from its string representation. + * + * @param string|mixed $range + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($range, $flags = 0) + { + if (!is_string($range)) { + return null; + } + $parts = explode('/', $range); + if (count($parts) !== 2) { + return null; + } + $flags = (int) $flags; + if (strpos($parts[0], ':') === false && $flags & ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT) { + $missingDots = 3 - substr_count($parts[0], '.'); + if ($missingDots > 0) { + $parts[0] .= str_repeat('.0', $missingDots); + } + } + $address = Factory::parseAddressString($parts[0], $flags); + if ($address === null) { + return null; + } + if (!preg_match('/^[0-9]{1,9}$/', $parts[1])) { + return null; + } + $networkPrefix = (int) $parts[1]; + $addressBytes = $address->getBytes(); + $totalBytes = count($addressBytes); + $numDifferentBits = $totalBytes * 8 - $networkPrefix; + if ($numDifferentBits < 0) { + return null; + } + $numSameBytes = $networkPrefix >> 3; + $sameBytes = array_slice($addressBytes, 0, $numSameBytes); + $differentBytesStart = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 0); + $differentBytesEnd = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 255); + $startSameBits = $networkPrefix % 8; + if ($startSameBits !== 0) { + $varyingByte = $addressBytes[$numSameBytes]; + $differentBytesStart[0] = $varyingByte & bindec(str_pad(str_repeat('1', $startSameBits), 8, '0', STR_PAD_RIGHT)); + $differentBytesEnd[0] = $differentBytesStart[0] + bindec(str_repeat('1', 8 - $startSameBits)); + } + + return new static( + Factory::addressFromBytes(array_merge($sameBytes, $differentBytesStart)), + Factory::addressFromBytes(array_merge($sameBytes, $differentBytesEnd)), + $networkPrefix + ); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::toString() + */ + public function toString($long = false) + { + return $this->fromAddress->toString($long) . '/' . $this->networkPrefix; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressType() + */ + public function getAddressType() + { + return $this->fromAddress->getAddressType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getStartAddress() + */ + public function getStartAddress() + { + return $this->fromAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getEndAddress() + */ + public function getEndAddress() + { + return $this->toAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableStartString() + */ + public function getComparableStartString() + { + return $this->fromAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableEndString() + */ + public function getComparableEndString() + { + return $this->toAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asSubnet() + */ + public function asSubnet() + { + return $this; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asPattern() + * @since 1.8.0 + */ + public function asPattern() + { + $address = $this->getStartAddress(); + $networkPrefix = $this->getNetworkPrefix(); + switch ($address->getAddressType()) { + case AddressType::T_IPv4: + return $networkPrefix % 8 === 0 ? new Pattern($address, $address, 4 - $networkPrefix / 8) : null; + case AddressType::T_IPv6: + return $networkPrefix % 16 === 0 ? new Pattern($address, $address, 8 - $networkPrefix / 16) : null; + } + } + + /** + * Get the 6to4 address IPv6 address range. + * + * @return self + * + * @since 1.5.0 + */ + public static function get6to4() + { + if (self::$sixToFour === null) { + self::$sixToFour = self::parseString('2002::/16'); + } + + return self::$sixToFour; + } + + /** + * Get subnet prefix. + * + * @return int + * + * @since 1.7.0 + */ + public function getNetworkPrefix() + { + return $this->networkPrefix; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSubnetMask() + */ + public function getSubnetMask() + { + if ($this->getAddressType() !== AddressType::T_IPv4) { + return null; + } + $bytes = array(); + $prefix = $this->getNetworkPrefix(); + while ($prefix >= 8) { + $bytes[] = 255; + $prefix -= 8; + } + if ($prefix !== 0) { + $bytes[] = bindec(str_pad(str_repeat('1', $prefix), 8, '0')); + } + $bytes = array_pad($bytes, 4, 0); + + return IPv4::fromBytes($bytes); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + switch ($this->getAddressType()) { + case AddressType::T_IPv4: + $unitSize = 8; // bytes + $maxUnits = 4; + $isHex = false; + $rxUnit = '\d+'; + break; + case AddressType::T_IPv6: + $unitSize = 4; // nibbles + $maxUnits = 32; + $isHex = true; + $rxUnit = '[0-9A-Fa-f]'; + break; + } + $totBits = $unitSize * $maxUnits; + $prefixUnits = (int) ($this->networkPrefix / $unitSize); + $extraBits = ($totBits - $this->networkPrefix) % $unitSize; + if ($extraBits !== 0) { + $prefixUnits += 1; + } + $numVariants = 1 << $extraBits; + $result = array(); + $unitsToRemove = $maxUnits - $prefixUnits; + $initialPointer = preg_replace("/^(({$rxUnit})\.){{$unitsToRemove}}/", '', $this->getStartAddress()->getReverseDNSLookupName()); + $chunks = explode('.', $initialPointer, 2); + for ($index = 0; $index < $numVariants; $index++) { + if ($index !== 0) { + $chunks[0] = $isHex ? dechex(1 + hexdec($chunks[0])) : (string) (1 + (int) $chunks[0]); + } + $result[] = implode('.', $chunks); + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSize() + */ + public function getSize() + { + $fromAddress = $this->fromAddress; + $maxPrefix = $fromAddress::getNumberOfBits(); + $prefix = $this->getNetworkPrefix(); + + return pow(2, ($maxPrefix - $prefix)); + } +} diff --git a/mlocati/ip-lib/src/Range/Type.php b/mlocati/ip-lib/src/Range/Type.php new file mode 100644 index 000000000..b2ba8eb7c --- /dev/null +++ b/mlocati/ip-lib/src/Range/Type.php @@ -0,0 +1,152 @@ +toSameLength($a, $b); + + return $a < $b ? -1 : ($a > $b ? 1 : 0); + } + + /** + * Add 1 to a non-negative integer represented in binary form. + * + * @param string $value + * + * @return string + */ + public function increment($value) + { + $lastZeroIndex = strrpos($value, '0'); + if ($lastZeroIndex === false) { + return '1' . str_repeat('0', strlen($value)); + } + + return ltrim(substr($value, 0, $lastZeroIndex), '0') . '1' . str_repeat('0', strlen($value) - $lastZeroIndex - 1); + } + + /** + * Calculate the bitwise AND of two non-negative integers represented in binary form. + * + * @param string $operand1 + * @param string $operand2 + * + * @return string + */ + public function andX($operand1, $operand2) + { + $operand1 = $this->reduce($operand1); + $operand2 = $this->reduce($operand2); + $numBits = min(strlen($operand1), strlen($operand2)); + $operand1 = substr(str_pad($operand1, $numBits, '0', STR_PAD_LEFT), -$numBits); + $operand2 = substr(str_pad($operand2, $numBits, '0', STR_PAD_LEFT), -$numBits); + $result = ''; + for ($index = 0; $index < $numBits; $index++) { + $result .= $operand1[$index] === '1' && $operand2[$index] === '1' ? '1' : '0'; + } + + return $this->reduce($result); + } + + /** + * Calculate the bitwise OR of two non-negative integers represented in binary form. + * + * @param string $operand1 + * @param string $operand2 + * + * @return string + */ + public function orX($operand1, $operand2) + { + list($operand1, $operand2, $numBits) = $this->toSameLength($operand1, $operand2); + $result = ''; + for ($index = 0; $index < $numBits; $index++) { + $result .= $operand1[$index] === '1' || $operand2[$index] === '1' ? '1' : '0'; + } + + return $result; + } + + /** + * Zero-padding of two non-negative integers represented in binary form, so that they have the same length. + * + * @param string $num1 + * @param string $num2 + * + * @return string[],int[] The first array element is $num1 (padded), the first array element is $num2 (padded), the third array element is the number of bits + */ + private function toSameLength($num1, $num2) + { + $num1 = $this->reduce($num1); + $num2 = $this->reduce($num2); + $numBits = max(strlen($num1), strlen($num2)); + + return array( + str_pad($num1, $numBits, '0', STR_PAD_LEFT), + str_pad($num2, $numBits, '0', STR_PAD_LEFT), + $numBits, + ); + } +} diff --git a/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php b/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php new file mode 100644 index 000000000..7a127e791 --- /dev/null +++ b/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php @@ -0,0 +1,168 @@ +math = new BinaryMath(); + $this->setNumBits($numBits); + } + + /** + * Calculate the subnets describing all (and only all) the addresses between two boundaries. + * + * @param \IPLib\Address\AddressInterface $from + * @param \IPLib\Address\AddressInterface $to + * + * @return \IPLib\Range\Subnet[]|null return NULL if the two addresses have an invalid number of bits (that is, different from the one passed to the constructor of this class) + */ + public function getRanges(AddressInterface $from, AddressInterface $to) + { + if ($from->getNumberOfBits() !== $this->numBits || $to->getNumberOfBits() !== $this->numBits) { + return null; + } + if ($from->getComparableString() > $to->getComparableString()) { + list($from, $to) = array($to, $from); + } + $result = array(); + $this->calculate($this->math->reduce($from->getBits()), $this->math->reduce($to->getBits()), $this->numBits, $result); + + return $result; + } + + /** + * Set the number of bits used to represent addresses (32 for IPv4, 128 for IPv6). + * + * @param int $numBits + */ + private function setNumBits($numBits) + { + $numBits = (int) $numBits; + $masks = array(); + $unmasks = array(); + for ($bit = 0; $bit < $numBits; $bit++) { + $masks[$bit] = str_repeat('1', $numBits - $bit) . str_repeat('0', $bit); + $unmasks[$bit] = $bit === 0 ? '0' : str_repeat('1', $bit); + } + $this->numBits = $numBits; + $this->masks = $masks; + $this->unmasks = $unmasks; + } + + /** + * Calculate the subnets. + * + * @param string $start the start address (represented in reduced bit form) + * @param string $end the end address (represented in reduced bit form) + * @param int $position the number of bits in the mask we are comparing at this cycle + * @param \IPLib\Range\Subnet[] $result found ranges will be added to this variable + */ + private function calculate($start, $end, $position, array &$result) + { + if ($start === $end) { + $result[] = $this->subnetFromBits($start, $this->numBits); + + return; + } + for ($index = $position - 1; $index >= 0; $index--) { + $startMasked = $this->math->andX($start, $this->masks[$index]); + $endMasked = $this->math->andX($end, $this->masks[$index]); + if ($startMasked !== $endMasked) { + $position = $index; + break; + } + } + if ($startMasked === $start && $this->math->andX($this->math->increment($end), $this->unmasks[$position]) === '0') { + $result[] = $this->subnetFromBits($start, $this->numBits - 1 - $position); + + return; + } + $middleAddress = $this->math->orX($start, $this->unmasks[$position]); + $this->calculate($start, $middleAddress, $position, $result); + $this->calculate($this->math->increment($middleAddress), $end, $position, $result); + } + + /** + * Create an address instance starting from its bits. + * + * @param string $bits the bits of the address (represented in reduced bit form) + * + * @return \IPLib\Address\AddressInterface + */ + private function addressFromBits($bits) + { + $bits = str_pad($bits, $this->numBits, '0', STR_PAD_LEFT); + $bytes = array(); + foreach (explode("\n", trim(chunk_split($bits, 8, "\n"))) as $byteBits) { + $bytes[] = bindec($byteBits); + } + + return Factory::addressFromBytes($bytes); + } + + /** + * Create an range instance starting from the bits if the address and the length of the network prefix. + * + * @param string $bits the bits of the address (represented in reduced bit form) + * @param int $networkPrefix the length of the network prefix + * + * @return \IPLib\Range\Subnet + */ + private function subnetFromBits($bits, $networkPrefix) + { + $startAddress = $this->addressFromBits($bits); + $numOnes = $this->numBits - $networkPrefix; + if ($numOnes === 0) { + return new Subnet($startAddress, $startAddress, $networkPrefix); + } + $endAddress = $this->addressFromBits(substr($bits, 0, -$numOnes) . str_repeat('1', $numOnes)); + + return new Subnet($startAddress, $endAddress, $networkPrefix); + } +} diff --git a/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php b/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php new file mode 100644 index 000000000..3179f7468 --- /dev/null +++ b/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php @@ -0,0 +1,171 @@ +getBytesFromDecimal($m[1], $numBytes); + } + } else { + if (preg_match('/^0[Xx]0*([0-9A-Fa-f]+)$/', $value, $m)) { + return $this->getBytesFromHexadecimal($m[1], $numBytes); + } + if (preg_match('/^0+([0-7]*)$/', $value, $m)) { + return $this->getBytesFromOctal($m[1], $numBytes); + } + if (preg_match('/^[1-9][0-9]*$/', $value)) { + return $this->getBytesFromDecimal($value, $numBytes); + } + } + + // Not a valid number + return null; + } + + /** + * @return int + */ + protected function getMaxSignedInt() + { + return PHP_INT_MAX; + } + + /** + * @param string $value never zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromBits($value, $numBytes) + { + $valueLength = strlen($value); + if ($valueLength > $numBytes << 3) { + // overflow + return null; + } + $remainderBits = $valueLength % 8; + if ($remainderBits !== 0) { + $value = str_pad($value, $valueLength + 8 - $remainderBits, '0', STR_PAD_LEFT); + } + $bytes = array_map('bindec', str_split($value, 8)); + + return array_pad($bytes, -$numBytes, 0); + } + + /** + * @param string $value may be zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromOctal($value, $numBytes) + { + if ($value === '') { + return array_fill(0, $numBytes, 0); + } + $bits = implode( + '', + array_map( + function ($octalDigit) { + return str_pad(decbin(octdec($octalDigit)), 3, '0', STR_PAD_LEFT); + }, + str_split($value, 1) + ) + ); + $bits = ltrim($bits, '0'); + + return $bits === '' ? array_fill(0, $numBytes, 0) : static::getBytesFromBits($bits, $numBytes); + } + + /** + * @param string $value never zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromDecimal($value, $numBytes) + { + $valueLength = strlen($value); + $maxSignedIntLength = strlen((string) $this->getMaxSignedInt()); + if ($valueLength < $maxSignedIntLength) { + return $this->getBytesFromBits(decbin((int) $value), $numBytes); + } + // Divide by two, so that we have 1 less bit + $carry = 0; + $halfValue = ltrim( + implode( + '', + array_map( + function ($digit) use (&$carry) { + $number = $carry + (int) $digit; + $carry = ($number % 2) * 10; + + return (string) $number >> 1; + }, + str_split($value, 1) + ) + ), + '0' + ); + $halfValueBytes = $this->getBytesFromDecimal($halfValue, $numBytes); + if ($halfValueBytes === null) { + return null; + } + $carry = $carry === 0 ? 0 : 1; + $result = array_fill(0, $numBytes, 0); + for ($index = $numBytes - 1; $index >= 0; $index--) { + $byte = $carry + ($halfValueBytes[$index] << 1); + if ($byte <= 0xFF) { + $carry = 0; + } else { + $carry = ($byte & ~0xFF) >> 8; + $byte -= 0x100; + } + $result[$index] = $byte; + } + if ($carry !== 0) { + // Overflow + return null; + } + + return $result; + } + + /** + * @param string $value never zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromHexadecimal($value, $numBytes) + { + $valueLength = strlen($value); + if ($valueLength > $numBytes << 1) { + // overflow + return null; + } + $value = str_pad($value, $valueLength + $valueLength % 2, '0', STR_PAD_LEFT); + $bytes = array_map('hexdec', str_split($value, 2)); + + return array_pad($bytes, -$numBytes, 0); + } +}