Passed
Push — 2.x ( 0267a9...364dd8 )
by Terry
02:05
created

Kernel::getSessionCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
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\Firewall\Kernel\RuleTrait;
48
use Shieldon\Messenger\Messenger\MessengerInterface;
49
use function Shieldon\Firewall\__;
50
use function Shieldon\Firewall\get_cpu_usage;
51
use function Shieldon\Firewall\get_default_properties;
52
use function Shieldon\Firewall\get_memory_usage;
53
use function Shieldon\Firewall\get_request;
54
use function Shieldon\Firewall\get_response;
55
use function Shieldon\Firewall\get_session;
56
57
58
use Closure;
59
use InvalidArgumentException;
60
use LogicException;
61
use RuntimeException;
62
use function file_exists;
63
use function file_put_contents;
64
use function filter_var;
65
use function get_class;
66
use function gethostbyaddr;
67
use function is_dir;
68
use function is_writable;
69
use function microtime;
70
use function ob_end_clean;
71
use function ob_get_contents;
72
use function ob_start;
73
use function str_replace;
74
use function strpos;
75
use function strrpos;
76
use function substr;
77
use function time;
78
79
/**
80
 * The primary Shiendon class.
81
 */
82
class Kernel
83
{
84
    use IpTrait;
85
    use FilterTrait;
86
    use RuleTrait;
87
88
    // Reason codes (allow)
89
    const REASON_IS_SEARCH_ENGINE = 100;
90
    const REASON_IS_GOOGLE = 101;
91
    const REASON_IS_BING = 102;
92
    const REASON_IS_YAHOO = 103;
93
    const REASON_IS_SOCIAL_NETWORK = 110;
94
    const REASON_IS_FACEBOOK = 111;
95
    const REASON_IS_TWITTER = 112;
96
97
    // Reason codes (deny)
98
    const REASON_TOO_MANY_SESSIONS = 1;
99
    const REASON_TOO_MANY_ACCESSES = 2; // (not used)
100
    const REASON_EMPTY_JS_COOKIE = 3;
101
    const REASON_EMPTY_REFERER = 4;
102
    
103
    const REASON_REACHED_LIMIT_DAY = 11;
104
    const REASON_REACHED_LIMIT_HOUR = 12;
105
    const REASON_REACHED_LIMIT_MINUTE = 13;
106
    const REASON_REACHED_LIMIT_SECOND = 14;
107
108
    const REASON_INVALID_IP = 40;
109
    const REASON_DENY_IP = 41;
110
    const REASON_ALLOW_IP = 42;
111
112
    const REASON_COMPONENT_IP = 81;
113
    const REASON_COMPONENT_RDNS = 82;
114
    const REASON_COMPONENT_HEADER = 83;
115
    const REASON_COMPONENT_USERAGENT = 84;
116
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
117
118
    const REASON_MANUAL_BAN = 99;
119
120
    // Action codes
121
    const ACTION_DENY = 0;
122
    const ACTION_ALLOW = 1;
123
    const ACTION_TEMPORARILY_DENY = 2;
124
    const ACTION_UNBAN = 9;
125
126
    // Result codes
127
    const RESPONSE_DENY = 0;
128
    const RESPONSE_ALLOW = 1;
129
    const RESPONSE_TEMPORARILY_DENY = 2;
130
    const RESPONSE_LIMIT_SESSION = 3;
131
132
    const LOG_LIMIT = 3;
133
    const LOG_PAGEVIEW = 11;
134
    const LOG_BLACKLIST = 98;
135
    const LOG_CAPTCHA = 99;
136
137
    const KERNEL_DIR = __DIR__;
138
139
    /**
140
     * Driver for storing data.
141
     *
142
     * @var \Shieldon\Firewall\Driver\DriverProvider
143
     */
144
    public $driver;
145
146
    /**
147
     * Container for Shieldon components.
148
     *
149
     * @var array
150
     */
151
    public $component = [];
152
153
    /**
154
     * Logger instance.
155
     *
156
     * @var ActionLogger
157
     */
158
    public $logger;
159
160
    /**
161
     * The closure functions that will be executed in this->run()
162
     *
163
     * @var array
164
     */
165
    protected $closures = [];
166
167
    /**
168
     * default settings
169
     *
170
     * @var array
171
     */
172
    protected $properties = [];
173
174
    /**
175
     * This is for creating data tables automatically
176
     * Turn it off, if you don't want to check data tables every connection.
177
     *
178
     * @var bool
179
     */
180
    protected $autoCreateDatabase = true;
181
182
    /**
183
     * Container for captcha addons.
184
     * The collection of \Shieldon\Firewall\Captcha\CaptchaInterface
185
     *
186
     * @var array
187
     */
188
    protected $captcha = [];
189
190
    /**
191
     * The ways Shieldon send a message to when someone has been blocked.
192
     * The collection of \Shieldon\Messenger\Messenger\MessengerInterface
193
     *
194
     * @var array
195
     */
196
    protected $messenger = [];
197
198
    /**
199
     * Is to limit traffic?
200
     *
201
     * @var array
202
     */
203
    protected $sessionLimit = [
204
205
        // How many sessions will be available?
206
        // 0 = no limit.
207
        'count' => 0,
208
209
        // How many minutes will a session be availe to visit?
210
        // 0 = no limit.
211
        'period' => 0, 
212
    ];
213
214
    /**
215
     * Record the online session status.
216
     * This will be enabled when $sessionLimit[count] > 0
217
     *
218
     * @var array
219
     */
220
    protected $sessionStatus = [
221
222
        // Online session count.
223
        'count' => 0,
224
225
        // Current session order.
226
        'order' => 0,
227
228
        // Current waiting queue.
229
        'queue' => 0,
230
    ];
231
232
    /**
233
     * The events.
234
     *
235
     * @var array
236
     */
237
    protected $event = [
238
239
        // Update rule table when this value true.
240
        'update_rule_table' => false,
241
242
        // Send notifications when this value true.
243
        'trigger_messengers' => false,
244
    ];
245
246
    /**
247
     * Result.
248
     *
249
     * @var int
250
     */
251
    protected $result = 1;
252
253
    /**
254
     * URLs that are excluded from Shieldon's protection.
255
     *
256
     * @var array
257
     */
258
    protected $excludedUrls = [];
259
260
    /**
261
     * Which type of configuration source that Shieldon firewall managed?
262
     *
263
     * @var string
264
     */
265
    protected $firewallType = 'self'; // managed | config | self | demo
266
267
    /**
268
     * Custom dialog UI settings.
269
     *
270
     * @var array
271
     */
272
    protected $dialogUI = [];
273
274
    /**
275
     * Store the class information used in Shieldon.
276
     *
277
     * @var array
278
     */
279
    protected $registrar = [];
280
281
    /**
282
     * Strict mode.
283
     * 
284
     * Set by `strictMode()` only. The default value of this propertry is undefined.
285
     *
286
     * @var bool
287
     */
288
    protected $strictMode;
289
290
    /**
291
     * The directory in where the frontend template files are placed.
292
     *
293
     * @var string
294
     */
295
    protected $templateDirectory = '';
296
297
    /**
298
     * The message that will be sent to the third-party API.
299
     *
300
     * @var string
301
     */
302
    protected $msgBody = '';
303
304
    /**
305
     * Shieldon constructor.
306
     * 
307
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
308
     * 
309
     * @return void
310
     */
311
    public function __construct(?ServerRequestInterface $request  = null, ?ResponseInterface $response = null)
312
    {
313
        // Load helper functions. This is the must.
314
        new Helpers();
315
316
        if (is_null($request)) {
317
            $request = HttpFactory::createRequest();
318
        }
319
320
        if (is_null($response)) {
321
            $response = HttpFactory::createResponse();
322
        }
323
324
        $session = HttpFactory::createSession();
325
326
        $this->properties = get_default_properties();
327
        $this->add(new Foundation());
328
329
        Container::set('request', $request);
330
        Container::set('response', $response);
331
        Container::set('session', $session);
332
        Container::set('shieldon', $this);
333
    }
334
335
    /**
336
     * Log actions.
337
     *
338
     * @param int $actionCode The code number of the action.
339
     *
340
     * @return void
341
     */
342
    protected function log(int $actionCode): void
343
    {
344
        if (null !== $this->logger) {
345
            $logData = [];
346
            $logData['ip'] = $this->getIp();
347
            $logData['session_id'] = get_session()->get('id');
348
            $logData['action_code'] = $actionCode;
349
            $logData['timesamp'] = time();
350
    
351
            $this->logger->add($logData);
352
        }
353
    }
354
355
    /**
356
     * Initialize components.
357
     *
358
     * @return void
359
     */
360
    private function initComponents()
361
    {
362
        foreach (array_keys($this->component) as $name) {
363
            $this->component[$name]->setIp($this->ip);
364
            $this->component[$name]->setRdns($this->rdns);
365
366
            // Apply global strict mode to all components by `strictMode()` if nesscessary.
367
            if (isset($this->strictMode)) {
368
                $this->component[$name]->setStrict($this->strictMode);
369
            }
370
        }
371
    }
372
373
    
374
375
    /**
376
     * Prepare the message body for messenger modules to sent.
377
     *
378
     * @param array $logData
379
     * @param int   $handleType
380
     * 
381
     * @return void
382
     */
383
    private function prepareMessengerBody(array $logData, int $handleType): void
0 ignored issues
show
Unused Code introduced by
The method prepareMessengerBody() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
384
    {
385
        // The data strings that will be appended to message body.
386
        $prepareMessageData = [
387
            __('core', 'messenger_text_ip')       => $logData['log_ip'],
388
            __('core', 'messenger_text_rdns')     => $logData['ip_resolve'],
389
            __('core', 'messenger_text_reason')   => __('core', 'messenger_text_reason_code_' . $logData['reason']),
390
            __('core', 'messenger_text_handle')   => __('core', 'messenger_text_handle_type_' . $handleType),
391
            __('core', 'messenger_text_system')   => '',
392
            __('core', 'messenger_text_cpu')      => get_cpu_usage(),
393
            __('core', 'messenger_text_memory')   => get_memory_usage(),
394
            __('core', 'messenger_text_time')     => date('Y-m-d H:i:s', $logData['time']),
395
            __('core', 'messenger_text_timezone') => date_default_timezone_get(),
396
        ];
397
398
        $message = __('core', 'messenger_notification_subject', 'Notification for {0}', [$this->ip]) . "\n\n";
399
400
        foreach ($prepareMessageData as $key => $value) {
401
            $message .= $key . ': ' . $value . "\n";
402
        }
403
404
        $this->msgBody = $message;
405
    }
406
407
    /**
408
     * Check if current IP is trusted or not.
409
     *
410
     * @return bool
411
     */
412
    private function isTrustedBot()
413
    {
414
        if ($this->getComponent('TrustedBot')) {
415
416
            // We want to put all the allowed robot into the rule list, so that the checking of IP's resolved hostname 
417
            // is no more needed for that IP.
418
            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

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

420
                if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isGoogle()) {
Loading history...
421
                    // Add current IP into allowed list, because it is from real Google domain.
422
                    $this->action(
423
                        self::ACTION_ALLOW,
424
                        self::REASON_IS_GOOGLE
425
                    );
426
427
                } 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

427
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isBing()) {
Loading history...
428
                    // Add current IP into allowed list, because it is from real Bing domain.
429
                    $this->action(
430
                        self::ACTION_ALLOW,
431
                        self::REASON_IS_BING
432
                    );
433
434
                } 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

434
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isYahoo()) {
Loading history...
435
                    // Add current IP into allowed list, because it is from real Yahoo domain.
436
                    $this->action(
437
                        self::ACTION_ALLOW,
438
                        self::REASON_IS_YAHOO
439
                    );
440
441
                } else {
442
                    // Add current IP into allowed list, because you trust it.
443
                    // You have already defined it in the settings.
444
                    $this->action(
445
                        self::ACTION_ALLOW,
446
                        self::REASON_IS_SEARCH_ENGINE
447
                    );
448
                }
