Passed
Push — 2.x ( a2237c...c5863b )
by Terry
02:56
created

Firewall::setIpSource()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 22
rs 10
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->setFilters();
147
148
        $this->setComponents();
149
150
        $this->setCaptchas();
151
152
        $this->setSessionLimit();
153
154
        $this->setCronJob();
155
156
        $this->setExcludedUrls();
157
158
        $this->setXssProtection();
159
160
        $this->setAuthentication();
161
162
        $this->setDialogUI();
163
164
        $this->setMessengers();
165
166
        $this->setMessageEvents();
167
168
        $this->setDenyAttempts();
169
170
        $this->setIptablesWatchingFolder();
171
    }
172
173
    /**
174
     * Just, run!
175
     *
176
     * @return ResponseInterface
177
     */
178
    public function run(): ResponseInterface
179
    {
180
        // If settings are ready, let's start monitoring requests.
181
        if ($this->status) {
182
183
            $response = get_request();
184
185
            // PSR-15 request handler.
186
            $requestHandler = new RequestHandler();
187
188
            foreach ($this->middlewares as $middleware) {
189
                $requestHandler->add($middleware);
190
            }
191
192
            $response = $requestHandler->handle($response);
193
194
            // Something is detected by Middlewares, return.
195
            if ($response->getStatusCode() !== 200) {
196
                return $response;
197
            }
198
199
            $result = $this->kernel->run();
200
201
            if ($result !== $this->kernel::RESPONSE_ALLOW) {
202
203
                if ($this->kernel->captchaResponse()) {
204
                    $this->kernel->unban();
205
206
                    $response = $response->withHeader('Location', $this->kernel->getCurrentUrl());
207
                    $response = $response->withStatus(303);
208
209
                    return $response;
210
                }
211
            }
212
        }
213
214
        return $this->kernel->respond();
215
    }
216
217
    /**
218
     * Set the channel ID.
219
     *
220
     * @return void
221
     */
222
    protected function setChannel(): void
223
    {
224
        $channelId = $this->getOption('channel_id');
225
226
        if ($channelId) {
227
            $this->kernel->setChannel($channelId);
228
        }
229
    }
230
231
    /**
232
     * Set a data driver for the use of Shiedon Firewall.
233
     * Currently supports File, Redis, MySQL and SQLite.
234
     *
235
     * @return void
236
     */
237
    protected function setDriver(): void
238
    {
239
        $driverType = $this->getOption('driver_type');
240
        $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

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

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

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