Completed
Push — master ( 0ad74b...6413d7 )
by Sebastian
02:51
created

IPRange::checkVersion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Linna Filter
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2018, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna\Filter\Rules;
13
14
use InvalidArgumentException;
15
16
/**
17
 * Check if provided IP is in CIDR range.
18
 * Support IPv4 and IPv6.
19
 */
20
class IPRange extends Ip implements RuleValidateInterface
21
{
22
    /**
23
     * @var array Rule properties
24
     */
25
    public static $config = [
26
        'class' => 'IPRange',
27
        'full_class' => __CLASS__,
28
        'alias' => ['iprange', 'iprng', 'ipr'],
29
        'args_count' => 1,
30
        'args_type' => [],
31
        'has_validate' => true
32
    ];
33
34
    /**
35
     * @var string Error message
36
     */
37
    private $message = '';
38
39
    /**
40
     * Validate.
41
     *
42
     * @return bool
43
     */
44 208
    public function validate(): bool
45
    {
46 208
        $args = func_get_args();
47
48 208
        return $this->concreteValidate($args[0], $args[1]);
49
    }
50
51
    /**
52
     * Concrete validate.
53
     *
54
     * @param string $received
55
     * @param string $range
56
     *
57
     * @return bool
58
     */
59 208
    public function concreteValidate(string $received, string $range): bool
60
    {
61 208
        if (parent::validate($received)) {
62 2
            $this->message = 'Received value is not a valid ip address';
63 2
            return true;
64
        }
65
66
        //separate address and bit suffix
67 206
        $cidr = explode('/', $range, 2);
68
69 206
        $address = $cidr[0];
70 206
        $version = $this->checkVersion($cidr[0]);
71
72 204
        $bits = $cidr[1] ?? 0;
73 204
        $bits = $this->checkSuffix((int) $bits, $version);
74
75 186
        $ipv4 = $ipv6 = true;
76
77 186
        if ($version === 4) {
78 45
            $ipv4 = !$this->inRangeIpv4($received, $address, $bits);
79
        }
80
81 186
        if ($version === 6) {
82 141
            $ipv6 = !$this->inRangeIpv6($received, $address, $bits);
83
        }
84
85 186
        if ($ipv4 && $ipv6) {
86 10
            $this->message = "Received ip is not in ({$range}) range";
87 10
            return true;
88
        }
89
90 176
        return false;
91
    }
92
93
    /**
94
     * Check the version of ip address.
95
     *
96
     * @param string $ip
97
     *
98
     * @return int
99
     *
100
     * @throws InvalidArgumentException If provided address is not a valid ip.
101
     */
102 206
    private function checkVersion(string $ip): int
103
    {
104 206
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
105 54
            return 4;
106
        }
107
108 152
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
109 150
            return 6;
110
        }
111
112 2
        throw new InvalidArgumentException('Range must be in valid IP/CIDR format, invalid address.');
113
    }
114
115
    /**
116
     * Check if an ipv4 is in cidr range.
117
     *
118
     * @param string $received
119
     * @param string $address
120
     * @param int    $bits
121
     *
122
     * @return bool
123
     */
124 45
    private function inRangeIpv4(string $received, string $address, int $bits): bool
125
    {
126 45
        $decimalWildcard = pow(2, (32 - $bits)) - 1;
127 45
        $decimalBits = ~ $decimalWildcard;
128
129 45
        return ((ip2long($received) & $decimalBits) === (ip2long($address) & $decimalBits));
130
    }
131
132
    /**
133
     * Check if an ipv6 is in cidr range.
134
     *
135
     * @param string $received
136
     * @param string $address
137
     * @param int    $bits
138
     *
139
     * @return bool
140
     */
141 141
    private function inRangeIpv6(string $received, string $address, int $bits): bool
142
    {
143 141
        $binaryIp = $this->inetToBits($received);
144 141
        $binaryNet = $this->inetToBits($address);
145
146 141
        $ipNetBits = substr($binaryIp, 0, $bits);
147 141
        $netBits = substr($binaryNet, 0, $bits);
148
149 141
        return $ipNetBits === $netBits;
150
    }
151
152
    /**
153
     * Convert an ipv6 address to a string of bits.
154
     *
155
     * @param string $ipv6
156
     *
157
     * @return string
158
     */
159 141
    private function inetToBits(string $ipv6): string
160
    {
161 141
        $unpck = str_split(unpack('A16', inet_pton($ipv6))[1]);
162
163 141
        foreach ($unpck as $key => $char) {
164 141
            $unpck[$key] = str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
165
        }
166
167 141
        return implode('', $unpck);
168
    }
169
170
    /**
171
     * Check for a valid bits suffix in cidr notation.
172
     *
173
     * @param int $bits
174
     * @param int $version
175
     *
176
     * @return int
177
     *
178
     * @throws InvalidArgumentException If suffix is empty and if suffix is out of range.
179
     */
180 204
    private function checkSuffix(int $bits, int $version): int
181
    {
182 204
        $versions = [4 => 32, 6 => 128];
183 204
        $maxBits = $versions[$version];
184
185 204
        if (empty($bits)) {
186 6
            throw new InvalidArgumentException('Range must be in valid IP/CIDR format, empty bits for suffix.');
187
        }
188
189 198
        if ((int) $bits < 1 || (int) $bits > $maxBits) {
190 12
            throw new InvalidArgumentException('Range must be in valid IP/CIDR format, invalid bits suffix range.');
191
        }
192
193 186
        return $bits;
194
    }
195
196
    /**
197
     * Return error message.
198
     *
199
     * @return string Error message
200
     */
201 4
    public function getMessage(): string
202
    {
203 4
        return $this->message;
204
    }
205
}
206