Passed
Push — 2.x ( a76a74...57e8e7 )
by Terry
02:45
created

Firewall::add()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
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
11
declare(strict_types=1);
12
13
namespace Shieldon\Firewall;
14
15
use Psr\Http\Message\ServerRequestInterface;
16
use Psr\Http\Message\ResponseInterface;
17
use Psr\Http\Server\MiddlewareInterface;
18
use Shieldon\Firewall\Kernel;
19
use Shieldon\Firewall\Captcha as Captcha;
20
21
use Shieldon\Firewall\Middleware as Middleware;
22
23
use Shieldon\Firewall\Firewall\Driver\DriverFactory;
24
use Shieldon\Firewall\Firewall\Messenger\MessengerFactory;
25
use Shieldon\Firewall\Firewall\Captcha\CaptchaFactory;
26
use Shieldon\Firewall\Utils\Container;
27
use Shieldon\Firewall\Log\ActionLogger;
28
use Shieldon\Firewall\FirewallTrait;
29
use Shieldon\Firewall\Firewall\XssProtectionTrait;
30
use Shieldon\Psr15\RequestHandler;
31
use function Shieldon\Firewall\get_request;
32
use function Shieldon\Firewall\get_response;
33
34
use PDO;
35
use PDOException;
36
use Redis;
37
use RedisException;
38
use RuntimeException;
39
40
use function array_column;
41
use function defined;
42
use function file_exists;
43
use function file_get_contents;
44
use function file_put_contents;
45
use function is_dir;
46
use function json_decode;
47
use function json_encode;
48
use function mkdir;
49
use function rtrim;
50
use function strpos;
51
use function umask;
52
use function time;
53
use function strtotime;
54
use function date;
55
56
/**
57
 * Managed Firewall.
58
 */
