IPRange::concreteValidate()   A
last analyzed

Complexity

Conditions 6
Paths 9

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6

Importance

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