1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <contact@terryl.in>
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 <contact@terryl.in>
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 = 40;
98
    const REASON_DENY_IP    = 41;
99
    const REASON_ALLOW_IP   = 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 3
    public function check(string $ip): array
120
    {
121 3
        $this->setIp($ip);
122

123 3
        if (!filter_var($this->ip, FILTER_VALIDATE_IP)) {
124
            return [
125 3
                'status' => 'deny',
126 3
                'code' => self::REASON_INVALID_IP,
127 3
                'comment' => 'Invalid IP.',
128
            ];
129
        }
130

131 3
        if ($this->isAllowed()) {
132
            return [
133 3
                'status' => 'allow',
134 3
                'code' => self::REASON_ALLOW_IP,
135 3
                'comment' => 'IP is in allowed list.',
136
            ];
137
        }
138

139 3
        if ($this->isDenied()) {
140
            return [
141 3
                'status' => 'deny',
142 3
                'code' => self::REASON_DENY_IP,
143 3
                'comment' => 'IP is in denied list.',
144
            ];
145
        }
146

147 3
        if ($this->isDenyAll) {
148
            return [
149 3
                'status' => 'deny',
150 3
                'code' => self::REASON_DENY_IP,
151 3
                'comment' => 'Deny all in strict mode.',
152
            ];
153
        }
154

155 3
        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 3
    public function inRange(string $ip, string $ipRange): bool
210
    {
211 3
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
212 3
            return $this->inRangeIp4($ip, $ipRange);
213

214 3
        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
215 3
            return $this->inRangeIp6($ip, $ipRange);
216
        }
217 3
        return false;
218
    }
219

220
    /**
221
     * A child function of inRange(), check for IPv4
222
     *
223
     * @param string $ip      IP to check in IPV4 and IPV6 format
224
     * @param string $ipRange IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1
225
     *                        is accepted and /32 assumed
226
     *
227
     * @return bool
228
     */
229 3
    protected function inRangeIp4(string $ip, string $ipRange): bool
230
    {
231 3
        if (strpos($ipRange, '/') === false) {
232 3
            $ipRange .= '/32';
233
        }
234

235
        // $range is in IP/CIDR format eg 127.0.0.1/24
236 3
        list($ipRange, $netmask) = explode('/', $ipRange, 2);
237

238 3
        $rangeDecimal = ip2long($ipRange);
239 3
        $ipDecimal = ip2long($ip);
240 3
        $wildcardDecimal = pow(2, (32 - $netmask)) - 1;
241

242
        // Bits that are set in $wildcardDecimal are not set, and vice versa.
243
        // Bitwise Operators:
244
        // https://www.php.net/manual/zh/language.operators.bitwise.php
245

246 3
        $netmaskDecimal = ~ $wildcardDecimal;
247

248 3
        return (($ipDecimal & $netmaskDecimal) === ($rangeDecimal & $netmaskDecimal));
249
    }
250

251
    /**
252
     * A child function of inRange(), check for IPv6
253
     *
254
     * @param string $ip      IP to check in IPV4 and IPV6 format
255
     * @param string $ipRange IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1
256
     *                        is accepted and /32 assumed
257
     *
258
     * @return bool
259
     */
260 3
    protected function inRangeIp6(string $ip, string $ipRange): bool
