Passed
Push — 2.x ( 3c49c1...b272d3 )
by Terry
02:18
created

Kernel::enablePerformanceReport()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * php version 7.1.0
11
 *
12
 * @category  Web-security
13
 * @package   Shieldon
14
 * @author    Terry Lin <[email protected]>
15
 * @copyright 2019 terrylinooo
16
 * @license   https://github.com/terrylinooo/shieldon/blob/2.x/LICENSE MIT
17
 * @link      https://github.com/terrylinooo/shieldon
18
 * @see       https://shieldon.io
19
 */
20
21
declare(strict_types=1);
22
23
namespace Shieldon\Firewall;
24
25
use Psr\Http\Message\ResponseInterface;
26
use Psr\Http\Message\ServerRequestInterface;
27
use Shieldon\Firewall\Captcha\Foundation;
28
use Shieldon\Firewall\Helpers;
29
use Shieldon\Firewall\HttpFactory;
30
use Shieldon\Firewall\IpTrait;
31
use Shieldon\Firewall\Kernel\CaptchaTrait;
32
use Shieldon\Firewall\Kernel\ComponentTrait;
33
use Shieldon\Firewall\Kernel\DriverTrait;
34
use Shieldon\Firewall\Kernel\FilterTrait;
35
use Shieldon\Firewall\Kernel\MessengerTrait;
36
use Shieldon\Firewall\Kernel\RuleTrait;
37
use Shieldon\Firewall\Kernel\SessionTrait;
38
use Shieldon\Firewall\Kernel\TemplateTrait;
39
use Shieldon\Firewall\Log\ActionLogger;
40
use Shieldon\Firewall\Container;
41
use Shieldon\Event\Event;
42
use Closure;
43
use function Shieldon\Firewall\get_default_properties;
44
use function Shieldon\Firewall\get_request;
45
use function Shieldon\Firewall\get_session_instance;
46
use function array_push;
47
use function get_class;
48
use function gethostbyaddr;
49
use function ltrim;
50
use function strpos;
51
use function strrpos;
52
use function substr;
53
use function time;
54
55
/**
56
 * The primary Shiendon class.
57
 */
