Passed
Push — master ( 72229f...bb511d )
by Alexander
01:27
created

IpHelper   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 159
Duplicated Lines 0 %

Test Coverage

Coverage 93.55%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 27
eloc 63
c 1
b 0
f 0
dl 0
loc 159
ccs 58
cts 62
cp 0.9355
rs 10

5 Methods

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