Ip::decimalIpv6Confirm()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 7
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * php version 7.1.0
11
 *
12
 * @category  Web-security
13
 * @package   Shieldon
14
 * @author    Terry Lin <[email protected]>
15
 * @copyright 2019 terrylinooo
16
 * @license   https://github.com/terrylinooo/shieldon/blob/2.x/LICENSE MIT
17
 * @link      https://github.com/terrylinooo/shieldon
18
 * @see       https://shieldon.io
19
 */
20
21
declare(strict_types=1);
22
23
namespace Shieldon\Firewall\Component;
24
25
use Shieldon\Firewall\Component\ComponentProvider;
26
use Shieldon\Firewall\Component\AllowedTrait;
27
use Shieldon\Firewall\Component\DeniedTrait;
28
use Shieldon\Firewall\IpTrait;
29
30
use function array_keys;
31
use function base_convert;
32
use function count;
33
use function explode;
34
use function filter_var;
35
use function ip2long;
36
use function pow;
37
use function str_pad;
38
use function strpos;
39
use function substr_count;
40
use function unpack;
41
42
/**
43
 * Ip component.
44
 */
45
class Ip extends ComponentProvider
46
{
47
    /**
48
     *   Public methods       | Desctiotion
49
     *  ----------------------|---------------------------------------------
50
     *   setIp                | Set an IP address.
51
     *   getIp                | Get current set IP.
52
     *   setRdns              | Set a RDNS record for the check.
53
     *   getRdns              | Get IP resolved hostname.
54
     *  ----------------------|---------------------------------------------
55
     */
56
    use IpTrait;
57
58
    /**
59
     *   Public methods       | Desctiotion
60
     *  ----------------------|---------------------------------------------
61
     *   setAllowedItems      | Add items to the whitelist pool.
62
     *   setAllowedItem       | Add an item to the whitelist pool.
63
     *   getAllowedItems      | Get items from the whitelist pool.
64
     *   getAllowedItem       | Get an item from the whitelist pool.
65
     *   removeAllowedItem    | Remove an allowed item if exists.
66
     *   removeAllowedItems   | Remove all allowed items.
67
     *   hasAllowedItem       | Check if an allowed item exists.
68
     *   getAllowByPrefix     | Check if an allowed item exists have the same prefix.
69
     *   removeAllowByPrefix  | Remove allowed items with the same prefix.
70
     *   isAllowed            | Check if an item is allowed?
71
     *  ----------------------|---------------------------------------------
72
     */
73
    use AllowedTrait;
74
75
    /**
76
     *   Public methods       | Desctiotion
77
     *  ----------------------|---------------------------------------------
78
     *   setDeniedItems       | Add items to the blacklist pool.
79
     *   setDeniedItem        | Add an item to the blacklist pool.
80
     *   getDeniedItems       | Get items from the blacklist pool.
81
     *   getDeniedItem        | Get items from the blacklist pool.
82
     *   removeDeniedItem     | Remove a denied item if exists.
83
     *   removeDeniedItems    | Remove all denied items.
84
     *   hasDeniedItem        | Check if a denied item exists.
85
     *   getDenyWithPrefix    | Check if a denied item exists have the same prefix.
86
     *   removeDenyWithPrefix | Remove denied items with the same prefix.
87
     *   isDenied             | Check if an item is denied?
88
     *  ----------------------|---------------------------------------------
89
     */
90
    use DeniedTrait;
91
92
    /**
93
     * Constant
94
     */
95
    const STATUS_CODE = 81;
96
97
    const REASON_INVALID_IP_DENIED = 40;
98
    const REASON_DENY_IP_DENIED    = 41;
99
    const REASON_ALLOW_IP_DENIED   = 42;
100
101
    /**
102
     * Only allow IPs in allowedList, then deny all.
103
     *
104
     * @param bool
105
     */
106
    protected $isDenyAll = false;
107
108
    /**
109
     * Check an IP if it exists in Anti-Scraping allow/deny list.
110
     *
111
     * @param string $ip The IP address.
112
     *
113
     * @return array If data entry exists, it will return an array structure:
114
     *               - status: ALLOW | DENY
115
     *               - code: status identification code.
116
     *
117
     *               if nothing found, it will return an empty array instead.
118
     */
119 23
    public function check(string $ip): array
120
    {
121 23
        $this->setIp($ip);
122
123 23
        if (!filter_var($this->ip, FILTER_VALIDATE_IP)) {
124 2
            return [
125 2
                'status' => 'deny',
126 2
                'code' => self::REASON_INVALID_IP_DENIED,
127 2
                'comment' => 'Invalid IP.',
128 2
            ];
129
        }
130
131 22
        if ($this->isAllowed()) {
132 2
            return [
133 2
                'status' => 'allow',
134 2
                'code' => self::REASON_ALLOW_IP_DENIED,
135 2
                'comment' => 'IP is in allowed list.',
136 2
            ];
137
        }
138
139 22
        if ($this->isDenied()) {
140 2
            return [
141 2
                'status' => 'deny',
142 2
                'code' => self::REASON_DENY_IP_DENIED,
143 2
                'comment' => 'IP is in denied list.',
144 2
            ];
145
        }
146
147 21
        if ($this->isDenyAll) {
148 1
            return [
149 1
                'status' => 'deny',
150 1
                'code' => self::REASON_DENY_IP_DENIED,
151 1
                'comment' => 'Deny all in strict mode.',
152 1
            ];
153
        }
154
155 20
        return [];
156
    }
157
158
    /**
159
     * Check if a given IP is in a network
160
     *
161
     * This method is modified from: https://gist.github.com/tott/7684443
162
     *                https://github.com/cloudflare/CloudFlare-Tools/blob/master/cloudflare/inRange.php
163
     * We can it test here: http://jodies.de/ipcalc
164
     *
165
     * -------------------------------------------------------------------------------
166
     *  Netmask          Netmask (binary)                    CIDR  Notes
167
     * -------------------------------------------------------------------------------
168
     *  255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
169
     *  255.255.255.254  11111111.11111111.11111111.11111110  /31  Unuseable
170
     *  255.255.255.252  11111111.11111111.11111111.11111100  /30  2   useable
171
     *  255.255.255.248  11111111.11111111.11111111.11111000  /29  6   useable
172
     *  255.255.255.240  11111111.11111111.11111111.11110000  /28  14  useable
173
     *  255.255.255.224  11111111.11111111.11111111.11100000  /27  30  useable
174
     *  255.255.255.192  11111111.11111111.11111111.11000000  /26  62  useable
175
     *  255.255.255.128  11111111.11111111.11111111.10000000  /25  126 useable
176
     *  255.255.255.0    11111111.11111111.11111111.00000000  /24  Class C 254 useable
177
     *  255.255.254.0    11111111.11111111.11111110.00000000  /23  2   Class C's
178
     *  255.255.252.0    11111111.11111111.11111100.00000000  /22  4   Class C's
179
     *  255.255.248.0    11111111.11111111.11111000.00000000  /21  8   Class C's
180
     *  255.255.240.0    11111111.11111111.11110000.00000000  /20  16  Class C's
181
     *  255.255.224.0    11111111.11111111.11100000.00000000  /19  32  Class C's
182
     *  255.255.192.0    11111111.11111111.11000000.00000000  /18  64  Class C's
183
     *  255.255.128.0    11111111.11111111.10000000.00000000  /17  128 Class C's
184
     *  255.255.0.0      11111111.11111111.00000000.00000000  /16  Class B
185
     *  255.254.0.0      11111111.11111110.00000000.00000000  /15  2   Class B's
186
     *  255.252.0.0      11111111.11111100.00000000.00000000  /14  4   Class B's
187
     *  255.248.0.0      11111111.11111000.00000000.00000000  /13  8   Class B's
188
     *  255.240.0.0      11111111.11110000.00000000.00000000  /12  16  Class B's
189
     *  255.224.0.0      11111111.11100000.00000000.00000000  /11  32  Class B's
190
     *  255.192.0.0      11111111.11000000.00000000.00000000  /10  64  Class B's
191
     *  255.128.0.0      11111111.10000000.00000000.00000000  /9   128 Class B's
192
     *  255.0.0.0        11111111.00000000.00000000.00000000  /8   Class A
193
     *  254.0.0.0        11111110.00000000.00000000.00000000  /7
194
     *  252.0.0.0        11111100.00000000.00000000.00000000  /6
195
     *  248.0.0.0        11111000.00000000.00000000.00000000  /5
196
     *  240.0.0.0        11110000.00000000.00000000.00000000  /4
197
     *  224.0.0.0        11100000.00000000.00000000.00000000  /3
198
     *  192.0.0.0        11000000.00000000.00000000.00000000  /2
199
     *  128.0.0.0        10000000.00000000.00000000.00000000  /1
200
     *  0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space
201
     * -------------------------------------------------------------------------------
202
     *
203
     * @param string $ip      IP to check in IPV4 and IPV6 format
204
     * @param string $ipRange IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1
205
     *                        is accepted and /32 assumed
206
     *
207
     * @return bool true if the ip is in this range / false if not.
208
     */
209 2
    public function inRange(string $ip, string $ipRange): bool
210
    {
211 2
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
212 2
            return $this->inRangeIp4($ip, $ipRange);
213
        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
214 1
            return $this->inRangeIp6($ip, $ipRange);
215 1
        }
216
        return false;
217 1
    }
