Passed
Push — 2.x ( 4516a4...0d32ef )
by Terry
01:53
created

Ip::check()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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