261
    {
262 3
        $ip = $this->decimalIpv6($ip);
263

264 3
        $pieces = explode('/', $ipRange, 2);
265 3
        $leftPiece = $pieces[0];
266

267
        // Extract out the main IP pieces
268 3
        $ipPieces = explode('::', $leftPiece, 2);
269 3
        $mainIpPiece = $ipPieces[0];
270 3
        $lastIpPiece = $ipPieces[1];
271

272
        // Pad out the shorthand entries.
273 3
        $mainIpPieces = explode(':', $mainIpPiece);
274

275 3
        foreach (array_keys($mainIpPieces) as $key) {
276 3
            $mainIpPieces[$key] = str_pad($mainIpPieces[$key], 4, '0', STR_PAD_LEFT);
277
        }
278

279
        // Create the first and last pieces that will denote the IPV6 range.
280 3
        $first = $mainIpPieces;
281 3
        $last = $mainIpPieces;
282

283
        // Check to see if the last IP block (part after ::) is set
284 3
        $size = count($mainIpPieces);
285

286 3
        if (trim($lastIpPiece) !== '') {
287 3
            $lastPiece = str_pad($lastIpPiece, 4, '0', STR_PAD_LEFT);
288

289
            // Build the full form of the IPV6 address considering the last IP block set
290 3
            for ($i = $size; $i < 7; $i++) {
291 3
                $first[$i] = '0000';
292 3
                $last[$i] = 'ffff';
293
            }
294

295 3
            $mainIpPieces[7] = $lastPiece;
296

297
        } else {
298

299
            // Build the full form of the IPV6 address
300 3
            for ($i = $size; $i < 8; $i++) {
301 3
                $first[$i] = '0000';
302 3
                $last[$i] = 'ffff';
303
            }
304
        }
305

306
        // Rebuild the final long form IPV6 address
307 3
        $first = $this->decimalIpv6(implode(':', $first));
308 3
        $last = $this->decimalIpv6(implode(':', $last));
309

310 3
        return ($ip >= $first && $ip <= $last);
311
    }
312

313
    /**
314
     * Get the ipv6 full format and return it as a decimal value.
315
     *
316
     * @param string $ip The IP address.
317
     *
318
     * @return string
319
     */
320 3
    public function decimalIpv6(string $ip): string
321
    {
322 3
        if (substr_count($ip, '::')) {
323 3
            $ip = str_replace('::', str_repeat(':0000', 8 - substr_count($ip, ':')) . ':', $ip);
324
        }
325

326 3
        $ip = explode(':', $ip);
327 3
        $rIp = '';
328

329 3
        foreach ($ip as $v) {
330 3
            $rIp .= str_pad(base_convert($v, 16, 2), 16, '0', STR_PAD_LEFT);
331
        }
332 3
        return base_convert($rIp, 2, 10);
333
    }
334

335
    /**
336
     * Get the ipv6 full format and return it as a decimal value. (Confirmation version)
337
     *
338
     * @param string $ip The IP address.
339
     *
340
     * @return string
341
     */
342 3
    public function decimalIpv6Confirm($ip): string
343
    {
344 3
        $binNum = '';
345 3
        foreach (unpack('C*', inet_pton($ip)) as $byte) {
346 3
            $binNum .= str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
347
        }
348 3
        return base_convert(ltrim($binNum, '0'), 2, 10);
349
    }
350

351
    /**
352
     * {@inheritDoc}
353
     * 
354
     * @return bool
355
     */
356 3
    public function isDenied(): bool
357
    {
358 3
        foreach ($this->deniedList as $deniedIp) {
359 3
            if (strpos($deniedIp, '/') !== false) {
360 3
                if ($this->inRange($this->ip, $deniedIp)) {
361 3
                    return true;
362
                }
363
            } else {
364 3
                if ($deniedIp === $this->ip) {
365 3
                    return true;
366
                }
367
            }
368
        }
369

370 3
        return false;
371
    }
372

373
    /**
374
     * {@inheritDoc}
375
     * 
376
     * @return bool
377
     */
378 3
    public function isAllowed(): bool
379
    {
380 3
        foreach ($this->allowedList as $allowedIp) {
381 3
            if (strpos($allowedIp, '/') !== false) {
382 3
                if ($this->inRange($this->ip, $allowedIp)) {
383 3
                    return true;
384
                }
385
            } else {
386 3
                if ($allowedIp === $this->ip) {
387 3
                    return true;
388
                }
389
            }
390
        }
391

392 3
        return false;
393
    }
394

395
    /**
396
     * Only allow IPs in allowedList, then deny all.
397
     *
398
     * @return bool
399
     */
400 3
    public function denyAll(): bool
401
    {
402 3
        return $this->isDenyAll = true;
403
    }
404

405
    /**
406
     * Unique deny status code.
407
     *
408
     * @return int
409
     */
410 3
    public function getDenyStatusCode(): int
411
    {
412 3
        return self::STATUS_CODE;
413
    }
414
}

Read our documentation on viewing source code .

Loading