218
219
    /**
220
     * A child function of inRange(), check for IPv4
221
     *
222
     * @param string $ip      IP to check in IPV4 and IPV6 format
223
     * @param string $ipRange IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1
224
     *                        is accepted and /32 assumed
225
     *
226
     * @return bool
227
     */
228
    protected function inRangeIp4(string $ip, string $ipRange): bool
229 2
    {
230
        if (strpos($ipRange, '/') === false) {
231 2
            $ipRange .= '/32';
232 1
        }
233
234
        // $range is in IP/CIDR format eg 127.0.0.1/24
235
        list($ipRange, $netmask) = explode('/', $ipRange, 2);
236 2
237
        $rangeDecimal = ip2long($ipRange);
238 2
        $ipDecimal = ip2long($ip);
239 2
        $wildcardDecimal = pow(2, (32 - $netmask)) - 1;
240 2
241
        // Bits that are set in $wildcardDecimal are not set, and vice versa.
242
        // Bitwise Operators:
243
        // https://www.php.net/manual/zh/language.operators.bitwise.php
244
245
        $netmaskDecimal = ~ $wildcardDecimal;
246 2
247
        return (($ipDecimal & $netmaskDecimal) === ($rangeDecimal & $netmaskDecimal));
248 2
    }
249
250
    /**
251
     * A child function of inRange(), check for IPv6
252
     *
253
     * @param string $ip      IP to check in IPV4 and IPV6 format
254
     * @param string $ipRange IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1
255
     *                        is accepted and /32 assumed
256
     *
257
     * @return bool
258
     */
