Passed
Push — 2.x ( 39da0b...2267ff )
by Terry
02:03
created

Ip::inRangeIp6()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 51
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 25
nc 16
nop 2
dl 0
loc 51
rs 8.8977
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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