Completed
Push — b0.3.0 ( 0ad74b...196bec )
by Sebastian
02:59
created

IPRange::getMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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