259
    protected function inRangeIp6(string $ip, string $ipRange): bool
260 1
    {
261
        $ip = $this->decimalIpv6($ip);
262 1
263
        $pieces = explode('/', $ipRange, 2);
264 1
        $leftPiece = $pieces[0];
265 1
266
        // Extract out the main IP pieces
267
        $ipPieces = explode('::', $leftPiece, 2);
268 1
        $mainIpPiece = $ipPieces[0];
269 1
        $lastIpPiece = $ipPieces[1];
270 1
271
        // Pad out the shorthand entries.
272
        $mainIpPieces = explode(':', $mainIpPiece);
273 1
274
        foreach (array_keys($mainIpPieces) as $key) {
275 1
            $mainIpPieces[$key] = str_pad($mainIpPieces[$key], 4, '0', STR_PAD_LEFT);
276 1
        }
277
278
        // Create the first and last pieces that will denote the IPV6 range.
279
        $first = $mainIpPieces;
280 1
        $last = $mainIpPieces;
281 1
282
        // Check to see if the last IP block (part after ::) is set
283
        $size = count($mainIpPieces);
284 1
285
        if (trim($lastIpPiece) !== '') {
286 1
            $lastPiece = str_pad($lastIpPiece, 4, '0', STR_PAD_LEFT);
287 1
288
            // Build the full form of the IPV6 address considering the last IP block set
289
            for ($i = $size; $i < 7; $i++) {
290 1
                $first[$i] = '0000';
291 1
                $last[$i] = 'ffff';
292 1
            }
293
294
            $mainIpPieces[7] = $lastPiece;
295 1
        } else {
296
            // Build the full form of the IPV6 address
297
            for ($i = $size; $i < 8; $i++) {
298
                $first[$i] = '0000';
299
                $last[$i] = 'ffff';
300 1
            }
301 1
        }
302 1
303
        // Rebuild the final long form IPV6 address
304
        $first = $this->decimalIpv6(implode(':', $first));
305
        $last = $this->decimalIpv6(implode(':', $last));
306
307 1
        return ($ip >= $first && $ip <= $last);
308 1
    }
