Passed
Push — 2.x ( a16b00...154feb )
by Terry
03:08 queued 33s
created

Kernel::captchaResponse()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 5
nop 0
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
/*
3
 * @name        Shieldon Firewall
4
 * @author      Terry Lin
5
 * @link        https://github.com/terrylinooo/shieldon
6
 * @package     Shieldon
7
 * @since       1.0.0
8
 * @version     2.0.0
9
 * @license     MIT
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a copy
12
 * of this software and associated documentation files (the "Software"), to deal
13
 * in the Software without restriction, including without limitation the rights
14
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
 * copies of the Software, and to permit persons to whom the Software is
16
 * furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
 * THE SOFTWARE.
28
 */
29
30
declare(strict_types=1);
31
32
namespace Shieldon\Firewall;
33
34
use Psr\Http\Message\ServerRequestInterface;
35
use Psr\Http\Message\ResponseInterface;
36
use Shieldon\Firewall\Captcha\CaptchaInterface;
37
use Shieldon\Firewall\Captcha\Foundation;
38
use Shieldon\Firewall\Component\ComponentInterface;
39
use Shieldon\Firewall\Component\ComponentProvider;
40
use Shieldon\Firewall\Driver\DriverProvider;
41
use Shieldon\Firewall\Helpers;
42
use Shieldon\Firewall\HttpFactory;
43
use Shieldon\Firewall\Log\ActionLogger;
44
use Shieldon\Firewall\Utils\Container;
45
use Shieldon\Firewall\IpTrait;
46
use Shieldon\Firewall\Kernel\FilterTrait;
47
use Shieldon\Firewall\Kernel\RuleTrait;
48
use Shieldon\Firewall\Kernel\LimitSessionTrait;
49
use Shieldon\Messenger\Messenger\MessengerInterface;
50
use function Shieldon\Firewall\__;
51
use function Shieldon\Firewall\get_cpu_usage;
52
use function Shieldon\Firewall\get_default_properties;
53
use function Shieldon\Firewall\get_memory_usage;
54
use function Shieldon\Firewall\get_request;
55
use function Shieldon\Firewall\get_response;
56
use function Shieldon\Firewall\get_session;
57
58
59
use Closure;
60
use InvalidArgumentException;
61
use LogicException;
62
use RuntimeException;
63
use function file_exists;
64
use function file_put_contents;
65
use function filter_var;
66
use function get_class;
67
use function gethostbyaddr;
68
use function is_dir;
69
use function is_writable;
70
use function microtime;
71
use function ob_end_clean;
72
use function ob_get_contents;
73
use function ob_start;
74
use function str_replace;
75
use function strpos;
76
use function strrpos;
77
use function substr;
78
use function time;
79
80
/**
81
 * The primary Shiendon class.
82
 */
