1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <contact@terryl.in>
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 <contact@terryl.in>
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
     *   setCaptcha           | Set a captcha.
82
     *   captchaResponse      | Return the result from Captchas.
83
     *   disableCaptcha       | Mostly be used in unit testing purpose.
84
     *  ----------------------|---------------------------------------------
85
     */
86
    use CaptchaTrait;
87

88
    /**
89
     *   Public methods       | Desctiotion
90
     *  ----------------------|---------------------------------------------
91
     *   setComponent         | Set a commponent.
92
     *   getComponent         | Get a component instance from component's container.
93
     *   disableComponents    | Disable all components.
94
     *  ----------------------|---------------------------------------------
95
     */
96
    use ComponentTrait;
97

98
    /**
99
     *   Public methods       | Desctiotion
100
     *  ----------------------|---------------------------------------------
101
     *   setDriver            | Set a data driver.
102
     *   setChannel           | Set a data channel.
103
     *   disableDbBuilder     | disable creating data tables.
104
     *  ----------------------|---------------------------------------------
105
     */
106
    use DriverTrait;
107

108
    /**
109
     *   Public methods       | Desctiotion
110
     *  ----------------------|---------------------------------------------
111
     *   setFilters           | Set the filters.
112
     *   setFilter            | Set a filter.
113
     *   disableFilters       | Disable all filters.
114
     *  ----------------------|---------------------------------------------
115
     */
116
    use FilterTrait;
117

118
    /**
119
     *   Public methods       | Desctiotion
120
     *  ----------------------|---------------------------------------------
121
     *   setIp                | Set an IP address.
122
     *   getIp                | Get current set IP.
123
     *   setRdns              | Set a RDNS record for the check.
124
     *   getRdns              | Get IP resolved hostname.
125
     *  ----------------------|---------------------------------------------
126
     */
127
    use IpTrait;
128

129
    /**
130
     *   Public methods       | Desctiotion
131
     *  ----------------------|---------------------------------------------
132
     *   setMessenger         | Set a messenger
133
     *  ----------------------|---------------------------------------------
134
     */
135
    use MessengerTrait;
136

137
    /**
138
     *   Public methods       | Desctiotion
139
     *  ----------------------|---------------------------------------------
140
     *                        | No public methods.
141
     *  ----------------------|---------------------------------------------
142
     */
143
    use RuleTrait;
144

145
    /**
146
     *   Public methods       | Desctiotion
147
     *  ----------------------|---------------------------------------------
148
     *   limitSession         | Limit the amount of the online users.
149
     *   getSessionCount      | Get the amount of the sessions.
150
     *  ----------------------|---------------------------------------------
151
     */
152
    use SessionTrait;
153

154
    /**
155
     *   Public methods       | Desctiotion
156
     *  ----------------------|---------------------------------------------
157
     *   setDialog            | Set the dialog UI.
158
     *   respond              | Respond the result.
159
     *   setTemplateDirectory | Set the frontend template directory.
160
     *   getJavascript        | Print a JavaScript snippet in the pages.
161
     *  ----------------------|---------------------------------------------
162
     */
163
    use TemplateTrait;
164

165
    /**
166
     * HTTP Status Codes
167
     */
168
    const HTTP_STATUS_OK                 = 200;
169
    const HTTP_STATUS_SEE_OTHER          = 303;
170
    const HTTP_STATUS_BAD_REQUEST        = 400;
171
    const HTTP_STATUS_FORBIDDEN          = 403;
172
    const HTTP_STATUS_TOO_MANY_REQUESTS  = 429;
173

174
    /**
175
     * Reason Codes (ALLOW)
176
     */
177
    const REASON_IS_SEARCH_ENGINE        = 100;
178
    const REASON_IS_GOOGLE               = 101;
179
    const REASON_IS_BING                 = 102;
180
    const REASON_IS_YAHOO                = 103;
181
    const REASON_IS_SOCIAL_NETWORK       = 110;