309
310 1
    /**
311
     * Get the ipv6 full format and return it as a decimal value.
312
     *
313
     * @param string $ip The IP address.
314
     *
315
     * @return string
316
     */
317
    public function decimalIpv6(string $ip): string
318
    {
319
        if (substr_count($ip, '::')) {
320 2
            $ip = str_replace('::', str_repeat(':0000', 8 - substr_count($ip, ':')) . ':', $ip);
321
        }
322 2
323 1
        $ip = explode(':', $ip);
324
        $rIp = '';
325
326 2
        foreach ($ip as $v) {
327 2
            $rIp .= str_pad(base_convert($v, 16, 2), 16, '0', STR_PAD_LEFT);
328
        }
329 2
        return base_convert($rIp, 2, 10);
330 2
    }
331
332 2
    /**
333
     * Get the ipv6 full format and return it as a decimal value. (Confirmation version)
334
     *
335
     * @param string $ip The IP address.
336
     *
337
     * @return string
338
     */
339
    public function decimalIpv6Confirm($ip): string
340
    {
341
        $binNum = '';
342 1
        foreach (unpack('C*', inet_pton($ip)) as $byte) {
343
            $binNum .= str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
344 1
        }
345 1
        return base_convert(ltrim($binNum, '0'), 2, 10);
346 1
    }
347
348 1
    /**
349
     * {@inheritDoc}
350
     *
351
     * @return bool
352
     */
353
    public function isDenied(): bool
354
    {
355
        foreach ($this->deniedList as $deniedIp) {
356 22
            if (strpos($deniedIp, '/') !== false) {
357
                if ($this->inRange($this->ip, $deniedIp)) {
358 22
                    return true;
359 9
                }
360 1
            } else {
361 1
                if ($deniedIp === $this->ip) {
362
                    return true;
363
                }
364 9
            }
365 9
        }
366
367
        return false;
368
    }
369
370 21
    /**
371
     * {@inheritDoc}
372
     *
373
     * @return bool
374
     */
375
    public function isAllowed(): bool
376
    {
377
        foreach ($this->allowedList as $allowedIp) {
378 22
            if (strpos($allowedIp, '/') !== false) {
379
                if ($this->inRange($this->ip, $allowedIp)) {
380 22
                    return true;
381 9
                }
382 1
            } else {
383 1
                if ($allowedIp === $this->ip) {
384
                    return true;
385
                }
386 9
            }
387 9
        }
388
389
        return false;
390
    }
391
392 22
    /**
393
     * Only allow IPs in allowedList, then deny all.
394
     *
395
     * @return bool
396
     */
397
    public function denyAll(): bool
398
    {
399
        return $this->isDenyAll = true;
400 2
    }
401
402 2
    /**
403
     * Unique deny status code.
404
     *
405
     * @return int
406
     */
407
    public function getDenyStatusCode(): int
408
    {
409
        return self::STATUS_CODE;
410 1
    }
411
}
412