Passed
Push — 2.x ( b3a28e...4bb95a )
by Terry
01:45
created

Kernel::managedBy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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