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