449
                // Allowed robots not join to our traffic handler.
450
                $this->result = self::RESPONSE_ALLOW;
451
                return true;
452
            }
453
        }
454
        return false;
455
    }
456
457
    /**
458
     * Check whether the IP is fake search engine or not.
459
     * The method "isTrustedBot()" must be executed before this method.
460
     *
461
     * @return bool
462
     */
463
    private function isFakeRobot(): bool
464
    {
465
        if ($this->getComponent('TrustedBot')) {
466
            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

466
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isFakeRobot()) {
Loading history...
467
                $this->action(
468
                    self::ACTION_DENY,
469
                    self::REASON_COMPONENT_TRUSTED_ROBOT
470
                );
471
                $this->result = self::RESPONSE_DENY;
472
                return true;
473
            }
474
        }
475
        return false;
476
    }
477
478
    /**
479
     * Run, run, run!
480
     *
481
     * Check the rule tables first, if an IP address has been listed.
482
     * Call function filter() if an IP address is not listed in rule tables.
483
     *
484
     * @return int The response code.
485
     */
486
    protected function process(): int
487
    {
488
        $this->driver->init($this->autoCreateDatabase);
489
490
        $this->initComponents();
491
492
        /*
493
        |--------------------------------------------------------------------------
494
        | Stage - Looking for rule table.
495
        |--------------------------------------------------------------------------
496
        */
497
498
        if ($this->DoesRuleExist()) {
499
            return $this->result;
500
        }
501
502
        /*
503
        |--------------------------------------------------------------------------
504
        | Statge - Detect popular search engine.
505
        |--------------------------------------------------------------------------
506
        */
507
508
        if ($this->isTrustedBot()) {
509
            return $this->result;
510
        }
511
512
        if ($this->isFakeRobot()) {
513
            return $this->result;
514
        }
515
        
516
        /*
517
        |--------------------------------------------------------------------------
518
        | Stage - IP component.
519
        |--------------------------------------------------------------------------
520
        */
521
522
        if ($this->getComponent('Ip')) {
523
524
            $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

524
            $result = $this->getComponent('Ip')->/** @scrutinizer ignore-call */ check();
Loading history...
525
            $actionCode = self::ACTION_DENY;
526
527
            if (!empty($result)) {
528
529
                switch ($result['status']) {
530
531
                    case 'allow':
532
                        $actionCode = self::ACTION_ALLOW;
533
                        $reasonCode = $result['code'];
534
                        break;
535
    
536
                    case 'deny':
537
                        $actionCode = self::ACTION_DENY;
538
                        $reasonCode = $result['code']; 
539
                        break;
540
                }
541
542
                $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...
543
544
                // $resultCode = $actionCode
545
                return $this->result = $this->sessionHandler($actionCode);
546
            }
547
        }
548
549
        /*
550
        |--------------------------------------------------------------------------
551
        | Stage - Check all other components.
552
        |--------------------------------------------------------------------------
553
        */
554
555
        foreach ($this->component as $component) {
556
557
            // check if is a a bad robot already defined in settings.
558
            if ($component->isDenied()) {
559
560
                // @since 0.1.8
561
                $this->action(
562
                    self::ACTION_DENY,
563
                    $component->getDenyStatusCode()
564
                );
565
566
                return $this->result = self::RESPONSE_DENY;
567
            }
568
        }
569
        
570
571
        /*
572
        |--------------------------------------------------------------------------
573
        | Stage - Filters
574
        |--------------------------------------------------------------------------
575
        | This IP address is not listed in rule table, let's detect it.
576
        |
577
        */
578
579
        if (
580
            $this->filterStatus['frequency'] ||
581
            $this->filterStatus['referer'] ||
582
            $this->filterStatus['session'] ||
583
            $this->filterStatus['cookie']
584
        ) {
585
            return $this->result = $this->sessionHandler($this->filter());
586
        }
587
588
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
589
    }
