Passed
Push — 2.x ( a032c9...255d06 )
by Terry
02:04
created

Kernel::filter()   B

Complexity

Conditions 11
Paths 28

Size

Total Lines 99
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 11
eloc 45
c 3
b 0
f 0
nc 28
nop 0
dl 0
loc 99
rs 7.3166

How to fix   Long Method    Complexity   

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
 * @name        Shieldon Firewall
4
 * @author      Terry Lin
5
 * @link        https://github.com/terrylinooo/shieldon
6
 * @package     Shieldon
7
 * @since       1.0.0
8
 * @version     2.0.0
9
 * @license     MIT
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a copy
12
 * of this software and associated documentation files (the "Software"), to deal
13
 * in the Software without restriction, including without limitation the rights
14
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
 * copies of the Software, and to permit persons to whom the Software is
16
 * furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
 * THE SOFTWARE.
28
 */
29
30
declare(strict_types=1);
31
32
namespace Shieldon\Firewall;
33
34
use Psr\Http\Message\ServerRequestInterface;
35
use Psr\Http\Message\ResponseInterface;
36
use Shieldon\Firewall\Captcha\CaptchaInterface;
37
use Shieldon\Firewall\Captcha\Foundation;
38
use Shieldon\Firewall\Component\ComponentInterface;
39
use Shieldon\Firewall\Component\ComponentProvider;
40
use Shieldon\Firewall\Driver\DriverProvider;
41
use Shieldon\Firewall\Helpers;
42
use Shieldon\Firewall\HttpFactory;
43
use Shieldon\Firewall\Log\ActionLogger;
44
use Shieldon\Firewall\Utils\Container;
45
use Shieldon\Messenger\Messenger\MessengerInterface;
46
use function Shieldon\Firewall\__;
47
use function Shieldon\Firewall\get_cpu_usage;
48
use function Shieldon\Firewall\get_default_properties;
49
use function Shieldon\Firewall\get_memory_usage;
50
use function Shieldon\Firewall\get_request;
51
use function Shieldon\Firewall\get_response;
52
use function Shieldon\Firewall\get_session;
53
use function Shieldon\Firewall\unset_superglobal;
54
55
use Closure;
56
use InvalidArgumentException;
57
use LogicException;
58
use RuntimeException;
59
use function file_exists;
60
use function file_put_contents;
61
use function filter_var;
62
use function get_class;
63
use function gethostbyaddr;
64
use function is_dir;
65
use function is_writable;
66
use function microtime;
67
use function ob_end_clean;
68
use function ob_get_contents;
69
use function ob_start;
70
use function str_replace;
71
use function strpos;
72
use function strrpos;
73
use function substr;
74
use function time;
75
76
/**
77
 * The primary Shiendon class.
78
 */