58
class Kernel
59
{
60
    /**
61
     *   Public methods       | Desctiotion
62
     *  ----------------------|---------------------------------------------
63
     *   ban                  | Ban an IP.
64
     *   getCurrentUrl        | Get current user's browsing path.
65
     *   managedBy            | Used on testing purpose.
66
     *   run                  | Run the checking process.
67
     *   setClosure           | Set a closure function.
68
     *   exclude              | Set a URL you want them excluded them from protection.
69
     *   setExcludedList      | Set the URLs you want them excluded them from protection.
70
     *   setLogger            | Set the action log logger.
71
     *   setProperties        | Set the property settings.
72
     *   setProperty          | Set a property setting.
73
     *   setStrict            | Strict mode apply to all components.
74
     *   unban                | Unban an IP.
75
     *  ----------------------|---------------------------------------------
76
     */
77
78
    /**
79
     *   Public methods       | Desctiotion
80
     *  ----------------------|---------------------------------------------
81
     *   setIp                | Ban an IP.
82
     *   getIp                | Get current user's browsing path.
83
     *   setRdns              | Print a JavaScript snippet in the pages.
84
     *   getRdns              | Used on testing purpose.
85
     *  ----------------------|---------------------------------------------
86
     */
87
    use CaptchaTrait;
88
89
    /**
90
     *   Public methods       | Desctiotion
91
     *  ----------------------|---------------------------------------------
92
     *   setComponent         | Set a commponent.
93
     *   getComponent         | Get a component instance from component's container.
94
     *   disableComponents    | Disable all components.
95
     *  ----------------------|---------------------------------------------
96
     */
97
    use ComponentTrait;
98
99
    /**
100
     *   Public methods       | Desctiotion
101
     *  ----------------------|---------------------------------------------
102
     *   setDriver            | Set a data driver.
103
     *   setChannel           | Set a data channel.
104
     *   disableDbBuilder     | disable creating data tables.
105
     *  ----------------------|---------------------------------------------
106
     */
107
    use DriverTrait;
108
109
    /**
110
     *   Public methods       | Desctiotion
111
     *  ----------------------|---------------------------------------------
112
     *   setFilters           | Set the filters.
113
     *   setFilter            | Set a filter.
114
     *   disableFilters       | Disable all filters.
115
     *  ----------------------|---------------------------------------------
116
     */
117
    use FilterTrait;
118
119
    /**
120
     *   Public methods       | Desctiotion
121
     *  ----------------------|---------------------------------------------
122
     *   setIp                | Set an IP address.
123
     *   getIp                | Get current set IP.
124
     *   setRdns              | Set a RDNS record for the check.
125
     *   getRdns              | Get IP resolved hostname.
126
     *  ----------------------|---------------------------------------------
127
     */
128
    use IpTrait;
129
130
    /**
131
     *   Public methods       | Desctiotion
132
     *  ----------------------|---------------------------------------------
133
     *   setMessenger         | Set a messenger
134
     *  ----------------------|---------------------------------------------
135
     */
136
    use MessengerTrait;
137
138
    /**
139
     *   Public methods       | Desctiotion
140
     *  ----------------------|---------------------------------------------
141
     *                        | No public methods.
142
     *  ----------------------|---------------------------------------------
143
     */
144
    use RuleTrait;
145
146
    /**
147
     *   Public methods       | Desctiotion
148
     *  ----------------------|---------------------------------------------
149
     *   limitSession         | Limit the amount of the online users.
150
     *   getSessionCount      | Get the amount of the sessions.
151
     *   removeSessionsByIp   | Remove sessions using the same IP address.
152
     *  ----------------------|---------------------------------------------
153
     */
154
    use SessionTrait;
155
156
    /**
157
     *   Public methods       | Desctiotion
158
     *  ----------------------|---------------------------------------------
159
     *   respond              | Respond the result.
160
     *   setTemplateDirectory | Set the frontend template directory.
161
     *   getJavascript        | Print a JavaScript snippet in the pages.
162
     *  ----------------------|---------------------------------------------
163
     */
164
    use TemplateTrait;
165
166
    /**
167
     * HTTP Status Codes
168
     */
169
    const HTTP_STATUS_OK                 = 200;
170
    const HTTP_STATUS_SEE_OTHER          = 303;
171
    const HTTP_STATUS_BAD_REQUEST        = 400;
172
    const HTTP_STATUS_FORBIDDEN          = 403;
173
    const HTTP_STATUS_TOO_MANY_REQUESTS  = 429;
174
175
    /**
176
     * Reason Codes (ALLOW)
177
     */
178
    const REASON_IS_SEARCH_ENGINE        = 100;
179
    const REASON_IS_GOOGLE               = 101;
180
    const REASON_IS_BING                 = 102;
181
    const REASON_IS_YAHOO                = 103;
182
    const REASON_IS_SOCIAL_NETWORK       = 110;
183
    const REASON_IS_FACEBOOK             = 111;
184
    const REASON_IS_TWITTER              = 112;
185
186
    /**
187
     * Reason Codes (DENY)
188
     */
189
    const REASON_TOO_MANY_SESSIONS       = 1;
190
    const REASON_TOO_MANY_ACCESSES       = 2; // (not used)
191
    const REASON_EMPTY_JS_COOKIE         = 3;
192
    const REASON_EMPTY_REFERER           = 4;
193
    const REASON_REACHED_LIMIT_DAY       = 11;
194
    const REASON_REACHED_LIMIT_HOUR      = 12;
195
    const REASON_REACHED_LIMIT_MINUTE    = 13;
196
    const REASON_REACHED_LIMIT_SECOND    = 14;
197
    const REASON_INVALID_IP              = 40;
198
    const REASON_DENY_IP                 = 41;
199
    const REASON_ALLOW_IP                = 42;
200
    const REASON_COMPONENT_IP            = 81;
201
    const REASON_COMPONENT_RDNS          = 82;
202
    const REASON_COMPONENT_HEADER        = 83;
203
    const REASON_COMPONENT_USERAGENT     = 84;
204
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
205
    const REASON_MANUAL_BAN              = 99;
206
207
    /**
208
     * Action Codes
209
     */
210
    const ACTION_DENY                    = 0;
211
    const ACTION_ALLOW                   = 1;
212
    const ACTION_TEMPORARILY_DENY        = 2;
213
    const ACTION_UNBAN                   = 9;
214
215
    /**
216
     * Result Codes
217
     */
218
    const RESPONSE_DENY                  = 0;
219
    const RESPONSE_ALLOW                 = 1;
220
    const RESPONSE_TEMPORARILY_DENY      = 2;
221
    const RESPONSE_LIMIT_SESSION         = 3;
222
223
    /**
224
     * Logger Codes
225
     */
226
    const LOG_LIMIT                      = 3;
227
    const LOG_PAGEVIEW                   = 11;
228
    const LOG_BLACKLIST                  = 98;
229
    const LOG_CAPTCHA                    = 99;
230
231
    const KERNEL_DIR = __DIR__;
232
233
    /**
234
     * The result passed from filters, compoents, etc.
235
     * 
236
     * DENY    : 0
237
     * ALLOW   : 1
238
     * CAPTCHA : 2
239
     *
240
     * @var int
241
     */
242
    protected $result = 1;
243
244
    /**
245
     * Default settings
246
     *
247
     * @var array
248
     */
249
    protected $properties = [];
250
251
    /**
252
     * Logger instance.
253
     *
254
     * @var ActionLogger
255
     */
256
    public $logger;
257
258
    /**
259
     * The closure functions that will be executed in this->run()
260
     *
261
     * @var array
262
     */
263
    protected $closures = [];
264
265
    /**
266
     * URLs that are excluded from Shieldon's protection.
267
     *
268
     * @var array
269
     */
270
    protected $excludedUrls = [];
271
272
    /**
273
     * Strict mode.
274
     * 
275
     * Set by `strictMode()` only. The default value of this propertry is undefined.
276
     *
277
     * @var bool|null
278
     */
279
    protected $strictMode;
280
281
    /**
282
     * Which type of configuration source that Shieldon firewall managed?
283
     * value: managed | config | self | demo
284
     *
285
     * @var string
286
     */
287
    protected $firewallType = 'self'; 
288
289
    /**
290
     * The session cookie will be created by the PSR-7 HTTP resolver.
291
     * If this option is false, created by PHP native function `setcookie`.
292
     *
293
     * @var bool
294
     */
295
    public $psr7 = true;
296
297
    /**
298
     * Shieldon constructor.
299
     *
300
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
301
     * @param ResponseInterface|null      $response A PSR-7 server response.
302
     *
303
     * @return void
304
     */
305
    public function __construct(?ServerRequestInterface $request = null, ?ResponseInterface $response = null)
306
    {
307
        // Load helper functions. This is the must and first.
308
        new Helpers();
309
310
        if (is_null($request)) {
311
            $request = HttpFactory::createRequest();
312
            $this->psr7 = false;
313
        }
314
315
        if (is_null($response)) {
316
            $response = HttpFactory::createResponse();
317
        }
318
319
        // Load default settings.
320
        $this->properties = get_default_properties();
321
322
        // Basic Captcha form.
323
        $this->setCaptcha(new Foundation());
324
325
        Container::set('request', $request);
326
        Container::set('response', $response);
327
        Container::set('shieldon', $this);
328
329
        Event::AddListener('set_session_driver', function($args) {
330
            $session = get_session_instance();
331
            $session->init(
332
                $args['driver'],
333
                $args['gc_expires'],
334
                $args['gc_probability'],
335
                $args['gc_divisor'],
336
                $args['psr7']
337
            );
338
            set_session_instance($session);
339
        });
340
    }
341
342
    /**
343
     * Run, run, run!
344
     *
345
     * Check the rule tables first, if an IP address has been listed.
346
     * Call function filter() if an IP address is not listed in rule tables.
347
     *
348
     * @return int
349
     */
350
    public function run(): int
351
    {
352
        $this->assertDriver();
353
354
        // Ignore the excluded urls.
355
        foreach ($this->excludedUrls as $url) {
356
            if (strpos($this->getCurrentUrl(), $url) === 0) {
357
                return $this->result = self::RESPONSE_ALLOW;
358
            }
359
        }
360
361
        // Execute closure functions.
362
        foreach ($this->closures as $closure) {
363
            $closure();
364
        }
365
366
        $result = $this->process();
367
368
        if ($result !== self::RESPONSE_ALLOW) {
369
370
            // Current session did not pass the CAPTCHA, it is still stuck in 
371
            // CAPTCHA page.
372
            $actionCode = self::LOG_CAPTCHA;
373
374
            // If current session's respone code is RESPONSE_DENY, record it as 
375
            // `blacklist_count` in our logs.
376
            // It is stuck in warning page, not CAPTCHA.
377
            if ($result === self::RESPONSE_DENY) {
378
                $actionCode = self::LOG_BLACKLIST;
379
            }
380
381
            if ($result === self::RESPONSE_LIMIT_SESSION) {
382
                $actionCode = self::LOG_LIMIT;
383
            }
384
385
            $this->log($actionCode);
386
387
        } else {
388
389
            $this->log(self::LOG_PAGEVIEW);
390
        }
391
392
        // @ MessengerTrait
393
        $this->triggerMessengers();
394
395
        /**
396
         * Hook - kernel_end
397
         */
398
        Event::doDispatch('kernel_end');
399
400
        return $result;
401
    }
402
403
    /**
404
     * Display the performance report as showing dialogs.
405
     *
406
     * @return void
407
     */
408
    public function enablePerformanceReport(): void
409
    {
410
        $this->performanceCheck = true;
0 ignored issues
show
Bug Best Practice introduced by
The property performanceCheck does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
411
    }
412
413
    /**
414
     * Ban an IP.
415
     *
416
     * @param string $ip A valid IP address.
417
     *
418
     * @return void
419
     */
420
    public function ban(string $ip = ''): void
421
    {
422
        if ('' === $ip) {
423
            $ip = $this->ip;
424
        }
425
 
426
        $this->action(
427
            self::ACTION_DENY,
428
            self::REASON_MANUAL_BAN,
429
            $ip
430
        );
431
    }
432
433
    /**
434
     * Unban an IP.
435
     *
436
     * @param string $ip A valid IP address.
437
     *
438
     * @return void
439
     */
440
    public function unban(string $ip = ''): void
441
    {
442
        if ($ip === '') {
443
            $ip = $this->ip;
444
        }
445
446
        $this->action(
447
            self::ACTION_UNBAN,
448
            self::REASON_MANUAL_BAN,
449
            $ip
450
        );
451
        $this->log(self::ACTION_UNBAN);
452
453
        $this->result = self::RESPONSE_ALLOW;
454
    }
455
456
    /**
457
     * Set a property setting.
458
     *
459
     * @param string $key   The key of a property setting.
460
     * @param mixed  $value The value of a property setting.
461
     *
462
     * @return void
463
     */
464
    public function setProperty(string $key = '', $value = '')
465
    {
466
        if (isset($this->properties[$key])) {
467
            $this->properties[$key] = $value;
468
        }
469
    }
470
471
    /**
472
     * Set the property settings.
473
     * 
474
     * @param array $settings The settings.
475
     *
476
     * @return void
477
     */
478
    public function setProperties(array $settings): void
479
    {
480
        foreach (array_keys($this->properties) as $k) {
481
            if (isset($settings[$k])) {
482
                $this->properties[$k] = $settings[$k];
483
            }
484
        }
485
    }
486
487
    /**
488
     * Strict mode.
489
     * This option will take effects to all components.
490
     * 
491
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
492
     *
493
     * @return void
494
     */
495
    public function setStrict(bool $bool)
496
    {
497
        $this->strictMode = $bool;
498
    }
499
500
    /**
501
     * Set an action log logger.
502
     *
503
     * @param ActionLogger $logger Record action logs for users.
504
     *
505
     * @return void
506
     */
507
    public function setLogger(ActionLogger $logger): void
508
    {
509
        $this->logger = $logger;
510
    }
511
512
    /**
513
     * Add a path into the excluded list.
514
     *
515
     * @param string $uriPath The path component of a URI.
516
     * 
517
     * @return void
518
     */
519
    public function exclude(string $uriPath): void
520
    {
521
        $uriPath = '/' . ltrim($uriPath, '/');
522
523
        array_push($this->excludedUrls, $uriPath);
524
    }
525
526
    /**
527
     * Set the URLs you want them excluded them from protection.
528
     *
529
     * @param array $urls The list of URL want to be excluded.
530
     *
531
     * @return void
532
     */
533
    public function setExcludedList(array $urls = []): void
534
    {
535
        $this->excludedUrls = $urls;
536
    }
537
538
    /**
539
     * Set a closure function.
540
     *
541
     * @param string  $key     The name for the closure class.
542
     * @param Closure $closure An instance will be later called.
543
     *
544
     * @return void
545
     */
546
    public function setClosure(string $key, Closure $closure): void
547
    {
548
        $this->closures[$key] = $closure;
549
    }
550
551
    /**
552
     * Get current visior's path.
553
     *
554
     * @return string
555
     */
556
    public function getCurrentUrl(): string
557
    {
558
        return get_request()->getUri()->getPath();
559
    }
560
561
    /**
562
     * Displayed on Firewall Panel, telling you current what type of 
563
     * configuration is used.
564
     * 
565
     * @param string $type The type of configuration.
566
     *                     accepted value: demo | managed | config
567
     *
568
     * @return void
569
     */
570
    public function managedBy(string $type = ''): void
571
    {
572
        if (in_array($type, ['managed', 'config', 'demo'])) {
573
            $this->firewallType = $type;
574
        }
575
    }
576
577
    /*
578
    |-------------------------------------------------------------------
579
    | Non-public methods.
580
    |-------------------------------------------------------------------
581
    */
582
583
    /**
584
     * Run, run, run!
585
     *
586
     * Check the rule tables first, if an IP address has been listed.
587
     * Call function filter() if an IP address is not listed in rule tables.
588
     *
589
     * @return int The response code.
590
     */
591
    protected function process(): int
592
    {
593
        $this->initComponents();
594
595
        $processMethods = [
596
            'isRuleExist',   // Stage 1 - Looking for rule table.
597
            'isTrustedBot',  // Stage 2 - Detect popular search engine.
598
            'isFakeRobot',   // Stage 3 - Reject fake search engine crawlers.
599
            'isIpComponent', // Stage 4 - IP manager.
600
            'isComponents'   // Stage 5 - Check other components.
601
        ];
602
603
        foreach ($processMethods as $method) {
604
            if ($this->{$method}()) {
605
                return $this->result;
606
            }
607
        }
608
609
        // Stage 6 - Check filters if set.
610
        if (array_search(true, $this->filterStatus)) {
611
            return $this->result = $this->sessionHandler($this->filter());
612
        }
613
614
        // Stage 7 - Go into session limit check.
615
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
616
    }
617
618
    /**
619
     * Start an action for this IP address, allow or deny, and give a reason for it.
620
     *
621
     * @param int    $actionCode The action code. - 0: deny, 1: allow, 9: unban.
622
     * @param string $reasonCode The response code.
623
     * @param string $assignIp   The IP address.
624
     * 
625
     * @return void
626
     */
627
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
628
    {
629
        $ip = $this->ip;
630
        $rdns = $this->rdns;
631
        $now = time();
632
        $logData = [];
633
    
634
        if ('' !== $assignIp) {
635
            $ip = $assignIp;
636
            $rdns = gethostbyaddr($ip);
637
        }
638
639
        if ($actionCode === self::ACTION_UNBAN) {
640
            $this->driver->delete($ip, 'rule');
641
        } else {
642
            $logData['log_ip']     = $ip;
643
            $logData['ip_resolve'] = $rdns;
644
            $logData['time']       = $now;
645
            $logData['type']       = $actionCode;
646
            $logData['reason']     = $reasonCode;
647
            $logData['attempts']   = 0;
648
649
            $this->driver->save($ip, $logData, 'rule');
650
        }
651
652
        // Remove logs for this IP address because It already has it's own rule on system.
653
        // No need to count for it anymore.
654
        $this->driver->delete($ip, 'filter');
655
656
        $this->removeSessionsByIp($ip);
657
658
        // Log this action.
659
        $this->log($actionCode, $ip);
660
661
        $this->reason = $reasonCode;
662
    }
663
664
    /**
665
     * Log actions.
666
     *
667
     * @param int    $actionCode The code number of the action.
668
     * @param string $ip         The IP address.
669
     *
670
     * @return void
671
     */
672
    protected function log(int $actionCode, $ip = ''): void
673
    {
674
        if (!$this->logger) {
675
            return;
676
        }
677
678
        $logData = [];
679
 
680
        $logData['ip'] = $ip ?: $this->getIp();
681
        $logData['session_id'] = get_session_instance()->getId();
682
        $logData['action_code'] = $actionCode;
683
        $logData['timesamp'] = time();
684
685
        $this->logger->add($logData);
686
    }
687
688
    /**
689
     * Get a class name without namespace string.
690
     *
691
     * @param object $instance Class
692
     * 
693
     * @return string
694
     */
695
    protected function getClassName($instance): string
696
    {
697
        $class = get_class($instance);
698
        return substr($class, strrpos($class, '\\') + 1); 
699
    }
700
701
    /**
702
     * Save and return the result identifier.
703
     * This method is for passing value from traits.
704
     *
705
     * @param int $resultCode The result identifier.
706
     *
707
     * @return int
708
     */
709
    protected function setResultCode(int $resultCode): int
710
    {
711
        return $this->result = $resultCode;
712
    }
713
}
714