59
class Firewall
60
{
61
    use FirewallTrait;
62
    use XssProtectionTrait;
63
64
    /**
65
     * Collection of PSR-7 or PSR-15 middlewares.
66
     *
67
     * @var array
68
     */
69
    protected $middlewares = [];
70
71
    /**
72
     * Constructor.
73
     */
74
    public function __construct(?ServerRequestInterface $request = null, ?ResponseInterface $response = null)
75
    {
76
        Container::set('firewall', $this);
77
78
        $this->kernel = new Kernel($request, $response);
79
    }
80
81
    /**
82
     * Set up the path of the configuration file.
83
     *
84
     * @param string $source The path.
85
     * @param string $type   The type.
86
     * 
87
     * @return void
88
     */
89
    public function configure(string $source, string $type = 'json')
90
    {
91
        if ($type === 'json') {
92
            $this->directory = rtrim($source, '\\/');
93
            $configFilePath = $this->directory . '/' . $this->filename;
94
95
            if (file_exists($configFilePath)) {
96
                $jsonString = file_get_contents($configFilePath);
97
98
            } else {
99
                $jsonString = file_get_contents(__DIR__ . '/../../config.json');
100
101
                if (defined('PHP_UNIT_TEST')) {
102
                    $jsonString = file_get_contents(__DIR__ . '/../../tests/config.json');
103
                }
104
            }
105
106
            $this->configuration = json_decode($jsonString, true);
107
            $this->kernel->managedBy('managed');
108
109
        } elseif ($type === 'php') {
110
            $this->configuration = require $source;
111
            $this->kernel->managedBy('config');
112
        }
113
114
        $this->setup();
115
    }
116
117
    /**
118
     * Add middlewares and use them before going into Shieldon kernal.
119
     *
120
     * @param MiddlewareInterface $middleware A PSR-15 middlewares.
121
     *
122
     * @return void
123
     */
124
    public function add(MiddlewareInterface $middleware)
125
    {
126
        $this->middlewares[] = $middleware;
127
    }
128
129
    /**
130
     * Setup everything we need.
131
     *
132
     * @return void
133
     */
134
    public function setup(): void
135
    {
136
        $this->status = $this->getOption('daemon');
137
138
        $this->setDriver();
139
140
        $this->setChannel();
141
142
        $this->setIpSource();
143
144
        $this->setLogger();
145
146
        $this->setCoreModules();
147
148
        $this->setSessionLimit();
149
150
        $this->setCronJob();
151
152
        $this->setExcludedUrls();
153
154
        $this->setXssProtection();
155
156
        $this->setAuthentication();
157
158
        $this->setDialogUI();
159
160
        $this->setMessengers();
161
162
        $this->setMessageEvents();
0 ignored issues
show
Bug introduced by
The method setMessageEvents() does not exist on Shieldon\Firewall\Firewall. Did you maybe mean setMessengers()? ( Ignorable by Annotation )

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

162
        $this->/** @scrutinizer ignore-call */ 
163
               setMessageEvents();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
163
164
        $this->setDenyAttempts();
165
166
        $this->setIptablesWatchingFolder();
167
    }
168
169
    /**
170
     * Just, run!
171
     *
172
     * @return ResponseInterface
173
     */
174
    public function run(): ResponseInterface
175
    {
176
        // If settings are ready, let's start monitoring requests.
177
        if ($this->status) {
178
179
            $response = get_request();
180
181
            // PSR-15 request handler.
182
            $requestHandler = new RequestHandler();
183
184
            foreach ($this->middlewares as $middleware) {
185
                $requestHandler->add($middleware);
186
            }
187
188
            $response = $requestHandler->handle($response);
189
190
            // Something is detected by Middlewares, return.
191
            if ($response->getStatusCode() !== 200) {
192
                return $response;
193
            }
194
195
            $result = $this->kernel->run();
196
197
            if ($result !== $this->kernel::RESPONSE_ALLOW) {
198
199
                if ($this->kernel->captchaResponse()) {
200
                    $this->kernel->unban();
201
202
                    $response = $response->withHeader('Location', $this->kernel->getCurrentUrl());
203
                    $response = $response->withStatus(303);
204
205
                    return $response;
206
                }
207
            }
208
        }
209
210
        return $this->kernel->respond();
211
    }
212
213
    /**
214
     * Set the channel ID.
215
     *
216
     * @return void
217
     */
218
    protected function setChannel(): void
219
    {
220
        $channelId = $this->getOption('channel_id');
221
222
        if ($channelId) {
223
            $this->kernel->setChannel($channelId);
224
        }
225
    }
226
227
    /**
228
     * Set a data driver for the use of Shiedon Firewall.
229
     * Currently supports File, Redis, MySQL and SQLite.
230
     *
231
     * @return void
232
     */
233
    protected function setDriver(): void
234
    {
235
        $driverType = $this->getOption('driver_type');
236
        $driverSetting = $this->getOption($driverType, 'drivers');
0 ignored issues
show
Bug introduced by
It seems like $driverType can also be of type false; however, parameter $option of Shieldon\Firewall\Firewall::getOption() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

236
        $driverSetting = $this->getOption(/** @scrutinizer ignore-type */ $driverType, 'drivers');
Loading history...
237
238
        if (isset($driverSetting['directory_path'])) {
239
            $driverSetting['directory_path'] = $driverSetting['directory_path'] ?: $this->directory;
240
        }
241
242
        $driverInstance = DriverFactory::getInstance($driverType, $driverSetting);
0 ignored issues
show
Bug introduced by
It seems like $driverType can also be of type false; however, parameter $type of Shieldon\Firewall\Firewa...rFactory::getInstance() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

242
        $driverInstance = DriverFactory::getInstance(/** @scrutinizer ignore-type */ $driverType, $driverSetting);
Loading history...
243
244
        $this->status = false;
245
        if ($driverInstance !== null) {
246
            $this->kernel->add($driverInstance);
247
            $this->status = true;
248
        }
249
    }
250
251
    /**
252
     * Set up the action logger.
253
     *
254
     * @return void
255
     */
256
    protected function setLogger(): void
257
    {
258
        $loggerSetting = $this->getOption('action', 'loggers');
259
260
        if ($loggerSetting['enable']) {
261
            if (!empty($loggerSetting['config']['directory_path'])) {
262
                $this->kernel->add(new ActionLogger($loggerSetting['config']['directory_path']));
263
            }
264
        }
265
    }
266
267
    /**
268
     * If you use CDN, please choose the real IP source.
269
     *
270
     * @return void
271
     */
272
    protected function setIpSource(): void
273
    {
274
        $ipSourceType = $this->getOption('ip_variable_source');
275
        $serverParams = get_request()->getServerParams();
276
277
        /**
278
         * REMOTE_ADDR: general
279
         * HTTP_CF_CONNECTING_IP: Cloudflare
280
         * HTTP_X_FORWARDED_FOR: Google Cloud CDN, Google Load-balancer, AWS.
281
         * HTTP_X_FORWARDED_HOST: KeyCDN, or other CDN providers not listed here.
282
         * 
283
         */
284
        $key = array_search(true, $ipSourceType);
0 ignored issues
show
Bug introduced by
It seems like $ipSourceType can also be of type false; however, parameter $haystack of array_search() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

284
        $key = array_search(true, /** @scrutinizer ignore-type */ $ipSourceType);
Loading history...
285
        $ip = $serverParams[$key];
286
287
        if (empty($ip)) {
288
            // @codeCoverageIgnoreStart
289
            throw new RuntimeException('IP source is not set correctly.');
290
            // @codeCoverageIgnoreEnd
291
        }
292
293
        $this->kernel->setIp($ip);
294
    }
295
296
    protected function setCoreModules(): void
297
    {
298
        /*
299
        |--------------------------------------------------------------------------
300
        | Filters
301
        |--------------------------------------------------------------------------
302
        | (1) Session.
303
        | (2) Cookie generated by JavaScript code.
304
        | (3) HTTP referrer information.
305
        | (4) Pageview frequency.
306
        */
307
308
        $sessionSetting   = $this->getOption('session', 'filters');
309
        $cookieSetting    = $this->getOption('cookie', 'filters');
310
        $refererSetting   = $this->getOption('referer', 'filters');
311
        $frequencySetting = $this->getOption('frequency', 'filters');
312
313
        $filterConfig = [
314
            'session'   => $sessionSetting['enable'],
315
            'cookie'    => $cookieSetting['enable'],
316
            'referer'   => $refererSetting['enable'],
317
            'frequency' => $frequencySetting['enable'],
318
        ];
319
320
        $this->kernel->setFilters($filterConfig);
321
322
        $this->kernel->setProperty('limit_unusual_behavior', [
323
            'session' => $sessionSetting['config']['quota'] ?? 5,
324
            'cookie'  => $cookieSetting['config']['quota'] ?? 5,
325
            'referer' => $refererSetting['config']['quota'] ?? 5,
326
        ]);
327
328
        // if ($frequencySetting['enable']) {
329
        $frequencyQuota = [
330
            's' => $frequencySetting['config']['quota_s'] ?? 2,
331
            'm' => $frequencySetting['config']['quota_m'] ?? 10,
332
            'h' => $frequencySetting['config']['quota_h'] ?? 30,
333
            'd' => $frequencySetting['config']['quota_d'] ?? 60,
334
        ];
335
336
        $this->kernel->setProperty('time_unit_quota', $frequencyQuota);
337
338
        // if ($cookieSetting['enable']) {
339
        $cookieName = $cookieSetting['config']['cookie_name'] ?? 'ssjd';
340
        $cookieDomain = $cookieSetting['config']['cookie_domain'] ?? '';
341
        $cookieValue = $cookieSetting['config']['cookie_value'] ?? '1';
342
343
        $this->kernel->setProperty('cookie_name', $cookieName);
344
        $this->kernel->setProperty('cookie_domain', $cookieDomain);
345
        $this->kernel->setProperty('cookie_value', $cookieValue);
346
347
        // if ($refererSetting['enable']) {
348
        $this->kernel->setProperty('interval_check_referer', $refererSetting['config']['time_buffer']);
349
350
        // if ($sessionSetting['enable']) {
351
        $this->kernel->setProperty('interval_check_session', $sessionSetting['config']['time_buffer']);
352
353
        /*
354
        |--------------------------------------------------------------------------
355
        | Components
356
        |--------------------------------------------------------------------------
357
        | (1) Ip
358
        | (2) Rdns
359
        | (3) Header
360
        | (4) User-agent
361
        | (5) Trusted bot
362
        */
363
364
        $componentConfig = [
365
            'Ip' => $this->getOption('ip', 'components'),
366
            'Rdns' => $this->getOption('rdns', 'components'),
367
            'Header' => $this->getOption('header', 'components'),
368
            'UserAgent' => $this->getOption('user_agent', 'components'),
369
            'TrustedBot' => $this->getOption('trusted_bot', 'components'),
370
        ];
371
372
        foreach ($componentConfig as $className => $config) {
373
            $class = 'Shieldon\Firewall\Component\\' . $className;
374
375
            if ($config['enable']) {
376
                $componentInstance = new $class();
377
378
                if ($className === 'Ip') {
379
                    $this->kernel->add($componentInstance);
380
381
                    // Need Ip component to be loaded before calling this method.
382
                    $this->applyComponentIpManager();
383
                    
384
                } elseif ($config['strict_mode']) {
385
                    $componentInstance->setStrict(true);
386
                    $this->kernel->add($componentInstance);
387
                }
388
            }
389
        }
390
391
        /*
392
        |--------------------------------------------------------------------------
393
        | Captcha modules.
394
        |--------------------------------------------------------------------------
395
        | (1) Google ReCaptcha
396
        | (2) Simple image captcha.
397
        */
398
399
        $captchaList = [
400
            'recaptcha',
401
            'image',
402
        ];
403
404
        foreach ($captchaList as $captcha) {
405
            $setting = $this->getOption($captcha, 'captcha_modules');
406
407
            if (is_array($setting)) {
408
409
                // Initialize messenger instances from the factory/
410
                if (CaptchaFactory::check($captcha, $setting)) {
411
    
412
                    $this->kernel->add(
413
                        CaptchaFactory::getInstance(
414
                            // The ID of the captcha module in the configuration.
415
                            $captcha, 
416
                            // The settings of the captcha module in the configuration.
417
                            $setting    
418
                        )
419
                    );
420
                }
421
            }
422
423
            unset($setting);
424
        }
425
    }
426
427
    /**
428
     * Set the messenger modules.
429
     *
430
     * @return void
431
     */
432
    protected function setMessengers(): void
433
    {
434
        /*
435
        |--------------------------------------------------------------------------
436
        | Set messenger modules.
437
        |--------------------------------------------------------------------------
438
        */
439
440
        $messengerList = [
441
            'telegram',
442
            'line_notify',
443
            'sendgrid',
444
            'native_php_mail',
445
            'smtp',
446
            'mailgun',
447
            'rocket_chat',
448
            'slack',
449
            'slack_webhook',
450
        ];
451
452
        foreach ($messengerList as $messenger) {
453
            $setting = $this->getOption($messenger, 'messengers');
454
455
            if (is_array($setting)) {
456
457
                // Initialize messenger instances from the factory/
458
                if (MessengerFactory::check($messenger, $setting)) {
459
    
460
                    $this->kernel->add(
461
                        MessengerFactory::getInstance(
462
                            // The ID of the messenger module in the configuration.
463
                            $messenger, 
464
                            // The settings of the messenger module in the configuration.
465
                            $setting    
466
                        )
467
                    );
468
                }
469
            }
470
471
            unset($setting);
472
        }
473
474
       /*
475
        |--------------------------------------------------------------------------
476
        | Set messenger events.
477
        |--------------------------------------------------------------------------
478
        */
479
480
        $setting = $this->getOption('failed_attempts_in_a_row', 'events');
481
482
        $notifyDataCircle     = $setting['data_circle']['messenger']     ?: false;
483
        $notifySystemFirewall = $setting['system_firewall']['messenger'] ?: false;
484
485
        $this->kernel->setProperty('deny_attempt_notify', [
486
            'data_circle'     => $notifyDataCircle,
487
            'system_firewall' => $notifySystemFirewall,
488
        ]);
489
    }
490
491
    /**
492
     * Set deny attempts.
493
     *
494
     * @return void
495
     */
496
    protected function setDenyAttempts(): void
497
    {
498
        $setting = $this->getOption('failed_attempts_in_a_row', 'events');
499
500
        $enableDataCircle     = $setting['data_circle']['enable']     ?: false;
501
        $enableSystemFirewall = $setting['system_firewall']['enable'] ?: false;
502
503
        $this->kernel->setProperty('deny_attempt_enable', [
504
            'data_circle'     => $enableDataCircle,
505
            'system_firewall' => $enableSystemFirewall,
506
        ]);
507
508
        $this->kernel->setProperty('deny_attempt_buffer', [
509
            'data_circle'     => $setting['data_circle']['buffer'] ?? 10,
510
            'system_firewall' => $setting['data_circle']['buffer'] ?? 10,
511
        ]);
512
513
        // Check the time of the last failed attempt. @since 0.2.0
514
        $recordAttempt = $this->getOption('record_attempt');
515
516
        $detectionPeriod = $recordAttempt['detection_period'] ?? 5;
517
        $timeToReset     = $recordAttempt['time_to_reset']    ?? 1800;
518
519
        $this->kernel->setProperty('record_attempt_detection_period', $detectionPeriod);
520
        $this->kernel->setProperty('reset_attempt_counter', $timeToReset);
521
    }
522
523
    /**
524
     * Set iptables working folder.
525
     *
526
     * @return void
527
     */
528
    protected function setIptablesWatchingFolder(): void
529
    {
530
        $iptablesSetting = $this->getOption('config', 'iptables');
531
        $this->kernel->setProperty('iptables_watching_folder',  $iptablesSetting['watching_folder']);
532
    }
533
534
    /**
535
     * Set the online session limit.
536
     *
537
     * @return void
538
     */
539
    protected function setSessionLimit(): void
540
    {
541
        $sessionLimitSetting = $this->getOption('online_session_limit');
542
543
        if ($sessionLimitSetting['enable']) {
544
545
            $onlineUsers = $sessionLimitSetting['config']['count']  ?? 100;
546
            $alivePeriod = $sessionLimitSetting['config']['period'] ?? 300;
547
548
            $this->kernel->limitSession($onlineUsers, $alivePeriod);
549
        }
550
    }
551
552
    /**
553
     * Set the cron job.
554
     * This is triggered by the pageviews, not system cron job.
555
     *
556
     * @return void
557
     */
558
    protected function setCronJob(): void 
559
    {
560
        if (!$this->status) {
561
            return;
562
        }
563
564
        $cronjobSetting = $this->getOption('reset_circle', 'cronjob');
565
566
        if ($cronjobSetting['enable']) {
567
568
            $nowTime = time();
569
570
            $lastResetTime = $cronjobSetting['config']['last_update'];
571
572
            if (!empty($lastResetTime) ) {
573
                $lastResetTime = strtotime($lastResetTime);
574
            } else {
575
                // @codeCoverageIgnoreStart
576
                $lastResetTime = strtotime(date('Y-m-d 00:00:00'));
577
                // @codeCoverageIgnoreEnd
578
            }
579
580
            if (($nowTime - $lastResetTime) > $cronjobSetting['config']['period']) {
581
582
                $updateResetTime = date('Y-m-d 00:00:00');
583
584
                // Update new reset time.
585
                $this->setConfig('cronjob.reset_circle.config.last_update', $updateResetTime);
586
                $this->updateConfig();
587
588
                // Remove all logs.
589
                $this->kernel->driver->rebuild();
590
            }
591
        }
592
    }
593
594
    /**
595
     * Set the URLs that want to be excluded from Shieldon protection.
596
     *
597
     * @return void
598
     */
599
    protected function setExcludedUrls(): void
600
    {
601
        $excludedUrls = $this->getOption('excluded_urls');
602
603
        if (!empty($excludedUrls)) {
604
            $list = array_column($excludedUrls, 'url');
605
606
            $this->kernel->setExcludedUrls($list);
607
        }
608
    }
609
610
611
612
    /**
613
     * WWW-Athentication.
614
     *
615
     * @return void
616
     */
617
    protected function setAuthentication(): void
618
    {
619
        $authenticateList = $this->getOption('www_authenticate');
620
621
        if (is_array($authenticateList)) {
622
            $this->add(new Middleware\httpAuthentication($authenticateList));
623
        }
624
    }
625
626
    /**
627
     * Apply the denied list and the allowed list to Ip Component.
628
     */
629
    protected function applyComponentIpManager()
630
    {
631
        $ipList = $this->getOption('ip_manager');
632
633
        $allowedList = [];
634
        $deniedList = [];
635
636
        if (is_array($ipList)) {
637
            foreach ($ipList as $ip) {
638
639
                if (0 === strpos($this->kernel->getCurrentUrl(), $ip['url']) ) {
640
    
641
                    if ('allow' === $ip['rule']) {
642
                        $allowedList[] = $ip['ip'];
643
                    }
644
    
645
                    if ('deny' === $ip['rule']) {
646
                        $deniedList[] = $ip['ip'];
647
                    }
648
                }
649
            }
650
        }
651
652
        $this->kernel->component['Ip']->setAllowedItems($allowedList);
653
        $this->kernel->component['Ip']->setDeniedItems($deniedList);
654
    }
655
656
    /**
657
     * Set dialog UI.
658
     *
659
     * @return void
660
     */
661
    protected function setDialogUI()
662
    {
663
        $ui = $this->getOption('dialog_ui');
664
665
        if (!empty($ui)) {
666
            get_session()->set('shieldon_ui_lang', $ui['lang']);
667
            $this->kernel->setDialogUI($this->getOption('dialog_ui'));
668
        }
669
    }
670
}
671