79
class Kernel
80
{
81
    use IpTrait;
82
83
    // Reason codes (allow)
84
    const REASON_IS_SEARCH_ENGINE = 100;
85
    const REASON_IS_GOOGLE = 101;
86
    const REASON_IS_BING = 102;
87
    const REASON_IS_YAHOO = 103;
88
    const REASON_IS_SOCIAL_NETWORK = 110;
89
    const REASON_IS_FACEBOOK = 111;
90
    const REASON_IS_TWITTER = 112;
91
92
    // Reason codes (deny)
93
    const REASON_TOO_MANY_SESSIONS = 1;
94
    const REASON_TOO_MANY_ACCESSES = 2; // (not used)
95
    const REASON_EMPTY_JS_COOKIE = 3;
96
    const REASON_EMPTY_REFERER = 4;
97
    
98
    const REASON_REACHED_LIMIT_DAY = 11;
99
    const REASON_REACHED_LIMIT_HOUR = 12;
100
    const REASON_REACHED_LIMIT_MINUTE = 13;
101
    const REASON_REACHED_LIMIT_SECOND = 14;
102
103
    const REASON_INVALID_IP = 40;
104
    const REASON_DENY_IP = 41;
105
    const REASON_ALLOW_IP = 42;
106
107
    const REASON_COMPONENT_IP = 81;
108
    const REASON_COMPONENT_RDNS = 82;
109
    const REASON_COMPONENT_HEADER = 83;
110
    const REASON_COMPONENT_USERAGENT = 84;
111
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
112
113
    const REASON_MANUAL_BAN = 99;
114
115
    // Action codes
116
    const ACTION_DENY = 0;
117
    const ACTION_ALLOW = 1;
118
    const ACTION_TEMPORARILY_DENY = 2;
119
    const ACTION_UNBAN = 9;
120
121
    // Result codes
122
    const RESPONSE_DENY = 0;
123
    const RESPONSE_ALLOW = 1;
124
    const RESPONSE_TEMPORARILY_DENY = 2;
125
    const RESPONSE_LIMIT_SESSION = 3;
126
127
    const LOG_LIMIT = 3;
128
    const LOG_PAGEVIEW = 11;
129
    const LOG_BLACKLIST = 98;
130
    const LOG_CAPTCHA = 99;
131
132
    const KERNEL_DIR = __DIR__;
133
134
    /**
135
     * Driver for storing data.
136
     *
137
     * @var \Shieldon\Firewall\Driver\DriverProvider
138
     */
139
    public $driver;
140
141
    /**
142
     * Container for Shieldon components.
143
     *
144
     * @var array
145
     */
146
    public $component = [];
147
148
    /**
149
     * Logger instance.
150
     *
151
     * @var ActionLogger
152
     */
153
    public $logger;
154
155
    /**
156
     * The closure functions that will be executed in this->run()
157
     *
158
     * @var array
159
     */
160
    protected $closures = [];
161
162
    /**
163
     * Enable or disable the filters.
164
     *
165
     * @var array
166
     */
167
    protected $filterStatus = [
168
        /**
169
         * Check how many pageviews an user made in a short period time.
170
         * For example, limit an user can only view 30 pages in 60 minutes.
171
         */
172
        'frequency' => true,
173
174
        /**
175
         * If an user checks any internal link on your website, the user's
176
         * browser will generate HTTP_REFERER information.
177
         * When a user view many pages without HTTP_REFERER information meaning
178
         * that the user MUST be a web crawler.
179
         */
180
        'referer' => false,
181
182
        /**
183
         * Most of web crawlers do not render JavaScript, they only get the 
184
         * content they want, so we can check whether the cookie can be created
185
         * by JavaScript or not.
186
         */
187
        'cookie' => false,
188
189
        /**
190
         * Every unique user should only has a unique session, but if a user
191
         * creates different sessions every connection... meaning that the 
192
         * user's browser doesn't support cookie.
193
         * It is almost impossible that modern browsers not support cookie,
194
         * therefore the user MUST be a web crawler.
195
         */
196
        'session' => false,
197
    ];
198
199
    /**
200
     * The status for Filters to reset.
201
     *
202
     * @var array
203
     */
204
    protected $filterResetStatus = [
205
        's' => false, // second.
206
        'm' => false, // minute.
207
        'h' => false, // hour.
208
        'd' => false, // day.
209
    ];
210
211
    /**
212
     * default settings
213
     *
214
     * @var array
215
     */
216
    protected $properties = [];
217
218
    /**
219
     * This is for creating data tables automatically
220
     * Turn it off, if you don't want to check data tables every connection.
221
     *
222
     * @var bool
223
     */
224
    protected $autoCreateDatabase = true;
225
226
    /**
227
     * Container for captcha addons.
228
     * The collection of \Shieldon\Firewall\Captcha\CaptchaInterface
229
     *
230
     * @var array
231
     */
232
    protected $captcha = [];
233
234
    /**
235
     * The ways Shieldon send a message to when someone has been blocked.
236
     * The collection of \Shieldon\Messenger\Messenger\MessengerInterface
237
     *
238
     * @var array
239
     */
240
    protected $messenger = [];
241
242
    /**
243
     * If the IP is in the rule table, the rule status will change.
244
     *
245
     * @var array
246
     */
247
    protected $ruleStatus = [
248
249
        // IP is marked as allow in the rule table.
250
        'allow' => false,
251
252
        // IP is marked as deny in the rule table.
253
        'deny' => false,
254
    ];
255
256
    /**
257
     * Is to limit traffic?
258
     *
259
     * @var array
260
     */
261
    protected $sessionLimit = [
262
263
        // How many sessions will be available?
264
        // 0 = no limit.
265
        'count' => 0,
266
267
        // How many minutes will a session be availe to visit?
268
        // 0 = no limit.
269
        'period' => 0, 
270
    ];
271
272
    /**
273
     * Record the online session status.
274
     * This will be enabled when $sessionLimit[count] > 0
275
     *
276
     * @var array
277
     */
278
    protected $sessionStatus = [
279
280
        // Online session count.
281
        'count' => 0,
282
283
        // Current session order.
284
        'order' => 0,
285
286
        // Current waiting queue.
287
        'queue' => 0,
288
    ];
289
290
    /**
291
     * Result.
292
     *
293
     * @var int
294
     */
295
    protected $result = 1;
296
297
    /**
298
     * URLs that are excluded from Shieldon's protection.
299
     *
300
     * @var array
301
     */
302
    protected $excludedUrls = [];
303
304
    /**
305
     * Which type of configuration source that Shieldon firewall managed?
306
     *
307
     * @var string
308
     */
309
    protected $firewallType = 'self'; // managed | config | self | demo
310
311
    /**
312
     * Custom dialog UI settings.
313
     *
314
     * @var array
315
     */
316
    protected $dialogUI = [];
317
318
    /**
319
     * Store the class information used in Shieldon.
320
     *
321
     * @var array
322
     */
323
    protected $registrar = [];
324
325
    /**
326
     * Strict mode.
327
     * 
328
     * Set by `strictMode()` only. The default value of this propertry is undefined.
329
     *
330
     * @var bool
331
     */
332
    protected $strictMode;
333
334
    /**
335
     * The directory in where the frontend template files are placed.
336
     *
337
     * @var string
338
     */
339
    protected $templateDirectory = '';
340
341
    /**
342
     * The message that will be sent to the third-party API.
343
     *
344
     * @var string
345
     */
346
    protected $msgBody = '';
347
348
    /**
349
     * Shieldon constructor.
350
     * 
351
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
352
     * 
353
     * @return void
354
     */
355
    public function __construct(?ServerRequestInterface $request  = null, ?ResponseInterface $response = null)
356
    {
357
        // Load helper functions. This is the must.
358
        new Helpers();
359
360
        if (is_null($request)) {
361
            $request = HttpFactory::createRequest();
362
        }
363
364
        if (is_null($response)) {
365
            $response = HttpFactory::createResponse();
366
        }
367
368
        $session = HttpFactory::createSession();
369
370
        $this->properties = get_default_properties();
371
        $this->add(new Foundation());
372
373
        Container::set('request', $request);
374
        Container::set('response', $response);
375
        Container::set('session', $session);
376
        Container::set('shieldon', $this);
377
    }
378
379
    /**
380
     * Log actions.
381
     *
382
     * @param int $actionCode The code number of the action.
383
     *
384
     * @return void
385
     */
386
    protected function log(int $actionCode): void
387
    {
388
        if (null !== $this->logger) {
389
            $logData = [];
390
            $logData['ip'] = $this->getIp();
391
            $logData['session_id'] = get_session()->get('id');
392
            $logData['action_code'] = $actionCode;
393
            $logData['timesamp'] = time();
394
    
395
            $this->logger->add($logData);
396
        }
397
    }
398
399
    /**
400
     * Run, run, run!
401
     *
402
     * Check the rule tables first, if an IP address has been listed.
403
     * Call function filter() if an IP address is not listed in rule tables.
404
     *
405
     * @return int The response code.
406
     */
407
    protected function process(): int
408
    {
409
        $this->driver->init($this->autoCreateDatabase);
410
411
        foreach (array_keys($this->component) as $name) {
412
            $this->component[$name]->setIp($this->ip);
413
            $this->component[$name]->setRdns($this->rdns);
414
415
            // Apply global strict mode to all components by `strictMode()` if nesscessary.
416
            if (isset($this->strictMode)) {
417
                $this->component[$name]->setStrict($this->strictMode);
418
            }
419
        }
420
421
        /*
422
        |--------------------------------------------------------------------------
423
        | Stage - Looking for rule table.
424
        |--------------------------------------------------------------------------
425
        */
426
427
        $ipRule = $this->driver->get($this->ip, 'rule');
428
429
        if (!empty($ipRule)) {
430
431
            $ruleType = (int) $ipRule['type'];
432
433
            if ($ruleType === self::ACTION_ALLOW) {
434
                $this->ruleStatus['allow'] = true;
435
                
436
            } else {
437
                
438
                // Current visitor has been blocked. If he still attempts accessing the site, 
439
                // then we can drop him into the permanent block list.
440
                $attempts = $ipRule['attempts'] ?? 0;
441
                $now = time();
442
                $logData = [];
443
444
                $logData['log_ip']     = $ipRule['log_ip'];
445
                $logData['ip_resolve'] = $ipRule['ip_resolve'];
446
                $logData['time']       = $now;
447
                $logData['type']       = $ipRule['type'];
448
                $logData['reason']     = $ipRule['reason'];
449
                $logData['attempts']   = $attempts;
450
451
                // @since 0.2.0
452
                $attemptPeriod = $this->properties['record_attempt_detection_period'];
453
                $attemptReset  = $this->properties['reset_attempt_counter'];
454
455
                $lastTimeDiff = $now - $ipRule['time'];
456
457
                if ($lastTimeDiff <= $attemptPeriod) {
458
                    $logData['attempts'] = ++$attempts;
459
                }
460
461
                if ($lastTimeDiff > $attemptReset) {
462
                    $logData['attempts'] = 0;
463
                }
464
465
                $isMessengerTriggered = false;
466
                $isUpdatRuleTable = false;
467
468
                $handleType = 0;
469
470
                if ($this->properties['deny_attempt_enable']['data_circle']) {
471
472
                    if ($ruleType === self::ACTION_TEMPORARILY_DENY) {
473
474
                        $isUpdatRuleTable = true;
475
476
                        $buffer = $this->properties['deny_attempt_buffer']['data_circle'];
477
478
                        if ($attempts >= $buffer) {
479
480
                            if ($this->properties['deny_attempt_notify']['data_circle']) {
481
                                $isMessengerTriggered = true;
482
                            }
483
484
                            $logData['type'] = self::ACTION_DENY;
485
486
                            // Reset this value for next checking process - iptables.
487
                            $logData['attempts'] = 0;
488
                            $handleType = 1;
489
                        }
490
                    }
491
                }
492
493
                if ($this->properties['deny_attempt_enable']['system_firewall']) {
494
                    
495
                    if ($ruleType === self::ACTION_DENY) {
496
497
                        $isUpdatRuleTable = true;
498
499
                        // For the requests that are already banned, but they are still attempting access, that means 
500
                        // that they are programmably accessing your website. Consider put them in the system-layer fireall
501
                        // such as IPTABLE.
502
                        $bufferIptable = $this->properties['deny_attempt_buffer']['system_firewall'];
503
504
                        if ($attempts >= $bufferIptable) {
505
506
                            if ($this->properties['deny_attempt_notify']['system_firewall']) {
507
                                $isMessengerTriggered = true;
508
                            }
509
510
                            $folder = rtrim($this->properties['iptables_watching_folder'], '/');
511
512
                            if (file_exists($folder) && is_writable($folder)) {
513
                                $filePath = $folder . '/iptables_queue.log';
514
515
                                // command, ipv4/6, ip, subnet, port, protocol, action
516
                                // add,4,127.0.0.1,null,all,all,drop  (example)
517
                                // add,4,127.0.0.1,null,80,tcp,drop   (example)
518
                                $command = 'add,4,' . $this->ip . ',null,all,all,deny';
519
520
                                if (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
521
                                    $command = 'add,6,' . $this->ip . ',null,all,allow';
522
                                }
523
524
                                // Add this IP address to itables_queue.log
525
                                // Use `bin/iptables.sh` for adding it into IPTABLES. See document for more information. 
526
                                file_put_contents($filePath, $command . "\n", FILE_APPEND | LOCK_EX);
527
528
                                $logData['attempts'] = 0;
529
                                $handleType = 2;
530
                            }
531
                        }
532
                    }
533
                }
534
535
                // We only update data when `deny_attempt_enable` is enable.
536
                // Because we want to get the last visited time and attempt counter.
537
                // Otherwise we don't update it everytime to avoid wasting CPU resource.
538
                if ($isUpdatRuleTable) {
539
                    $this->driver->save($this->ip, $logData, 'rule');
540
                }
541
542
                /**
543
                 * Notify this event to messenger.
544
                 */
545
                if ($isMessengerTriggered) {
546
547
                    // The data strings that will be appended to message body.
548
                    $prepareMessageData = [
549
                        __('core', 'messenger_text_ip')       => $logData['log_ip'],
550
                        __('core', 'messenger_text_rdns')     => $logData['ip_resolve'],
551
                        __('core', 'messenger_text_reason')   => __('core', 'messenger_text_reason_code_' . $logData['reason']),
552
                        __('core', 'messenger_text_handle')   => __('core', 'messenger_text_handle_type_' . $handleType),
553
                        __('core', 'messenger_text_system')   => '',
554
                        __('core', 'messenger_text_cpu')      => get_cpu_usage(),
555
                        __('core', 'messenger_text_memory')   => get_memory_usage(),
556
                        __('core', 'messenger_text_time')     => date('Y-m-d H:i:s', $logData['time']),
557
                        __('core', 'messenger_text_timezone') => date_default_timezone_get(),
558
                    ];
559
560
                    $message = __('core', 'messenger_notification_subject', 'Notification for {0}', [$this->ip]) . "\n\n";
561
562
                    foreach ($prepareMessageData as $key => $value) {
563
                        $message .= $key . ': ' . $value . "\n";
564
                    }
565
566
                    $this->msgBody = $message;
567
                }
568
569
                // For an incoming request already in the rule list, return the rule type immediately.
570
                return $this->result = $ruleType;
571
            }
572
        }
573
574
        if ($this->ruleStatus['allow']) {
575
576
            // The requests that are allowed in rule table will not go into sessionHandler.
577
            return $this->result = self::RESPONSE_ALLOW;
578
579
        } else {
580
581
            /*
582
            |--------------------------------------------------------------------------
583
            | Statge - Detect popular search engine.
584
            |--------------------------------------------------------------------------
585
            */
586
587
            if ($this->getComponent('TrustedBot')) {
588
 
589
                // We want to put all the allowed robot into the rule list, so that the checking of IP's resolved hostname 
590
                // is no more needed for that IP.
591
                if ($this->getComponent('TrustedBot')->isAllowed()) {
0 ignored issues
show
Bug introduced by
The method isAllowed() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\Ip or Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

591
                if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isAllowed()) {
Loading history...
592
593
                    if ($this->getComponent('TrustedBot')->isGoogle()) {
0 ignored issues
show
Bug introduced by
The method isGoogle() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

593
                    if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isGoogle()) {
Loading history...
594
                        // Add current IP into allowed list, because it is from real Google domain.
595
                        $this->action(
596
                            self::ACTION_ALLOW,
597
                            self::REASON_IS_GOOGLE
598
                        );
599
600
                    } elseif ($this->getComponent('TrustedBot')->isBing()) {
0 ignored issues
show
Bug introduced by
The method isBing() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

600
                    } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isBing()) {
Loading history...
601
                        // Add current IP into allowed list, because it is from real Bing domain.
602
                        $this->action(
603
                            self::ACTION_ALLOW,
604
                            self::REASON_IS_BING
605
                        );
606
607
                    } elseif ($this->getComponent('TrustedBot')->isYahoo()) {
0 ignored issues
show
Bug introduced by
The method isYahoo() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

607
                    } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isYahoo()) {
Loading history...
608
                        // Add current IP into allowed list, because it is from real Yahoo domain.
609
                        $this->action(
610
                            self::ACTION_ALLOW,
611
                            self::REASON_IS_YAHOO
612
                        );
613
614
                    } else {
615
                        // Add current IP into allowed list, because you trust it.
616
                        // You have already defined it in the settings.
617
                        $this->action(
618
                            self::ACTION_ALLOW,
619
                            self::REASON_IS_SEARCH_ENGINE
620
                        );
621
                    }
622
                    // Allowed robots not join to our traffic handler.
623
                    return $this->result = self::RESPONSE_ALLOW;
624
                }
625
626
                // After `isAllowed()` executed, we can check if the currect access is fake by `isFakeRobot()`.
627
                if ($this->getComponent('TrustedBot')->isFakeRobot()) {
0 ignored issues
show
Bug introduced by
The method isFakeRobot() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

627
                if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isFakeRobot()) {
Loading history...
628
                    $this->action(
629
                        self::ACTION_DENY,
630
                        self::REASON_COMPONENT_TRUSTED_ROBOT
631
                    );
632
633
                    return $this->result = self::RESPONSE_DENY;
634
                }
635
            }
636
637
            /*
638
            |--------------------------------------------------------------------------
639
            | Stage - IP component.
640
            |--------------------------------------------------------------------------
641
            */
642
643
            if ($this->getComponent('Ip')) {
644
645
                $result = $this->getComponent('Ip')->check();
0 ignored issues
show
Bug introduced by
The method check() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\Ip. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

645
                $result = $this->getComponent('Ip')->/** @scrutinizer ignore-call */ check();
Loading history...
646
                $actionCode = self::ACTION_DENY;
647
648
                if (!empty($result)) {
649
650
                    switch ($result['status']) {
651
652
                        case 'allow':
653
                            $actionCode = self::ACTION_ALLOW;
654
                            $reasonCode = $result['code'];
655
                            break;
656
        
657
                        case 'deny':
658
                            $actionCode = self::ACTION_DENY;
659
                            $reasonCode = $result['code']; 
660
                            break;
661
                    }
662
    
663
                    $this->action($actionCode, $reasonCode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $reasonCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
664
    
665
                    // $resultCode = $actionCode
666
                    return $this->result = $this->sessionHandler($actionCode);
667
                }
668
            }
669
    
670
            /*
671
            |--------------------------------------------------------------------------
672
            | Stage - Check all other components.
673
            |--------------------------------------------------------------------------
674
            */
675
676
            foreach ($this->component as $component) {
677
    
678
                // check if is a a bad robot already defined in settings.
679
                if ($component->isDenied()) {
680
    
681
                    // @since 0.1.8
682
                    $this->action(
683
                        self::ACTION_DENY,
684
                        $component->getDenyStatusCode()
685
                    );
686
    
687
                    return $this->result = self::RESPONSE_DENY;
688
                }
689
            }
690
        }
691
692
        /*
693
        |--------------------------------------------------------------------------
694
        | Stage - Filters
695
        |--------------------------------------------------------------------------
696
        | This IP address is not listed in rule table, let's detect it.
697
        |
698
        */
699
700
        if (
701
            $this->filterStatus['frequency'] ||
702
            $this->filterStatus['referer'] ||
703
            $this->filterStatus['session'] ||
704
            $this->filterStatus['cookie']
705
        ) {
706
            return $this->result = $this->sessionHandler($this->filter());
707
        }
708
709
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
710
    }
