Passed
Push — 2.x ( c85191...0267a9 )
by Terry
02:01
created

Kernel::filterFrequency()   B

Complexity

Conditions 11
Paths 2

Size

Total Lines 72
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 37
c 2
b 0
f 0
nc 2
nop 3
dl 0
loc 72
rs 7.3166

4 Methods

Rating   Name   Duplication   Size   Complexity  
A Kernel::ban() 0 9 2
A Kernel::captchaResponse() 0 14 4
A Kernel::unban() 0 13 2
A Kernel::setProperty() 0 4 2

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\Firewall\Kernel\IpTrait;
46
use Shieldon\Firewall\Kernel\FilterTrait;
47
use Shieldon\Messenger\Messenger\MessengerInterface;
48
use function Shieldon\Firewall\__;
49
use function Shieldon\Firewall\get_cpu_usage;
50
use function Shieldon\Firewall\get_default_properties;
51
use function Shieldon\Firewall\get_memory_usage;
52
use function Shieldon\Firewall\get_request;
53
use function Shieldon\Firewall\get_response;
54
use function Shieldon\Firewall\get_session;
55
56
57
use Closure;
58
use InvalidArgumentException;
59
use LogicException;
60
use RuntimeException;
61
use function file_exists;
62
use function file_put_contents;
63
use function filter_var;
64
use function get_class;
65
use function gethostbyaddr;
66
use function is_dir;
67
use function is_writable;
68
use function microtime;
69
use function ob_end_clean;
70
use function ob_get_contents;
71
use function ob_start;
72
use function str_replace;
73
use function strpos;
74
use function strrpos;
75
use function substr;
76
use function time;
77
78
/**
79
 * The primary Shiendon class.
80
 */
