Passed
Push — 2.x ( 57e8e7...97fc32 )
by Terry
02:21
created

Firewall::setLogger()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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