711
712
    /**
713
     * Detect and analyze an user's behavior.
714
     *
715
     * @return int The response code.
716
     */
717
    protected function filter(): int
718
    {
719
        $now = time();
720
        $isFlagged = false;
721
722
        // Fetch an IP data from Shieldon log table.
723
        $ipDetail = $this->driver->get($this->ip, 'filter_log');
724
725
        $ipDetail = $this->driver->parseData($ipDetail, 'filter_log');
726
        $logData = $ipDetail;
727
728
        // Counting user pageviews.
729
        foreach (array_keys($this->filterResetStatus) as $unit) {
730
731
            // Each time unit will increase by 1.
732
            $logData['pageviews_' . $unit] = $ipDetail['pageviews_' . $unit] + 1;
733
            $logData['first_time_' . $unit] = $ipDetail['first_time_' . $unit];
734
        }
735
736
        $logData['first_time_flag'] = $ipDetail['first_time_flag'];
737
738
        if (!empty($ipDetail['ip'])) {
739
            $logData['ip'] = $this->ip;
740
            $logData['session'] = get_session()->get('id');
741
            $logData['hostname'] = $this->rdns;
742
            $logData['last_time'] = $now;
743
744
            /*
745
            |--------------------------------------------------------------------------
746
            | BEGIN
747
            |--------------------------------------------------------------------------
748
            */
749
750
            // HTTP_REFERER
751
            $filterReferer = $this->filterReferer($logData, $ipDetail, $isFlagged);
752
            $isFlagged = $filterReferer['is_flagged'];
753
            $logData = $filterReferer['log_data'];
754
755
            if ($filterReferer['is_reject']) {
756
                return self::RESPONSE_TEMPORARILY_DENY;
757
            }
758
759
            // SESSION
760
            $filterSession = $this->filterSession($logData, $ipDetail, $isFlagged);
761
            $isFlagged = $filterSession['is_flagged'];
762
            $logData = $filterSession['log_data'];
763
764
            if ($filterSession['is_reject']) {
765
                return self::RESPONSE_TEMPORARILY_DENY;
766
            }
767
768
            // JAVASCRIPT COOKIE
769
            $filterCookie = $this->filterCookie($logData, $ipDetail, $isFlagged);
770
            $isFlagged = $filterCookie['is_flagged'];
771
            $logData = $filterCookie['log_data'];
772
773
            if ($filterCookie['is_reject']) {
774
                return self::RESPONSE_TEMPORARILY_DENY;
775
            }
776
777
            // ACCESS FREQUENCY
778
            $filterFrequency = $this->filterFrequency($logData, $ipDetail, $isFlagged);
779
            $isFlagged = $filterFrequency['is_flagged'];
780
            $logData = $filterFrequency['log_data'];
781
782
            if ($filterFrequency['is_reject']) {
783
                return self::RESPONSE_TEMPORARILY_DENY;
784
            }
785
786
            /*
787
            |--------------------------------------------------------------------------
788
            | END
789
            |--------------------------------------------------------------------------
790
            */
791
792
            // Is fagged as unusual beavior? Count the first time.
793
            if ($isFlagged) {
794
                $logData['first_time_flag'] = (!empty($logData['first_time_flag'])) ? $logData['first_time_flag'] : $now;
795
            }
796
797
            // Reset the flagged factor check.
798
            if (!empty($ipDetail['first_time_flag'])) {
799
                if ($now - $ipDetail['first_time_flag'] >= $this->properties['time_reset_limit']) {
800
                    $logData['flag_multi_session'] = 0;
801
                    $logData['flag_empty_referer'] = 0;
802
                    $logData['flag_js_cookie']     = 0;
803
                }
804
            }
805
806
            $this->driver->save($this->ip, $logData, 'filter_log');
807
808
        } else {
809
810
            // If $ipDetail[ip] is empty.
811
            // It means that the user is first time visiting our webiste.
812
            $this->filterDataInitializedForFirstTimeVisit($logData);
813
        }
814
815
        return self::RESPONSE_ALLOW;
816
    }
