Passed
Push — 2.x ( e966a7...a2237c )
by Terry
02:13
created

Firewall::setDriver()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
c 0
b 0
f 0
nc 4
nop 0
dl 0
loc 15
rs 9.9666
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
use Shieldon\Firewall\Firewall\Driver\DriverFactory;
21
use Shieldon\Firewall\Middleware as Middleware;
22
23
use Shieldon\Messenger as Messenger;
24
use Shieldon\Firewall\Messenger\MessengerFactory;
25
use Shieldon\Firewall\Utils\Container;
26
use Shieldon\Firewall\Log\ActionLogger;
27
use Shieldon\Firewall\FirewallTrait;
28
use Shieldon\Firewall\Firewall\XssProtectionTrait;
29
use Shieldon\Psr15\RequestHandler;
30
use function Shieldon\Firewall\get_request;
31
use function Shieldon\Firewall\get_response;
32
33
use PDO;
34
use PDOException;
35
use Redis;
36
use RedisException;
37
use RuntimeException;
38
39
use function array_column;
40
use function defined;
41
use function file_exists;
42
use function file_get_contents;
43
use function file_put_contents;
44
use function is_dir;
45
use function json_decode;
46
use function json_encode;
47
use function mkdir;
48
use function rtrim;
49
use function strpos;
50
use function umask;
51
use function time;
52
use function strtotime;
53
use function date;
54
55
/**
56
 * Managed Firewall.
57
 */