590
591
    /**
592
     * Start an action for this IP address, allow or deny, and give a reason for it.
593
     *
594
     * @param int    $actionCode - 0: deny, 1: allow, 9: unban.
595
     * @param string $reasonCode
596
     * @param string $assignIp
597
     * 
598
     * @return void
599
     */
600
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
601
    {
602
        $ip = $this->ip;
603
        $rdns = $this->rdns;
604
        $now = time();
605
        $logData = [];
606
    
607
        if ('' !== $assignIp) {
608
            $ip = $assignIp;
609
            $rdns = gethostbyaddr($ip);
610
        }
611
612
        switch ($actionCode) {
613
            case self::ACTION_ALLOW: // acutally not used.
614
            case self::ACTION_DENY:  // actually not used.
615
            case self::ACTION_TEMPORARILY_DENY:
616
                $logData['log_ip']     = $ip;
617
                $logData['ip_resolve'] = $rdns;
618
                $logData['time']       = $now;
619
                $logData['type']       = $actionCode;
620
                $logData['reason']     = $reasonCode;
621
                $logData['attempts']   = 0;
622
623
                $this->driver->save($ip, $logData, 'rule');
624
                break;
625
            
626
            case self::ACTION_UNBAN:
627
                $this->driver->delete($ip, 'rule');
628
                break;
629
        }
630
631
        // Remove logs for this IP address because It already has it's own rule on system.
632
        // No need to count it anymore.
633
        $this->driver->delete($ip, 'filter');
634
635
        if (null !== $this->logger) {
636
            $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...
637
            $log['session_id']  = get_session()->get('id');
638
            $log['action_code'] = $actionCode;
639
            $log['timesamp']    = $now;
640
641
            $this->logger->add($log);
642
        }
643
    }