81
class Kernel
82
{
83
    use IpTrait;
84
    use FilterTrait;
85
86
    // Reason codes (allow)
87
    const REASON_IS_SEARCH_ENGINE = 100;
88
    const REASON_IS_GOOGLE = 101;
89
    const REASON_IS_BING = 102;
90
    const REASON_IS_YAHOO = 103;
91
    const REASON_IS_SOCIAL_NETWORK = 110;
92
    const REASON_IS_FACEBOOK = 111;
93
    const REASON_IS_TWITTER = 112;
94
95
    // Reason codes (deny)
96
    const REASON_TOO_MANY_SESSIONS = 1;
97
    const REASON_TOO_MANY_ACCESSES = 2; // (not used)
98
    const REASON_EMPTY_JS_COOKIE = 3;
99
    const REASON_EMPTY_REFERER = 4;
100
    
101
    const REASON_REACHED_LIMIT_DAY = 11;
102
    const REASON_REACHED_LIMIT_HOUR = 12;
103
    const REASON_REACHED_LIMIT_MINUTE = 13;
104
    const REASON_REACHED_LIMIT_SECOND = 14;
105
106
    const REASON_INVALID_IP = 40;
107
    const REASON_DENY_IP = 41;
108
    const REASON_ALLOW_IP = 42;
109
110
    const REASON_COMPONENT_IP = 81;
111
    const REASON_COMPONENT_RDNS = 82;
112
    const REASON_COMPONENT_HEADER = 83;
113
    const REASON_COMPONENT_USERAGENT = 84;
114
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
115
116
    const REASON_MANUAL_BAN = 99;
117
118
    // Action codes
119
    const ACTION_DENY = 0;
120
    const ACTION_ALLOW = 1;
121
    const ACTION_TEMPORARILY_DENY = 2;
122
    const ACTION_UNBAN = 9;
123
124
    // Result codes
125
    const RESPONSE_DENY = 0;
126
    const RESPONSE_ALLOW = 1;
127
    const RESPONSE_TEMPORARILY_DENY = 2;
128
    const RESPONSE_LIMIT_SESSION = 3;
129
130
    const LOG_LIMIT = 3;
131
    const LOG_PAGEVIEW = 11;
132
    const LOG_BLACKLIST = 98;
133
    const LOG_CAPTCHA = 99;
134
135
    const KERNEL_DIR = __DIR__;
136
137
    /**
138
     * Driver for storing data.
139
     *
140
     * @var \Shieldon\Firewall\Driver\DriverProvider
141
     */
142
    public $driver;
143
144
    /**
145
     * Container for Shieldon components.
146
     *
147
     * @var array
148
     */
149
    public $component = [];
150
151
    /**
152
     * Logger instance.
153
     *
154
     * @var ActionLogger
155
     */
156
    public $logger;
157
158
    /**
159
     * The closure functions that will be executed in this->run()
160
     *
161
     * @var array
162
     */
163
    protected $closures = [];
164
165
    /**
166
     * default settings
167
     *
168
     * @var array
169
     */
170
    protected $properties = [];
171
172
    /**
173
     * This is for creating data tables automatically
174
     * Turn it off, if you don't want to check data tables every connection.
175
     *
176
     * @var bool
177
     */
178
    protected $autoCreateDatabase = true;
179
180
    /**
181
     * Container for captcha addons.
182
     * The collection of \Shieldon\Firewall\Captcha\CaptchaInterface
183
     *
184
     * @var array
185
     */
186
    protected $captcha = [];
187
188
    /**
189
     * The ways Shieldon send a message to when someone has been blocked.
190
     * The collection of \Shieldon\Messenger\Messenger\MessengerInterface
191
     *
192
     * @var array
193
     */
194
    protected $messenger = [];
195
196
    /**
197
     * Is to limit traffic?
198
     *
199
     * @var array
200
     */
201
    protected $sessionLimit = [
202
203
        // How many sessions will be available?
204
        // 0 = no limit.
205
        'count' => 0,
206
207
        // How many minutes will a session be availe to visit?
208
        // 0 = no limit.
209
        'period' => 0, 
210
    ];
211
212
    /**
213
     * Record the online session status.
214
     * This will be enabled when $sessionLimit[count] > 0
215
     *
216
     * @var array
217
     */
218
    protected $sessionStatus = [
219
220
        // Online session count.
221
        'count' => 0,
222
223
        // Current session order.
224
        'order' => 0,
225
226
        // Current waiting queue.
227
        'queue' => 0,
228
    ];
229
230
    /**
231
     * The events.
232
     *
233
     * @var array
234
     */
235
    protected $event = [
236
237
        // Update rule table when this value true.
238
        'update_rule_table' => false,
239
240
        // Send notifications when this value true.
241
        'trigger_messengers' => false,
242
    ];
243
244
    /**
245
     * Result.
246
     *
247
     * @var int
248
     */
249
    protected $result = 1;
250
251
    /**
252
     * URLs that are excluded from Shieldon's protection.
253
     *
254
     * @var array
255
     */
256
    protected $excludedUrls = [];
257
258
    /**
259
     * Which type of configuration source that Shieldon firewall managed?
260
     *
261
     * @var string
262
     */
263
    protected $firewallType = 'self'; // managed | config | self | demo
264
265
    /**
266
     * Custom dialog UI settings.
267
     *
268
     * @var array
269
     */
270
    protected $dialogUI = [];
271
272
    /**
273
     * Store the class information used in Shieldon.
274
     *
275
     * @var array
276
     */
277
    protected $registrar = [];
278
279
    /**
280
     * Strict mode.
281
     * 
282
     * Set by `strictMode()` only. The default value of this propertry is undefined.
283
     *
284
     * @var bool
285
     */
286
    protected $strictMode;
287
288
    /**
289
     * The directory in where the frontend template files are placed.
290
     *
291
     * @var string
292
     */
293
    protected $templateDirectory = '';
294
295
    /**
296
     * The message that will be sent to the third-party API.
297
     *
298
     * @var string
299
     */
300
    protected $msgBody = '';
301
302
    /**
303
     * Shieldon constructor.
304
     * 
305
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
306
     * 
307
     * @return void
308
     */
309
    public function __construct(?ServerRequestInterface $request  = null, ?ResponseInterface $response = null)
310
    {
311
        // Load helper functions. This is the must.
312
        new Helpers();
313
314
        if (is_null($request)) {
315
            $request = HttpFactory::createRequest();
316
        }
317
318
        if (is_null($response)) {
319
            $response = HttpFactory::createResponse();
320
        }
321
322
        $session = HttpFactory::createSession();
323
324
        $this->properties = get_default_properties();
325
        $this->add(new Foundation());
326
327
        Container::set('request', $request);
328
        Container::set('response', $response);
329
        Container::set('session', $session);
330
        Container::set('shieldon', $this);
331
    }
332
333
    /**
334
     * Log actions.
335
     *
336
     * @param int $actionCode The code number of the action.
337
     *
338
     * @return void
339
     */
340
    protected function log(int $actionCode): void
341
    {
342
        if (null !== $this->logger) {
343
            $logData = [];
344
            $logData['ip'] = $this->getIp();
345
            $logData['session_id'] = get_session()->get('id');
346
            $logData['action_code'] = $actionCode;
347
            $logData['timesamp'] = time();
348
    
349
            $this->logger->add($logData);
350
        }
351
    }
352
353
    /**
354
     * Initialize components.
355
     *
356
     * @return void
357
     */
358
    private function initComponents()
359
    {
360
        foreach (array_keys($this->component) as $name) {
361
            $this->component[$name]->setIp($this->ip);
362
            $this->component[$name]->setRdns($this->rdns);
363
364
            // Apply global strict mode to all components by `strictMode()` if nesscessary.
365
            if (isset($this->strictMode)) {
366
                $this->component[$name]->setStrict($this->strictMode);
367
            }
368
        }
369
    }
370
371
    /**
372
     * Look up the rule table.
373
     *
374
     * If a specific IP address doesn't exist, return false. 
375
     * Otherwise, return true.
376
     *
377
     * @return bool
378
     */
379
    private function isRuleTable()
380
    {
381
        $ipRule = $this->driver->get($this->ip, 'rule');
382
383
        if (empty($ipRule)) {
384
            return false;
385
        }
386
387
        $ruleType = (int) $ipRule['type'];
388
389
        // Apply the status code.
390
        $this->result = $ruleType;
391
392
        if ($ruleType === self::ACTION_ALLOW) {
393
            return true;
394
        }
395
396
        // Current visitor has been blocked. If he still attempts accessing the site, 
397
        // then we can drop him into the permanent block list.
398
        $attempts = $ipRule['attempts'] ?? 0;
399
        $attempts = (int) $attempts;
400
        $now = time();
401
        $logData = [];
402
        $handleType = 0;
403
404
        $logData['log_ip']     = $ipRule['log_ip'];
405
        $logData['ip_resolve'] = $ipRule['ip_resolve'];
406
        $logData['time']       = $now;
407
        $logData['type']       = $ipRule['type'];
408
        $logData['reason']     = $ipRule['reason'];
409
        $logData['attempts']   = $attempts;
410
411
        // @since 0.2.0
412
        $attemptPeriod = $this->properties['record_attempt_detection_period'];
413
        $attemptReset  = $this->properties['reset_attempt_counter'];
414
415
        $lastTimeDiff = $now - $ipRule['time'];
416
417
        if ($lastTimeDiff <= $attemptPeriod) {
418
            $logData['attempts'] = ++$attempts;
419
        }
420
421
        if ($lastTimeDiff > $attemptReset) {
422
            $logData['attempts'] = 0;
423
        }
424
425
        if ($ruleType === self::ACTION_TEMPORARILY_DENY) {
426
            $ratd = $this->determineAttemptsTemporaryDeny($logData, $handleType, $attempts);
427
            $logData = $ratd['log_data'];
428
            $handleType = $ratd['handle_type'];
429
        }
430
431
        if ($ruleType === self::ACTION_DENY) {
432
            $rapd = $this->determineAttemptsPermanentDeny($logData, $handleType, $attempts);
433
            $logData = $rapd['log_data'];
434
            $handleType = $rapd['handle_type'];
435
        }
436
437
        // We only update data when `deny_attempt_enable` is enable.
438
        // Because we want to get the last visited time and attempt counter.
439
        // Otherwise, we don't update it everytime to avoid wasting CPU resource.
440
        if ($this->event['update_rule_table']) {
441
            $this->driver->save($this->ip, $logData, 'rule');
442
        }
443
444
        // Notify this event to messenger.
445
        if ($this->event['trigger_messengers']) {
446
            $this->prepareMessengerBody($logData, $handleType);
447
        }
448
449
        return true;
450
    }
451
452
    /**
453
     * Record the attempts when the user is temporarily denied by rule table.
454
     *
455
     * @param array $logData
456
     * @param int   $handleType
457
     * @param int   $attempts
458
     * 
459
     * @return array
460
     */
461
    private function determineAttemptsTemporaryDeny(array $logData, int $handleType, int $attempts): array
462
    {
463
        if ($this->properties['deny_attempt_enable']['data_circle']) {
464
            $this->event['update_rule_table'] = true;
465
466
            $buffer = $this->properties['deny_attempt_buffer']['data_circle'];
467
468
            if ($attempts >= $buffer) {
469
470
                if ($this->properties['deny_attempt_notify']['data_circle']) {
471
                    $this->event['trigger_messengers'] = true;
472
                }
473
474
                $logData['type'] = self::ACTION_DENY;
475
476
                // Reset this value for next checking process - iptables.
477
                $logData['attempts'] = 0;
478
                $handleType = 1;
479
            }
480
        }
481
482
        return [
483
            'log_data' => $logData,
484
            'handle_type' => $handleType,
485
        ];
486
    }
487
488
    /**
489
     * Record the attempts when the user is permanently denied by rule table.
490
     *
491
     * @param array $logData
492
     * @param int   $handleType
493
     * @param int   $attempts
494
     * 
495
     * @return array
496
     */
497
    private function determineAttemptsPermanentDeny(array $logData, int $handleType, int $attempts): array
498
    {
499
        if ($this->properties['deny_attempt_enable']['system_firewall']) {
500
            $this->event['update_rule_table'] = true;
501
502
            // For the requests that are already banned, but they are still attempting access, that means 
503
            // that they are programmably accessing your website. Consider put them in the system-layer fireall
504
            // such as IPTABLE.
505
            $bufferIptable = $this->properties['deny_attempt_buffer']['system_firewall'];
506
507
            if ($attempts >= $bufferIptable) {
508
509
                if ($this->properties['deny_attempt_notify']['system_firewall']) {
510
                    $this->event['trigger_messengers'] = true;
511
                }
512
513
                $folder = rtrim($this->properties['iptables_watching_folder'], '/');
514
515
                if (file_exists($folder) && is_writable($folder)) {
516
                    $filePath = $folder . '/iptables_queue.log';
517
518
                    // command, ipv4/6, ip, subnet, port, protocol, action
519
                    // add,4,127.0.0.1,null,all,all,drop  (example)
520
                    // add,4,127.0.0.1,null,80,tcp,drop   (example)
521
                    $command = 'add,4,' . $this->ip . ',null,all,all,deny';
522
523
                    if (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
524
                        $command = 'add,6,' . $this->ip . ',null,all,allow';
525
                    }
526
527
                    // Add this IP address to itables_queue.log
528
                    // Use `bin/iptables.sh` for adding it into IPTABLES. See document for more information. 
529
                    file_put_contents($filePath, $command . "\n", FILE_APPEND | LOCK_EX);
530
531
                    $logData['attempts'] = 0;
532
                    $handleType = 2;
533
                }
534
            }
535
        }
536
537
        return [
538
            'log_data' => $logData,
539
            'handle_type' => $handleType,
540
        ];
541
    }
542
543
    /**
544
     * Prepare the message body for messenger modules to sent.
545
     *
546
     * @param array $logData
547
     * @param int   $handleType
548
     * 
549
     * @return void
550
     */
551
    private function prepareMessengerBody(array $logData, int $handleType): void
552
    {
553
        // The data strings that will be appended to message body.
554
        $prepareMessageData = [
555
            __('core', 'messenger_text_ip')       => $logData['log_ip'],
556
            __('core', 'messenger_text_rdns')     => $logData['ip_resolve'],
557
            __('core', 'messenger_text_reason')   => __('core', 'messenger_text_reason_code_' . $logData['reason']),
558
            __('core', 'messenger_text_handle')   => __('core', 'messenger_text_handle_type_' . $handleType),
559
            __('core', 'messenger_text_system')   => '',
560
            __('core', 'messenger_text_cpu')      => get_cpu_usage(),
561
            __('core', 'messenger_text_memory')   => get_memory_usage(),
562
            __('core', 'messenger_text_time')     => date('Y-m-d H:i:s', $logData['time']),
563
            __('core', 'messenger_text_timezone') => date_default_timezone_get(),
564
        ];
565
566
        $message = __('core', 'messenger_notification_subject', 'Notification for {0}', [$this->ip]) . "\n\n";
567
568
        foreach ($prepareMessageData as $key => $value) {
569
            $message .= $key . ': ' . $value . "\n";
570
        }
571
572
        $this->msgBody = $message;
573
    }
574
575
    /**
576
     * Check if current IP is trusted or not.
577
     *
578
     * @return bool
579
     */
580
    private function isTrustedBot()
581
    {
582
        if ($this->getComponent('TrustedBot')) {
583
584
            // We want to put all the allowed robot into the rule list, so that the checking of IP's resolved hostname 
585
            // is no more needed for that IP.
586
            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

586
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isAllowed()) {
Loading history...
587
588
                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

588
                if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isGoogle()) {
Loading history...
589
                    // Add current IP into allowed list, because it is from real Google domain.
590
                    $this->action(
591
                        self::ACTION_ALLOW,
592
                        self::REASON_IS_GOOGLE
593
                    );
594
595
                } 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

595
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isBing()) {
Loading history...
596
                    // Add current IP into allowed list, because it is from real Bing domain.
597
                    $this->action(
598
                        self::ACTION_ALLOW,
599
                        self::REASON_IS_BING
600
                    );
601
602
                } 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

602
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isYahoo()) {
Loading history...
603
                    // Add current IP into allowed list, because it is from real Yahoo domain.
604
                    $this->action(
605
                        self::ACTION_ALLOW,
606
                        self::REASON_IS_YAHOO
607
                    );
608
609
                } else {
610
                    // Add current IP into allowed list, because you trust it.
611
                    // You have already defined it in the settings.
612
                    $this->action(
613
                        self::ACTION_ALLOW,
614
                        self::REASON_IS_SEARCH_ENGINE
615
                    );
616
                }