817
818
    /**
819
     * Start an action for this IP address, allow or deny, and give a reason for it.
820
     *
821
     * @param int    $actionCode - 0: deny, 1: allow, 9: unban.
822
     * @param string $reasonCode
823
     * @param string $assignIp
824
     * 
825
     * @return void
826
     */
827
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
828
    {
829
        $ip = $this->ip;
830
        $rdns = $this->rdns;
831
        $now = time();
832
        $logData = [];
833
    
834
        if ('' !== $assignIp) {
835
            $ip = $assignIp;
836
            $rdns = gethostbyaddr($ip);
837
        }
838
839
        switch ($actionCode) {
840
            case self::ACTION_ALLOW: // acutally not used.
841
            case self::ACTION_DENY:  // actually not used.
842
            case self::ACTION_TEMPORARILY_DENY:
843
                $logData['log_ip']     = $ip;
844
                $logData['ip_resolve'] = $rdns;
845
                $logData['time']       = $now;
846
                $logData['type']       = $actionCode;
847
                $logData['reason']     = $reasonCode;
848
                $logData['attempts']   = 0;
849
850
                $this->driver->save($ip, $logData, 'rule');
851
                break;
852
            
853
            case self::ACTION_UNBAN:
854
                $this->driver->delete($ip, 'rule');
855
                break;
856
        }
857
858
        // Remove logs for this IP address because It already has it's own rule on system.
859
        // No need to count it anymore.
860
        $this->driver->delete($ip, 'filter_log');
861
862
        if (null !== $this->logger) {
863
            $log['ip']          = $ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$log was never initialized. Although not strictly required by PHP, it is generally a good practice to add $log = array(); before regardless.
Loading history...
864
            $log['session_id']  = get_session()->get('id');
865
            $log['action_code'] = $actionCode;
866
            $log['timesamp']    = $now;
867
868
            $this->logger->add($log);
869
        }
870
    }
871
872
    /**
873
     * Deal with online sessions.
874
     *
875
     * @param int $statusCode The response code.
876
     *
877
     * @return int The response code.
878
     */
879
    protected function sessionHandler($statusCode): int
880
    {
881
        if (self::RESPONSE_ALLOW !== $statusCode) {
882
            return $statusCode;
883
        }
884
885
        // If you don't enable `limit traffic`, ignore the following steps.
886
        if (empty($this->sessionLimit['count'])) {
887
            return self::RESPONSE_ALLOW;
888
889
        } else {
890
891
            // Get the proerties.
892
            $limit = (int) ($this->sessionLimit['count'] ?? 0);
893
            $period = (int) ($this->sessionLimit['period'] ?? 300);
894
            $now = time();
895
896
            $sessionData = $this->driver->getAll('session');
897
            $sessionPools = [];
898
899
            $i = 1;
900
            $sessionOrder = 0;
901
902
            if (!empty($sessionData)) {
903
                foreach ($sessionData as $v) {
904
                    $sessionPools[] = $v['id'];
905
                    $lasttime = (int) $v['time'];
906
    
907
                    if (get_session()->get('id') === $v['id']) {
908
                        $sessionOrder = $i;
909
                    }
910
    
911
                    // Remove session if it expires.
912
                    if ($now - $lasttime > $period) {
913
                        $this->driver->delete($v['id'], 'session');
914
                    }
915
                    $i++;
916
                }
917
918
                if (0 === $sessionOrder) {
919
                    $sessionOrder = $i;
920
                }
921
            } else {
922
                $sessionOrder = 0;
923
            }
924
925
            // Count the online sessions.
926
            $this->sessionStatus['count'] = count($sessionPools);
927
            $this->sessionStatus['order'] = $sessionOrder;
928
            $this->sessionStatus['queue'] = $sessionOrder - $limit;
929
930
            if (!in_array(get_session()->get('id'), $sessionPools)) {
931
                $this->sessionStatus['count']++;
932
933
                // New session, record this data.
934
                $data['id'] = get_session()->get('id');
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
935
                $data['ip'] = $this->ip;
936
                $data['time'] = $now;
937
938
                $microtimesamp = explode(' ', microtime());
939
                $microtimesamp = $microtimesamp[1] . str_replace('0.', '', $microtimesamp[0]);
940
                $data['microtimesamp'] = $microtimesamp;
941
942
                $this->driver->save(get_session()->get('id'), $data, 'session');
943
            }
944
945
            // Online session count reached the limit. So return RESPONSE_LIMIT_SESSION response code.
946
            if ($sessionOrder >= $limit) {
947
                return self::RESPONSE_LIMIT_SESSION;
948
            }
949
        }
950
951
        return self::RESPONSE_ALLOW;
952
    }