58
class Firewall
59
{
60
    use FirewallTrait;
61
    use XssProtectionTrait;
62
63
    /**
64
     * Collection of PSR-7 or PSR-15 middlewares.
65
     *
66
     * @var array
67
     */
68
    protected $middlewares = [];
69
70
    /**
71
     * Constructor.
72
     */
73
    public function __construct(?ServerRequestInterface $request = null, ?ResponseInterface $response = null)
74
    {
75
        Container::set('firewall', $this);
76
77
        $this->kernel = new Kernel($request, $response);
78
    }
79
80
    /**
81
     * Set up the path of the configuration file.
82
     *
83
     * @param string $source The path.
84
     * @param string $type   The type.
85
     * 
86
     * @return void
87
     */
88
    public function configure(string $source, string $type = 'json')
89
    {
90
        if ($type === 'json') {
91
            $this->directory = rtrim($source, '\\/');
92
            $configFilePath = $this->directory . '/' . $this->filename;
93
94
            if (file_exists($configFilePath)) {
95
                $jsonString = file_get_contents($configFilePath);
96
97
            } else {
98
                $jsonString = file_get_contents(__DIR__ . '/../../config.json');
99
100
                if (defined('PHP_UNIT_TEST')) {
101
                    $jsonString = file_get_contents(__DIR__ . '/../../tests/config.json');
102
                }
103
            }
104
105
            $this->configuration = json_decode($jsonString, true);
106
            $this->kernel->managedBy('managed');
107
108
        } elseif ($type === 'php') {
109
            $this->configuration = require $source;
110
            $this->kernel->managedBy('config');
111
        }
112
113
        $this->setup();
114
    }
115
116
    /**
117
     * Add middlewares and use them before going into Shieldon kernal.
118
     *
119
     * @param MiddlewareInterface $middleware A PSR-15 middlewares.
120
     *
121
     * @return void
122
     */
123
    public function add(MiddlewareInterface $middleware)
124
    {
125
        $this->middlewares[] = $middleware;
126
    }
127
128
    /**
129
     * Setup everything we need.
130
     *
131
     * @return void
132
     */
133
    public function setup(): void
134
    {
135
        $this->status = $this->getOption('daemon');
136
137
        $this->setDriver();
138
139
        $this->setChannel();
140
141
        $this->setIpSource();
142
143
        $this->setLogger();
144
145
        $this->setFilters();
146
147
        $this->setComponents();
148
149
        $this->setCaptchas();
150
151
        $this->setSessionLimit();
152
153
        $this->setCronJob();
154
155
        $this->setExcludedUrls();
156
157
        $this->setXssProtection();
158
159
        $this->setAuthentication();
160
161
        $this->setDialogUI();
162
163
        $this->setMessengers();
164
165
        $this->setMessageEvents();
166
167
        $this->setDenyAttempts();
168
169
        $this->setIptablesWatchingFolder();
170
    }
171
172
    /**
173
     * Just, run!
174
     *
175
     * @return ResponseInterface
176
     */
177
    public function run(): ResponseInterface
178
    {
179
        // If settings are ready, let's start monitoring requests.
180
        if ($this->status) {
181
182
            $response = get_request();
183
184
            // PSR-15 request handler.
185
            $requestHandler = new RequestHandler();
186
187
            foreach ($this->middlewares as $middleware) {
188
                $requestHandler->add($middleware);
189
            }
190
191
            $response = $requestHandler->handle($response);
192
193
            // Something is detected by Middlewares, return.
194
            if ($response->getStatusCode() !== 200) {
195
                return $response;
196
            }
197
198
            $result = $this->kernel->run();
199
200
            if ($result !== $this->kernel::RESPONSE_ALLOW) {
201
202
                if ($this->kernel->captchaResponse()) {
203
                    $this->kernel->unban();
204
205
                    $response = $response->withHeader('Location', $this->kernel->getCurrentUrl());
206
                    $response = $response->withStatus(303);
207
208
                    return $response;
209
                }
210
            }
211
        }
212
213
        return $this->kernel->respond();
214
    }
215
216
    /**
217
     * Set the channel ID.
218
     *
219
     * @return void
220
     */
221
    protected function setChannel(): void
222
    {
223
        $channelId = $this->getOption('channel_id');
224
225
        if ($channelId) {
226
            $this->kernel->setChannel($channelId);
227
        }
228
    }
229
230
    /**
231
     * Set a data driver for the use of Shiedon Firewall.
232
     * Currently supports File, Redis, MySQL and SQLite.
233
     *
234
     * @return void
235
     */
236
    protected function setDriver(): void
237
    {
238
        $driverType = $this->getOption('driver_type');
239
        $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

239
        $driverSetting = $this->getOption(/** @scrutinizer ignore-type */ $driverType, 'drivers');
Loading history...
240
241
        if (isset($driverSetting['directory_path'])) {
242
            $driverSetting['directory_path'] = $driverSetting['directory_path'] ?: $this->directory;
243
        }
244
245
        $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

245
        $driverInstance = DriverFactory::getInstance(/** @scrutinizer ignore-type */ $driverType, $driverSetting);
Loading history...
246
247
        $this->status = false;
248
        if ($driverInstance !== null) {
249
            $this->kernel->add($driverInstance);
250
            $this->status = true;
251
        }
252
    }
253
254
    /**
255
     * Set up the action logger.
256
     *
257
     * @return void
258
     */
259
    protected function setLogger(): void
260
    {
261
        $loggerSetting = $this->getOption('action', 'loggers');
262
263
        if ($loggerSetting['enable']) {
264
            if (!empty($loggerSetting['config']['directory_path'])) {
265
                $this->kernel->add(new ActionLogger($loggerSetting['config']['directory_path']));
266
            }
267
        }
268
    }
269
270
    /**
271
     * If you use CDN, please choose the real IP source.
272
     *
273
     * @return void
274
     */
275
    protected function setIpSource(): void
276
    {
277
        $ipSourceType = $this->getOption('ip_variable_source');
278
        $serverParams = get_request()->getServerParams();
279
280
        /**
281
         * REMOTE_ADDR: general
282
         * HTTP_CF_CONNECTING_IP: Cloudflare
283
         * HTTP_X_FORWARDED_FOR: Google Cloud CDN, Google Load-balancer, AWS.
284
         * HTTP_X_FORWARDED_HOST: KeyCDN, or other CDN providers not listed here.
285
         * 
286
         */
287
        $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

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