├── .gitattributes ├── .gitignore ├── src ├── Exception │ ├── IpException.php │ ├── Formatter │ │ └── FormatException.php │ ├── InvalidIpAddressException.php │ ├── WrongVersionException.php │ ├── InvalidCidrException.php │ └── Strategy │ │ ├── PackingException.php │ │ └── ExtractionException.php ├── Formatter │ ├── ProtocolFormatterInterface.php │ ├── NativeFormatter.php │ └── ConsistentFormatter.php ├── Version │ ├── MultiVersionInterface.php │ ├── Version4Interface.php │ ├── Version6Interface.php │ ├── IPv4.php │ ├── IPv6.php │ └── Multi.php ├── Strategy │ ├── EmbeddingStrategyInterface.php │ ├── Compatible.php │ ├── Mapped.php │ └── Derived.php ├── Util │ ├── MbString.php │ └── Binary.php ├── IpInterface.php └── AbstractIP.php ├── phpstan.neon ├── composer.json ├── LICENSE.md ├── CHANGELOG.md ├── README.md └── CODE_OF_CONDUCT.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | /.github/ export-ignore 4 | /docs/ export-ignore 5 | /tests/ export-ignore 6 | /mkdocs.yaml export-ignore 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /build/ 3 | /gh-pages/ 4 | /vendor/ 5 | # Because this library will never be the root package, don't commit Composer's lock file. 6 | /composer.lock 7 | -------------------------------------------------------------------------------- /src/Exception/IpException.php: -------------------------------------------------------------------------------- 1 | binary = $binary; 17 | parent::__construct('Cannot format invalid binary sequence; must be a string either 4 or 16 bytes long.', 0, $previous); 18 | } 19 | 20 | public function getSuppliedBinary(): string 21 | { 22 | return $this->binary; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/InvalidIpAddressException.php: -------------------------------------------------------------------------------- 1 | ip = $ip; 20 | parent::__construct('The IP address supplied is not valid.', 0, $previous); 21 | } 22 | 23 | /** 24 | * @return mixed 25 | */ 26 | public function getSuppliedIp() 27 | { 28 | return $this->ip; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exception/WrongVersionException.php: -------------------------------------------------------------------------------- 1 | expected = $expected; 22 | $this->actual = $actual; 23 | parent::__construct($ip, $previous); 24 | } 25 | 26 | public function getExpectedVersion(): int 27 | { 28 | return $this->expected; 29 | } 30 | 31 | public function getActualVersion(): int 32 | { 33 | return $this->actual; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Version/MultiVersionInterface.php: -------------------------------------------------------------------------------- 1 | cidr = $cidr; 21 | $message = 'The supplied CIDR is not valid; it must be an integer '; 22 | if (!\is_int($addressLengthInBytes)) { 23 | $message .= '(could not determine valid CIDR range).'; 24 | } else { 25 | $message .= \sprintf('(between 0 and %d).', $addressLengthInBytes * 8); 26 | } 27 | parent::__construct($message, 0, $previous); 28 | } 29 | 30 | /** 31 | * @return mixed 32 | */ 33 | public function getSuppliedCidr() 34 | { 35 | return $this->cidr; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darsyn/ip", 3 | "description": "An immutable IP Address value object that provides several different notations, including helper functions.", 4 | "license": "MIT", 5 | "keywords": ["library", "value-object", "immutable", "ip", "ipv4", "ipv6"], 6 | "type": "library", 7 | "homepage": "https://github.com/darsyn/ip", 8 | "authors": [ 9 | { 10 | "name": "Zan Baldwin", 11 | "email": "hello@zanbaldwin.com", 12 | "homepage": "https://zanbaldwin.com" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Darsyn\\IP\\": "src/" 18 | } 19 | }, 20 | "require": { 21 | "php-64bit": ">=7.1", 22 | "php-ipv6": ">=7.1", 23 | "ext-ctype": "*" 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Darsyn\\IP\\Tests\\": "tests/" 28 | } 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "*" 32 | }, 33 | "prefer-stable": true, 34 | "config": { 35 | "sort-packages": true 36 | }, 37 | "suggest": { 38 | "darsyn/ip-doctrine": "to use IP as a Doctrine column type" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/Strategy/PackingException.php: -------------------------------------------------------------------------------- 1 | binary = $binary; 21 | $this->embeddingStrategy = $embeddingStrategy; 22 | parent::__construct(\sprintf( 23 | 'Could not pack IPv4 address into IPv6 binary string using the "%s" strategy.', 24 | \get_class($embeddingStrategy) 25 | ), 0, $previous); 26 | } 27 | 28 | public function getSuppliedBinary(): string 29 | { 30 | return $this->binary; 31 | } 32 | 33 | public function getEmbeddingStrategy(): EmbeddingStrategyInterface 34 | { 35 | return $this->embeddingStrategy; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Strategy/Compatible.php: -------------------------------------------------------------------------------- 1 | binary = $binary; 26 | $this->embeddingStrategy = $embeddingStrategy; 27 | parent::__construct(\sprintf( 28 | 'Could not extract IPv4 address from IPv6 binary string using the "%s" strategy.', 29 | \get_class($embeddingStrategy) 30 | ), 0, $previous); 31 | } 32 | 33 | public function getSuppliedBinary(): string 34 | { 35 | return $this->binary; 36 | } 37 | 38 | public function getEmbeddingStrategy(): EmbeddingStrategyInterface 39 | { 40 | return $this->embeddingStrategy; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Util/MbString.php: -------------------------------------------------------------------------------- 1 | ntopVersion6($binary); 21 | } 22 | if ($length === 4) { 23 | return $this->ntopVersion4($binary); 24 | } 25 | throw new FormatException($binary); 26 | } 27 | 28 | private function ntopVersion6(string $binary): string 29 | { 30 | $hex = Binary::toHex($binary); 31 | $parts = \str_split($hex, 4); 32 | $zeroes = \array_map(function ($part) { 33 | return $part === '0000'; 34 | }, $parts); 35 | $length = $i = 0; 36 | $sequences = []; 37 | foreach ($zeroes as $zero) { 38 | $length = $zero ? ++$length : 0; 39 | $sequences[++$i] = $length; 40 | } 41 | if (\count($sequences) > 0) { 42 | $maxLength = \max($sequences); 43 | $endPosition = \array_search($maxLength, $sequences, true); 44 | if (!\is_int($endPosition)) { 45 | throw new \RuntimeException; 46 | } 47 | $startPosition = $endPosition - $maxLength; 48 | } else { 49 | $maxLength = $startPosition = 0; 50 | } 51 | $parts = \array_map(function ($part) { 52 | return \ltrim($part, '0') ?: '0'; 53 | }, $parts); 54 | if ($maxLength > 0) { 55 | \array_splice($parts, $startPosition, $maxLength, ':'); 56 | } 57 | if (null === $shortened = \preg_replace('/\:{2,}/', '::', \implode(':', $parts))) { 58 | throw new FormatException($binary); 59 | } 60 | return \str_pad($shortened, 2, ':'); 61 | } 62 | 63 | private function ntopVersion4(string $binary): string 64 | { 65 | // $pack return type is `string|false` below PHP 8 and `string` 66 | // above PHP 8. 67 | $pack = \pack('A4', $binary); 68 | // @phpstan-ignore identical.alwaysFalse 69 | if (false === $pack || false === $protocol = \inet_ntop($pack)) { 70 | throw new FormatException($binary); 71 | } 72 | return $protocol; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Darsyn IP 2 | 3 | ## `5.0.0` 4 | 5 | - Removed Doctrine functionality, and split it off into its own package: 6 | [`darsyn/ip-doctrine`](https://packagist.org/packages/darsyn/ip-doctrine). 7 | List it as a Composer dependency suggestion. 8 | - Change from [Psalm](https://psalm.dev/) to [PHPStan](https://phpstan.org/) for 9 | static analysis. 10 | - Add types to all function arguments lists and return values. 11 | - Update the codebase to pass static analysis on `max` level (standard, 12 | deprecation, and bleeding edge rules). 13 | - Test against PHP versions `8.2` and `8.3` in CI pipeline. 14 | - Update README with notes on version compatibility. 15 | - Explicitly state the requirement of the `ctype` PHP extension. 16 | - Add PHPUnit attributes alongside annotations to be compatible with the highest 17 | version of PHPUnit for any supported PHP version. 18 | 19 | ## `4.1.0` 20 | 21 | - Added `IpInterface::equals()` method for comparing two IP addresses. 22 | - Added `getCommonCidr(IpInterface $ip): int` for determining how in range two 23 | IP addresses are according to their common CIDR value. 24 | - Added `isBenchmarking()`, `isDocumentation()`, and `isPublicUse()` type 25 | methods for both IPv4 and IPv6 addresses. 26 | - Added `isBroadcast()`, `isShared()`, and `isFutureReserved()` type methods for 27 | IPv4 addresses. 28 | - Added `getMulticastScope()`, `isUniqueLocal()`, `isUnicast()`, and 29 | `isUnicastGlobal()` type methods for IPv6 addresses. 30 | - Added `Ipv6::fromEmbedded()` factory method to create an instance of an 31 | IPv4-embedded address as IPv6 instead of Multi. 32 | - Made internal helper methods for dealing with binary data into utility 33 | classes: `Darsyn\IP\Util\Binary` and `Darsyn\IP\Util\MbString`. 34 | - Complete documentation overhaul 35 | - Increase test coverage. 36 | - Started using static analysis both locally and via GitHub actions. 37 | - Documentation and tests are excluded from the Git archive to reduce download 38 | size when installing Composer dependency as dist. 39 | - Updated Code of Conduct to Contributor Covenant v2.1 40 | 41 | ## `4.0.2` 42 | 43 | - Add return types to DocComments to prevent 44 | [`symfony/error-handler`](https://github.com/symfony/symfony/tree/5.4/src/Symfony/Component/ErrorHandler) 45 | from throwing deprecation errors 46 | 47 | ## `4.0.1` 48 | 49 | - Add Code of Conduct to project. 50 | - Add new internal helper for dealing with binary strings. 51 | - Add namespace indicator to function calls to speed up symbol resolution. 52 | - Add `__toString()` to IP objects. 53 | - Update unit tests, now runnable on all PHP versions 5.6 to 8.1 54 | 55 | ## `4.0.0` 56 | 57 | - Complete rewrite of library. 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IP is an immutable value object for (both version 4 and 6) IP addresses. Several 2 | helper methods are provided for ranges, broadcast and network addresses, subnet 3 | masks, whether an IP is a certain type (defined by RFC's), etc. 4 | 5 | This project aims for simplicity of use and any contribution towards that goal - 6 | whether a bug report, modifications to the codebase, or an improvement to the 7 | accuracy or readability of the documentation - are always welcome. 8 | 9 | # Documentation 10 | 11 | Full documentation is available in the [`docs/`](docs/) folder. 12 | 13 | ## Compatibility 14 | 15 | This library has extensive test coverage using PHPUnit on PHP versions: `7.1`, 16 | `7.2`, `7.3`, `7.4`, `8.0`, `8.1`, `8.2`, `8.3` and `8.4`. 17 | 18 | > Versions `5.x.x` of this library are compatible with PHP versions `5.6` to 19 | > `8.3` (and will continue to receive bug fixes). 20 | > 21 | > Versions `6.x.x` of this library are compatible with PHP versions `7.1` and 22 | > above, and will receive continued support for future versions of PHP (also 23 | > accepting proposed new features). 24 | 25 | Static analysis is performed with PHPStan at `max` level on PHP `8.4`, using 26 | core, bleeding edge, and deprecation rules. 27 | 28 | > The Doctrine features for this library have been split off into their own 29 | > package, [`darsyn/ip-doctrine`](https://packagist.org/packages/darsyn/ip-doctrine). 30 | 31 | ## Brief Example 32 | 33 | - There are three main classes: [`IPv4`](src/Version/IPv4.php), 34 | [`IPv6`](src/Version/IPv6.php), and [`Multi`](src/Version/Multi.php) (for both 35 | version 4 and 6 addresses). 36 | - Objects are created using a static factory method 37 | [`IpInterface::factory()`](src/IpInterface.php) instead of the constructor to 38 | speed up internal processes. 39 | - When using `Multi`, the default strategy for representing version 4 addresses 40 | internally is [IPv4-mapped](docs/05-strategies.md). 41 | 42 | ```php 43 | inRange($companyNetwork, 25)) { 56 | throw new \Exception('Request not from a known company IP address.'); 57 | } 58 | 59 | // Is it coming from the local network? 60 | if (!$ip->isPrivateUse()) { 61 | record_visit($ip->getBinary(), $_SERVER['HTTP_USER_AGENT']); 62 | } 63 | ``` 64 | 65 | ## Code of Conduct 66 | 67 | This project includes and adheres to the [Contributor Covenant as a Code of 68 | Conduct](CODE_OF_CONDUCT.md). 69 | 70 | # License 71 | 72 | Please see the [separate license file](LICENSE.md) included in this repository 73 | for a full copy of the MIT license, which this project is licensed under. 74 | 75 | # Authors 76 | 77 | - [Zan Baldwin](https://zanbaldwin.com) 78 | - [Jaume Casado Ruiz](http://jau.cat) 79 | - [Pascal Hofmann](http://pascalhofmann.de) 80 | 81 | If you make a contribution (submit a pull request), don't forget to add your 82 | name here! 83 | -------------------------------------------------------------------------------- /src/IpInterface.php: -------------------------------------------------------------------------------- 1 | 25 | * @link https://github.com/darsyn/ip 26 | * @copyright 2015 Zan Baldwin 27 | * @license MIT/X11 28 | */ 29 | class IPv4 extends AbstractIP implements Version4Interface 30 | { 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public static function factory(string $ip) 35 | { 36 | try { 37 | // Convert from protocol notation to binary sequence. 38 | $binary = self::getProtocolFormatter()->pton($ip); 39 | // If the string was not 4 bytes long, then the IP supplied was 40 | // neither in protocol notation or binary sequence notation. Throw 41 | // an exception. 42 | if (MbString::getLength($binary) !== 4) { 43 | if (MbString::getLength($ip) !== 4) { 44 | throw new Exception\WrongVersionException(4, 6, $ip); 45 | } 46 | $binary = $ip; 47 | } 48 | } catch(Exception\IpException $e) { 49 | throw new Exception\InvalidIpAddressException($ip, $e); 50 | } 51 | return new static($binary); 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function getDotAddress(): string 58 | { 59 | try { 60 | return self::getProtocolFormatter()->ntop($this->getBinary()); 61 | } catch (Exception\Formatter\FormatException $e) { 62 | throw new Exception\IpException('An unknown error occured internally.', 0, $e); 63 | } 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function getVersion(): int 70 | { 71 | return 4; 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public function isLinkLocal(): bool 78 | { 79 | return $this->inRange(new self(Binary::fromHex('a9fe0000')), 16); 80 | } 81 | 82 | /** 83 | * {@inheritDoc} 84 | */ 85 | public function isLoopback(): bool 86 | { 87 | return $this->inRange(new self(Binary::fromHex('7f000000')), 8); 88 | } 89 | 90 | /** 91 | * {@inheritDoc} 92 | */ 93 | public function isMulticast(): bool 94 | { 95 | return $this->inRange(new self(Binary::fromHex('e0000000')), 4); 96 | } 97 | 98 | /** 99 | * {@inheritDoc} 100 | */ 101 | public function isPrivateUse(): bool 102 | { 103 | return $this->inRange(new self(Binary::fromHex('0a000000')), 8) 104 | || $this->inRange(new self(Binary::fromHex('ac100000')), 12) 105 | || $this->inRange(new self(Binary::fromHex('c0a80000')), 16); 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function isUnspecified(): bool 112 | { 113 | return $this->getBinary() === "\0\0\0\0"; 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | */ 119 | public function isBenchmarking(): bool 120 | { 121 | return $this->inRange(new self(Binary::fromHex('c6120000')), 15); 122 | } 123 | 124 | /** 125 | * {@inheritDoc} 126 | */ 127 | public function isDocumentation(): bool 128 | { 129 | return $this->inRange(new self(Binary::fromHex('c0000200')), 24) 130 | || $this->inRange(new self(Binary::fromHex('c6336400')), 24) 131 | || $this->inRange(new self(Binary::fromHex('cb007100')), 24); 132 | } 133 | 134 | /** 135 | * {@inheritDoc} 136 | */ 137 | public function isPublicUse(): bool 138 | { 139 | // Both 192.0.0.9 and 192.0.0.10 are globally routable, despite being in the future reserved block. 140 | if (in_array(Binary::toHex($this->getBinary()), ['c0000009', 'c000000a'], true)) { 141 | return true; 142 | } 143 | 144 | // The whole 0.0.0.0/8 block is not for public use. 145 | if ($this->inRange(new self(Binary::fromHex('00000000')), 8)) { 146 | return false; 147 | } 148 | 149 | // Addresses reserved for future protocols are not globally routable (different to reserved for future use). 150 | if ($this->inRange(new self(Binary::fromHex('c0000000')), 24)) { 151 | return false; 152 | } 153 | 154 | return !$this->isPrivateUse() 155 | && !$this->isLoopback() 156 | && !$this->isLinkLocal() 157 | && !$this->isBroadcast() 158 | && !$this->isShared() 159 | && !$this->isDocumentation() 160 | && !$this->isFutureReserved() 161 | && !$this->isBenchmarking(); 162 | } 163 | 164 | /** 165 | * {@inheritDoc} 166 | */ 167 | public function isBroadcast(): bool 168 | { 169 | return $this->getBinary() === Binary::fromHex('ffffffff'); 170 | } 171 | 172 | /** 173 | * {@inheritDoc} 174 | */ 175 | public function isShared(): bool 176 | { 177 | return $this->inRange(new self(Binary::fromHex('64400000')), 10); 178 | } 179 | 180 | /** 181 | * {@inheritDoc} 182 | */ 183 | public function isFutureReserved(): bool 184 | { 185 | return $this->getBinary() !== Binary::fromHex('ffffffff') 186 | && $this->inRange(new self(Binary::fromHex('f0000000')), 4); 187 | } 188 | 189 | /** 190 | * {@inheritDoc} 191 | */ 192 | public function __toString(): string 193 | { 194 | return $this->getDotAddress(); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Version/IPv6.php: -------------------------------------------------------------------------------- 1 | 26 | * @link https://github.com/darsyn/ip 27 | * @copyright 2015 Zan Baldwin 28 | * @license MIT/X11 29 | */ 30 | class IPv6 extends AbstractIP implements Version6Interface 31 | { 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public static function factory(string $ip) 37 | { 38 | try { 39 | // Convert from protocol notation to binary sequence. 40 | $binary = self::getProtocolFormatter()->pton($ip); 41 | // If the string was not 4 bytes long, then the IP supplied was neither 42 | // in protocol notation or binary sequence notation. Throw an exception. 43 | if (MbString::getLength($binary) !== 16) { 44 | throw new Exception\WrongVersionException(6, 4, $ip); 45 | } 46 | } catch (Exception\IpException $e) { 47 | throw new Exception\InvalidIpAddressException($ip, $e); 48 | } 49 | return new static($binary); 50 | } 51 | 52 | /** 53 | * @param \Darsyn\IP\Strategy\EmbeddingStrategyInterface|null $strategy 54 | * @throws \Darsyn\IP\Exception\InvalidIpAddressException 55 | * @throws \Darsyn\IP\Exception\WrongVersionException 56 | * @return static 57 | */ 58 | public static function fromEmbedded(string $ip, ?EmbeddingStrategyInterface $strategy = null) 59 | { 60 | return new static(Multi::factory($ip, $strategy)->getBinary()); 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function getExpandedAddress(): string 67 | { 68 | // Convert the 16-byte binary sequence into a hexadecimal-string 69 | // representation, insert a colon between every block of 4 characters, 70 | // and return the resulting IP address in full IPv6 protocol notation. 71 | $expanded = \preg_replace('/([a-fA-F0-9]{4})/', '$1:', Binary::toHex($this->getBinary())); 72 | return MbString::subString(\is_string($expanded) ? $expanded : '', 0, -1); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function getCompactedAddress(): string 79 | { 80 | try { 81 | return self::getProtocolFormatter()->ntop($this->getBinary()); 82 | } catch (Exception\Formatter\FormatException $e) { 83 | throw new Exception\IpException('An unknown error occured internally.', 0, $e); 84 | } 85 | } 86 | 87 | /** 88 | * {@inheritDoc} 89 | */ 90 | public function getVersion(): int 91 | { 92 | return 6; 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | */ 98 | public function isLinkLocal(): bool 99 | { 100 | return $this->inRange(new self(Binary::fromHex('fe800000000000000000000000000000')), 10); 101 | } 102 | 103 | /** 104 | * {@inheritDoc} 105 | */ 106 | public function isLoopback(): bool 107 | { 108 | return $this->inRange(new self(Binary::fromHex('00000000000000000000000000000001')), 128); 109 | } 110 | 111 | /** 112 | * {@inheritDoc} 113 | */ 114 | public function isMulticast(): bool 115 | { 116 | return $this->inRange(new self(Binary::fromHex('ff000000000000000000000000000000')), 8); 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | public function getMulticastScope(): ?int 123 | { 124 | if (!$this->isMulticast()) { 125 | return null; 126 | } 127 | $firstSegment = MbString::subString($this->getBinary(), 0, 2); 128 | return (int) hexdec(Binary::toHex($firstSegment & Binary::fromHex('000f'))); 129 | } 130 | 131 | /** 132 | * {@inheritDoc} 133 | */ 134 | public function isPrivateUse(): bool 135 | { 136 | return $this->inRange(new self(Binary::fromHex('fd000000000000000000000000000000')), 8); 137 | } 138 | 139 | /** 140 | * {@inheritDoc} 141 | */ 142 | public function isUnspecified(): bool 143 | { 144 | return $this->getBinary() === "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; 145 | } 146 | 147 | /** 148 | * @inheritDoc 149 | */ 150 | public function isBenchmarking(): bool 151 | { 152 | return $this->inRange(new self(Binary::fromHex('20010002000000000000000000000000')), 48); 153 | } 154 | 155 | /** 156 | * @inheritDoc 157 | */ 158 | public function isDocumentation(): bool 159 | { 160 | return $this->inRange(new self(Binary::fromHex('20010db8000000000000000000000000')), 32); 161 | } 162 | 163 | /** 164 | * @inheritDoc 165 | */ 166 | public function isPublicUse(): bool 167 | { 168 | return $this->getMulticastScope() === self::MULTICAST_GLOBAL || $this->isUnicastGlobal(); 169 | } 170 | 171 | /** 172 | * @inheritDoc 173 | */ 174 | public function isUniqueLocal(): bool 175 | { 176 | return $this->inRange(new self(Binary::fromHex('fc000000000000000000000000000000')), 7); 177 | } 178 | 179 | /** 180 | * @inheritDoc 181 | */ 182 | public function isUnicast(): bool 183 | { 184 | return !$this->isMulticast(); 185 | } 186 | 187 | /** 188 | * @inheritDoc 189 | */ 190 | public function isUnicastGlobal(): bool 191 | { 192 | return $this->isUnicast() 193 | && !$this->isLoopback() 194 | && !$this->isLinkLocal() 195 | && !$this->isUniqueLocal() 196 | && !$this->isUnspecified() 197 | && !$this->isDocumentation(); 198 | } 199 | 200 | /** 201 | * {@inheritDoc} 202 | */ 203 | public function __toString(): string 204 | { 205 | return $this->getCompactedAddress(); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/AbstractIP.php: -------------------------------------------------------------------------------- 1 | ip = $ip; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | final public function getBinary(): string 52 | { 53 | return $this->ip; 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function equals(IpInterface $ip): bool 60 | { 61 | return $this->getBinary() === $ip->getBinary(); 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function isVersion(int $version): bool 68 | { 69 | return $this->getVersion() === $version; 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public function isVersion4(): bool 76 | { 77 | return $this->isVersion(4); 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public function isVersion6(): bool 84 | { 85 | return $this->isVersion(6); 86 | } 87 | 88 | /** 89 | * {@inheritDoc} 90 | */ 91 | public function getNetworkIp(int $cidr) 92 | { 93 | // Providing that the CIDR is valid, bitwise AND the IP address binary 94 | // sequence with the mask generated from the CIDR. 95 | return new static($this->getBinary() & $this->generateBinaryMask( 96 | $cidr, 97 | MbString::getLength($this->getBinary()) 98 | )); 99 | } 100 | 101 | /** 102 | * {@inheritDoc} 103 | */ 104 | public function getBroadcastIp(int $cidr) 105 | { 106 | // Providing that the CIDR is valid, bitwise OR the IP address binary 107 | // sequence with the inverse of the mask generated from the CIDR. 108 | return new static($this->getBinary() | ~$this->generateBinaryMask( 109 | $cidr, 110 | MbString::getLength($this->getBinary()) 111 | )); 112 | } 113 | 114 | /** 115 | * {@inheritDoc} 116 | */ 117 | public function inRange(IpInterface $ip, int $cidr): bool 118 | { 119 | if (!$this->isSameByteLength($ip)) { 120 | // Cannot calculate if one IP is in range of another if they of different byte-lengths. 121 | throw new WrongVersionException($this->getVersion(), $ip->getVersion(), (string) $ip); 122 | } 123 | // If this method is being called, it means Multi may have failed it's 124 | // IPv4 check, and we must proceed as IPv6 only. We must perform 125 | // getNetworkIp() as IPv6, otherwise instances of Multi with IPv4-embedded 126 | // addresses and CIDR below 32 will return an incorrect network IP for 127 | // comparison. 128 | $ours = $this instanceof Version\MultiVersionInterface ? new Version\IPv6($this->getBinary()) : $this; 129 | $theirs = $ip instanceof Version\MultiVersionInterface ? new Version\IPv6($ip->getBinary()) : $ip; 130 | return $ours->getNetworkIp($cidr)->getBinary() === $theirs->getNetworkIp($cidr)->getBinary(); 131 | } 132 | 133 | /** {@inheritDoc} */ 134 | public function getCommonCidr(IpInterface $ip): int 135 | { 136 | // Cannot calculate the greatest common CIDR between an IPv4 and 137 | // IPv6/IPv4-embedded address, they are fundamentally incompatible. 138 | if (!$this->isSameByteLength($ip)) { 139 | throw new WrongVersionException( 140 | MbString::getLength($this->getBinary()) === 4 ? 4 : 6, 141 | MbString::getLength($ip->getBinary()) === 4 ? 4 : 6, 142 | (string) $ip 143 | ); 144 | } 145 | $mask = $this->getBinary() ^ $ip->getBinary(); 146 | $parts = explode('1', Binary::toHumanReadable($mask), 2); 147 | return MbString::getLength($parts[0]); 148 | } 149 | 150 | /** 151 | * {@inheritDoc} 152 | */ 153 | public function isMapped(): bool 154 | { 155 | return (new Strategy\Mapped)->isEmbedded($this->getBinary()); 156 | } 157 | 158 | /** 159 | * {@inheritDoc} 160 | */ 161 | public function isDerived(): bool 162 | { 163 | return (new Strategy\Derived)->isEmbedded($this->getBinary()); 164 | } 165 | 166 | /** 167 | * {@inheritDoc} 168 | */ 169 | public function isCompatible(): bool 170 | { 171 | return (new Strategy\Compatible)->isEmbedded($this->getBinary()); 172 | } 173 | 174 | /** 175 | * {@inheritDoc} 176 | */ 177 | public function isEmbedded(): bool 178 | { 179 | return false; 180 | } 181 | 182 | protected function isSameByteLength(IpInterface $ip): bool 183 | { 184 | return MbString::getLength($this->getBinary()) === MbString::getLength($ip->getBinary()); 185 | } 186 | 187 | /** 188 | * 128-bit masks can often evaluate to integers over PHP_MAX_INT, so we have 189 | * to construct the bitmask as a string instead of doing any mathematical 190 | * operations (such as base_convert). 191 | * 192 | * @throws \Darsyn\IP\Exception\InvalidCidrException 193 | */ 194 | protected function generateBinaryMask(int $cidr, int $lengthInBytes): string 195 | { 196 | if ($cidr < 0 || $lengthInBytes < 0 197 | // CIDR is measured in bits; we're describing the length in bytes. 198 | || $cidr > $lengthInBytes * 8 199 | ) { 200 | throw new Exception\InvalidCidrException($cidr, $lengthInBytes); 201 | } 202 | // Eg, a CIDR of 24 and length of 4 bytes (IPv4) would make a mask of: 203 | // 11111111111111111111111100000000. 204 | $asciiBinarySequence = MbString::padString( 205 | \str_repeat('1', $cidr), 206 | $lengthInBytes * 8, 207 | '0', 208 | \STR_PAD_RIGHT 209 | ); 210 | return Binary::fromHumanReadable($asciiBinarySequence); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Version/Multi.php: -------------------------------------------------------------------------------- 1 | 28 | * @link https://github.com/darsyn/ip 29 | * @copyright 2015 Zan Baldwin 30 | * @license MIT/X11 31 | */ 32 | class Multi extends IPv6 implements MultiVersionInterface 33 | { 34 | /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface|null $defaultEmbeddingStrategy */ 35 | private static $defaultEmbeddingStrategy; 36 | 37 | /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface $embeddingStrategy */ 38 | private $embeddingStrategy; 39 | 40 | /** @var bool $embedded */ 41 | private $embedded; 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public static function setDefaultEmbeddingStrategy(EmbeddingStrategyInterface $strategy): void 47 | { 48 | self::$defaultEmbeddingStrategy = $strategy; 49 | } 50 | 51 | /** 52 | * Get the default embedding strategy set. Default to the IPv4-mapped IPv6 53 | * embedding strategy if the user has not set one globally. 54 | */ 55 | private static function getDefaultEmbeddingStrategy(): EmbeddingStrategyInterface 56 | { 57 | return self::$defaultEmbeddingStrategy ?: new MappedEmbeddingStrategy; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | * @param \Darsyn\IP\Strategy\EmbeddingStrategyInterface $strategy 63 | */ 64 | public static function factory(string $ip, ?EmbeddingStrategyInterface $strategy = null): self 65 | { 66 | // We need a strategy to pack version 4 addresses. 67 | $strategy = $strategy ?: self::getDefaultEmbeddingStrategy(); 68 | 69 | try { 70 | // Convert from protocol notation to binary sequence. 71 | $binary = self::getProtocolFormatter()->pton($ip); 72 | 73 | // If the IP address is a binary sequence of 4 bytes, then pack it into 74 | // a 16 byte IPv6 binary sequence according to the embedding strategy. 75 | if (MbString::getLength($binary) === 4) { 76 | $binary = $strategy->pack($binary); 77 | } 78 | } catch (Exception\IpException $e) { 79 | throw new Exception\InvalidIpAddressException($ip, $e); 80 | } 81 | return new static($binary, $strategy); 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | protected function __construct(string $ip, ?EmbeddingStrategyInterface $strategy = null) 88 | { 89 | // Fallback to default in case this instance was created from static in 90 | // the abstract IP class. 91 | $this->embeddingStrategy = $strategy ?: self::getDefaultEmbeddingStrategy(); 92 | parent::__construct($ip); 93 | } 94 | 95 | /** {@inheritDoc} */ 96 | public function getProtocolAppropriateAddress(): string 97 | { 98 | // If binary string contains an embedded IPv4 address, then extract it. 99 | $ip = $this->isEmbedded() 100 | ? $this->getShortBinary() 101 | : $this->getBinary(); 102 | // Render the IP address in the correct notation according to its 103 | // protocol (based on how long the binary string is). 104 | return self::getProtocolFormatter()->ntop($ip); 105 | } 106 | 107 | /** 108 | * @throws \Darsyn\IP\Exception\WrongVersionException 109 | * @throws \Darsyn\IP\Exception\IpException 110 | */ 111 | public function getDotAddress(): string 112 | { 113 | if ($this->isEmbedded()) { 114 | try { 115 | return self::getProtocolFormatter()->ntop($this->getShortBinary()); 116 | } catch (Exception\Formatter\FormatException $e) { 117 | throw new Exception\IpException('An unknown error occured internally.', 0, $e); 118 | } 119 | } 120 | throw new Exception\WrongVersionException(4, 6, (string) $this); 121 | } 122 | 123 | /** {@inheritDoc} */ 124 | public function getVersion(): int 125 | { 126 | return $this->isEmbedded() ? 4 : 6; 127 | } 128 | 129 | /** {@inheritDoc} */ 130 | public function getNetworkIp(int $cidr): self 131 | { 132 | try { 133 | if ($this->isVersion4WithAppropriateCidr($cidr)) { 134 | $v4 = (new IPv4($this->getShortBinary()))->getNetworkIp($cidr)->getBinary(); 135 | return new static( 136 | $this->embeddingStrategy->pack($v4), 137 | clone $this->embeddingStrategy 138 | ); 139 | } 140 | } catch (Exception\IpException $e) { 141 | } 142 | return new static(parent::getNetworkIp($cidr)->getBinary(), clone $this->embeddingStrategy); 143 | } 144 | 145 | /** {@inheritDoc} */ 146 | public function getBroadcastIp(int $cidr): self 147 | { 148 | try { 149 | if ($this->isVersion4WithAppropriateCidr($cidr)) { 150 | $v4 = (new IPv4($this->getShortBinary()))->getBroadcastIp($cidr)->getBinary(); 151 | return new static( 152 | $this->embeddingStrategy->pack($v4), 153 | clone $this->embeddingStrategy 154 | ); 155 | } 156 | } catch (Exception\IpException $e) { 157 | } 158 | return new static(parent::getBroadcastIp($cidr)->getBinary(), clone $this->embeddingStrategy); 159 | } 160 | 161 | /** {@inheritDoc} */ 162 | public function inRange(IpInterface $ip, int $cidr): bool 163 | { 164 | try { 165 | if ($this->isVersion4WithAppropriateCidr($cidr) && $this->isVersion4CompatibleWithCurrentStrategy($ip)) { 166 | $ours = $this->getShortBinary(); 167 | $theirs = $this->embeddingStrategy->extract($ip->getBinary()); 168 | return (new IPv4($ours))->inRange(new IPv4($theirs), $cidr); 169 | } 170 | } catch (Exception\IpException $e) { 171 | // If an exception was thrown, the two IP addresses were incompatible 172 | // and should not have been checked as IPv4 addresses, fallback to 173 | // performing the operation as IPv6 addresses. 174 | } 175 | return parent::inRange($ip, $cidr); 176 | } 177 | 178 | /** {@inheritDoc} */ 179 | public function getCommonCidr(IpInterface $ip): int 180 | { 181 | try { 182 | if ($this->isVersion4CompatibleWithCurrentStrategy($ip)) { 183 | $ours = $this->getShortBinary(); 184 | $theirs = $this->embeddingStrategy->extract($ip->getBinary()); 185 | return (new IPv4($ours))->getCommonCidr(new IPv4($theirs)); 186 | } 187 | } catch (Exception\IpException $e) { 188 | // If an exception was thrown, the two IP addresses were incompatible 189 | // and should not have been checked as IPv4 addresses, fallback to 190 | // performing the operation as IPv6 addresses. 191 | } 192 | return parent::getCommonCidr($ip); 193 | } 194 | 195 | /** {@inheritDoc} */ 196 | public function isEmbedded(): bool 197 | { 198 | if (null === $this->embedded) { 199 | $this->embedded = $this->embeddingStrategy->isEmbedded($this->getBinary()); 200 | } 201 | return $this->embedded; 202 | } 203 | 204 | /** {@inheritDoc} */ 205 | public function isLinkLocal(): bool 206 | { 207 | return $this->isEmbedded() 208 | ? (new IPv4($this->getShortBinary()))->isLinkLocal() 209 | : parent::isLinkLocal(); 210 | } 211 | 212 | /** {@inheritDoc} */ 213 | public function isLoopback(): bool 214 | { 215 | return $this->isEmbedded() 216 | ? (new IPv4($this->getShortBinary()))->isLoopback() 217 | : parent::isLoopback(); 218 | } 219 | 220 | /** * {@inheritDoc} */ 221 | public function isMulticast(): bool 222 | { 223 | return $this->isEmbedded() 224 | ? (new IPv4($this->getShortBinary()))->isMulticast() 225 | : parent::isMulticast(); 226 | } 227 | 228 | /** {@inheritDoc} */ 229 | public function isPrivateUse(): bool 230 | { 231 | return $this->isEmbedded() 232 | ? (new IPv4($this->getShortBinary()))->isPrivateUse() 233 | : parent::isPrivateUse(); 234 | } 235 | 236 | /** {@inheritDoc} */ 237 | public function isUnspecified(): bool 238 | { 239 | return $this->isEmbedded() 240 | ? (new IPv4($this->getShortBinary()))->isUnspecified() 241 | : parent::isUnspecified(); 242 | } 243 | 244 | /** {@inheritDoc} */ 245 | public function isBenchmarking(): bool 246 | { 247 | return $this->isEmbedded() 248 | ? (new IPv4($this->getShortBinary()))->isBenchmarking() 249 | : parent::isBenchmarking(); 250 | } 251 | 252 | /** {@inheritDoc} */ 253 | public function isDocumentation(): bool 254 | { 255 | return $this->isEmbedded() 256 | ? (new IPv4($this->getShortBinary()))->isDocumentation() 257 | : parent::isDocumentation(); 258 | } 259 | 260 | /** {@inheritDoc} */ 261 | public function isPublicUse(): bool 262 | { 263 | return $this->isEmbedded() 264 | ? (new IPv4($this->getShortBinary()))->isPublicUse() 265 | : parent::isPublicUse(); 266 | } 267 | 268 | /** 269 | * @inheritDoc 270 | */ 271 | public function isUniqueLocal(): bool 272 | { 273 | if ($this->isEmbedded()) { 274 | throw new Exception\WrongVersionException(6, 4, (string) $this); 275 | } 276 | return parent::isUniqueLocal(); 277 | } 278 | 279 | /** 280 | * @inheritDoc 281 | */ 282 | public function isUnicast(): bool 283 | { 284 | if ($this->isEmbedded()) { 285 | throw new Exception\WrongVersionException(6, 4, (string) $this); 286 | } 287 | return parent::isUnicast(); 288 | } 289 | 290 | /** 291 | * @inheritDoc 292 | */ 293 | public function isUnicastGlobal(): bool 294 | { 295 | if ($this->isEmbedded()) { 296 | throw new Exception\WrongVersionException(6, 4, (string) $this); 297 | } 298 | return parent::isUnicastGlobal(); 299 | } 300 | 301 | /** 302 | * {@inheritDoc} 303 | */ 304 | public function isBroadcast(): bool 305 | { 306 | if ($this->isEmbedded()) { 307 | return (new IPv4($this->getShortBinary()))->isBroadcast(); 308 | } 309 | throw new Exception\WrongVersionException(4, 6, (string) $this); 310 | } 311 | 312 | /** 313 | * {@inheritDoc} 314 | */ 315 | public function isShared(): bool 316 | { 317 | if ($this->isEmbedded()) { 318 | return (new IPv4($this->getShortBinary()))->isShared(); 319 | } 320 | throw new Exception\WrongVersionException(4, 6, (string) $this); 321 | } 322 | 323 | /** 324 | * {@inheritDoc} 325 | */ 326 | public function isFutureReserved(): bool 327 | { 328 | if ($this->isEmbedded()) { 329 | return (new IPv4($this->getShortBinary()))->isFutureReserved(); 330 | } 331 | throw new Exception\WrongVersionException(4, 6, (string) $this); 332 | } 333 | 334 | /** 335 | * @throws \Darsyn\IP\Exception\Strategy\ExtractionException 336 | */ 337 | private function getShortBinary(): string 338 | { 339 | return $this->embeddingStrategy->extract($this->getBinary()); 340 | } 341 | 342 | /** 343 | * Can the supplied CIDR and current version be considered as an IPv4 operation? 344 | */ 345 | private function isVersion4WithAppropriateCidr(int $cidr): bool 346 | { 347 | return $cidr <= 32 && $this->isVersion4(); 348 | } 349 | 350 | /** 351 | * Can the supplied and current IP be considered as an IPv4 operation? 352 | */ 353 | private function isVersion4CompatibleWithCurrentStrategy(IpInterface $ip): bool 354 | { 355 | return $this->isVersion4() && $ip->isVersion4() && $this->embeddingStrategy->isEmbedded($ip->getBinary()); 356 | } 357 | 358 | /** 359 | * {@inheritDoc} 360 | */ 361 | public function __toString(): string 362 | { 363 | return $this->getProtocolAppropriateAddress(); 364 | } 365 | } 366 | --------------------------------------------------------------------------------