953
954
    /**
955
     * When the user is first time visiting our webiste.
956
     * Initialize the log data.
957
     * 
958
     * @param array $logData The user's log data.
959
     *
960
     * @return void
961
     */
962
    protected function filterDataInitializedForFirstTimeVisit($logData)
963
    {
964
        $now = time();
965
966
        $logData['ip']        = $this->ip;
967
        $logData['session']   = get_session()->get('id');
968
        $logData['hostname']  = $this->rdns;
969
        $logData['last_time'] = $now;
970
971
        foreach (array_keys($this->filterResetStatus) as $unit) {
972
            $logData['first_time_' . $unit] = $now;
973
        }
974
975
        $this->driver->save($this->ip, $logData, 'filter_log');
976
    }
977
978
    /**
979
     * Filter - Referer.
980
     *
981
     * @param array $logData   IP data from Shieldon log table.
982
     * @param array $ipData    The IP log data.
983
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
984
     *
985
     * @return array
986
     */
987
    protected function filterReferer(array $logData, array $ipDetail, bool $isFlagged): array
988
    {
989
        $isReject = false;
990
991
        if ($this->filterStatus['referer']) {
992
993
            if ($logData['last_time'] - $ipDetail['last_time'] > $this->properties['interval_check_referer']) {
994
995
                // Get values from data table. We will count it and save it back to data table.
996
                // If an user is already in your website, it is impossible no referer when he views other pages.
997
                $logData['flag_empty_referer'] = $ipDetail['flag_empty_referer'] ?? 0;
998
999
                if (empty(get_request()->getHeaderLine('referer'))) {
1000
                    $logData['flag_empty_referer']++;
1001
                    $isFlagged = true;
1002
                }
1003
1004
                // Ban this IP if they reached the limit.
1005
                if ($logData['flag_empty_referer'] > $this->properties['limit_unusual_behavior']['referer']) {
1006
                    $this->action(
1007
                        self::ACTION_TEMPORARILY_DENY,
1008
                        self::REASON_EMPTY_REFERER
1009
                    );
1010
                    $isReject = true;
1011
                }
1012
            }
1013
        }
1014
1015
        return [
1016
            'is_flagged' => $isFlagged,
1017
            'is_reject' => $isReject,
1018
            'log_data' => $logData,
1019
        ];
1020
    }
1021
1022
    /**
1023
     * Filter - Session
1024
     *
1025
     * @param array $logData   IP data from Shieldon log table.
1026
     * @param array $ipData    The IP log data.
1027
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
1028
     *
1029
     * @return array
1030
     */
1031
    protected function filterSession(array $logData, array $ipDetail, bool $isFlagged): array
1032
    {
1033
        $isReject = false;
1034
1035
        if ($this->filterStatus['session']) {
1036
1037
            if ($logData['last_time'] - $ipDetail['last_time'] > $this->properties['interval_check_session']) {
1038
1039
                // Get values from data table. We will count it and save it back to data table.
1040
                $logData['flag_multi_session'] = $ipDetail['flag_multi_session'] ?? 0;
1041
                
1042
                if (get_session()->get('id') !== $ipDetail['session']) {
1043
1044
                    // Is is possible because of direct access by the same user many times.
1045
                    // Or they don't have session cookie set.
1046
                    $logData['flag_multi_session']++;
1047
                    $isFlagged = true;
1048
                }
1049
1050
                // Ban this IP if they reached the limit.
1051
                if ($logData['flag_multi_session'] > $this->properties['limit_unusual_behavior']['session']) {
1052
                    $this->action(
1053
                        self::ACTION_TEMPORARILY_DENY,
1054
                        self::REASON_TOO_MANY_SESSIONS
1055
                    );
1056
                    $isReject = true;
1057
                }
1058
            }
1059
        }
1060
1061
1062
        return [
1063
            'is_flagged' => $isFlagged,
1064
            'is_reject' => $isReject,
1065
            'log_data' => $logData,
1066
        ];
1067
    }
1068
1069
    /**
1070
     * Filter - Cookie
1071
     *
1072
     * @param array $logData   IP data from Shieldon log table.
1073
     * @param array $ipData    The IP log data.
1074
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
1075
     *
1076
     * @return array
1077
     */
1078
    protected function filterCookie(array $logData, array $ipDetail, bool $isFlagged): array
