IpHelper::getIpVersion()   B
last analyzed

Complexity

Conditions 10
Paths 13

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 10.0296

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 14
c 1
b 0
f 0
nc 13
nop 2
dl 0
loc 21
ccs 14
cts 15
cp 0.9333
crap 10.0296
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\NetworkUtilities;
6
7
use InvalidArgumentException;
8
use RuntimeException;
9
10
use function assert;
11
use function is_string;
12
use function strlen;
13
14
/**
15
 * DnsHelper contains static methods to work with IPs.
16
 */
17
final class IpHelper
18
{
19
    public const IPV4 = 4;
20
    public const IPV6 = 6;
21
22
    /**
23
     * IPv4 address pattern. This pattern is PHP and JavaScript compatible.
24
     * Allows to define your own IP regexp eg. `'/^'.IpHelper::IPV4_PATTERN.'/(\d+)$/'`.
25
     */
26
    public const IPV4_PATTERN = '((2(5[0-5]|[0-4]\d)|1\d{2}|[1-9]?\d)\.){3}(2(5[0-5]|[0-4]\d)|1\d{2}|[1-9]?\d)';
27
    /**
28
     * IPv6 address regexp. This regexp is PHP and Javascript compatible.
29
     */
30
    public const IPV4_REGEXP = '/^' . self::IPV4_PATTERN . '$/';
31
    /**
32
     * IPv6 address pattern. This pattern is PHP and Javascript compatible.
33
     * Allows to define your own IP regexp eg. `'/^'.IpHelper::IPV6_PATTERN.'/(\d+)$/'`.
34
     */
35
    public const IPV6_PATTERN = '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:' . self::IPV4_PATTERN . ')';
36
    /**
37
     * IPv6 address regexp. This regexp is PHP and JavaScript compatible.
38
     */
39
    public const IPV6_REGEXP = '/^' . self::IPV6_PATTERN . '$/';
40
    /**
41
     * The length of IPv6 address in bits.
42
     */
43
    public const IPV6_ADDRESS_LENGTH = 128;
44
    /**
45
     * The length of IPv4 address in bits.
46
     */
47
    public const IPV4_ADDRESS_LENGTH = 32;
48
49
    /**
50
     * Gets the IP version.
51
     *
52
     * @param string $ip The valid IPv4 or IPv6 address.
53
     * @param bool $validate Enable perform IP address validation. False is best practice if the data comes from a trusted source.
54
     *
55
     * @return int {@see IPV4} or {@see IPV6}.
56
     */
57 53
    public static function getIpVersion(string $ip, bool $validate = true): int
58
    {
59 53
        $ipStringLength = strlen($ip);
60 53
        if ($ipStringLength < 2) {
61 4
            throw new InvalidArgumentException("Unrecognized address $ip", 10);
62
        }
63 49
        $preIpVersion = strpos($ip, ':') === false ? self::IPV4 : self::IPV6;
64 49
        if ($preIpVersion === self::IPV4 && $ipStringLength < 7) {
65 1
            throw new InvalidArgumentException("Unrecognized address $ip", 11);
66
        }
67 48
        if (!$validate) {
68 9
            return $preIpVersion;
69
        }
70 39
        $rawIp = @inet_pton($ip);
71 39
        if ($rawIp !== false) {
72 34
            return strlen($rawIp) === self::IPV4_ADDRESS_LENGTH >> 3 ? self::IPV4 : self::IPV6;
73
        }
74 5
        if ($preIpVersion === self::IPV6 && preg_match(self::IPV6_REGEXP, $ip) === 1) {
75
            return self::IPV6;
76
        }
77 5
        throw new InvalidArgumentException("Unrecognized address $ip.", 12);
78
    }
79
80
    /**
81
     * Checks whether IP address or subnet $subnet is contained by $subnet.
82
     *
83
     * For example, the following code checks whether subnet `192.168.1.0/24` is in subnet `192.168.0.0/22`:
84
     *
85
     * ```php
86
     * IpHelper::inRange('192.168.1.0/24', '192.168.0.0/22'); // true
87
     * ```
88
     *
89
     * In case you need to check whether a single IP address `192.168.1.21` is in the subnet `192.168.1.0/24`,
90
     * you can use any of theses examples:
91
     *
92
     * ```php
93
     * IpHelper::inRange('192.168.1.21', '192.168.1.0/24'); // true
94
     * IpHelper::inRange('192.168.1.21/32', '192.168.1.0/24'); // true
95
     * ```
96
     *
97
     * @param string $subnet The valid IPv4 or IPv6 address or CIDR range, e.g.: `10.0.0.0/8` or `2001:af::/64`.
98
     * @param string $range The valid IPv4 or IPv6 CIDR range, e.g. `10.0.0.0/8` or `2001:af::/64`.
99
     *
100
     * @return bool Whether $subnet is contained by $range.
101
     *
102
     * @see https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
103
     */
104 11
    public static function inRange(string $subnet, string $range): bool
105
    {
106 11
        [$ip, $mask] = array_pad(explode('/', $subnet), 2, null);
107 11
        [$net, $netMask] = array_pad(explode('/', $range), 2, null);
108
109 11
        assert(is_string($ip));
110 11
        assert(is_string($net));
111
112 11
        $ipVersion = self::getIpVersion($ip);
113 11
        $netVersion = self::getIpVersion($net);
114 11
        if ($ipVersion !== $netVersion) {
115 1
            return false;
116
        }
117
118 10
        $maxMask = $ipVersion === self::IPV4 ? self::IPV4_ADDRESS_LENGTH : self::IPV6_ADDRESS_LENGTH;
119 10
        $mask ??= $maxMask;
120 10
        $netMask ??= $maxMask;
121
122 10
        $binIp = self::ip2bin($ip);
123 10
        $binNet = self::ip2bin($net);
124 10
        $masked = substr($binNet, 0, (int) $netMask);
125
126 10
        return ($masked === '' || strpos($binIp, $masked) === 0) && $mask >= $netMask;
127
    }
128
129
    /**
130
     * Expands an IPv6 address to it's full notation.
131
     *
132
     * For example `2001:db8::1` will be expanded to `2001:0db8:0000:0000:0000:0000:0000:0001`.
133
     *
134
     * @param string $ip The original valid IPv6 address.
135
     *
136
     * @return string The expanded IPv6 address.
137
     */
138 3
    public static function expandIPv6(string $ip): string
139
    {
140 3
        $ipRaw = @inet_pton($ip);
141 3
        if ($ipRaw === false) {
142 1
            if (@inet_pton('::1') === false) {
143
                throw new RuntimeException('IPv6 is not supported by inet_pton()!');
144
            }
145 1
            throw new InvalidArgumentException("Unrecognized address $ip.");
146
        }
147
148
        /** @psalm-var array{hex:string} $hex */
149 2
        $hex = unpack('H*hex', $ipRaw);
150
151 2
        return substr(preg_replace('/([a-f0-9]{4})/i', '$1:', $hex['hex']), 0, -1);
152
    }
153
154
    /**
155
     * Converts IP address to bits representation.
156
     *
157
     * @param string $ip The valid IPv4 or IPv6 address.
158
     *
159
     * @return string Bits as a string.
160
     */
161 14
    public static function ip2bin(string $ip): string
162
    {
163 14
        if (self::getIpVersion($ip) === self::IPV4) {
164 7
            $ipBinary = pack('N', ip2long($ip));
165 7
        } elseif (@inet_pton('::1') === false) {
166
            throw new RuntimeException('IPv6 is not supported by inet_pton()!');
167
        } else {
168 7
            $ipBinary = inet_pton($ip);
169
        }
170
171 14
        assert(is_string($ipBinary));
172
173 14
        $result = '';
174 14
        for ($i = 0, $iMax = strlen($ipBinary); $i < $iMax; $i += 4) {
175 14
            $data = substr($ipBinary, $i, 4);
176 14
            if (!is_string($data)) {
177
                throw new RuntimeException('An error occurred while converting IP address to bits representation.');
178
            }
179
            /** @psalm-suppress MixedArgument */
180 14
            $result .= str_pad(decbin(unpack('N', $data)[1]), 32, '0', STR_PAD_LEFT);
181
        }
182 14
        return $result;
183
    }
184
185
    /**
186
     * Gets the bits from CIDR Notation.
187
     *
188
     * @param string $ip IP or IP with CIDR Notation (`127.0.0.1`, `2001:db8:a::123/64`).
189
     *
190
     * @return int Bits.
191
     */
192 19
    public static function getCidrBits(string $ip): int
193
    {
194 19
        if (preg_match('/^(?<ip>.{2,}?)(?:\/(?<bits>-?\d+))?$/', $ip, $matches) === 0) {
195 2
            throw new InvalidArgumentException("Unrecognized address $ip.", 1);
196
        }
197 17
        $ipVersion = self::getIpVersion($matches['ip']);
198 15
        $maxBits = $ipVersion === self::IPV6 ? self::IPV6_ADDRESS_LENGTH : self::IPV4_ADDRESS_LENGTH;
199 15
        $bits = $matches['bits'] ?? null;
200 15
        if ($bits === null) {
201 3
            return $maxBits;
202
        }
203 12
        $bits = (int) $bits;
204 12
        if ($bits < 0) {
205 2
            throw new InvalidArgumentException('The number of CIDR bits cannot be negative.', 2);
206
        }
207 10
        if ($bits > $maxBits) {
208 2
            throw new InvalidArgumentException("CIDR bits is greater than $bits.", 3);
209
        }
210 8
        return $bits;
211
    }
212
}
213