182
    const REASON_IS_FACEBOOK             = 111;
183
    const REASON_IS_TWITTER              = 112;
184

185
    /**
186
     * Reason Codes (DENY)
187
     */
188
    const REASON_TOO_MANY_SESSIONS       = 1;
189
    const REASON_TOO_MANY_ACCESSES       = 2; // (not used)
190
    const REASON_EMPTY_JS_COOKIE         = 3;
191
    const REASON_EMPTY_REFERER           = 4;
192
    const REASON_REACHED_LIMIT_DAY       = 11;
193
    const REASON_REACHED_LIMIT_HOUR      = 12;
194
    const REASON_REACHED_LIMIT_MINUTE    = 13;
195
    const REASON_REACHED_LIMIT_SECOND    = 14;
196
    const REASON_INVALID_IP              = 40;
197
    const REASON_DENY_IP                 = 41;
198
    const REASON_ALLOW_IP                = 42;
199
    const REASON_COMPONENT_IP            = 81;
200
    const REASON_COMPONENT_RDNS          = 82;
201
    const REASON_COMPONENT_HEADER        = 83;
202
    const REASON_COMPONENT_USERAGENT     = 84;
203
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
204
    const REASON_MANUAL_BAN              = 99;
205

206
    /**
207
     * Action Codes
208
     */
209
    const ACTION_DENY                    = 0;
210
    const ACTION_ALLOW                   = 1;
211
    const ACTION_TEMPORARILY_DENY        = 2;
212
    const ACTION_UNBAN                   = 9;
213

214
    /**
215
     * Result Codes
216
     */
217
    const RESPONSE_DENY                  = 0;
218
    const RESPONSE_ALLOW                 = 1;
219
    const RESPONSE_TEMPORARILY_DENY      = 2;
220
    const RESPONSE_LIMIT_SESSION         = 3;
221

222
    /**
223
     * Logger Codes
224
     */
225
    const LOG_LIMIT                      = 3;
226
    const LOG_PAGEVIEW                   = 11;
227
    const LOG_BLACKLIST                  = 98;
228
    const LOG_CAPTCHA                    = 99;
229

230
    const KERNEL_DIR = __DIR__;
231

232
    /**
233
     * The result passed from filters, compoents, etc.
234
     * 
235
     * DENY    : 0
236
     * ALLOW   : 1
237
     * CAPTCHA : 2
238
     *
239
     * @var int
240
     */
241
    protected $result = 1;
242

243
    /**
244
     * Default settings
245
     *
246
     * @var array
247
     */
248
    protected $properties = [];
249

250
    /**
251
     * Logger instance.
252
     *
253
     * @var ActionLogger
254
     */
255
    public $logger;
256

257
    /**
258
     * The closure functions that will be executed in this->run()
259
     *
260
     * @var array
261
     */
262
    protected $closures = [];
263

264
    /**
265
     * URLs that are excluded from Shieldon's protection.
266
     *
267
     * @var array
268
     */
269
    protected $excludedUrls = [];
270

271
    /**
272
     * Strict mode.
273
     * 
274
     * Set by `strictMode()` only. The default value of this propertry is undefined.
275
     *
276
     * @var bool|null
277
     */
278
    protected $strictMode;
279

280
    /**
281
     * Which type of configuration source that Shieldon firewall managed?
282
     * value: managed | config | self | demo
283
     *
284
     * @var string
285
     */
286
    protected $firewallType = 'self';
287

288
   /**
289
     * The reason code of a user to be allowed or denied.
290
     *
291
     * @var int|null
292
     */
293
    protected $reason;
294

295
    /**
296
     * The session cookie will be created by the PSR-7 HTTP resolver.
297
     * If this option is false, created by PHP native function `setcookie`.
298
     *
299
     * @var bool
300
     */
301
    public $psr7 = true;
302

303
    /**
304
     * Shieldon constructor.
305
     *
306
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
307
     * @param ResponseInterface|null      $response A PSR-7 server response.
308
     *
309
     * @return void
310
     */
