Passed
Push — 2.x ( 97fc32...008340 )
by Terry
02:14
created

Firewall::setExcludedUrls()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

254
        $key = array_search(true, /** @scrutinizer ignore-type */ $ipSourceType);
Loading history...
255
        $ip = $serverParams[$key];
256
257
        if (empty($ip)) {
258
            // @codeCoverageIgnoreStart
259
            throw new RuntimeException('IP source is not set correctly.');
260
            // @codeCoverageIgnoreEnd
261
        }
262
263
        $this->kernel->setIp($ip);
264
    }
265
266
    /**
267
     * Set deny attempts.
268
     *
269
     * @return void
270
     */
271
    protected function setDenyAttempts(): void
272
    {
273
        $setting = $this->getOption('failed_attempts_in_a_row', 'events');
274
275
        $enableDataCircle     = $setting['data_circle']['enable']     ?: false;
276
        $enableSystemFirewall = $setting['system_firewall']['enable'] ?: false;
277
278
        $this->kernel->setProperty('deny_attempt_enable', [
279
            'data_circle'     => $enableDataCircle,
280
            'system_firewall' => $enableSystemFirewall,
281
        ]);
282
283
        $this->kernel->setProperty('deny_attempt_buffer', [
284
            'data_circle'     => $setting['data_circle']['buffer'] ?? 10,
285
            'system_firewall' => $setting['data_circle']['buffer'] ?? 10,
286
        ]);
287
288
        // Check the time of the last failed attempt. @since 0.2.0
289
        $recordAttempt = $this->getOption('record_attempt');
290
291
        $detectionPeriod = $recordAttempt['detection_period'] ?? 5;
292
        $timeToReset     = $recordAttempt['time_to_reset']    ?? 1800;
293
294
        $this->kernel->setProperty('record_attempt_detection_period', $detectionPeriod);
295
        $this->kernel->setProperty('reset_attempt_counter', $timeToReset);
296
    }
297
298
    /**
299
     * Set iptables working folder.
300
     *
301
     * @return void
302
     */
303
    protected function setIptablesWatchingFolder(): void
304
    {
305
        $iptablesSetting = $this->getOption('config', 'iptables');
306
        $this->kernel->setProperty('iptables_watching_folder',  $iptablesSetting['watching_folder']);
307
    }
308
309
    /**
310
     * Set the online session limit.
311
     *
312
     * @return void
313
     */
314
    protected function setSessionLimit(): void
315
    {
316
        $sessionLimitSetting = $this->getOption('online_session_limit');
317
318
        if ($sessionLimitSetting['enable']) {
319
320
            $onlineUsers = $sessionLimitSetting['config']['count']  ?? 100;
321
            $alivePeriod = $sessionLimitSetting['config']['period'] ?? 300;
322
323
            $this->kernel->limitSession($onlineUsers, $alivePeriod);
324
        }
325
    }
326
327
    /**
328
     * Set the cron job.
329
     * This is triggered by the pageviews, not system cron job.
330
     *
331
     * @return void
332
     */
333
    protected function setCronJob(): void 
334
    {
335
        if (!$this->status) {
336
            return;
337
        }
338
339
        $cronjobSetting = $this->getOption('reset_circle', 'cronjob');
340
341
        if ($cronjobSetting['enable']) {
342
343
            $nowTime = time();
344
345
            $lastResetTime = $cronjobSetting['config']['last_update'];
346
347
            if (!empty($lastResetTime) ) {
348
                $lastResetTime = strtotime($lastResetTime);
349
            } else {
350
                // @codeCoverageIgnoreStart
351
                $lastResetTime = strtotime(date('Y-m-d 00:00:00'));
352
                // @codeCoverageIgnoreEnd
353
            }
354
355
            if (($nowTime - $lastResetTime) > $cronjobSetting['config']['period']) {
356
357
                $updateResetTime = date('Y-m-d 00:00:00');
358
359
                // Update new reset time.
360
                $this->setConfig('cronjob.reset_circle.config.last_update', $updateResetTime);
361
                $this->updateConfig();
362
363
                // Remove all logs.
364
                $this->kernel->driver->rebuild();
365
            }
366
        }
367
    }
368
369
    /**
370
     * Set the URLs that want to be excluded from Shieldon protection.
371
     *
372
     * @return void
373
     */
374
    protected function setExcludedUrls(): void
375
    {
376
        $excludedUrls = $this->getOption('excluded_urls');
377
378
        if (!empty($excludedUrls)) {
379
            $list = array_column($excludedUrls, 'url');
380
381
            $this->kernel->setExcludedUrls($list);
382
        }
383
    }
384
385
    /**
386
     * WWW-Athentication.
387
     *
388
     * @return void
389
     */
390
    protected function setAuthentication(): void
391
    {
392
        $authenticateList = $this->getOption('www_authenticate');
393
394
        if (is_array($authenticateList)) {
395
            $this->add(new Middleware\httpAuthentication($authenticateList));
396
        }
397
    }
398
399
    /**
400
     * Apply the denied list and the allowed list to Ip Component.
401
     */
402
    protected function applyComponentIpManager()
403
    {
404
        $ipList = $this->getOption('ip_manager');
405
406
        $allowedList = [];
407
        $deniedList = [];
408
409
        if (is_array($ipList)) {
410
            foreach ($ipList as $ip) {
411
412
                if (0 === strpos($this->kernel->getCurrentUrl(), $ip['url']) ) {
413
    
414
                    if ('allow' === $ip['rule']) {
415
                        $allowedList[] = $ip['ip'];
416
                    }
417
    
418
                    if ('deny' === $ip['rule']) {
419
                        $deniedList[] = $ip['ip'];
420
                    }
421
                }
422
            }
423
        }
424
425
        $this->kernel->component['Ip']->setAllowedItems($allowedList);
426
        $this->kernel->component['Ip']->setDeniedItems($deniedList);
427
    }
428
429
    /**
430
     * Set dialog UI.
431
     *
432
     * @return void
433
     */
434
    protected function setDialogUI()
435
    {
436
        $ui = $this->getOption('dialog_ui');
437
438
        if (!empty($ui)) {
439
            get_session()->set('shieldon_ui_lang', $ui['lang']);
440
            $this->kernel->setDialogUI($this->getOption('dialog_ui'));
441
        }
442
    }
443
}
444