644
645
    /**
646
     * Deal with online sessions.
647
     *
648
     * @param int $statusCode The response code.
649
     *
650
     * @return int The response code.
651
     */
652
    protected function sessionHandler($statusCode): int
653
    {
654
        if (self::RESPONSE_ALLOW !== $statusCode) {
655
            return $statusCode;
656
        }
657
658
        // If you don't enable `limit traffic`, ignore the following steps.
659
        if (empty($this->sessionLimit['count'])) {
660
            return self::RESPONSE_ALLOW;
661
662
        } else {
663
664
            // Get the proerties.
665
            $limit = (int) ($this->sessionLimit['count'] ?? 0);
666
            $period = (int) ($this->sessionLimit['period'] ?? 300);
667
            $now = time();
668
669
            $sessionData = $this->driver->getAll('session');
670
            $sessionPools = [];
671
672
            $i = 1;
673
            $sessionOrder = 0;
674
675
            if (!empty($sessionData)) {
676
                foreach ($sessionData as $v) {
677
                    $sessionPools[] = $v['id'];
678
                    $lasttime = (int) $v['time'];
679
    
680
                    if (get_session()->get('id') === $v['id']) {
681
                        $sessionOrder = $i;
682
                    }
683
    
684
                    // Remove session if it expires.
685
                    if ($now - $lasttime > $period) {
686
                        $this->driver->delete($v['id'], 'session');
687
                    }
688
                    $i++;
689
                }
690
691
                if (0 === $sessionOrder) {
692
                    $sessionOrder = $i;
693
                }
694
            } else {
695
                $sessionOrder = 0;
696
            }
697
698
            // Count the online sessions.
699
            $this->sessionStatus['count'] = count($sessionPools);
700
            $this->sessionStatus['order'] = $sessionOrder;
701
            $this->sessionStatus['queue'] = $sessionOrder - $limit;
702
703
            if (!in_array(get_session()->get('id'), $sessionPools)) {
704
                $this->sessionStatus['count']++;
705
706
                // New session, record this data.
707
                $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...
708
                $data['ip'] = $this->ip;
709
                $data['time'] = $now;
710
711
                $microtimesamp = explode(' ', microtime());
712
                $microtimesamp = $microtimesamp[1] . str_replace('0.', '', $microtimesamp[0]);
713
                $data['microtimesamp'] = $microtimesamp;
714
715
                $this->driver->save(get_session()->get('id'), $data, 'session');
716
            }
717
718
            // Online session count reached the limit. So return RESPONSE_LIMIT_SESSION response code.
719
            if ($sessionOrder >= $limit) {
720
                return self::RESPONSE_LIMIT_SESSION;
721
            }
722
        }
723
724
        return self::RESPONSE_ALLOW;
725
    }