83
class Kernel
84
{
85
    use IpTrait;
86
    use FilterTrait;
87
    use RuleTrait;
88
    use LimitSessionTrait;
89
90
    // Reason codes (allow)
91
    const REASON_IS_SEARCH_ENGINE = 100;
92
    const REASON_IS_GOOGLE = 101;
93
    const REASON_IS_BING = 102;
94
    const REASON_IS_YAHOO = 103;
95
    const REASON_IS_SOCIAL_NETWORK = 110;
96
    const REASON_IS_FACEBOOK = 111;
97
    const REASON_IS_TWITTER = 112;
98
99
    // Reason codes (deny)
100
    const REASON_TOO_MANY_SESSIONS = 1;
101
    const REASON_TOO_MANY_ACCESSES = 2; // (not used)
102
    const REASON_EMPTY_JS_COOKIE = 3;
103
    const REASON_EMPTY_REFERER = 4;
104
    
105
    const REASON_REACHED_LIMIT_DAY = 11;
106
    const REASON_REACHED_LIMIT_HOUR = 12;
107
    const REASON_REACHED_LIMIT_MINUTE = 13;
108
    const REASON_REACHED_LIMIT_SECOND = 14;
109
110
    const REASON_INVALID_IP = 40;
111
    const REASON_DENY_IP = 41;
112
    const REASON_ALLOW_IP = 42;
113
114
    const REASON_COMPONENT_IP = 81;
115
    const REASON_COMPONENT_RDNS = 82;
116
    const REASON_COMPONENT_HEADER = 83;
117
    const REASON_COMPONENT_USERAGENT = 84;
118
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
119
120
    const REASON_MANUAL_BAN = 99;
121
122
    // Action codes
123
    const ACTION_DENY = 0;
124
    const ACTION_ALLOW = 1;
125
    const ACTION_TEMPORARILY_DENY = 2;
126
    const ACTION_UNBAN = 9;
127
128
    // Result codes
129
    const RESPONSE_DENY = 0;
130
    const RESPONSE_ALLOW = 1;
131
    const RESPONSE_TEMPORARILY_DENY = 2;
132
    const RESPONSE_LIMIT_SESSION = 3;
133
134
    const LOG_LIMIT = 3;
135
    const LOG_PAGEVIEW = 11;
136
    const LOG_BLACKLIST = 98;
137
    const LOG_CAPTCHA = 99;
138
139
    const KERNEL_DIR = __DIR__;
140
141
    /**
142
     * Driver for storing data.
143
     *
144
     * @var \Shieldon\Firewall\Driver\DriverProvider
145
     */
146
    public $driver;
147
148
    /**
149
     * Container for Shieldon components.
150
     *
151
     * @var array
152
     */
153
    public $component = [];
154
155
    /**
156
     * Logger instance.
157
     *
158
     * @var ActionLogger
159
     */
160
    public $logger;
161
162
    /**
163
     * The closure functions that will be executed in this->run()
164
     *
165
     * @var array
166
     */
167
    protected $closures = [];
168
169
    /**
170
     * default settings
171
     *
172
     * @var array
173
     */
174
    protected $properties = [];
175
176
    /**
177
     * This is for creating data tables automatically
178
     * Turn it off, if you don't want to check data tables every connection.
179
     *
180
     * @var bool
181
     */
182
    protected $autoCreateDatabase = true;
183
184
    /**
185
     * Container for captcha addons.
186
     * The collection of \Shieldon\Firewall\Captcha\CaptchaInterface
187
     *
188
     * @var array
189
     */
190
    protected $captcha = [];
191
192
    /**
193
     * The ways Shieldon send a message to when someone has been blocked.
194
     * The collection of \Shieldon\Messenger\Messenger\MessengerInterface
195
     *
196
     * @var array
197
     */
198
    protected $messenger = [];
199
200
    /**
201
     * Result.
202
     *
203
     * @var int
204
     */
205
    protected $result = 1;
206
207
    /**
208
     * URLs that are excluded from Shieldon's protection.
209
     *
210
     * @var array
211
     */
212
    protected $excludedUrls = [];
213
214
    /**
215
     * Which type of configuration source that Shieldon firewall managed?
216
     *
217
     * @var string
218
     */
219
    protected $firewallType = 'self'; // managed | config | self | demo
220
221
    /**
222
     * Custom dialog UI settings.
223
     *
224
     * @var array
225
     */
226
    protected $dialogUI = [];
227
228
    /**
229
     * Strict mode.
230
     * 
231
     * Set by `strictMode()` only. The default value of this propertry is undefined.
232
     *
233
     * @var bool|null
234
     */
235
    protected $strictMode;
236
237
    /**
238
     * The directory in where the frontend template files are placed.
239
     *
240
     * @var string
241
     */
242
    protected $templateDirectory = '';
243
244
    /**
245
     * The message that will be sent to the third-party API.
246
     *
247
     * @var string
248
     */
249
    protected $msgBody = '';
250
251
    /**
252
     * Shieldon constructor.
253
     * 
254
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
255
     * 
256
     * @return void
257
     */
258
    public function __construct(?ServerRequestInterface $request  = null, ?ResponseInterface $response = null)
259
    {
260
        // Load helper functions. This is the must.
261
        new Helpers();
262
263
        if (is_null($request)) {
264
            $request = HttpFactory::createRequest();
265
        }
266
267
        if (is_null($response)) {
268
            $response = HttpFactory::createResponse();
269
        }
270
271
        $session = HttpFactory::createSession();
272
273
        $this->properties = get_default_properties();
274
        $this->setCaptcha(new Foundation());
275
276
        Container::set('request', $request);
277
        Container::set('response', $response);
278
        Container::set('session', $session);
279
        Container::set('shieldon', $this);
280
    }
281
282
    /**
283
     * Log actions.
284
     *
285
     * @param int $actionCode The code number of the action.
286
     *
287
     * @return void
288
     */
289
    protected function log(int $actionCode): void
290
    {
291
        if (null !== $this->logger) {
292
            $logData = [];
293
            $logData['ip'] = $this->getIp();
294
            $logData['session_id'] = get_session()->get('id');
295
            $logData['action_code'] = $actionCode;
296
            $logData['timesamp'] = time();
297
    
298
            $this->logger->add($logData);
299
        }
300
    }
301
302
    /**
303
     * Initialize components.
304
     *
305
     * @return void
306
     */
307
    private function initComponents()
308
    {
309
        foreach (array_keys($this->component) as $name) {
310
            $this->component[$name]->setIp($this->ip);
311
            $this->component[$name]->setRdns($this->rdns);
312
313
            // Apply global strict mode to all components by `strictMode()` if nesscessary.
314
            if (isset($this->strictMode)) {
315
                $this->component[$name]->setStrict($this->strictMode);
316
            }
317
        }
318
    }
319
320
    /**
321
     * Check if current IP is trusted or not.
322
     *
323
     * @return bool
324
     */
325
    private function isTrustedBot()
326
    {
327
        if ($this->getComponent('TrustedBot')) {
328
329
            // We want to put all the allowed robot into the rule list, so that the checking of IP's resolved hostname 
330
            // is no more needed for that IP.
331
            if ($this->getComponent('TrustedBot')->isAllowed()) {
0 ignored issues
show
Bug introduced by
The method isAllowed() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\Ip or Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

331
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isAllowed()) {
Loading history...
332
333
                if ($this->getComponent('TrustedBot')->isGoogle()) {
0 ignored issues
show
Bug introduced by
The method isGoogle() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

333
                if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isGoogle()) {
Loading history...
334
                    // Add current IP into allowed list, because it is from real Google domain.
335
                    $this->action(
336
                        self::ACTION_ALLOW,
337
                        self::REASON_IS_GOOGLE
338
                    );
339
340
                } elseif ($this->getComponent('TrustedBot')->isBing()) {
0 ignored issues
show
Bug introduced by
The method isBing() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

340
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isBing()) {
Loading history...
341
                    // Add current IP into allowed list, because it is from real Bing domain.
342
                    $this->action(
343
                        self::ACTION_ALLOW,
344
                        self::REASON_IS_BING
345
                    );
346
347
                } elseif ($this->getComponent('TrustedBot')->isYahoo()) {
0 ignored issues
show
Bug introduced by
The method isYahoo() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

347
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isYahoo()) {
Loading history...
348
                    // Add current IP into allowed list, because it is from real Yahoo domain.
349
                    $this->action(
350
                        self::ACTION_ALLOW,
351
                        self::REASON_IS_YAHOO
352
                    );
353
354
                } else {
355
                    // Add current IP into allowed list, because you trust it.
356
                    // You have already defined it in the settings.
357
                    $this->action(
358
                        self::ACTION_ALLOW,
359
                        self::REASON_IS_SEARCH_ENGINE
360
                    );
361
                }
362
                // Allowed robots not join to our traffic handler.
363
                $this->result = self::RESPONSE_ALLOW;
364
                return true;
365
            }
366
        }
367
        return false;
368
    }
369
370
    /**
371
     * Check whether the IP is fake search engine or not.
372
     * The method "isTrustedBot()" must be executed before this method.
373
     *
374
     * @return bool
375
     */
376
    private function isFakeRobot(): bool
377
    {
378
        if ($this->getComponent('TrustedBot')) {
379
            if ($this->getComponent('TrustedBot')->isFakeRobot()) {
0 ignored issues
show
Bug introduced by
The method isFakeRobot() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

379
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isFakeRobot()) {
Loading history...
380
                $this->action(
381
                    self::ACTION_DENY,
382
                    self::REASON_COMPONENT_TRUSTED_ROBOT
383
                );
384
                $this->result = self::RESPONSE_DENY;
385
                return true;
386
            }
387
        }
388
        return false;
389
    }
390
391
    /**
392
     * Run, run, run!
393
     *
394
     * Check the rule tables first, if an IP address has been listed.
395
     * Call function filter() if an IP address is not listed in rule tables.
396
     *
397
     * @return int The response code.
398
     */
399
    protected function process(): int
400
    {
401
        $this->driver->init($this->autoCreateDatabase);
402
403
        $this->initComponents();
404
405
        /*
406
        |--------------------------------------------------------------------------
407
        | Stage - Looking for rule table.
408
        |--------------------------------------------------------------------------
409
        */
410
411
        if ($this->DoesRuleExist()) {
412
            return $this->result;
413
        }
414
415
        /*
416
        |--------------------------------------------------------------------------
417
        | Statge - Detect popular search engine.
418
        |--------------------------------------------------------------------------
419
        */
420
421
        if ($this->isTrustedBot()) {
422
            return $this->result;
423
        }
424
425
        if ($this->isFakeRobot()) {
426
            return $this->result;
427
        }
428
        
429
        /*
430
        |--------------------------------------------------------------------------
431
        | Stage - IP component.
432
        |--------------------------------------------------------------------------
433
        */
434
435
        if ($this->getComponent('Ip')) {
436
437
            $result = $this->getComponent('Ip')->check();
0 ignored issues
show
Bug introduced by
The method check() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\Ip. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

437
            $result = $this->getComponent('Ip')->/** @scrutinizer ignore-call */ check();
Loading history...
438
            $actionCode = self::ACTION_DENY;
439
440
            if (!empty($result)) {
441
442
                switch ($result['status']) {
443
444
                    case 'allow':
445
                        $actionCode = self::ACTION_ALLOW;
446
                        $reasonCode = $result['code'];
447
                        break;
448
    
449
                    case 'deny':
450
                        $actionCode = self::ACTION_DENY;
451
                        $reasonCode = $result['code']; 
452
                        break;
453
                }
454
455
                $this->action($actionCode, $reasonCode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $reasonCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
456
457
                // $resultCode = $actionCode
458
                return $this->result = $this->sessionHandler($actionCode);
459
            }
460
        }
461
462
        /*
463
        |--------------------------------------------------------------------------
464
        | Stage - Check all other components.
465
        |--------------------------------------------------------------------------
466
        */
467
468
        foreach ($this->component as $component) {
469
470
            // check if is a a bad robot already defined in settings.
471
            if ($component->isDenied()) {
472
473
                // @since 0.1.8
474
                $this->action(
475
                    self::ACTION_DENY,
476
                    $component->getDenyStatusCode()
477
                );
478
479
                return $this->result = self::RESPONSE_DENY;
480
            }
481
        }
482
483
        /*
484
        |--------------------------------------------------------------------------
485
        | Stage - Filters
486
        |--------------------------------------------------------------------------
487
        | This IP address is not listed in rule table, let's detect it.
488
        |
489
        */
490
491
        if (
492
            $this->filterStatus['frequency'] ||
493
            $this->filterStatus['referer'] ||
494
            $this->filterStatus['session'] ||
495
            $this->filterStatus['cookie']
496
        ) {
497
            return $this->result = $this->sessionHandler($this->filter());
498
        }
499
500
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
501
    }
502
503
    /**
504
     * Start an action for this IP address, allow or deny, and give a reason for it.
505
     *
506
     * @param int    $actionCode - 0: deny, 1: allow, 9: unban.
507
     * @param string $reasonCode
508
     * @param string $assignIp
509
     * 
510
     * @return void
511
     */
512
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
513
    {
514
        $ip = $this->ip;
515
        $rdns = $this->rdns;
516
        $now = time();
517
        $logData = [];
518
    
519
        if ('' !== $assignIp) {
520
            $ip = $assignIp;
521
            $rdns = gethostbyaddr($ip);
522
        }
523
524
        switch ($actionCode) {
525
            case self::ACTION_ALLOW: // acutally not used.
526
            case self::ACTION_DENY:  // actually not used.
527
            case self::ACTION_TEMPORARILY_DENY:
528
                $logData['log_ip']     = $ip;
529
                $logData['ip_resolve'] = $rdns;
530
                $logData['time']       = $now;
531
                $logData['type']       = $actionCode;
532
                $logData['reason']     = $reasonCode;
533
                $logData['attempts']   = 0;
534
535
                $this->driver->save($ip, $logData, 'rule');
536
                break;
537
            
538
            case self::ACTION_UNBAN:
539
                $this->driver->delete($ip, 'rule');
540
                break;
541
        }
542
543
        // Remove logs for this IP address because It already has it's own rule on system.
544
        // No need to count it anymore.
545
        $this->driver->delete($ip, 'filter');
546
547
        if (null !== $this->logger) {
548
            $log['ip']          = $ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$log was never initialized. Although not strictly required by PHP, it is generally a good practice to add $log = array(); before regardless.
Loading history...
549
            $log['session_id']  = get_session()->get('id');
550
            $log['action_code'] = $actionCode;
551
            $log['timesamp']    = $now;
552
553
            $this->logger->add($log);
554
        }
555
    }
556
557
    // @codeCoverageIgnoreEnd
558
559
    /*
560
    | -------------------------------------------------------------------
561
    |                            Public APIs
562
    | -------------------------------------------------------------------
563
    */
564
565
    /**
566
     * Set a commponent.
567
     *
568
     * @param ComponentProvider $instance
569
     *
570
     * @return void
571
     */
572
    public function setComponent(ComponentProvider $instance): void
573
    {
574
        $class = $this->getClassName($instance);
575
        $this->component[$class] = $instance;
576
    }
577
578
    /**
579
     * Set a captcha.
580
     *
581
     * @param CaptchaInterface $instance
582
     *
583
     * @return void
584
     */
585
    public function setCaptcha(CaptchaInterface $instance): void
586
    {
587
        $class = $this->getClassName($instance);
588
        $this->captcha[$class] = $instance;
589
    }
590
591
    /**
592
     * Set a data driver.
593
     *
594
     * @param DriverProvider $driver Query data from the driver you choose to use.
595
     *
596
     * @return void
597
     */
598
    public function setDriver(DriverProvider $driver): void
599
    {
600
        $this->driver = $driver;
601
    }
602
603
    /**
604
     * Set a action log logger.
605
     *
606
     * @param ActionLogger $logger
607
     *
608
     * @return void
609
     */
610
    public function setLogger(ActionLogger $logger): void
611
    {
612
        $this->logger = $logger;
613
    }
614
615
    /**
616
     * Set a messenger
617
     *
618
     * @param MessengerInterfa $instance
0 ignored issues
show
Bug introduced by
The type Shieldon\Firewall\MessengerInterfa was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
619
     *
620
     * @return void
621
     */
622
    public function setMessenger(MessengerInterface $instance): void
623
    {
624
        $class = $this->getClassName($instance);
625
        $this->messengers[$class] = $instance;
0 ignored issues
show
Bug Best Practice introduced by
The property messengers does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
626
    }
627
628
    /**
629
     * Get a component instance from component's container.
630
     *
631
     * @param string $name The component's class name.
632
     *
633
     * @return ComponentInterface|null
634
     */
635
    public function getComponent(string $name)
636
    {
637
        if (isset($this->component[$name])) {
638
            return $this->component[$name];
639
        }
640
641
        return null;
642
    }
643
644
    /**
645
     * Strict mode.
646
     * This option will take effects to all components.
647
     * 
648
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
649
     *
650
     * @return void
651
     */
652
    public function setStrict(bool $bool)
653
    {
654
        $this->strictMode = $bool;
655
    }
656
657
    /**
658
     * Disable filters.
659
     */
660
    public function disableFilters(): void
661
    {
662
        $this->setFilters([
663
            'session'   => false,
664
            'cookie'    => false,
665
            'referer'   => false,
666
            'frequency' => false,
667
        ]);
668
    }
669
670
    /**
671
     * For first time installation only. This is for creating data tables automatically.
672
     * Turning it on will check the data tables exist or not at every single pageview, 
673
     * it's not good for high traffic websites.
674
     *
675
     * @param bool $bool
676
     * 
677
     * @return void
678
     */
679
    public function createDatabase(bool $bool)
680
    {
681
        $this->autoCreateDatabase = $bool;
682
    }
683
684
    /**
685
     * Set a data channel.
686
     *
687
     * This will create databases for the channel.
688
     *
689
     * @param string $channel Specify a channel.
690
     *
691
     * @return void
692
     */
693
    public function setChannel(string $channel)
694
    {
695
        if (!$this->driver instanceof DriverProvider) {
0 ignored issues
show
introduced by
$this->driver is always a sub-type of Shieldon\Firewall\Driver\DriverProvider.
Loading history...
696
            throw new LogicException('setChannel method requires setDriver set first.');
697
        } else {
698
            $this->driver->setChannel($channel);
699
        }
700
    }
701
702
    /**
703
     * Return the result from Captchas.
704
     *
705
     * @return bool
706
     */
707
    public function captchaResponse(): bool
708
    {
709
        foreach ($this->captcha as $captcha) {
710
            
711
            if (!$captcha->response()) {
712
                return false;
713
            }
714
        }
715
716
        if (!empty($this->sessionLimit['count'])) {
717
            $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
718
        }
719
720
        return true;
721
    }
722
723
    /**
724
     * Ban an IP.
725
     *
726
     * @param string $ip A valid IP address.
727
     *
728
     * @return void
729
     */
730
    public function ban(string $ip = ''): void
731
    {
732
        if ('' === $ip) {
733
            $ip = $this->ip;
734
        }
735
 
736
        $this->action(
737
            self::ACTION_DENY,
738
            self::REASON_MANUAL_BAN, $ip
739
        );
740
    }
741
742
    /**
743
     * Unban an IP.
744
     *
745
     * @param string $ip A valid IP address.
746
     *
747
     * @return void
748
     */
749
    public function unban(string $ip = ''): void
750
    {
751
        if ('' === $ip) {
752
            $ip = $this->ip;
753
        }
754
755
        $this->action(
756
            self::ACTION_UNBAN,
757
            self::REASON_MANUAL_BAN, $ip
758
        );
759
        $this->log(self::ACTION_UNBAN);
760
761
        $this->result = self::RESPONSE_ALLOW;
762
    }
763
764
    /**
765
     * Set a property setting.
766
     *
767
     * @param string $key   The key of a property setting.
768
     * @param mixed  $value The value of a property setting.
769
     *
770
     * @return void
771
     */
772
    public function setProperty(string $key = '', $value = '')
773
    {
774
        if (isset($this->properties[$key])) {
775
            $this->properties[$key] = $value;
776
        }
777
    }
778
779
    /**
780
     * Set the property settings.
781
     * 
782
     * @param array $settings The settings.
783
     *
784
     * @return void
785
     */
786
    public function setProperties(array $settings): void
787
    {
788
        foreach (array_keys($this->properties) as $k) {
789
            if (isset($settings[$k])) {
790
                $this->properties[$k] = $settings[$k];
791
            }
792
        }
793
    }
794
795
    /**
796
     * Limt online sessions.
797
     *
798
     * @param int $count
799
     * @param int $period
800
     *
801
     * @return void
802
     */
803
    public function limitSession(int $count = 1000, int $period = 300): void
804
    {
805
        $this->sessionLimit = [
806
            'count' => $count,
807
            'period' => $period
808
        ];
809
    }
810
811
    /**
812
     * Customize the dialog UI.
813
     *
814
     * @return void
815
     */
816
    public function setDialogUI(array $settings): void
817
    {
818
        $this->dialogUI = $settings;
819
    }
820
821
    /**
822
     * Set the frontend template directory.
823
     *
824
     * @param string $directory
825
     *
826
     * @return void
827
     */
828
    public function setTemplateDirectory(string $directory)
829
    {
830
        if (!is_dir($directory)) {
831
            throw new InvalidArgumentException('The template directory does not exist.');
832
        }
833
        $this->templateDirectory = $directory;
834
    }
835
836
    /**
837
     * Get a template PHP file.
838
     *
839
     * @param string $type The template type.
840
     *
841
     * @return string
842
     */
843
    protected function getTemplate(string $type): string
844
    {
845
        $directory = self::KERNEL_DIR . '/../../templates/frontend';
846
847
        if (!empty($this->templateDirectory)) {
848
            $directory = $this->templateDirectory;
849
        }
850
851
        $path = $directory . '/' . $type . '.php';
852
853
        if (!file_exists($path)) {
854
            throw new RuntimeException(
855
                sprintf(
856
                    'The templeate file is missing. (%s)',
857
                    $path
858
                )
859
            );
860
        }
861
862
        return $path;
863
    }
864
865
    /**
866
     * Get a class name without namespace string.
867
     *
868
     * @param object $instance Class
869
     * 
870
     * @return void
871
     */
872
    protected function getClassName($instance): string
873
    {
874
        $class = get_class($instance);
875
        return substr($class, strrpos($class, '\\') + 1); 
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr($class, strrpos($class, '\') + 1) returns the type string which is incompatible with the documented return type void.
Loading history...
876
    }
877
878
    /**
879
     * Respond the result.
880
     *
881
     * @return ResponseInterface
882
     */
883
    public function respond(): ResponseInterface
884
    {
885
        $response = get_response();
886
        $type = '';
887
888
        if (self::RESPONSE_TEMPORARILY_DENY === $this->result) {
889
            $type = 'captcha';
890
            $statusCode = 403; // Forbidden.
891
892
        } elseif (self::RESPONSE_LIMIT_SESSION === $this->result) {
893
            $type = 'session_limitation';
894
            $statusCode = 429; // Too Many Requests.
895
896
        } elseif (self::RESPONSE_DENY === $this->result) {
897
            $type = 'rejection';
898
            $statusCode = 400; // Bad request.
899
        }
900
901
        // Nothing happened. Return.
902
        if (empty($type)) {
903
            // @codeCoverageIgnoreStart
904
            return $response;
905
            // @codeCoverageIgnoreEnd
906
        }
907
908
        $viewPath = $this->getTemplate($type);
909
910
        // The language of output UI. It is used on views.
911
        $langCode = get_session()->get('shieldon_ui_lang') ?? 'en';
912
        // Show online session count. It is used on views.
913
        $showOnlineInformation = true;
914
        // Show user information such as IP, user-agent, device name.
915
        $showUserInformation = true;
916
917
        if (empty($this->properties['display_online_info'])) {
918
            $showOnlineInformation = false;
919
        }
920
921
        if (empty($this->properties['display_user_info'])) {
922
            $showUserInformation = false;
923
        }
924
925
        if ($showUserInformation) {
926
            $dialoguserinfo['ip'] = $this->ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$dialoguserinfo was never initialized. Although not strictly required by PHP, it is generally a good practice to add $dialoguserinfo = array(); before regardless.
Loading history...
927
            $dialoguserinfo['rdns'] = $this->rdns;
928
            $dialoguserinfo['user_agent'] = get_request()->getHeaderLine('user-agent');
929
        }
930
931
        $ui = [
932
            'background_image' => $this->dialogUI['background_image'] ?? '',
933
            'bg_color'         => $this->dialogUI['bg_color']         ?? '#ffffff',
934
            'header_bg_color'  => $this->dialogUI['header_bg_color']  ?? '#212531',
935
            'header_color'     => $this->dialogUI['header_color']     ?? '#ffffff',
936
            'shadow_opacity'   => $this->dialogUI['shadow_opacity']   ?? '0.2',
937
        ];
938
939
        if (!defined('SHIELDON_VIEW')) {
940
            define('SHIELDON_VIEW', true);
941
        }
942
943
        $css = require $this->getTemplate('css/default');
944
945
        ob_start();
946
        require $viewPath;
947
        $output = ob_get_contents();
948
        ob_end_clean();
949
950
        // Remove unused variable notices generated from PHP intelephense.
951
        unset(
952
            $css,
953
            $ui,
954
            $langCode,
955
            $showOnlineInformation,
956
            $showLineupInformation,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $showLineupInformation seems to be never defined.
Loading history...
957
            $showUserInformation
958
        );
959
960
        $stream = $response->getBody();
961
        $stream->write($output);
962
        $stream->rewind();
963
964
        return $response->
965
            withHeader('X-Protected-By', 'shieldon.io')->
966
            withBody($stream)->
967
            withStatus($statusCode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $statusCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
968
    }
969
970
    /**
971
     * Run, run, run!
972
     *
973
     * Check the rule tables first, if an IP address has been listed.
974
     * Call function filter() if an IP address is not listed in rule tables.
975
     *
976
     * @return 
977
     */
978
    public function run(): int
979
    {
980
        if (!isset($this->driver)) {
981
            throw new RuntimeException(
982
                'Must register at least one data driver.'
983
            );
984
        }
985
        
986
        // Ignore the excluded urls.
987
        if (!empty($this->excludedUrls)) {
988
            foreach ($this->excludedUrls as $url) {
989
                if (0 === strpos(get_request()->getUri()->getPath(), $url)) {
990
                    return $this->result = self::RESPONSE_ALLOW;
991
                }
992
            }
993
        }
994
995
        // Execute closure functions.
996
        foreach ($this->closures as $closure) {
997
            $closure();
998
        }
999
1000
        $result = $this->process();
1001
1002
        if ($result !== self::RESPONSE_ALLOW) {
1003
1004
            // Current session did not pass the CAPTCHA, it is still stuck in CAPTCHA page.
1005
            $actionCode = self::LOG_CAPTCHA;
1006
1007
            // If current session's respone code is RESPONSE_DENY, record it as `blacklist_count` in our logs.
1008
            // It is stuck in warning page, not CAPTCHA.
1009
            if ($result === self::RESPONSE_DENY) {
1010
                $actionCode = self::LOG_BLACKLIST;
1011
            }
1012
1013
            if ($result === self::RESPONSE_LIMIT_SESSION) {
1014
                $actionCode = self::LOG_LIMIT;
1015
            }
1016
1017
            $this->log($actionCode);
1018
1019
        } else {
1020
1021
            $this->log(self::LOG_PAGEVIEW);
1022
        }
1023
1024
 
1025
        if (!empty($this->msgBody)) {
1026
 
1027
            // @codeCoverageIgnoreStart
1028
1029
            try {
1030
                foreach ($this->messenger as $messenger) {
1031
                    $messenger->setTimeout(2);
1032
                    $messenger->send($this->msgBody);
1033
                }
1034
            } catch (RuntimeException $e) {
1035
                // Do not throw error, becasue the third-party services might be unavailable.
1036
            }
1037
1038
            // @codeCoverageIgnoreEnd
1039
        }
1040
1041
1042
        return $result;
1043
    }
1044
1045
    /**
1046
     * Set the filters.
1047
     *
1048
     * @param array $settings filter settings.
1049
     *
1050
     * @return void
1051
     */
1052
    public function setFilters(array $settings)
1053
    {
1054
        foreach (array_keys($this->filterStatus) as $k) {
1055
            if (isset($settings[$k])) {
1056
                $this->filterStatus[$k] = $settings[$k] ?? false;
1057
            }
1058
        }
1059
    }
1060
1061
    /**
1062
     * Set a filter.
1063
     *
1064
     * @param string $filterName The filter's name.
1065
     * @param bool   $value      True for enabling the filter, overwise.
1066
     *
1067
     * @return void
1068
     */
1069
    public function setFilter(string $filterName, bool $value): void
1070
    {
1071
        if (isset($this->filterStatus[$filterName])) {
1072
            $this->filterStatus[$filterName] = $value;
1073
        }
1074
    }
1075
1076
1077
1078
    /**
1079
     * Set the URLs you want them to be excluded them from protection.
1080
     *
1081
     * @param array $urls The list of URL want to be excluded.
1082
     *
1083
     * @return void
1084
     */
1085
    public function setExcludedUrls(array $urls = []): void
1086
    {
1087
        $this->excludedUrls = $urls;
1088
    }
1089
1090
    /**
1091
     * Set a closure function.
1092
     *
1093
     * @param string  $key     The name for the closure class.
1094
     * @param Closure $closure An instance will be later called.
1095
     *
1096
     * @return void
1097
     */
1098
    public function setClosure(string $key, Closure $closure): void
1099
    {
1100
        $this->closures[$key] = $closure;
1101
    }
1102
1103
    /**
1104
     * Print a JavasSript snippet in your webpages.
1105
     * 
1106
     * This snippet generate cookie on client's browser,then we check the 
1107
     * cookie to identify the client is a rebot or not.
1108
     *
1109
     * @return string
1110
     */
1111
    public function outputJsSnippet(): string
1112
    {
1113
        $tmpCookieName = $this->properties['cookie_name'];
1114
        $tmpCookieDomain = $this->properties['cookie_domain'];
1115
1116
        if (empty($tmpCookieDomain) && get_request()->getHeaderLine('host')) {
1117
            $tmpCookieDomain = get_request()->getHeaderLine('host');
1118
        }
1119
1120
        $tmpCookieValue = $this->properties['cookie_value'];
1121
1122
        $jsString = '
1123
            <script>
1124
                var d = new Date();
1125
                d.setTime(d.getTime()+(60*60*24*30));
1126
                document.cookie = "' . $tmpCookieName . '=' . $tmpCookieValue . ';domain=.' . $tmpCookieDomain . ';expires="+d.toUTCString();
1127
            </script>
1128
        ';
1129
1130
        return $jsString;
1131
    }
1132
1133
    /**
1134
     * Get current visior's path.
1135
     *
1136
     * @return string
1137
     */
1138
    public function getCurrentUrl(): string
1139
    {
1140
        return get_request()->getUri()->getPath();
1141
    }
1142
1143
    /**
1144
     * Displayed on Firewall Panel, tell you current what type of current
1145
     * configuration is used for.
1146
     * 
1147
     * @param string $type The type of configuration.
1148
     *                     demo | managed | config
1149
     *
1150
     * @return void
1151
     */
1152
    public function managedBy(string $type = ''): void
1153
    {
1154
        if (in_array($type, ['managed', 'config', 'demo'])) {
1155
            $this->firewallType = $type;
1156
        }
1157
    }
1158
}
1159