1079
    {
1080
        $isReject = false;
1081
1082
        // Let's checking cookie created by javascript..
1083
        if ($this->filterStatus['cookie']) {
1084
1085
            // Get values from data table. We will count it and save it back to data table.
1086
            $logData['flag_js_cookie'] = $ipDetail['flag_js_cookie'] ?? 0;
1087
            $logData['pageviews_cookie'] = $ipDetail['pageviews_cookie'] ?? 0;
1088
1089
            $c = $this->properties['cookie_name'];
1090
1091
            $jsCookie = get_request()->getCookieParams()[$c] ?? 0;
1092
1093
            // Checking if a cookie is created by JavaScript.
1094
            if (!empty($jsCookie)) {
1095
1096
                if ($jsCookie == '1') {
1097
                    $logData['pageviews_cookie']++;
1098
1099
                } else {
1100
                    // Flag it if the value is not 1.
1101
                    $logData['flag_js_cookie']++;
1102
                    $isFlagged = true;
1103
                }
1104
            } else {
1105
                // If we cannot find the cookie, flag it.
1106
                $logData['flag_js_cookie']++;
1107
                $isFlagged = true;
1108
            }
1109
1110
            if ($logData['flag_js_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
1111
1112
                // Ban this IP if they reached the limit.
1113
                $this->action(
1114
                    self::ACTION_TEMPORARILY_DENY,
1115
                    self::REASON_EMPTY_JS_COOKIE
1116
                );
1117
                $isReject = true;
1118
            }
1119
1120
            // Remove JS cookie and reset.
1121
            if ($logData['pageviews_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
1122
                $logData['pageviews_cookie'] = 0; // Reset to 0.
1123
                $logData['flag_js_cookie'] = 0;
1124
                unset_superglobal($c, 'cookie');
1125
            }
1126
        }
1127
1128
        return [
1129
            'is_flagged' => $isFlagged,
1130
            'is_reject' => $isReject,
1131
            'log_data' => $logData,
1132
        ];
1133
    }
1134
1135
    /**
1136
     * Filter - Frequency
1137
     *
1138
     * @param array $logData   IP data from Shieldon log table.
1139
     * @param array $ipData    The IP log data.
1140
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
1141
     *
1142
     * @return array
1143
     */
1144
    protected function filterFrequency(array $logData, array $ipDetail, bool $isFlagged): array
1145
    {
1146
        $isReject = false;
1147
1148
        if ($this->filterStatus['frequency']) {
1149
            $timeSecond = [];
1150
            $timeSecond['s'] = 1;
1151
            $timeSecond['m'] = 60;
1152
            $timeSecond['h'] = 3600;
1153
            $timeSecond['d'] = 86400;
1154
1155
            foreach (array_keys($this->properties['time_unit_quota']) as $unit) {
1156
1157
                if (($logData['last_time'] - $ipDetail['first_time_' . $unit]) >= ($timeSecond[$unit] + 1)) {
1158
1159
                    // For example:
1160
                    // (1) minutely: now > first_time_m about 61, (2) hourly: now > first_time_h about 3601, 
1161
                    // Let's prepare to rest the the pageview count.
1162
                    $this->filterResetStatus[$unit] = true;
1163
1164
                } else {
1165
1166
                    // If an user's pageview count is more than the time period limit
1167
                    // He or she will get banned.
1168
                    if ($logData['pageviews_' . $unit] > $this->properties['time_unit_quota'][$unit]) {
1169
1170
                        if ($unit === 's') {
1171
                            $this->action(
1172
                                self::ACTION_TEMPORARILY_DENY,
1173
                                self::REASON_REACHED_LIMIT_SECOND
1174
                            );
1175
                        }
1176
1177
                        if ($unit === 'm') {
1178
                            $this->action(
1179
                                self::ACTION_TEMPORARILY_DENY,
1180
                                self::REASON_REACHED_LIMIT_MINUTE
1181
                            );
1182
                        }
1183
1184
                        if ($unit === 'h') {
1185
                            $this->action(
1186
                                self::ACTION_TEMPORARILY_DENY,
1187
                                self::REASON_REACHED_LIMIT_HOUR
1188
                            );
1189
                        }
1190
1191
                        if ($unit === 'd') {
1192
                            $this->action(
1193
                                self::ACTION_TEMPORARILY_DENY,
1194
                                self::REASON_REACHED_LIMIT_DAY
1195
                            );
1196
                        }
1197
1198
                        $isReject = true;
1199
                    }
1200
                }
1201
            }
1202
1203
            foreach ($this->filterResetStatus as $unit => $status) {
1204
                // Reset the pageview check for specfic time unit.
1205
                if ($status) {
1206
                    $logData['first_time_' . $unit] = $logData['last_time'];
1207
                    $logData['pageviews_' . $unit] = 0;
1208
                }
1209
            }
1210
        }
1211
1212
        return [
1213
            'is_flagged' => $isFlagged,
1214
            'is_reject' => $isReject,
1215
            'log_data' => $logData,
1216
        ];
1217
    }
1218
1219
    // @codeCoverageIgnoreStart
1220
1221
    /**
1222
     * For testing propose.
1223
     *
1224
     * @param string $sessionId
1225
     *
1226
     * @return void
1227
     */
1228
    protected function setSessionId(string $sessionId = ''): void
1229
    {
1230
        if ('' !== $sessionId) {
1231
            get_session()->set('id', $sessionId);
1232
        }
1233
    }
1234
1235
    // @codeCoverageIgnoreEnd
1236
1237
    /*
1238
    | -------------------------------------------------------------------
1239
    |                            Public APIs
1240
    | -------------------------------------------------------------------
1241
    */
1242
1243
    /**
1244
     * Register classes to Shieldon core.
1245
     * setDriver, setLogger, setComponent and setCaptcha are deprecated methods
1246
     * and no more used.
1247
     *
1248
     * @param object $instance Component classes that used on Shieldon.
1249
     *
1250
     * @return void
1251
     */
1252
    public function add($instance)
1253
    {
1254
        static $i = 2;
1255
1256
        $class = $this->getClassName($instance);
1257
1258
        if ($instance instanceof DriverProvider) {
1259
            $this->driver = $instance;
1260
            $this->registrar[0] = [
1261
                'category' => 'driver',
1262
                'class' => $class,
1263
            ];
1264
        }
1265
1266
        if ($instance instanceof ActionLogger) {
1267
            $this->logger = $instance;
1268
            $this->registrar[1] = [
1269
                'category' => 'logger',
1270
                'class' => $class,
1271
            ];
1272
        }
1273
1274
        if ($instance instanceof CaptchaInterface) {
1275
            $this->captcha[$class] = $instance;
1276
            $this->registrar[$i] = [
1277
                'category' => 'captcha',
1278
                'class' => $class,
1279
            ];
1280
            $i++;
1281
        }
1282
1283
        if ($instance instanceof ComponentProvider) {
1284
            $this->component[$class] = $instance;
1285
            $this->registrar[$i] = [
1286
                'category' => 'component',
1287
                'class' => $class,
1288
            ];
1289
            $i++;
1290
        }
1291
1292
        if ($instance instanceof MessengerInterface) {
1293
            $this->messenger[] = $instance;
1294
            $this->registrar[$i] = [
1295
                'category' => 'messenger',
1296
                'class' => $class,
1297
            ];
1298
            $i++;
1299
        }
1300
    }
1301
1302
    /**
1303
     * Remove registered classes from the Kernel.
1304
     *
1305
     * @param string $category  The class category.
1306
     * @param string $className The class name.
1307
     *
1308
     * @return void
1309
     */
1310
    public function remove(string $category, string $className = '')
1311
    {
1312
        if ($className !== '') {
1313
            foreach ($this->getRegistrar() as $k => $v) {
1314
                if ($category === $v['category'] && $className === $v['class']) {
1315
                    if (is_array($this->{$category})) {
1316
                        foreach ($this->{$category} as $k2 => $instance) {
1317
                            if ($this->getClassName($instance) === $className) {
1318
                                unset($this->{$category}[$k2]);
1319
                            }
1320
                        }
1321
                    } else {
1322
                        $this->{$category} = null;
1323
                    }
1324
                    unset($this->registrar[$k]);
1325
                }
1326
            }
1327
        } else {
1328
            foreach ($this->getRegistrar() as $k => $v) {
1329
                if ($category === $v['category']) {
1330
                    if (is_array($this->{$category})) {
1331
                        $this->{$category} = [];
1332
                    } else {
1333
                        $this->{$category} = null;
1334
                    }
1335
                    unset($this->registrar[$k]);
1336
                }
1337
            }
1338
        }
1339
    }
1340
1341
    /**
1342
     * Fetch the class list from registrar.
1343
     *
1344
     * @return array
1345
     */
1346
    public function getRegistrar(): array
1347
    {
1348
        return $this->registrar;
1349
    }
1350
1351
    /**
1352
     * Get a component instance from component's container.
1353
     *
1354
     * @param string $name The component's class name.
1355
     *
1356
     * @return ComponentInterface|null
1357
     */
1358
    public function getComponent(string $name)
1359
    {
1360
        if (isset($this->component[$name])) {
1361
            return $this->component[$name];
1362
        }
1363
1364
        return null;
1365
    }
1366
1367
    /**
1368
     * Strict mode.
1369
     * This option will take effects to all components.
1370
     * 
1371
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
1372
     *
1373
     * @return void
1374
     */
1375
    public function setStrict(bool $bool)
1376
    {
1377
        $this->strictMode = $bool;
1378
    }
1379
1380
    /**
1381
     * Disable filters.
1382
     */
1383
    public function disableFilters(): void
1384
    {
1385
        $this->setFilters([
1386
            'session'   => false,
1387
            'cookie'    => false,
1388
            'referer'   => false,
1389
            'frequency' => false,
1390
        ]);
1391
    }
1392
1393
    /**
1394
     * For first time installation only. This is for creating data tables automatically.
1395
     * Turning it on will check the data tables exist or not at every single pageview, 
1396
     * it's not good for high traffic websites.
1397
     *
1398
     * @param bool $bool
1399
     * 
1400
     * @return void
1401
     */
1402
    public function createDatabase(bool $bool)
1403
    {
1404
        $this->autoCreateDatabase = $bool;
1405
    }
1406
1407
    /**
1408
     * Set a data channel.
1409
     *
1410
     * This will create databases for the channel.
1411
     *
1412
     * @param string $channel Specify a channel.
1413
     *
1414
     * @return void
1415
     */
1416
    public function setChannel(string $channel)
1417
    {
1418
        if (!$this->driver instanceof DriverProvider) {
0 ignored issues
show
introduced by
$this->driver is always a sub-type of Shieldon\Firewall\Driver\DriverProvider.
Loading history...
1419
            throw new LogicException('setChannel method requires setDriver set first.');
1420
        } else {
1421
            $this->driver->setChannel($channel);
1422
        }
1423
    }
1424
1425
    /**
1426
     * Return the result from Captchas.
1427
     *
1428
     * @return bool
1429
     */
1430
    public function captchaResponse(): bool
1431
    {
1432
        foreach ($this->captcha as $captcha) {
1433
            
1434
            if (!$captcha->response()) {
1435
                return false;
1436
            }
1437
        }
1438
1439
        if (!empty($this->sessionLimit['count'])) {
1440
            $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
1441
        }
1442
1443
        return true;
1444
    }
1445
1446
    /**
1447
     * Ban an IP.
1448
     *
1449
     * @param string $ip A valid IP address.
1450
     *
1451
     * @return void
1452
     */
1453
    public function ban(string $ip = ''): void
1454
    {
1455
        if ('' === $ip) {
1456
            $ip = $this->ip;
1457
        }
1458
 
1459
        $this->action(
1460
            self::ACTION_DENY,
1461
            self::REASON_MANUAL_BAN, $ip
1462
        );
1463
    }
1464
1465
    /**
1466
     * Unban an IP.
1467
     *
1468
     * @param string $ip A valid IP address.
1469
     *
1470
     * @return void
1471
     */
1472
    public function unban(string $ip = ''): void
1473
    {
1474
        if ('' === $ip) {
1475
            $ip = $this->ip;
1476
        }
1477
1478
        $this->action(
1479
            self::ACTION_UNBAN,
1480
            self::REASON_MANUAL_BAN, $ip
1481
        );
1482
        $this->log(self::ACTION_UNBAN);
1483
1484
        $this->result = self::RESPONSE_ALLOW;
1485
    }
1486
1487
    /**
1488
     * Set a property setting.
1489
     *
1490
     * @param string $key   The key of a property setting.
1491
     * @param mixed  $value The value of a property setting.
1492
     *
1493
     * @return void
1494
     */
1495
    public function setProperty(string $key = '', $value = '')
1496
    {
1497
        if (isset($this->properties[$key])) {
1498
            $this->properties[$key] = $value;
1499
        }
1500
    }
1501
1502
    /**
1503
     * Set the property settings.
1504
     * 
1505
     * @param array $settings The settings.
1506
     *
1507
     * @return void
1508
     */
1509
    public function setProperties(array $settings): void
1510
    {
1511
        foreach (array_keys($this->properties) as $k) {
1512
            if (isset($settings[$k])) {
1513
                $this->properties[$k] = $settings[$k];
1514
            }
1515
        }
1516
    }
1517
1518
    /**
1519
     * Limt online sessions.
1520
     *
1521
     * @param int $count
1522
     * @param int $period
1523
     *
1524
     * @return void
1525
     */
1526
    public function limitSession(int $count = 1000, int $period = 300): void
1527
    {
1528
        $this->sessionLimit = [
1529
            'count' => $count,
1530
            'period' => $period
1531
        ];
1532
    }
1533
1534
    /**
1535
     * Customize the dialog UI.
1536
     *
1537
     * @return void
1538
     */
1539
    public function setDialogUI(array $settings): void
1540
    {
1541
        $this->dialogUI = $settings;
1542
    }
1543
1544
    /**
1545
     * Set the frontend template directory.
1546
     *
1547
     * @param string $directory
1548
     *
1549
     * @return void
1550
     */
1551
    public function setTemplateDirectory(string $directory)
1552
    {
1553
        if (!is_dir($directory)) {
1554
            throw new InvalidArgumentException('The template directory does not exist.');
1555
        }
1556
        $this->templateDirectory = $directory;
1557
    }
1558
1559
    /**
1560
     * Get a template PHP file.
1561
     *
1562
     * @param string $type The template type.
1563
     *
1564
     * @return string
1565
     */
1566
    protected function getTemplate(string $type): string
1567
    {
1568
        $directory = self::KERNEL_DIR . '/../../templates/frontend';
1569
1570
        if (!empty($this->templateDirectory)) {
1571
            $directory = $this->templateDirectory;
1572
        }
1573
1574
        $path = $directory . '/' . $type . '.php';
1575
1576
        if (!file_exists($path)) {
1577
            throw new RuntimeException(
1578
                sprintf(
1579
                    'The templeate file is missing. (%s)',
1580
                    $path
1581
                )
1582
            );
1583
        }
1584
1585
        return $path;
1586
    }
1587
1588
    /**
1589
     * Get a class name without namespace string.
1590
     *
1591
     * @param object $instance Class
1592
     * 
1593
     * @return void
1594
     */
1595
    protected function getClassName($instance): string
1596
    {
1597
        $class = get_class($instance);
1598
        return substr($class, strrpos($class, '\\') + 1); 
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr($class, strrpos($class, '\') + 1) returns the type string which is incompatible with the documented return type void.
Loading history...
1599
    }
1600
1601
    /**
1602
     * Respond the result.
1603
     *
1604
     * @return \Psr\Http\Message\ResponseInterface
1605
     */
1606
    public function respond(): ResponseInterface
1607
    {
1608
        $response = get_response();
1609
        $type = '';
1610
1611
        if (self::RESPONSE_TEMPORARILY_DENY === $this->result) {
1612
            $type = 'captcha';
1613
            $statusCode = 403; // Forbidden.
1614
1615
        } elseif (self::RESPONSE_LIMIT_SESSION === $this->result) {
1616
            $type = 'session_limitation';
1617
            $statusCode = 429; // Too Many Requests.
1618
1619
        } elseif (self::RESPONSE_DENY === $this->result) {
1620
            $type = 'rejection';
1621
            $statusCode = 400; // Bad request.
1622
        }
1623
1624
        // Nothing happened. Return.
1625
        if (empty($type)) {
1626
            // @codeCoverageIgnoreStart
1627
            return $response;
1628
            // @codeCoverageIgnoreEnd
1629
        }
1630
1631
        $viewPath = $this->getTemplate($type);
1632
1633
        // The language of output UI. It is used on views.
1634
        $langCode = get_session()->get('shieldon_ui_lang') ?? 'en';
1635
        // Show online session count. It is used on views.
1636
        $showOnlineInformation = true;
1637
        // Show user information such as IP, user-agent, device name.
1638
        $showUserInformation = true;
1639
1640
        if (empty($this->properties['display_online_info'])) {
1641
            $showOnlineInformation = false;
1642
        }
1643
1644
        if (empty($this->properties['display_user_info'])) {
1645
            $showUserInformation = false;
1646
        }
1647
1648
        if ($showUserInformation) {
1649
            $dialoguserinfo['ip'] = $this->ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$dialoguserinfo was never initialized. Although not strictly required by PHP, it is generally a good practice to add $dialoguserinfo = array(); before regardless.
Loading history...
1650
            $dialoguserinfo['rdns'] = $this->rdns;
1651
            $dialoguserinfo['user_agent'] = get_request()->getHeaderLine('user-agent');
1652
        }
1653
1654
        $ui = [
1655
            'background_image' => $this->dialogUI['background_image'] ?? '',
1656
            'bg_color'         => $this->dialogUI['bg_color']         ?? '#ffffff',
1657
            'header_bg_color'  => $this->dialogUI['header_bg_color']  ?? '#212531',
1658
            'header_color'     => $this->dialogUI['header_color']     ?? '#ffffff',
1659
            'shadow_opacity'   => $this->dialogUI['shadow_opacity']   ?? '0.2',
1660
        ];
1661
1662
        if (!defined('SHIELDON_VIEW')) {
1663
            define('SHIELDON_VIEW', true);
1664
        }
1665
1666
        $css = require $this->getTemplate('css/default');
1667
1668
        ob_start();
1669
        require $viewPath;
1670
        $output = ob_get_contents();
1671
        ob_end_clean();
1672
1673
        // Remove unused variable notices generated from PHP intelephense.
1674
        unset(
1675
            $css,
1676
            $ui,
1677
            $langCode,
1678
            $showOnlineInformation,
1679
            $showLineupInformation,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $showLineupInformation seems to be never defined.
Loading history...
1680
            $showUserInformation
1681
        );
1682
1683
        $stream = $response->getBody();
1684
        $stream->write($output);
1685
        $stream->rewind();
1686
1687
        return $response->
1688
            withHeader('X-Protected-By', 'shieldon.io')->
1689
            withBody($stream)->
1690
            withStatus($statusCode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $statusCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
1691
    }
1692
1693
    /**
1694
     * Run, run, run!
1695
     *
1696
     * Check the rule tables first, if an IP address has been listed.
1697
     * Call function filter() if an IP address is not listed in rule tables.
1698
     *
1699
     * @return 
1700
     */
1701
    public function run(): int
1702
    {
1703
        if (!isset($this->registrar[0])) {
1704
            throw new RuntimeException(
1705
                'Must register at least one data driver.'
1706
            );
1707
        }
1708
        
1709
        // Ignore the excluded urls.
1710
        if (!empty($this->excludedUrls)) {
1711
            foreach ($this->excludedUrls as $url) {
1712
                if (0 === strpos(get_request()->getUri()->getPath(), $url)) {
1713
                    return $this->result = self::RESPONSE_ALLOW;
1714
                }
1715
            }
1716
        }
1717
1718
        // Execute closure functions.
1719
        foreach ($this->closures as $closure) {
1720
            $closure();
1721
        }
1722
1723
        $result = $this->process();
1724
1725
        if ($result !== self::RESPONSE_ALLOW) {
1726
1727
            // Current session did not pass the CAPTCHA, it is still stuck in CAPTCHA page.
1728
            $actionCode = self::LOG_CAPTCHA;
1729
1730
            // If current session's respone code is RESPONSE_DENY, record it as `blacklist_count` in our logs.
1731
            // It is stuck in warning page, not CAPTCHA.
1732
            if ($result === self::RESPONSE_DENY) {
1733
                $actionCode = self::LOG_BLACKLIST;
1734
            }
1735
1736
            if ($result === self::RESPONSE_LIMIT_SESSION) {
1737
                $actionCode = self::LOG_LIMIT;
1738
            }
1739
1740
            $this->log($actionCode);
1741
1742
        } else {
1743
1744
            $this->log(self::LOG_PAGEVIEW);
1745
        }
1746
1747
 
1748
        if (!empty($this->msgBody)) {
1749
 
1750
            // @codeCoverageIgnoreStart
1751
1752
            try {
1753
                foreach ($this->messenger as $messenger) {
1754
                    $messenger->setTimeout(2);
1755
                    $messenger->send($this->msgBody);
1756
                }
1757
            } catch (RuntimeException $e) {
1758
                // Do not throw error, becasue the third-party services might be unavailable.
1759
            }
1760
1761
            // @codeCoverageIgnoreEnd
1762
        }
1763
1764
1765
        return $result;
1766
    }
1767
1768
    /**
1769
     * Set the filters.
1770
     *
1771
     * @param array $settings filter settings.
1772
     *
1773
     * @return void
1774
     */
1775
    public function setFilters(array $settings)
1776
    {
1777
        foreach (array_keys($this->filterStatus) as $k) {
1778
            if (isset($settings[$k])) {
1779
                $this->filterStatus[$k] = $settings[$k] ?? false;
1780
            }
1781
        }
1782
    }
1783
1784
    /**
1785
     * Set a filter.
1786
     *
1787
     * @param string $filterName The filter's name.
1788
     * @param bool   $value      True for enabling the filter, overwise.
1789
     *
1790
     * @return void
1791
     */
1792
    public function setFilter(string $filterName, bool $value): void
1793
    {
1794
        if (isset($this->filterStatus[$filterName])) {
1795
            $this->filterStatus[$filterName] = $value;
1796
        }
1797
    }
1798
1799
    /**
1800
     * Get online people count. If enable limitSession.
1801
     *
1802
     * @return int
1803
     */
1804
    public function getSessionCount(): int
1805
    {
1806
        return $this->sessionStatus['count'];
1807
    }
1808
1809
    /**
1810
     * Set the URLs you want them to be excluded them from protection.
1811
     *
1812
     * @param array $urls The list of URL want to be excluded.
1813
     *
1814
     * @return void
1815
     */
1816
    public function setExcludedUrls(array $urls = []): void
1817
    {
1818
        $this->excludedUrls = $urls;
1819
    }
1820
1821
    /**
1822
     * Set a closure function.
1823
     *
1824
     * @param string  $key     The name for the closure class.
1825
     * @param Closure $closure An instance will be later called.
1826
     *
1827
     * @return void
1828
     */
1829
    public function setClosure(string $key, Closure $closure): void
1830
    {
1831
        $this->closures[$key] = $closure;
1832
    }
1833
1834
    /**
1835
     * Print a JavasSript snippet in your webpages.
1836
     * 
1837
     * This snippet generate cookie on client's browser,then we check the 
1838
     * cookie to identify the client is a rebot or not.
1839
     *
1840
     * @return string
1841
     */
1842
    public function outputJsSnippet(): string
1843
    {
1844
        $tmpCookieName = $this->properties['cookie_name'];
1845
        $tmpCookieDomain = $this->properties['cookie_domain'];
1846
1847
        if (empty($tmpCookieDomain) && get_request()->getHeaderLine('host')) {
1848
            $tmpCookieDomain = get_request()->getHeaderLine('host');
1849
        }
1850
1851
        $tmpCookieValue = $this->properties['cookie_value'];
1852
1853
        $jsString = '
1854
            <script>
1855
                var d = new Date();
1856
                d.setTime(d.getTime()+(60*60*24*30));
1857
                document.cookie = "' . $tmpCookieName . '=' . $tmpCookieValue . ';domain=.' . $tmpCookieDomain . ';expires="+d.toUTCString();
1858
            </script>
1859
        ';
1860
1861
        return $jsString;
1862
    }
1863
1864
    /**
1865
     * Get current visior's path.
1866
     *
1867
     * @return string
1868
     */
1869
    public function getCurrentUrl(): string
1870
    {
1871
        return get_request()->getUri()->getPath();
1872
    }
1873
1874
    /**
1875
     * Displayed on Firewall Panel, tell you current what type of current
1876
     * configuration is used for.
1877
     * 
1878
     * @param string $type The type of configuration.
1879
     *                     demo | managed | config
1880
     *
1881
     * @return void
1882
     */
1883
    public function managedBy(string $type = ''): void
1884
    {
1885
        if (in_array($type, ['managed', 'config', 'demo'])) {
1886
            $this->firewallType = $type;
1887
        }
1888
    }
1889
}
1890