726
727
728
729
    // @codeCoverageIgnoreStart
730
731
    /**
732
     * For testing propose.
733
     *
734
     * @param string $sessionId
735
     *
736
     * @return void
737
     */
738
    protected function setSessionId(string $sessionId = ''): void
739
    {
740
        if ('' !== $sessionId) {
741
            get_session()->set('id', $sessionId);
742
        }
743
    }
744
745
    // @codeCoverageIgnoreEnd
746
747
    /*
748
    | -------------------------------------------------------------------
749
    |                            Public APIs
750
    | -------------------------------------------------------------------
751
    */
752
753
    /**
754
     * Register classes to Shieldon core.
755
     * setDriver, setLogger, setComponent and setCaptcha are deprecated methods
756
     * and no more used.
757
     *
758
     * @param object $instance Component classes that used on Shieldon.
759
     *
760
     * @return void
761
     */
762
    public function add($instance)
763
    {
764
        static $i = 2;
765
766
        $class = $this->getClassName($instance);
767
768
        if ($instance instanceof DriverProvider) {
769
            $this->driver = $instance;
770
            $this->registrar[0] = [
771
                'category' => 'driver',
772
                'class' => $class,
773
            ];
774
        }
775
776
        if ($instance instanceof ActionLogger) {
777
            $this->logger = $instance;
778
            $this->registrar[1] = [
779
                'category' => 'logger',
780
                'class' => $class,
781
            ];
782
        }
783
784
        if ($instance instanceof CaptchaInterface) {
785
            $this->captcha[$class] = $instance;
786
            $this->registrar[$i] = [
787
                'category' => 'captcha',
788
                'class' => $class,
789
            ];
790
            $i++;
791
        }
792
793
        if ($instance instanceof ComponentProvider) {
794
            $this->component[$class] = $instance;
795
            $this->registrar[$i] = [
796
                'category' => 'component',
797
                'class' => $class,
798
            ];
799
            $i++;
800
        }
801
802
        if ($instance instanceof MessengerInterface) {
803
            $this->messenger[] = $instance;
804
            $this->registrar[$i] = [
805
                'category' => 'messenger',
806
                'class' => $class,
807
            ];
808
            $i++;
809
        }
810
    }
811
812
    /**
813
     * Remove registered classes from the Kernel.
814
     *
815
     * @param string $category  The class category.
816
     * @param string $className The class name.
817
     *
818
     * @return void
819
     */
820
    public function remove(string $category, string $className = '')
821
    {
822
        if ($className !== '') {
823
            foreach ($this->getRegistrar() as $k => $v) {
824
                if ($category === $v['category'] && $className === $v['class']) {
825
                    if (is_array($this->{$category})) {
826
                        foreach ($this->{$category} as $k2 => $instance) {
827
                            if ($this->getClassName($instance) === $className) {
828
                                unset($this->{$category}[$k2]);
829
                            }
830
                        }
831
                    } else {
832
                        $this->{$category} = null;
833
                    }
834
                    unset($this->registrar[$k]);
835
                }
836
            }
837
        } else {
838
            foreach ($this->getRegistrar() as $k => $v) {
839
                if ($category === $v['category']) {
840
                    if (is_array($this->{$category})) {
841
                        $this->{$category} = [];
842
                    } else {
843
                        $this->{$category} = null;
844
                    }
845
                    unset($this->registrar[$k]);
846
                }
847
            }
848
        }
849
    }
850
851
    /**
852
     * Fetch the class list from registrar.
853
     *
854
     * @return array
855
     */
856
    public function getRegistrar(): array
857
    {
858
        return $this->registrar;
859
    }
860
861
    /**
862
     * Get a component instance from component's container.
863
     *
864
     * @param string $name The component's class name.
865
     *
866
     * @return ComponentInterface|null
867
     */
868
    public function getComponent(string $name)
869
    {
870
        if (isset($this->component[$name])) {
871
            return $this->component[$name];
872
        }
873
874
        return null;
875
    }
876
877
    /**
878
     * Strict mode.
879
     * This option will take effects to all components.
880
     * 
881
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
882
     *
883
     * @return void
884
     */
885
    public function setStrict(bool $bool)
886
    {
887
        $this->strictMode = $bool;
888
    }
889
890
    /**
891
     * Disable filters.
892
     */
893
    public function disableFilters(): void
