Passed
Push — master ( 61c905...ca26ac )
by Alexander
03:28 queued 01:32
created

IpHelper::getIpVersion()   B

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