├── .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 |
4 |
5 |
Yii Network Utilities
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/network-utilities)
10 | [](https://packagist.org/packages/yiisoft/network-utilities)
11 | [](https://github.com/yiisoft/network-utilities/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/network-utilities)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/network-utilities/master)
14 | [](https://github.com/yiisoft/network-utilities/actions?query=workflow%3A%22static+analysis%22)
15 | [](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 | [](https://opencollective.com/yiisoft)
116 |
117 | ## Follow updates
118 |
119 | [](https://www.yiiframework.com/)
120 | [](https://twitter.com/yiiframework)
121 | [](https://t.me/yii3en)
122 | [](https://www.facebook.com/groups/yiitalk)
123 | [](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 |
--------------------------------------------------------------------------------