311 3
    public function __construct(?ServerRequestInterface $request = null, ?ResponseInterface $response = null)
312
    {
313
        // Load helper functions. This is the must and first.
314 3
        new Helpers();
315

316 3
        if (is_null($request)) {
317 3
            $request = HttpFactory::createRequest();
318 3
            $this->psr7 = false;
319
        }
320

321 3
        if (is_null($response)) {
322 3
            $response = HttpFactory::createResponse();
323
        }
324

325
        // Load default settings.
326 3
        $this->properties = get_default_properties();
327

328
        // Basic form for Captcha.
329 3
        $this->setCaptcha(new Foundation());
330

331 3
        Container::set('request', $request);
332 3
        Container::set('response', $response);
333 3
        Container::set('shieldon', $this);
334

335
        Event::AddListener('set_session_driver', function($args) {
336 3
            $session = get_session_instance();
337

338 3
            $session->init(
339 3
                $args['driver'],
340 3
                $args['gc_expires'],
341 3
                $args['gc_probability'],
342 3
                $args['gc_divisor'],
343 3
                $args['psr7']
344
            );
345

346
            /**
347
             * Hook - session_init
348
             */
349 3
            Event::doDispatch('session_init');
350

351 3
            set_session_instance($session);
352 3
        });
353
    }
354

355
    /**
356
     * Run, run, run!
357
     *
358
     * Check the rule tables first, if an IP address has been listed.
359
     * Call function filter() if an IP address is not listed in rule tables.
360
     *
361
     * @return int
362
     */
363 3
    public function run(): int
364
    {
365 3
        $this->assertDriver();
366

367
        // Ignore the excluded urls.
368 3
        foreach ($this->excludedUrls as $url) {
369 3
            if (strpos($this->getCurrentUrl(), $url) === 0) {
370 3
                return $this->result = self::RESPONSE_ALLOW;
371
            }
372
        }
373

374
        // Execute closure functions.
375 3
        foreach ($this->closures as $closure) {
376 3
            $closure();
377
        }
378

379 3
        $result = $this->process();
380

381 3
        if ($result !== self::RESPONSE_ALLOW) {
382

383
            // Current session did not pass the CAPTCHA, it is still stuck in 
384
            // CAPTCHA page.
385 3
            $actionCode = self::LOG_CAPTCHA;
386

387
            // If current session's respone code is RESPONSE_DENY, record it as 
388
            // `blacklist_count` in our logs.
389
            // It is stuck in warning page, not CAPTCHA.
390 3
            if ($result === self::RESPONSE_DENY) {
391 3
                $actionCode = self::LOG_BLACKLIST;
392
            }
393

394 3
            if ($result === self::RESPONSE_LIMIT_SESSION) {
395 3
                $actionCode = self::LOG_LIMIT;
396
            }
397

398 3
            $this->log($actionCode);
399

400
        } else {
401

402 3
            $this->log(self::LOG_PAGEVIEW);
403
        }
404

405
        // @ MessengerTrait
406 3
        $this->triggerMessengers();
407

408
        /**
409
         * Hook - kernel_end
410
         */
411 3
        Event::doDispatch('kernel_end');
412

413 3
        return $result;
414
    }
415

416
    /**
417
     * Ban an IP.
418
     *
419
     * @param string $ip A valid IP address.
420
     *
421
     * @return void
422
     */
423 3
    public function ban(string $ip = ''): void
424
    {
425 3
        if ('' === $ip) {
426 3
            $ip = $this->ip;
427
        }
428
 
429 3
        $this->action(
430 3
            self::ACTION_DENY,
431 3
            self::REASON_MANUAL_BAN,
432 2
            $ip
433
        );
434
    }
435

436
    /**
437
     * Unban an IP.
438
     *
439
     * @param string $ip A valid IP address.
440
     *
441
     * @return void
442
     */
443 3
    public function unban(string $ip = ''): void
