├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── infection.json.dist ├── psalm.xml ├── rector.php └── src ├── DnsHelper.php ├── IpHelper.php └── IpRanges.php /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8.1 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | 11 | enabled: 12 | - alpha_ordered_traits 13 | - array_indentation 14 | - array_push 15 | - combine_consecutive_issets 16 | - combine_consecutive_unsets 17 | - combine_nested_dirname 18 | - declare_strict_types 19 | - dir_constant 20 | - fully_qualified_strict_types 21 | - function_to_constant 22 | - hash_to_slash_comment 23 | - is_null 24 | - logical_operators 25 | - magic_constant_casing 26 | - magic_method_casing 27 | - method_separation 28 | - modernize_types_casting 29 | - native_function_casing 30 | - native_function_type_declaration_casing 31 | - no_alias_functions 32 | - no_empty_comment 33 | - no_empty_phpdoc 34 | - no_empty_statement 35 | - no_extra_block_blank_lines 36 | - no_short_bool_cast 37 | - no_superfluous_elseif 38 | - no_unneeded_control_parentheses 39 | - no_unneeded_curly_braces 40 | - no_unneeded_final_method 41 | - no_unset_cast 42 | - no_unused_imports 43 | - no_unused_lambda_imports 44 | - no_useless_else 45 | - no_useless_return 46 | - normalize_index_brace 47 | - php_unit_dedicate_assert 48 | - php_unit_dedicate_assert_internal_type 49 | - php_unit_expectation 50 | - php_unit_mock 51 | - php_unit_mock_short_will_return 52 | - php_unit_namespaced 53 | - php_unit_no_expectation_annotation 54 | - phpdoc_no_empty_return 55 | - phpdoc_no_useless_inheritdoc 56 | - phpdoc_order 57 | - phpdoc_property 58 | - phpdoc_scalar 59 | - phpdoc_singular_inheritdoc 60 | - phpdoc_trim 61 | - phpdoc_trim_consecutive_blank_line_separation 62 | - phpdoc_type_to_var 63 | - phpdoc_types 64 | - phpdoc_types_order 65 | - print_to_echo 66 | - regular_callable_call 67 | - return_assignment 68 | - self_accessor 69 | - self_static_accessor 70 | - set_type_to_cast 71 | - short_array_syntax 72 | - short_list_syntax 73 | - simplified_if_return 74 | - single_quote 75 | - standardize_not_equals 76 | - ternary_to_null_coalescing 77 | - trailing_comma_in_multiline_array 78 | - unalign_double_arrow 79 | - unalign_equals 80 | - empty_loop_body_braces 81 | - integer_literal_case 82 | - union_type_without_spaces 83 | 84 | disabled: 85 | - function_declaration 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii Network Utilities Change Log 2 | 3 | ## 1.2.1 under development 4 | 5 | - Chg #68: Change PHP constraint in `composer.json` to `7.4.* || 8.0 - 8.4` (@vjik) 6 | 7 | ## 1.2.0 September 02, 2024 8 | 9 | - New #65: Add `IP_PATTERN` and `IP_REGEXP` constants to `IpHelper` for checking IP of both IPv4 and IPv6 versions 10 | (@arogachev) 11 | - New #65: Add `NEGATION_CHARACTER` constant to `IpRanges` used to negate ranges (@arogachev) 12 | - New #65: Add `isIpv4()`, `isIpv6()`, `isIp()` methods to `IpHelper` (@arogachev) 13 | 14 | ## 1.1.0 August 06, 2024 15 | 16 | - New #63: Add `IpRanges` that represents a set of IP ranges that are either allowed or forbidden (@vjik) 17 | - Bug #59: Fix error while converting IP address to bits representation in PHP 8.0+ (@vjik) 18 | 19 | ## 1.0.1 January 27, 2022 20 | 21 | - Bug #40: Fix return type for callback of `set_error_handler()` function (@devanych) 22 | 23 | ## 1.0.0 March 04, 2021 24 | 25 | - Initial release. 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 |

