Passed
Pull Request — master (#7)
by Alexander
01:23
created

IpHelper::getCidrBits()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

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