444
    {
445 3
        if ($ip === '') {
446 3
            $ip = $this->ip;
447
        }
448

449 3
        $this->action(
450 3
            self::ACTION_UNBAN,
451 3
            self::REASON_MANUAL_BAN,
452 2
            $ip
453
        );
454 3
        $this->log(self::ACTION_UNBAN);
455

456 3
        $this->result = self::RESPONSE_ALLOW;
457
    }
458

459
    /**
460
     * Set a property setting.
461
     *
462
     * @param string $key   The key of a property setting.
463
     * @param mixed  $value The value of a property setting.
464
     *
465
     * @return void
466
     */
467 3
    public function setProperty(string $key = '', $value = '')
468
    {
469 3
        if (isset($this->properties[$key])) {
470 3
            $this->properties[$key] = $value;
471
        }
472
    }
473

474
    /**
475
     * Set the property settings.
476
     * 
477
     * @param array $settings The settings.
478
     *
479
     * @return void
480
     */
481 3
    public function setProperties(array $settings): void
482
    {
483 3
        foreach (array_keys($this->properties) as $k) {
484 3
            if (isset($settings[$k])) {
485 3
                $this->properties[$k] = $settings[$k];
486
            }
487
        }
488
    }
489

490
    /**
491
     * Strict mode.
492
     * This option will take effects to all components.
493
     * 
494
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
495
     *
496
     * @return void
497
     */
498 3
    public function setStrict(bool $bool)
499
    {
500 3
        $this->strictMode = $bool;
501
    }
502

503
    /**
504
     * Set an action log logger.
505
     *
506
     * @param ActionLogger $logger Record action logs for users.
507
     *
508
     * @return void
509
     */
510 3
    public function setLogger(ActionLogger $logger): void
511
    {
512 3
        $this->logger = $logger;
513
    }
514

515
    /**
516
     * Add a path into the excluded list.
517
     *
518
     * @param string $uriPath The path component of a URI.
519
     * 
520
     * @return void
521
     */
522 3
    public function exclude(string $uriPath): void
523
    {
524 3
        $uriPath = '/' . ltrim($uriPath, '/');
525

526 3
        array_push($this->excludedUrls, $uriPath);
527
    }
528

529
    /**
530
     * Set the URLs you want them excluded them from protection.
531
     *
532
     * @param array $urls The list of URL want to be excluded.
533
     *
534
     * @return void
535
     */
536 3
    public function setExcludedList(array $urls = []): void
537
    {
538 3
        $this->excludedUrls = $urls;
539
    }
540

541
    /**
542
     * Set a closure function.
543
     *
544
     * @param string  $key     The name for the closure class.
545
     * @param Closure $closure An instance will be later called.
546
     *
547
     * @return void
548
     */
549 3
    public function setClosure(string $key, Closure $closure): void
550
    {
551 3
        $this->closures[$key] = $closure;
552
    }
553

554
    /**
555
     * Get current visior's path.
556
     *
557
     * @return string
558
     */
559 3
    public function getCurrentUrl(): string
560
    {
561 3
        return get_request()->getUri()->getPath();
562
    }
563

564
    /**
565
     * Displayed on Firewall Panel, telling you current what type of 
566
     * configuration is used.
567
     * 
568
     * @param string $type The type of configuration.
569
     *                     accepted value: demo | managed | config
570
     *
571
     * @return void
572
     */
573 3
    public function managedBy(string $type = ''): void
574
    {
575 3
        if (in_array($type, ['managed', 'config', 'demo'])) {
576 3
            $this->firewallType = $type;
577
        }
578
    }
579

580
    /*
581
    |-------------------------------------------------------------------
582
    | Non-public methods.
583
    |-------------------------------------------------------------------
584
    */
585

586
    /**
587
     * Run, run, run!
588
     *
589
     * Check the rule tables first, if an IP address has been listed.
590
     * Call function filter() if an IP address is not listed in rule tables.
591
     *
592
     * @return int The response code.
593
     */