617
                // Allowed robots not join to our traffic handler.
618
                $this->result = self::RESPONSE_ALLOW;
619
                return true;
620
            }
621
        }
622
        return false;
623
    }
624
625
    /**
626
     * Check whether the IP is fake search engine or not.
627
     * The method "isTrustedBot()" must be executed before this method.
628
     *
629
     * @return bool
630
     */
631
    private function isFakeRobot(): bool
632
    {
633
        if ($this->getComponent('TrustedBot')) {
634
            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

634
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isFakeRobot()) {
Loading history...
635
                $this->action(
636
                    self::ACTION_DENY,
637
                    self::REASON_COMPONENT_TRUSTED_ROBOT
638
                );
639
                $this->result = self::RESPONSE_DENY;
640
                return true;
641
            }
642
        }
643
        return false;
644
    }
645
646
    /**
647
     * Run, run, run!
648
     *
649
     * Check the rule tables first, if an IP address has been listed.
650
     * Call function filter() if an IP address is not listed in rule tables.
651
     *
652
     * @return int The response code.
653
     */
654
    protected function process(): int
655
    {
656
        $this->driver->init($this->autoCreateDatabase);
657
658
        $this->initComponents();
659
660
        /*
661
        |--------------------------------------------------------------------------
662
        | Stage - Looking for rule table.
663
        |--------------------------------------------------------------------------
664
        */
665
666
        if ($this->isRuleTable()) {
667
            return $this->result;
668
        }
669
670
        /*
671
        |--------------------------------------------------------------------------
672
        | Statge - Detect popular search engine.
673
        |--------------------------------------------------------------------------
674
        */
675
676
        if ($this->isTrustedBot()) {
677
            return $this->result;
678
        }
679
680
        if ($this->isFakeRobot()) {
681
            return $this->result;
682
        }
683
        
684
        /*
685
        |--------------------------------------------------------------------------
686
        | Stage - IP component.
687
        |--------------------------------------------------------------------------
688
        */
689
690
        if ($this->getComponent('Ip')) {
691
692
            $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

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