Passed
Push — master ( 2d53e1...1fde52 )
by Alexander
01:17
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
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
    /**
41
     * Gets the IP version.
42
     *
43
     * @param string $ip the valid IPv4 or IPv6 address.
44
     * @param bool $validate enable perform IP address validation. False is best practice if the data comes from a trusted source.
45
     * @return int {{IPV4}} or {{IPV6}}
46
     */
47 52
    public static function getIpVersion(string $ip, bool $validate = true): int
48
    {
49 52
        $ipStringLength = strlen($ip);
50 52
        if ($ipStringLength < 2) {
51 4
            throw new \InvalidArgumentException("Unrecognized address $ip", 10);
52
        }
53 48
        $preIpVersion = strpos($ip, ':') === false ? self::IPV4 : self::IPV6;
54 48
        if ($preIpVersion === self::IPV4 && $ipStringLength < 7) {
55 1
            throw new \InvalidArgumentException("Unrecognized address $ip", 11);
56
        }
57 47
        if (!$validate) {
58 9
            return $preIpVersion;
59
        }
60 38
        $rawIp = @inet_pton($ip);
61 38
        if ($rawIp !== false) {
62 33
            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
     * @return bool whether $subnet is contained by $range
90
     *
91
     * @see https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
92
     */
93 10
    public static function inRange(string $subnet, string $range): bool
94
    {
95 10
        [$ip, $mask] = array_pad(explode('/', $subnet), 2, null);
96 10
        [$net, $netMask] = array_pad(explode('/', $range), 2, null);
97
98
        assert(is_string($ip));
99
        assert(is_string($net));
100
101 10
        $ipVersion = static::getIpVersion($ip);
102 10
        $netVersion = static::getIpVersion($net);
103 10
        if ($ipVersion !== $netVersion) {
104
            return false;
105
        }
106
107 10
        $maxMask = $ipVersion === self::IPV4 ? self::IPV4_ADDRESS_LENGTH : self::IPV6_ADDRESS_LENGTH;
108 10
        $mask ??= $maxMask;
109 10
        $netMask ??= $maxMask;
110
111 10
        $binIp = static::ip2bin($ip);
112 10
        $binNet = static::ip2bin($net);
113 10
        $masked = substr($binNet, 0, (int)$netMask);
114
115 10
        return ($masked === '' || strpos($binIp, $masked) === 0) && $mask >= $netMask;
116
    }
117
118
    /**
119
     * Expands an IPv6 address to it's full notation.
120
     *
121
     * For example `2001:db8::1` will be expanded to `2001:0db8:0000:0000:0000:0000:0000:0001`
122
     *
123
     * @param string $ip the original valid IPv6 address
124
     * @return string the expanded IPv6 address
125
     */
126 3
    public static function expandIPv6(string $ip): string
127
    {
128 3
        $ipRaw = @inet_pton($ip);
129 3
        if ($ipRaw === false) {
130 1
            if (@inet_pton('::1') === false) {
131
                throw new \RuntimeException('IPv6 is not supported by inet_pton()!');
132
            }
133 1
            throw new \InvalidArgumentException("Unrecognized address $ip");
134
        }
135 2
        $hex = unpack('H*hex', $ipRaw);
136 2
        return substr(preg_replace('/([a-f0-9]{4})/i', '$1:', $hex['hex']), 0, -1);
137
    }
138
139
    /**
140
     * Converts IP address to bits representation.
141
     *
142
     * @param string $ip the valid IPv4 or IPv6 address
143
     * @return string bits as a string
144
     */
145 14
    public static function ip2bin(string $ip): string
146
    {
147 14
        if (static::getIpVersion($ip) === self::IPV4) {
148 7
            $ipBinary = pack('N', ip2long($ip));
149 7
        } elseif (@inet_pton('::1') === false) {
150
            throw new \RuntimeException('IPv6 is not supported by inet_pton()!');
151
        } else {
152 7
            $ipBinary = inet_pton($ip);
153
        }
154
155
        assert(is_string($ipBinary));
156
157 14
        $result = '';
158 14
        for ($i = 0, $iMax = strlen($ipBinary); $i < $iMax; $i += 4) {
159 14
            $data = substr($ipBinary, $i, 4);
160 14
            if (!is_string($data)) {
161
                throw new \RuntimeException('An error occurred while converting IP address to bits representation.');
162
            }
163 14
            $result .= str_pad(decbin(unpack('N', $data)[1]), 32, '0', STR_PAD_LEFT);
164
        }
165 14
        return $result;
166
    }
167
168
    /**
169
     * Gets the bits from CIDR Notation.
170
     *
171
     * @param string $ip IP or IP with CIDR Notation (`127.0.0.1`, `2001:db8:a::123/64`)
172
     * @return int
173
     */
174 19
    public static function getCidrBits(string $ip): int
175
    {
176 19
        if (preg_match('/^(?<ip>.{2,}?)(?:\/(?<bits>-?\d+))?$/', $ip, $matches) === 0) {
177 2
            throw new \InvalidArgumentException("Unrecognized address $ip", 1);
178
        }
179 17
        $ipVersion = static::getIpVersion($matches['ip']);
180 15
        $maxBits = $ipVersion === self::IPV6 ? self::IPV6_ADDRESS_LENGTH : self::IPV4_ADDRESS_LENGTH;
181 15
        $bits = $matches['bits'] ?? null;
182 15
        if ($bits === null) {
183 3
            return $maxBits;
184
        }
185 12
        $bits = (int)$bits;
186 12
        if ($bits < 0) {
187 2
            throw new \InvalidArgumentException('The number of CIDR bits cannot be negative', 2);
188
        }
189 10
        if ($bits > $maxBits) {
190 2
            throw new \InvalidArgumentException("CIDR bits is greater than $bits", 3);
191
        }
192 8
        return $bits;
193
    }
194
}
195