Yii Network Utilities

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/network-utilities/v)](https://packagist.org/packages/yiisoft/network-utilities) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/network-utilities/downloads)](https://packagist.org/packages/yiisoft/network-utilities) 11 | [![Build status](https://github.com/yiisoft/network-utilities/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/network-utilities/actions/workflows/build.yml) 12 | [![Code coverage](https://codecov.io/gh/yiisoft/network-utilities/graph/badge.svg?token=LSO6D4QK3O)](https://codecov.io/gh/yiisoft/network-utilities) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fnetwork-utilities%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/network-utilities/master) 14 | [![static analysis](https://github.com/yiisoft/network-utilities/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/network-utilities/actions?query=workflow%3A%22static+analysis%22) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/network-utilities/coverage.svg)](https://shepherd.dev/github/yiisoft/network-utilities) 16 | 17 | The package contains various network utilities useful for: 18 | 19 | - getting info about IP address; 20 | - checking if IP is in a certain range; 21 | - expanding IPv6; 22 | - converting IP to bits representation; 23 | - checking DNS record availability. 24 | 25 | ## Requirements 26 | 27 | - PHP 7.4 or higher. 28 | 29 | ## Installation 30 | 31 | The package could be installed with [Composer](https://getcomposer.org): 32 | 33 | ```shell 34 | composer require yiisoft/network-utilities 35 | ``` 36 | 37 | ## General usage 38 | 39 | ### `IpHelper` 40 | 41 | ```php 42 | use Yiisoft\NetworkUtilities\IpHelper; 43 | 44 | // Check IP version. 45 | $version = IpHelper::getIpVersion('192.168.1.1'); 46 | if ($version === IpHelper::IPV4) { 47 | // ... 48 | } 49 | 50 | // Check if IP is in a certain range. 51 | if (!IpHelper::inRange('192.168.1.21/32', '192.168.1.0/24')) { 52 | throw new \RuntimeException('Access denied!'); 53 | } 54 | 55 | // Expand IP v6. 56 | echo IpHelper::expandIPv6('2001:db8::1'); 57 | 58 | // Convert IP to bits representation. 59 | echo IpHelper::ip2bin('192.168.1.1'); 60 | 61 | // Get bits from CIDR Notation. 62 | echo IpHelper::getCidrBits('192.168.1.21/32'); 63 | ``` 64 | 65 | ### `DnsHelper` 66 | 67 | ```php 68 | use Yiisoft\NetworkUtilities\DnsHelper; 69 | 70 | // Check DNS record availability. 71 | if (!DnsHelper::existsA('yiiframework.com')) { 72 | // Record not found. 73 | } 74 | ``` 75 | 76 | ### `IpRanges` 77 | 78 | ```php 79 | use Yiisoft\NetworkUtilities\IpRanges; 80 | 81 | $ipRanges = new IpRanges( 82 | [ 83 | '10.0.1.0/24', 84 | '2001:db0:1:2::/64', 85 | IpRanges::LOCALHOST, 86 | 'myNetworkEu', 87 | '!' . IpRanges::ANY, 88 | ], 89 | [ 90 | 'myNetworkEu' => ['1.2.3.4/10', '5.6.7.8'], 91 | ], 92 | ); 93 | 94 | $ipRanges->isAllowed('10.0.1.28/28'); // true 95 | $ipRanges->isAllowed('1.2.3.4'); // true 96 | $ipRanges->isAllowed('192.168.0.1'); // false 97 | ``` 98 | 99 | ## Documentation 100 | 101 | - [Internals](docs/internals.md) 102 | 103 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. 104 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 105 | 106 | ## License 107 | 108 | The Yii Network Utilities is free software. It is released under the terms of the BSD License. 109 | Please see [`LICENSE`](./LICENSE.md) for more information. 110 | 111 | Maintained by [Yii Software](https://www.yiiframework.com/). 112 | 113 | ## Support the project 114 | 115 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 116 | 117 | ## Follow updates 118 | 119 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 120 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 121 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 122 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 123 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 124 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/network-utilities", 3 | "type": "library", 4 | "description": "Network related utilities", 5 | "keywords": [ 6 | "network", 7 | "ip", 8 | "mask", 9 | "dns" 10 | ], 11 | "homepage": "https://www.yiiframework.com/", 12 | "license": "BSD-3-Clause", 13 | "support": { 14 | "issues": "https://github.com/yiisoft/network-utilities/issues?state=open", 15 | "source": "https://github.com/yiisoft/network-utilities", 16 | "forum": "https://www.yiiframework.com/forum/", 17 | "wiki": "https://www.yiiframework.com/wiki/", 18 | "irc": "ircs://irc.libera.chat:6697/yii", 19 | "chat": "https://t.me/yii3en" 20 | }, 21 | "funding": [ 22 | { 23 | "type": "opencollective", 24 | "url": "https://opencollective.com/yiisoft" 25 | }, 26 | { 27 | "type": "github", 28 | "url": "https://github.com/sponsors/yiisoft" 29 | } 30 | ], 31 | "require": { 32 | "php": "7.4.* || 8.0 - 8.4" 33 | }, 34 | "require-dev": { 35 | "maglnet/composer-require-checker": "^3.8 || ^4.4", 36 | "phpunit/phpunit": "^9.6.22", 37 | "rector/rector": "^2.0.10", 38 | "roave/infection-static-analysis-plugin": "^1.18", 39 | "spatie/phpunit-watcher": "^1.23.6", 40 | "vimeo/psalm": "^4.30 || ^5.26.1 || ^6.8.8" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Yiisoft\\NetworkUtilities\\": "src" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Yiisoft\\NetworkUtilities\\Tests\\": "tests" 50 | } 51 | }, 52 | "scripts": { 53 | "test": "phpunit --testdox --no-interaction", 54 | "test-watch": "phpunit-watcher watch" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "bump-after-update": "dev", 59 | "allow-plugins": { 60 | "infection/extension-installer": true, 61 | "composer/package-versions-deprecated": true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php:\/\/stderr", 9 | "stryker": { 10 | "report": "master" 11 | } 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]); 15 | 16 | // register a single rule 17 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 18 | 19 | // define sets of rules 20 | $rectorConfig->sets([ 21 | LevelSetList::UP_TO_PHP_74, 22 | ]); 23 | 24 | $rectorConfig->skip([ 25 | ClosureToArrowFunctionRector::class, 26 | ]); 27 | }; 28 | -------------------------------------------------------------------------------- /src/DnsHelper.php: -------------------------------------------------------------------------------- 1 | 0; 40 | } 41 | 42 | /** 43 | * Checks DNS A record availability. 44 | * 45 | * @param string $hostname Hostname without dot at end. 46 | * 47 | * @return bool Whether A records exists. 48 | */ 49 | public static function existsA(string $hostname): bool 50 | { 51 | set_error_handler(static function (int $errorNumber, string $errorString) use ($hostname): bool { 52 | throw new RuntimeException( 53 | sprintf('Failed to get DNS record "%s". ', $hostname) . $errorString, 54 | $errorNumber 55 | ); 56 | }); 57 | 58 | /** 59 | * @var array $result We catch errors by `set_error_handler()` and throw exceptions if something goes wrong. 60 | * So `dns_get_record()` will always return an array. 61 | */ 62 | $result = dns_get_record($hostname, DNS_A); 63 | 64 | restore_error_handler(); 65 | 66 | return count($result) > 0; 67 | } 68 | 69 | /** 70 | * Checks email's domain availability. 71 | * 72 | * @link https://tools.ietf.org/html/rfc5321#section-5 73 | * 74 | * @param string $hostnameOrEmail Hostname without dot at end or an email. 75 | * 76 | * @return bool Whether email domain is available. 77 | */ 78 | public static function acceptsEmails(string $hostnameOrEmail): bool 79 | { 80 | if (strpos($hostnameOrEmail, '@') !== false) { 81 | /** 82 | * @psalm-suppress PossiblyUndefinedArrayOffset In this case `explode()` always returns an array with 2 elements. 83 | */ 84 | [, $hostnameOrEmail] = explode('@', $hostnameOrEmail, 2); 85 | } 86 | return self::existsMx($hostnameOrEmail) || self::existsA($hostnameOrEmail); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/IpHelper.php: -------------------------------------------------------------------------------- 1 | > 3 ? self::IPV4 : self::IPV6; 97 | } 98 | if ($preIpVersion === self::IPV6 && preg_match(self::IPV6_REGEXP, $ip) === 1) { 99 | return self::IPV6; 100 | } 101 | throw new InvalidArgumentException("Unrecognized address $ip.", 12); 102 | } 103 | 104 | /** 105 | * Checks whether IP address or subnet $subnet is contained by $subnet. 106 | * 107 | * For example, the following code checks whether subnet `192.168.1.0/24` is in subnet `192.168.0.0/22`: 108 | * 109 | * ```php 110 | * IpHelper::inRange('192.168.1.0/24', '192.168.0.0/22'); // true 111 | * ``` 112 | * 113 | * In case you need to check whether a single IP address `192.168.1.21` is in the subnet `192.168.1.0/24`, 114 | * you can use any of theses examples: 115 | * 116 | * ```php 117 | * IpHelper::inRange('192.168.1.21', '192.168.1.0/24'); // true 118 | * IpHelper::inRange('192.168.1.21/32', '192.168.1.0/24'); // true 119 | * ``` 120 | * 121 | * @param string $subnet The valid IPv4 or IPv6 address or CIDR range, e.g.: `10.0.0.0/8` or `2001:af::/64`. 122 | * @param string $range The valid IPv4 or IPv6 CIDR range, e.g. `10.0.0.0/8` or `2001:af::/64`. 123 | * 124 | * @return bool Whether $subnet is contained by $range. 125 | * 126 | * @see https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing 127 | */ 128 | public static function inRange(string $subnet, string $range): bool 129 | { 130 | [$ip, $mask] = array_pad(explode('/', $subnet), 2, null); 131 | [$net, $netMask] = array_pad(explode('/', $range), 2, null); 132 | 133 | assert(is_string($ip)); 134 | assert(is_string($net)); 135 | 136 | $ipVersion = self::getIpVersion($ip); 137 | $netVersion = self::getIpVersion($net); 138 | if ($ipVersion !== $netVersion) { 139 | return false; 140 | } 141 | 142 | $maxMask = $ipVersion === self::IPV4 ? self::IPV4_ADDRESS_LENGTH : self::IPV6_ADDRESS_LENGTH; 143 | $mask ??= $maxMask; 144 | $netMask ??= $maxMask; 145 | 146 | $binIp = self::ip2bin($ip); 147 | $binNet = self::ip2bin($net); 148 | $masked = substr($binNet, 0, (int) $netMask); 149 | 150 | return ($masked === '' || strpos($binIp, $masked) === 0) && $mask >= $netMask; 151 | } 152 | 153 | /** 154 | * Expands an IPv6 address to it's full notation. 155 | * 156 | * For example `2001:db8::1` will be expanded to `2001:0db8:0000:0000:0000:0000:0000:0001`. 157 | * 158 | * @param string $ip The original valid IPv6 address. 159 | * 160 | * @return string The expanded IPv6 address. 161 | */ 162 | public static function expandIPv6(string $ip): string 163 | { 164 | $ipRaw = @inet_pton($ip); 165 | if ($ipRaw === false) { 166 | if (@inet_pton('::1') === false) { 167 | throw new RuntimeException('IPv6 is not supported by inet_pton()!'); 168 | } 169 | throw new InvalidArgumentException("Unrecognized address $ip."); 170 | } 171 | 172 | /** @psalm-var array{hex:string} $hex */ 173 | $hex = unpack('H*hex', $ipRaw); 174 | 175 | /** 176 | * @psalm-suppress PossiblyNullArgument We use correct regular expression, so `preg_replace()` will always 177 | * return a string. 178 | */ 179 | return substr(preg_replace('/([a-f0-9]{4})/i', '$1:', $hex['hex']), 0, -1); 180 | } 181 | 182 | /** 183 | * Converts IP address to bits representation. 184 | * 185 | * @param string $ip The valid IPv4 or IPv6 address. 186 | * 187 | * @return string Bits as a string. 188 | */ 189 | public static function ip2bin(string $ip): string 190 | { 191 | if (self::getIpVersion($ip) === self::IPV4) { 192 | $ipBinary = pack('N', ip2long($ip)); 193 | } elseif (@inet_pton('::1') === false) { 194 | throw new RuntimeException('IPv6 is not supported by inet_pton()!'); 195 | } else { 196 | $ipBinary = inet_pton($ip); 197 | } 198 | 199 | assert(is_string($ipBinary)); 200 | 201 | $result = ''; 202 | for ($i = 0, $iMax = strlen($ipBinary); $i < $iMax; $i += 4) { 203 | $data = substr($ipBinary, $i, 4); 204 | if (empty($data)) { 205 | throw new RuntimeException('An error occurred while converting IP address to bits representation.'); 206 | } 207 | /** 208 | * @psalm-suppress MixedArgument, PossiblyInvalidArrayAccess 209 | */ 210 | $result .= str_pad(decbin(unpack('N', $data)[1]), 32, '0', STR_PAD_LEFT); 211 | } 212 | return $result; 213 | } 214 | 215 | /** 216 | * Gets the bits from CIDR Notation. 217 | * 218 | * @param string $ip IP or IP with CIDR Notation (`127.0.0.1`, `2001:db8:a::123/64`). 219 | * 220 | * @return int Bits. 221 | */ 222 | public static function getCidrBits(string $ip): int 223 | { 224 | if (preg_match('/^(?.{2,}?)(?:\/(?-?\d+))?$/', $ip, $matches) === 0) { 225 | throw new InvalidArgumentException("Unrecognized address $ip.", 1); 226 | } 227 | $ipVersion = self::getIpVersion($matches['ip']); 228 | $maxBits = $ipVersion === self::IPV6 ? self::IPV6_ADDRESS_LENGTH : self::IPV4_ADDRESS_LENGTH; 229 | $bits = $matches['bits'] ?? null; 230 | if ($bits === null) { 231 | return $maxBits; 232 | } 233 | $bits = (int) $bits; 234 | if ($bits < 0) { 235 | throw new InvalidArgumentException('The number of CIDR bits cannot be negative.', 2); 236 | } 237 | if ($bits > $maxBits) { 238 | throw new InvalidArgumentException("CIDR bits is greater than $bits.", 3); 239 | } 240 | return $bits; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/IpRanges.php: -------------------------------------------------------------------------------- 1 | [self::ANY], 38 | self::ANY => ['0.0.0.0/0', '::/0'], 39 | self::PRIVATE => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fd00::/8'], 40 | self::MULTICAST => ['224.0.0.0/4', 'ff00::/8'], 41 | self::LINK_LOCAL => ['169.254.0.0/16', 'fe80::/10'], 42 | self::LOCALHOST => ['127.0.0.0/8', '::1'], 43 | self::DOCUMENTATION => ['192.0.2.0/24', '198.51.100.0/24', '203.0.113.0/24', '2001:db8::/32'], 44 | self::SYSTEM => [self::MULTICAST, self::LINK_LOCAL, self::LOCALHOST, self::DOCUMENTATION], 45 | ]; 46 | 47 | /** 48 | * @var string[] 49 | */ 50 | private array $ranges; 51 | 52 | /** 53 | * @psalm-var array> 54 | */ 55 | private array $networks; 56 | 57 | /** 58 | * @param string[] $ranges The IPv4 or IPv6 ranges that are either allowed or forbidden. 59 | * 60 | * The following preparation tasks are performed: 61 | * - recursively substitute aliases (described in {@see $networks}) with their values; 62 | * - remove duplicates. 63 | * 64 | * When the array is empty or the option is not set, all IP addresses are allowed. 65 | * 66 | * Otherwise, the rules are checked sequentially until the first match is found. An IP address is forbidden 67 | * when it hasn't matched any of the rules. 68 | * 69 | * Example: 70 | * 71 | * ```php 72 | * new Ip(ranges: [ 73 | * '192.168.10.128' 74 | * '!192.168.10.0/24', 75 | * 'any' // allows any other IP addresses 76 | * ]); 77 | * ``` 78 | * 79 | * In this example, access is allowed for all the IPv4 and IPv6 addresses excluding the `192.168.10.0/24` 80 | * subnet. IPv4 address `192.168.10.128` is also allowed, because it is listed before the restriction. 81 | * @param array $networks Custom network aliases, that can be used in {@see $ranges}: 82 | * - key - alias name; 83 | * - value - array of strings. String can be an IP range, IP address or another alias. String can be negated 84 | * with `!` character. 85 | * The default aliases are defined in {@see self::DEFAULT_NETWORKS} and will be merged with custom ones. 86 | * 87 | * @psalm-param array> $networks 88 | */ 89 | public function __construct(array $ranges = [], array $networks = []) 90 | { 91 | foreach ($networks as $key => $_values) { 92 | if (array_key_exists($key, self::DEFAULT_NETWORKS)) { 93 | throw new InvalidArgumentException("Network alias \"{$key}\" already set as default."); 94 | } 95 | } 96 | $this->networks = array_merge(self::DEFAULT_NETWORKS, $networks); 97 | 98 | $this->ranges = $this->prepareRanges($ranges); 99 | } 100 | 101 | /** 102 | * Get the IPv4 or IPv6 ranges that are either allowed or forbidden. 103 | * 104 | * @return string[] The IPv4 or IPv6 ranges that are either allowed or forbidden. 105 | */ 106 | public function getRanges(): array 107 | { 108 | return $this->ranges; 109 | } 110 | 111 | /** 112 | * Get network aliases, that can be used in {@see $ranges}. 113 | * 114 | * @return array Network aliases. 115 | * 116 | * @see $networks 117 | */ 118 | public function getNetworks(): array 119 | { 120 | return $this->networks; 121 | } 122 | 123 | /** 124 | * Whether the IP address with specified CIDR is allowed according to the {@see $ranges} list. 125 | */ 126 | public function isAllowed(string $ip): bool 127 | { 128 | if (empty($this->ranges)) { 129 | return true; 130 | } 131 | 132 | foreach ($this->ranges as $string) { 133 | [$isNegated, $range] = $this->parseNegatedRange($string); 134 | if (IpHelper::inRange($ip, $range)) { 135 | return !$isNegated; 136 | } 137 | } 138 | 139 | return false; 140 | } 141 | 142 | /** 143 | * Prepares array to fill in {@see $ranges}: 144 | * - recursively substitutes aliases, described in `$networks` argument with their values; 145 | * - removes duplicates. 146 | * 147 | * @param string[] $ranges 148 | * @return string[] 149 | */ 150 | private function prepareRanges(array $ranges): array 151 | { 152 | $result = []; 153 | foreach ($ranges as $string) { 154 | [$isRangeNegated, $range] = $this->parseNegatedRange($string); 155 | if (isset($this->networks[$range])) { 156 | $replacements = $this->prepareRanges($this->networks[$range]); 157 | foreach ($replacements as &$replacement) { 158 | [$isReplacementNegated, $replacement] = $this->parseNegatedRange($replacement); 159 | $result[] = ($isRangeNegated && !$isReplacementNegated ? self::NEGATION_CHARACTER : '') 160 | . $replacement; 161 | } 162 | } else { 163 | $result[] = $string; 164 | } 165 | } 166 | 167 | return array_unique($result); 168 | } 169 | 170 | /** 171 | * Parses IP address/range for the negation with `!`. 172 | * 173 | * @return array The result array consists of 2 elements: 174 | * - `boolean` - whether the string is negated; 175 | * - `string` - the string without negation (when the negation were present). 176 | * 177 | * @psalm-return array{0: bool, 1: string} 178 | */ 179 | private function parseNegatedRange(string $string): array 180 | { 181 | $isNegated = strpos($string, self::NEGATION_CHARACTER) === 0; 182 | return [$isNegated, $isNegated ? substr($string, strlen(self::NEGATION_CHARACTER)) : $string]; 183 | } 184 | } 185 | --------------------------------------------------------------------------------