894
    {
895
        $this->setFilters([
896
            'session'   => false,
897
            'cookie'    => false,
898
            'referer'   => false,
899
            'frequency' => false,
900
        ]);
901
    }
902
903
    /**
904
     * For first time installation only. This is for creating data tables automatically.
905
     * Turning it on will check the data tables exist or not at every single pageview, 
906
     * it's not good for high traffic websites.
907
     *
908
     * @param bool $bool
909
     * 
910
     * @return void
911
     */
912
    public function createDatabase(bool $bool)
913
    {
914
        $this->autoCreateDatabase = $bool;
915
    }
916
917
    /**
918
     * Set a data channel.
919
     *
920
     * This will create databases for the channel.
921
     *
922
     * @param string $channel Specify a channel.
923
     *
924
     * @return void
925
     */
926
    public function setChannel(string $channel)
927
    {
928
        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...
929
            throw new LogicException('setChannel method requires setDriver set first.');
930
        } else {
931
            $this->driver->setChannel($channel);
932
        }
933
    }
934
935
    /**
936
     * Return the result from Captchas.
937
     *
938
     * @return bool
939
     */
940
    public function captchaResponse(): bool
941
    {
942
        foreach ($this->captcha as $captcha) {
943
            
944
            if (!$captcha->response()) {
945
                return false;
946
            }
947
        }
948
949
        if (!empty($this->sessionLimit['count'])) {
950
            $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
951
        }
952
953
        return true;
954
    }
955
956
    /**
957
     * Ban an IP.
958
     *
959
     * @param string $ip A valid IP address.
960
     *
961
     * @return void
962
     */
963
    public function ban(string $ip = ''): void
964
    {
965
        if ('' === $ip) {
966
            $ip = $this->ip;
967
        }
968
 
969
        $this->action(
970
            self::ACTION_DENY,
971
            self::REASON_MANUAL_BAN, $ip
972
        );
973
    }
974
975
    /**
976
     * Unban an IP.
977
     *
978
     * @param string $ip A valid IP address.
979
     *
980
     * @return void
981
     */
982
    public function unban(string $ip = ''): void
983
    {
984
        if ('' === $ip) {
985
            $ip = $this->ip;
986
        }
987
988
        $this->action(
989
            self::ACTION_UNBAN,
990
            self::REASON_MANUAL_BAN, $ip
991
        );
992
        $this->log(self::ACTION_UNBAN);
993
994
        $this->result = self::RESPONSE_ALLOW;
995
    }
996
997
    /**
998
     * Set a property setting.
999
     *
1000
     * @param string $key   The key of a property setting.
1001
     * @param mixed  $value The value of a property setting.
1002
     *
1003
     * @return void
1004
     */
1005
    public function setProperty(string $key = '', $value = '')
1006
    {
1007
        if (isset($this->properties[$key])) {
1008
            $this->properties[$key] = $value;
1009
        }
1010
    }
1011
1012
    /**
1013
     * Set the property settings.
1014
     * 
1015
     * @param array $settings The settings.
1016
     *
1017
     * @return void
1018
     */
1019
    public function setProperties(array $settings): void
1020
    {
1021
        foreach (array_keys($this->properties) as $k) {
1022
            if (isset($settings[$k])) {
1023
                $this->properties[$k] = $settings[$k];
1024
            }
1025
        }
1026
    }
1027
1028
    /**
1029
     * Limt online sessions.
1030
     *
1031
     * @param int $count
1032
     * @param int $period
1033
     *
1034
     * @return void
1035
     */
1036
    public function limitSession(int $count = 1000, int $period = 300): void
1037
    {
1038
        $this->sessionLimit = [
1039
            'count' => $count,
1040
            'period' => $period
1041
        ];
1042
    }
1043
1044
    /**
1045
     * Customize the dialog UI.
1046
     *
1047
     * @return void
1048
     */
1049
    public function setDialogUI(array $settings): void
1050
    {
1051
        $this->dialogUI = $settings;
1052
    }
1053
1054
    /**
1055
     * Set the frontend template directory.
1056
     *
1057
     * @param string $directory
1058
     *
1059
     * @return void
1060
     */
1061
    public function setTemplateDirectory(string $directory)
1062
    {
1063
        if (!is_dir($directory)) {
1064
            throw new InvalidArgumentException('The template directory does not exist.');
1065
        }
1066
        $this->templateDirectory = $directory;
1067
    }
1068
1069
    /**
1070
     * Get a template PHP file.
1071
     *
1072
     * @param string $type The template type.
1073
     *
1074
     * @return string
1075
     */
1076
    protected function getTemplate(string $type): string
1077
    {
1078
        $directory = self::KERNEL_DIR . '/../../templates/frontend';
1079
1080
        if (!empty($this->templateDirectory)) {
1081
            $directory = $this->templateDirectory;
1082
        }
1083
1084
        $path = $directory . '/' . $type . '.php';
1085
1086
        if (!file_exists($path)) {
1087
            throw new RuntimeException(
1088
                sprintf(
1089
                    'The templeate file is missing. (%s)',
1090
                    $path
1091
                )
1092
            );
1093
        }
1094
1095
        return $path;
1096
    }
