Passed
Push — master ( 86b887...72229f )
by Alexander
01:37
created

IpHelper::inRange()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 13
c 1
b 0
f 0
nc 7
nop 2
dl 0
loc 20
ccs 13
cts 14
cp 0.9286
crap 5.009
rs 9.5222
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 35
    public static function getIpVersion(string $ip, bool $validate = true): int
28
    {
29 35
        $ipStringLength = strlen($ip);
30 35
        if ($ipStringLength < 2) {
31 4
            throw new \InvalidArgumentException("Unrecognized address $ip", 10);
32
        }
33 31
        $preIpVersion = strpos($ip, ':') === false ? self::IPV4 : self::IPV6;
34 31
        if ($preIpVersion === self::IPV4 && $ipStringLength < 7) {
35 1
            throw new \InvalidArgumentException("Unrecognized address $ip", 11);
36
        }
37 30
        if (!$validate) {
38 9
            return $preIpVersion;
39
        }
40 21
        $rawIp = @inet_pton($ip);
41 21
        if ($rawIp === false) {
42 3
            if (@inet_pton('::1') === false) {
43
                throw new \RuntimeException('IPv6 is not supported by inet_pton()!');
44
            }
45 3
            throw new \InvalidArgumentException("Unrecognized address $ip", 12);
46
        }
47 18
        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