594 3
    protected function process(): int
595
    {
596 3
        $this->initComponents();
597

598
        $processMethods = [
599 3
            'isRuleExist',   // Stage 1 - Looking for rule table.
600
            'isTrustedBot',  // Stage 2 - Detect popular search engine.
601
            'isFakeRobot',   // Stage 3 - Reject fake search engine crawlers.
602
            'isIpComponent', // Stage 4 - IP manager.
603
            'isComponents'   // Stage 5 - Check other components.
604
        ];
605

606 3
        foreach ($processMethods as $method) {
607 3
            if ($this->{$method}()) {
608 3
                return $this->result;
609
            }
610
        }
611

612
        // Stage 6 - Check filters if set.
613 3
        if (array_search(true, $this->filterStatus)) {
614 3
            return $this->result = $this->sessionHandler($this->filter());
615
        }
616

617
        // Stage 7 - Go into session limit check.
618 3
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
619
    }
620

621
    /**
622
     * Start an action for this IP address, allow or deny, and give a reason for it.
623
     *
624
     * @param int    $actionCode The action code. - 0: deny, 1: allow, 9: unban.
625
     * @param string $reasonCode The response code.
626
     * @param string $assignIp   The IP address.
627
     * 
628
     * @return void
629
     */
630 3
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
631
    {
632 3
        $ip = $this->ip;
633 3
        $rdns = $this->rdns;
634 3
        $now = time();
635 3
        $logData = [];
636
    
637 3
        if ('' !== $assignIp) {
638 3
            $ip = $assignIp;
639 3
            $rdns = gethostbyaddr($ip);
640
        }
641

642 3
        if ($actionCode === self::ACTION_UNBAN) {
643 3
            $this->driver->delete($ip, 'rule');
644
        } else {
645 3
            $logData['log_ip']     = $ip;
646 3
            $logData['ip_resolve'] = $rdns;
647 3
            $logData['time']       = $now;
648 3
            $logData['type']       = $actionCode;
649 3
            $logData['reason']     = $reasonCode;
650 3
            $logData['attempts']   = 0;
651

652 3
            $this->driver->save($ip, $logData, 'rule');
653
        }
654

655
        // Remove logs for this IP address because It already has it's own rule on system.
656
        // No need to count for it anymore.
657 3
        $this->driver->delete($ip, 'filter');
658

659 3
        $this->removeSessionsByIp($ip);
660

661
        // Log this action.
662 3
        $this->log($actionCode, $ip);
663

664 3
        $this->reason = $reasonCode;
665
    }
666

667
    /**
668
     * Log actions.
669
     *
670
     * @param int    $actionCode The code number of the action.
671
     * @param string $ip         The IP address.
672
     *
673
     * @return void
674
     */
675 3
    protected function log(int $actionCode, $ip = ''): void
676
    {
677 3
        if (!$this->logger) {
678 3
            return;
679
        }
680

681 3
        $logData = [];
682
 
683 3
        $logData['ip'] = $ip ?: $this->getIp();
684 3
        $logData['session_id'] = get_session_instance()->getId();
685 3
        $logData['action_code'] = $actionCode;
686 3
        $logData['timestamp'] = time();
687

688 3
        $this->logger->add($logData);
689
    }
690

691
    /**
692
     * Get a class name without namespace string.
693
     *
694
     * @param object $instance Class
695
     * 
696
     * @return string
697
     */
698 3
    protected function getClassName($instance): string
699
    {
700 3
        $class = get_class($instance);
701 3
        return substr($class, strrpos($class, '\\') + 1); 
702
    }
703

704
    /**
705
     * Save and return the result identifier.
706
     * This method is for passing value from traits.
707
     *
708
     * @param int $resultCode The result identifier.
709
     *
710
     * @return int
711
     */
712 3
    protected function setResultCode(int $resultCode): int
713
    {
714 3
        return $this->result = $resultCode;
715
    }
716
}

Read our documentation on viewing source code .

Loading