1097
1098
    /**
1099
     * Get a class name without namespace string.
1100
     *
1101
     * @param object $instance Class
1102
     * 
1103
     * @return void
1104
     */
1105
    protected function getClassName($instance): string
1106
    {
1107
        $class = get_class($instance);
1108
        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...
1109
    }
1110
1111
    /**
1112
     * Respond the result.
1113
     *
1114
     * @return ResponseInterface
1115
     */
1116
    public function respond(): ResponseInterface
1117
    {
1118
        $response = get_response();
1119
        $type = '';
1120
1121
        if (self::RESPONSE_TEMPORARILY_DENY === $this->result) {
1122
            $type = 'captcha';
1123
            $statusCode = 403; // Forbidden.
1124
1125
        } elseif (self::RESPONSE_LIMIT_SESSION === $this->result) {
1126
            $type = 'session_limitation';
1127
            $statusCode = 429; // Too Many Requests.
1128
1129
        } elseif (self::RESPONSE_DENY === $this->result) {
1130
            $type = 'rejection';
1131
            $statusCode = 400; // Bad request.
1132
        }
1133
1134
        // Nothing happened. Return.
1135
        if (empty($type)) {
1136
            // @codeCoverageIgnoreStart
1137
            return $response;
1138
            // @codeCoverageIgnoreEnd
1139
        }
1140
1141
        $viewPath = $this->getTemplate($type);
1142
1143
        // The language of output UI. It is used on views.
1144
        $langCode = get_session()->get('shieldon_ui_lang') ?? 'en';
1145
        // Show online session count. It is used on views.
1146
        $showOnlineInformation = true;
1147
        // Show user information such as IP, user-agent, device name.
1148
        $showUserInformation = true;
1149
1150
        if (empty($this->properties['display_online_info'])) {
1151
            $showOnlineInformation = false;
1152
        }
1153
1154
        if (empty($this->properties['display_user_info'])) {
1155
            $showUserInformation = false;
1156
        }
1157
1158
        if ($showUserInformation) {
1159
            $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...
1160
            $dialoguserinfo['rdns'] = $this->rdns;
1161
            $dialoguserinfo['user_agent'] = get_request()->getHeaderLine('user-agent');
1162
        }
1163
1164
        $ui = [
1165
            'background_image' => $this->dialogUI['background_image'] ?? '',
1166
            'bg_color'         => $this->dialogUI['bg_color']         ?? '#ffffff',
1167
            'header_bg_color'  => $this->dialogUI['header_bg_color']  ?? '#212531',
1168
            'header_color'     => $this->dialogUI['header_color']     ?? '#ffffff',
1169
            'shadow_opacity'   => $this->dialogUI['shadow_opacity']   ?? '0.2',
1170
        ];
1171
1172
        if (!defined('SHIELDON_VIEW')) {
1173
            define('SHIELDON_VIEW', true);
1174
        }
1175
1176
        $css = require $this->getTemplate('css/default');
1177
1178
        ob_start();
1179
        require $viewPath;
1180
        $output = ob_get_contents();
1181
        ob_end_clean();
1182
1183
        // Remove unused variable notices generated from PHP intelephense.
1184
        unset(
1185
            $css,
1186
            $ui,
1187
            $langCode,
1188
            $showOnlineInformation,
1189
            $showLineupInformation,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $showLineupInformation seems to be never defined.
Loading history...
1190
            $showUserInformation
1191
        );
1192
1193
        $stream = $response->getBody();
1194
        $stream->write($output);
1195
        $stream->rewind();
1196
1197
        return $response->
1198
            withHeader('X-Protected-By', 'shieldon.io')->
1199
            withBody($stream)->
1200
            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...
1201
    }
1202
1203
    /**
1204
     * Run, run, run!
1205
     *
1206
     * Check the rule tables first, if an IP address has been listed.
1207
     * Call function filter() if an IP address is not listed in rule tables.
1208
     *
1209
     * @return 
1210
     */
1211
    public function run(): int
