Passed
Push — 2.x ( 43bb91...be385f )
by Terry
02:03
created

Kernel::setSessionId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

344
                if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isGoogle()) {
Loading history...
345
                    // Add current IP into allowed list, because it is from real Google domain.
346
                    $this->action(
347
                        self::ACTION_ALLOW,
348
                        self::REASON_IS_GOOGLE
349
                    );
350
351
                } 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

351
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isBing()) {
Loading history...
352
                    // Add current IP into allowed list, because it is from real Bing domain.
353
                    $this->action(
354
                        self::ACTION_ALLOW,
355
                        self::REASON_IS_BING
356
                    );
357
358
                } 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

358
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isYahoo()) {
Loading history...
359
                    // Add current IP into allowed list, because it is from real Yahoo domain.
360
                    $this->action(
361
                        self::ACTION_ALLOW,
362
                        self::REASON_IS_YAHOO
363
                    );
364
365
                } else {
366
                    // Add current IP into allowed list, because you trust it.
367
                    // You have already defined it in the settings.
368
                    $this->action(
369
                        self::ACTION_ALLOW,
370
                        self::REASON_IS_SEARCH_ENGINE
371
                    );
372
                }
373
                // Allowed robots not join to our traffic handler.
374
                $this->result = self::RESPONSE_ALLOW;
375
                return true;
376
            }
377
        }
378
        return false;
379
    }
380
381
    /**
382
     * Check whether the IP is fake search engine or not.
383
     * The method "isTrustedBot()" must be executed before this method.
384
     *
385
     * @return bool
386
     */
387
    private function isFakeRobot(): bool
388
    {
389
        if ($this->getComponent('TrustedBot')) {
390
            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

390
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isFakeRobot()) {
Loading history...
391
                $this->action(
392
                    self::ACTION_DENY,
393
                    self::REASON_COMPONENT_TRUSTED_ROBOT
394
                );
395
                $this->result = self::RESPONSE_DENY;
396
                return true;
397
            }
398
        }
399
        return false;
400
    }
401
402
    /**
403
     * Run, run, run!
404
     *
405
     * Check the rule tables first, if an IP address has been listed.
406
     * Call function filter() if an IP address is not listed in rule tables.
407
     *
408
     * @return int The response code.
409
     */
410
    protected function process(): int
411
    {
412
        $this->driver->init($this->autoCreateDatabase);
413
414
        $this->initComponents();
415
416
        /*
417
        |--------------------------------------------------------------------------
418
        | Stage - Looking for rule table.
419
        |--------------------------------------------------------------------------
420
        */
421
422
        if ($this->DoesRuleExist()) {
423
            return $this->result;
424
        }
425
426
        /*
427
        |--------------------------------------------------------------------------
428
        | Statge - Detect popular search engine.
429
        |--------------------------------------------------------------------------
430
        */
431
432
        if ($this->isTrustedBot()) {
433
            return $this->result;
434
        }
435
436
        if ($this->isFakeRobot()) {
437
            return $this->result;
438
        }
439
        
440
        /*
441
        |--------------------------------------------------------------------------
442
        | Stage - IP component.
443
        |--------------------------------------------------------------------------
444
        */
445
446
        if ($this->getComponent('Ip')) {
447
448
            $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

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