Kernel::exclude()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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\Kernel\Enum;
40
use Shieldon\Firewall\Log\ActionLogger;
41
use Shieldon\Firewall\Container;
42
use Shieldon\Event\Event;
43
use Closure;
44
use function Shieldon\Firewall\get_default_properties;
45
use function Shieldon\Firewall\get_request;
46
use function Shieldon\Firewall\get_session_instance;
47
use function array_push;
48
use function get_class;
49
use function gethostbyaddr;
50
use function ltrim;
51
use function strpos;
52
use function strrpos;
53
use function substr;
54
use function time;
55
56
/**
57
 * The primary Shiendon class.
58
 */
59
class Kernel
60
{
61
    /**
62
     *   Public methods       | Desctiotion
63
     *  ----------------------|---------------------------------------------
64
     *   ban                  | Ban an IP.
65
     *   getCurrentUrl        | Get current user's browsing path.
66
     *   managedBy            | Used on testing purpose.
67
     *   run                  | Run the checking process.
68
     *   setClosure           | Set a closure function.
69
     *   exclude              | Set a URL you want them excluded them from protection.
70
     *   setExcludedList      | Set the URLs you want them excluded them from protection.
71
     *   setLogger            | Set the action log logger.
72
     *   setProperties        | Set the property settings.
73
     *   setProperty          | Set a property setting.
74
     *   setStrict            | Strict mode apply to all components.
75
     *   unban                | Unban an IP.
76
     *  ----------------------|---------------------------------------------
77
     */
78
79
    /**
80
     *   Public methods       | Desctiotion
81
     *  ----------------------|---------------------------------------------
82
     *   setCaptcha           | Set a captcha.
83
     *   captchaResponse      | Return the result from Captchas.
84
     *   disableCaptcha       | Mostly be used in unit 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
     *  ----------------------|---------------------------------------------
152
     */
153
    use SessionTrait;
154
155
    /**
156
     *   Public methods       | Desctiotion
157
     *  ----------------------|---------------------------------------------
158
     *   setDialog            | Set the dialog UI.
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
     * The result passed from filters, compoents, etc.
168
     *
169
     * DENY    : 0
170
     * ALLOW   : 1
171
     * CAPTCHA : 2
172
     *
173
     * @var int
174
     */
175
    protected $result = 1;
176
177
    /**
178
     * Default settings
179
     *
180
     * @var array
181
     */
182
    protected $properties = [];
183
184
    /**
185
     * Logger instance.
186
     *
187
     * @var ActionLogger
188
     */
189
    public $logger;
190
191
    /**
192
     * The closure functions that will be executed in this->run()
193
     *
194
     * @var array
195
     */
196
    protected $closures = [];
197
198
    /**
199
     * URLs that are excluded from Shieldon's protection.
200
     *
201
     * @var array
202
     */
203
    protected $excludedUrls = [];
204
205
    /**
206
     * Strict mode.
207
     *
208
     * Set by `strictMode()` only. The default value of this propertry is undefined.
209
     *
210
     * @var bool|null
211
     */
212
    protected $strictMode;
213
214
    /**
215
     * Which type of configuration source that Shieldon firewall managed?
216
     * value: managed | config | self | demo
217
     *
218
     * @var string
219
     */
220
    protected $firewallType = 'self';
221
222
    /**
223
     * The reason code of a user to be allowed or denied.
224
     *
225
     * @var int|null
226
     */
227
    protected $reason;
228
229
    /**
230
     * The session cookie will be created by the PSR-7 HTTP resolver.
231
     * If this option is false, created by PHP native function `setcookie`.
232
     *
233
     * @var bool
234
     */
235
    public $psr7 = true;
236
237
    /**
238
     * Shieldon constructor.
239
     *
240
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
241
     * @param ResponseInterface|null      $response A PSR-7 server response.
242
     *
243
     * @return void
244
     */
245
    public function __construct(?ServerRequestInterface $request = null, ?ResponseInterface $response = null)
246
    {
247
        // Load helper functions. This is the must and first.
248
        new Helpers();
249
250
        if (is_null($request)) {
251
            $request = HttpFactory::createRequest();
252
            $this->psr7 = false;
253
        }
254
255
        if (is_null($response)) {
256
            $response = HttpFactory::createResponse();
257
        }
258
259
        // Load default settings.
260
        $this->properties = get_default_properties();
261
262
        // Basic form for Captcha.
263
        $this->setCaptcha(new Foundation());
264
265
        Container::set('request', $request);
266
        Container::set('response', $response);
267
        Container::set('shieldon', $this);
268
269
        Event::AddListener(
270
            'set_session_driver',
271
            function ($args) {
272
                $session = get_session_instance();
273
                $session->init(
274
                    $args['driver'],
275
                    $args['gc_expires'],
276
                    $args['gc_probability'],
277
                    $args['gc_divisor'],
278
                    $args['psr7']
279
                );
280
281
                /**
282
                 * Hook - session_init
283
                 */
284
                Event::doDispatch('session_init');
285
                set_session_instance($session);
286
            }
287
        );
288
    }
289
290
    /**
291
     * Run, run, run!
292
     *
293
     * Check the rule tables first, if an IP address has been listed.
294
     * Call function filter() if an IP address is not listed in rule tables.
295
     *
296
     * @return int
297
     */
298
    public function run(): int
299
    {
300
        $this->assertDriver();
301
302
        // Ignore the excluded urls.
303
        foreach ($this->excludedUrls as $url) {
304
            if (strpos($this->getCurrentUrl(), $url) === 0) {
305
                return $this->result = Enum::RESPONSE_ALLOW;
306
            }
307
        }
308
309
        // Execute closure functions.
310
        foreach ($this->closures as $closure) {
311 202
            $closure();
312
        }
313
314 202
        $result = $this->process();
315
316 202
        if ($result !== Enum::RESPONSE_ALLOW) {
317 202
            // Current session did not pass the CAPTCHA, it is still stuck in
318 202
            // CAPTCHA page.
319
            $actionCode = Enum::LOG_CAPTCHA;
320
321 202
            // If current session's respone code is RESPONSE_DENY, record it as
322 202
            // `blacklist_count` in our logs.
323
            // It is stuck in warning page, not CAPTCHA.
324
            if ($result === Enum::RESPONSE_DENY) {
325
                $actionCode = Enum::LOG_BLACKLIST;
326 202
            }
327
328
            if ($result === Enum::RESPONSE_LIMIT_SESSION) {
329 202
                $actionCode = Enum::LOG_LIMIT;
330
            }
331 202
332 202
            $this->log($actionCode);
333 202
        } else {
334
            $this->log(Enum::LOG_PAGEVIEW);
335 202
        }
336 150
337
        // @ MessengerTrait
338 150
        $this->triggerMessengers();
339 150
340 150
        /**
341 150
         * Hook - kernel_end
342 150
         */
343 150
        Event::doDispatch('kernel_end');
344 150
345
        return $result;
346
    }
347
348
    /**
349 150
     * Ban an IP.
350
     *
351 150
     * @param string $ip A valid IP address.
352 202
     *
353
     * @return void
354
     */
355
    public function ban(string $ip = ''): void
356
    {
357
        if ('' === $ip) {
358
            $ip = $this->ip;
359
        }
360
 
361
        $this->action(
362
            Enum::ACTION_DENY,
363 72
            Enum::REASON_MANUAL_BAN_DENIED,
364
            $ip
365 72
        );
366
    }
367
368 70
    /**
369 11
     * Unban an IP.
370 11
     *
371
     * @param string $ip A valid IP address.
372
     *
373
     * @return void
374
     */
375 69
    public function unban(string $ip = ''): void
376 10
    {
377
        if ($ip === '') {
378
            $ip = $this->ip;
379 69
        }
380
381 69
        $this->action(
382
            Enum::ACTION_UNBAN,
383
            Enum::REASON_MANUAL_BAN_DENIED,
384
            $ip
385 44
        );
386
        $this->log(Enum::ACTION_UNBAN);
387
388
        $this->result = Enum::RESPONSE_ALLOW;
389
    }
390 44
391 17
    /**
392
     * Set a property setting.
393
     *
394 44
     * @param string $key   The key of a property setting.
395 5
     * @param mixed  $value The value of a property setting.
396
     *
397
     * @return void
398 44
     */
399
    public function setProperty(string $key = '', $value = ''): void
400
    {
401
        if (isset($this->properties[$key])) {
402 67
            $this->properties[$key] = $value;
403
        }
404
    }
405
406 69
    /**
407
     * Set the property settings.
408
     *
409
     * @param array $settings The settings.
410
     *
411 69
     * @return void
412
     */
413 69
    public function setProperties(array $settings): void
414
    {
415
        foreach (array_keys($this->properties) as $k) {
416
            if (isset($settings[$k])) {
417
                $this->properties[$k] = $settings[$k];
418
            }
419
        }
420
    }
421
422
    /**
423 9
     * Strict mode.
424
     * This option will take effects to all components.
425 9
     *
426 4
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
427
     *
428
     * @return void
429 9
     */
430 9
    public function setStrict(bool $bool): void
431 9
    {
432 9
        $this->strictMode = $bool;
433 9
    }
434
435
    /**
436
     * Set an action log logger.
437
     *
438
     * @param ActionLogger $logger Record action logs for users.
439
     *
440
     * @return void
441
     */
442
    public function setLogger(ActionLogger $logger): void
443 9
    {
444
        $this->logger = $logger;
445 9
    }
446 5
447
    /**
448
     * Add a path into the excluded list.
449 9
     *
450 9
     * @param string $uriPath The path component of a URI.
451 9
     *
452 9
     * @return void
453 9
     */
454 9
    public function exclude(string $uriPath): void
455
    {
456 9
        $uriPath = '/' . ltrim($uriPath, '/');
457
458
        array_push($this->excludedUrls, $uriPath);
459
    }
460
461
    /**
462
     * Set the URLs you want them excluded them from protection.
463
     *
464
     * @param array $urls The list of URL want to be excluded.
465
     *
466
     * @return void
467 106
     */
468
    public function setExcludedList(array $urls = []): void
469 106
    {
470 106
        $this->excludedUrls = $urls;
471
    }
472
473
    /**
474
     * Set a closure function.
475
     *
476
     * @param string  $key     The name for the closure class.
477
     * @param Closure $closure An instance will be later called.
478
     *
479
     * @return void
480
     */
481 1
    public function setClosure(string $key, Closure $closure): void
482
    {
483 1
        $this->closures[$key] = $closure;
484 1
    }
485 1
486
    /**
487
     * Get current visior's path.
488
     *
489
     * @return string
490
     */
491
    public function getCurrentUrl(): string
492
    {
493
        return get_request()->getUri()->getPath();
494
    }
495
496
    /**
497
     * Displayed on Firewall Panel, telling you current what type of
498 12
     * configuration is used.
499
     *
500 12
     * @param string $type The type of configuration.
501
     *                     accepted value: demo | managed | config
502
     *
503
     * @return void
504
     */
505
    public function managedBy(string $type = ''): void
506
    {
507
        if (in_array($type, ['managed', 'config', 'demo'])) {
508
            $this->firewallType = $type;
509
        }
510 83
    }
511
512 83
    /*
513
    |-------------------------------------------------------------------
514
    | Non-public methods.
515
    |-------------------------------------------------------------------
516
    */
517
518
    /**
519
     * Run, run, run!
520
     *
521
     * Check the rule tables first, if an IP address has been listed.
522 3
     * Call function filter() if an IP address is not listed in rule tables.
523
     *
524 3
     * @return int The response code.
525
     */
526 3
    protected function process(): int
527
    {
528
        $this->initComponents();
529
530
        $processMethods = [
531
            'isRuleExist',   // Stage 1 - Looking for rule table.
532
            'isTrustedBot',  // Stage 2 - Detect popular search engine.
533
            'isFakeRobot',   // Stage 3 - Reject fake search engine crawlers.
534
            'isIpComponent', // Stage 4 - IP manager.
535
            'isComponents',  // Stage 5 - Check other components.
536 85
        ];
537
538 85
        foreach ($processMethods as $method) {
539
            if ($this->{$method}()) {
540
                return $this->result;
541
            }
542
        }
543
544
        // Stage 6 - Check filters if set.
545
        if (array_search(true, $this->filterStatus)) {
546
            return $this->result = $this->sessionHandler($this->filter());
547
        }
548
549 84
        // Stage 7 - Go into session limit check.
550
        return $this->result = $this->sessionHandler(Enum::RESPONSE_ALLOW);
551 84
    }
552
553
    /**
554
     * Start an action for this IP address, allow or deny, and give a reason for it.
555
     *
556
     * @param int    $actionCode The action code. - 0: deny, 1: allow, 9: unban.
557
     * @param string $reasonCode The response code.
558
     * @param string $assignIp   The IP address.
559 85
     *
560
     * @return void
561 85
     */
562
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
563
    {
564
        $ip = $this->ip;
565
        $rdns = $this->rdns;
566
        $now = time();
567
        $logData = [];
568
    
569
        if ('' !== $assignIp) {
570
            $ip = $assignIp;
571
            $rdns = gethostbyaddr($ip);
572
        }
573 87
574
        if ($actionCode === Enum::ACTION_UNBAN) {
575 87
            $this->driver->delete($ip, 'rule');
576 87
        } else {
577
            $logData['log_ip']     = $ip;
578
            $logData['ip_resolve'] = $rdns;
579
            $logData['time']       = $now;
580
            $logData['type']       = $actionCode;
581
            $logData['reason']     = $reasonCode;
582
            $logData['attempts']   = 0;
583
584
            $this->driver->save($ip, $logData, 'rule');
585
        }
586
587
        // Remove logs for this IP address because It already has it's own rule on system.
588
        // No need to count for it anymore.
589
        $this->driver->delete($ip, 'filter');
590
591
        $this->removeSessionsByIp($ip);
592
593
        // Log this action.
594 69
        $this->log($actionCode, $ip);
595
596 69
        $this->reason = $reasonCode;
597
    }
598 69
599 69
    /**
600 69
     * Log actions.
601 69
     *
602 69
     * @param int    $actionCode The code number of the action.
603 69
     * @param string $ip         The IP address.
604 69
     *
605
     * @return void
606 69
     */
607 69
    protected function log(int $actionCode, $ip = ''): void
608 69
    {
609
        if (!$this->logger) {
610
            return;
611
        }
612
613 66
        $logData = [];
614 54
 
615
        $logData['ip'] = $ip ?: $this->getIp();
616
        $logData['session_id'] = get_session_instance()->getId();
617
        $logData['action_code'] = $actionCode;
618 12
        $logData['timestamp'] = time();
619
620
        $this->logger->add($logData);
621
    }
622
623
    /**
624
     * Get a class name without namespace string.
625
     *
626
     * @param object $instance Class
627
     *
628
     * @return string
629
     */
630 40
    protected function getClassName($instance): string
631
    {
632 40
        $class = get_class($instance);
633 40
        return substr($class, strrpos($class, '\\') + 1);
634 40
    }
635 40
636
    /**
637 40
     * Save and return the result identifier.
638 10
     * This method is for passing value from traits.
639 10
     *
640
     * @param int $resultCode The result identifier.
641
     *
642 40
     * @return int
643 13
     */
644
    protected function setResultCode(int $resultCode): int
645 40
    {
646 40
        return $this->result = $resultCode;
647 40
    }
648
}
649