1212
    {
1213
        if (!isset($this->registrar[0])) {
1214
            throw new RuntimeException(
1215
                'Must register at least one data driver.'
1216
            );
1217
        }
1218
        
1219
        // Ignore the excluded urls.
1220
        if (!empty($this->excludedUrls)) {
1221
            foreach ($this->excludedUrls as $url) {
1222
                if (0 === strpos(get_request()->getUri()->getPath(), $url)) {
1223
                    return $this->result = self::RESPONSE_ALLOW;
1224
                }
1225
            }
1226
        }
1227
1228
        // Execute closure functions.
1229
        foreach ($this->closures as $closure) {
1230
            $closure();
1231
        }
1232
1233
        $result = $this->process();
1234
1235
        if ($result !== self::RESPONSE_ALLOW) {
1236
1237
            // Current session did not pass the CAPTCHA, it is still stuck in CAPTCHA page.
1238
            $actionCode = self::LOG_CAPTCHA;
1239
1240
            // If current session's respone code is RESPONSE_DENY, record it as `blacklist_count` in our logs.
1241
            // It is stuck in warning page, not CAPTCHA.
1242
            if ($result === self::RESPONSE_DENY) {
1243
                $actionCode = self::LOG_BLACKLIST;
1244
            }
1245
1246
            if ($result === self::RESPONSE_LIMIT_SESSION) {
1247
                $actionCode = self::LOG_LIMIT;
1248
            }
1249
1250
            $this->log($actionCode);
1251
1252
        } else {
1253
1254
            $this->log(self::LOG_PAGEVIEW);
1255
        }
1256
1257
 
1258
        if (!empty($this->msgBody)) {
1259
 
1260
            // @codeCoverageIgnoreStart
1261
1262
            try {
1263
                foreach ($this->messenger as $messenger) {
1264
                    $messenger->setTimeout(2);
1265
                    $messenger->send($this->msgBody);
1266
                }
1267
            } catch (RuntimeException $e) {
1268
                // Do not throw error, becasue the third-party services might be unavailable.
1269
            }
1270
1271
            // @codeCoverageIgnoreEnd
1272
        }
1273
1274
1275
        return $result;
1276
    }
1277
1278
    /**
1279
     * Set the filters.
1280
     *
1281
     * @param array $settings filter settings.
1282
     *
1283
     * @return void
1284
     */
1285
    public function setFilters(array $settings)
1286
    {
1287
        foreach (array_keys($this->filterStatus) as $k) {
1288
            if (isset($settings[$k])) {
1289
                $this->filterStatus[$k] = $settings[$k] ?? false;
1290
            }
1291
        }
1292
    }
1293
1294
    /**
1295
     * Set a filter.
1296
     *
1297
     * @param string $filterName The filter's name.
1298
     * @param bool   $value      True for enabling the filter, overwise.
1299
     *
1300
     * @return void
1301
     */
1302
    public function setFilter(string $filterName, bool $value): void
1303
    {
1304
        if (isset($this->filterStatus[$filterName])) {
1305
            $this->filterStatus[$filterName] = $value;
1306
        }
1307
    }
1308
1309
    /**
1310
     * Get online people count. If enable limitSession.
1311
     *
1312
     * @return int
1313
     */
1314
    public function getSessionCount(): int
1315
    {
1316
        return $this->sessionStatus['count'];
1317
    }
1318
1319
    /**
1320
     * Set the URLs you want them to be excluded them from protection.
1321
     *
1322
     * @param array $urls The list of URL want to be excluded.
1323
     *
1324
     * @return void
1325
     */
1326
    public function setExcludedUrls(array $urls = []): void
1327
    {
1328
        $this->excludedUrls = $urls;
1329
    }
1330
1331
    /**
1332
     * Set a closure function.
1333
     *
1334
     * @param string  $key     The name for the closure class.
1335
     * @param Closure $closure An instance will be later called.
1336
     *
1337
     * @return void
1338
     */
1339
    public function setClosure(string $key, Closure $closure): void
1340
    {
1341
        $this->closures[$key] = $closure;
1342
    }
1343
1344
    /**
1345
     * Print a JavasSript snippet in your webpages.
1346
     * 
1347
     * This snippet generate cookie on client's browser,then we check the 
1348
     * cookie to identify the client is a rebot or not.
1349
     *
1350
     * @return string
1351
     */
1352
    public function outputJsSnippet(): string
1353
    {
1354
        $tmpCookieName = $this->properties['cookie_name'];
1355
        $tmpCookieDomain = $this->properties['cookie_domain'];
1356
1357
        if (empty($tmpCookieDomain) && get_request()->getHeaderLine('host')) {
1358
            $tmpCookieDomain = get_request()->getHeaderLine('host');
1359
        }
1360
1361
        $tmpCookieValue = $this->properties['cookie_value'];
1362
1363
        $jsString = '
1364
            <script>
1365
                var d = new Date();
1366
                d.setTime(d.getTime()+(60*60*24*30));
1367
                document.cookie = "' . $tmpCookieName . '=' . $tmpCookieValue . ';domain=.' . $tmpCookieDomain . ';expires="+d.toUTCString();
1368
            </script>
1369
        ';
1370
1371
        return $jsString;
1372
    }
1373
1374
    /**
1375
     * Get current visior's path.
1376
     *
1377
     * @return string
1378
     */
1379
    public function getCurrentUrl(): string
1380
    {
1381
        return get_request()->getUri()->getPath();
1382
    }
1383
1384
    /**
1385
     * Displayed on Firewall Panel, tell you current what type of current
1386
     * configuration is used for.
1387
     * 
1388
     * @param string $type The type of configuration.
1389
     *                     demo | managed | config
1390
     *
1391
     * @return void
1392
     */
1393
    public function managedBy(string $type = ''): void
1394
    {
1395
        if (in_array($type, ['managed', 'config', 'demo'])) {
1396
            $this->firewallType = $type;
1397
        }
1398
    }
1399
}
1400