Passed
Push — 2.x ( 2dab0a...b10d5f )
by Terry
02:01
created

do_dispatch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
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
 * php version 7.1.0
11
 *
12
 * @category  Web-security
13
 * @package   Shieldon
14
 * @author    Terry Lin <[email protected]>
15
 * @copyright 2019 terrylinooo
16
 * @license   https://github.com/terrylinooo/shieldon/blob/2.x/LICENSE MIT
17
 * @link      https://github.com/terrylinooo/shieldon
18
 * @see       https://shieldon.io
19
 */
20
21
declare(strict_types=1);
22
23
namespace Shieldon\Firewall;
24
25
use Psr\Http\Message\ServerRequestInterface;
26
use Psr\Http\Message\ResponseInterface;
27
use Shieldon\Firewall\HttpFactory;
28
use Shieldon\Firewall\Session;
29
use Shieldon\Firewall\Container;
30
use Shieldon\Firewall\EventDispatcher;
0 ignored issues
show
Bug introduced by
The type Shieldon\Firewall\EventDispatcher 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...
31
use Shieldon\Firewall\Driver\FileDriver;
32
use function explode;
33
use function file_exists;
34
use function func_get_arg;
35
use function func_num_args;
36
use function implode;
37
use function is_array;
38
use function is_null;
39
use function preg_split;
40
use function round;
41
use function shell_exec;
42
use function str_repeat;
43
use function str_replace;
44
use function stripos;
45
use function strtoupper;
46
use function substr;
47
use function sys_getloadavg;
48
use function trim;
49
use const PHP_OS;
50
51
/**
52
 * This value will be only displayed on Firewall Panel.
53
 */
54
define('SHIELDON_FIREWALL_VERSION', '2.0');
55
56
/**
57
 * Just use PSR-4 autoloader to load those helper functions.
58
 */
59
class Helpers
60
{
61
62
}
63
64
/**
65
 *   Public methods       | Desctiotion
66
 *  ----------------------|---------------------------------------------
67
 *  __                    | Get locale message.
68
 *  _e                    | Echo string from __()
69
 *  get_user_lang         | Get user lang.
70
 *  include_i18n_file     | Include i18n file.
71
 *  mask_string           | Mask strings with asterisks.
72
 *  get_cpu_usage         | Get current CPU usage information.
73
 *  get_memory_usage      | Get current RAM usage information. 
74
 *  get_default_properties| The default settings of Shieldon core.
75
 *  get_request           | Get PSR-7 HTTP server request from container.
76
 *  get_response          | Get PSR-7 HTTP response from container.
77
 *  set_request           | Set PSR-7 HTTP server request to container.
78
 *  set_response          | Set PSR-7 HTTP response to container.
79
 *  unset_global_cookie   | Unset superglobal COOKIE variable.F
80
 *  unset_global_post     | Unset superglobal POST variable.
81
 *  unset_global_get      | Unset superglobal GET variable.
82
 *  unset_global_session  | Unset superglobal SESSION variable.
83
 *  unset_superglobal     | Unset superglobal variables.
84
 *  get_ip                | Get an IP address from container.
85
 *  set_ip                | Set an IP address to container.
86
 *  get_microtimesamp     | Get the microtimesamp.
87
 *  get_session_instance  | Get a session instance.
88
 *  create_new_session_i- | Create a new session instance for current user.
89
 *  n stance              |
90
 *  get_mock_session      | For unit testing purpose.
91
 *  set_session_instance  | Set a session instance to container.
92
 *  get_session_id        | Get session ID from cookie or creating new.
93
 *  create_session_id     | Create a new session ID.
94
 *  ----------------------|---------------------------------------------
95
 */
96
97
/**
98
 * Get locale message.
99
 *
100
 * @return string
101
 */
102
function __(): string
103
{
104
    /**
105
     * Load locale string from i18n files and store them into this array 
106
     * for further use.
107
     * 
108
     * @var array 
109
     */
110
    static $i18n;
111
112
    /**
113
     * Check the file exists for not.
114
     *
115
     * @var array 
116
     */
117
    static $fileChecked;
118
119
    $num = func_num_args();
120
121
    $filename    = func_get_arg(0); // required.
122
    $langcode    = func_get_arg(1); // required.
123
    $placeholder = ($num > 2) ? func_get_arg(2) : '';
124
    $replacement = ($num > 3) ? func_get_arg(3) : [];
125
    $lang        = get_user_lang();
126
127
    if (empty($i18n[$filename]) && empty($fileChecked[$filename])) {
128
        $fileChecked[$filename] = true;
129
        $i18n[$filename] = include_i18n_file($lang, $filename);
130
    }
131
132
    // If we don't get the string from the localization file, use placeholder 
133
    // instead.
134
    $resultString = $placeholder;
135
136
    if (!empty($i18n[$filename][$langcode])) {
137
        $resultString = $i18n[$filename][$langcode];
138
    }
139
140
    if (is_array($replacement)) { 
141
        /**
142
         * Example:
143
         *     __('test', 'example_string', 'Search results: {0} items. Total items: {1}.', [5, 150]);
144
         * 
145
         * Result:
146
         *     Search results: 5 items. Total items: 150.
147
         */
148
        foreach ($replacement as $i => $r) {
149
            $resultString = str_replace('{' . $i . '}', $replacement[$i], $resultString);
150
        }
151
    }
152
    
153
    return str_replace("'", '’', $resultString);
154
}
155
156
/**
157
 * Echo string from __()
158
 *
159
 * @return void
160
 */
161
function _e(): void
162
{
163
    $num = func_num_args();
164
165
    $filename    = func_get_arg(0); // required.
166
    $langcode    = func_get_arg(1); // required.
167
    $placeholder = ($num > 2) ? func_get_arg(2) : '';
168
    $replacement = ($num > 3) ? func_get_arg(3) : [];
169
170
    echo __($filename, $langcode, $placeholder, $replacement);
171
}
172
173
/**
174
 * Get user lang.
175
 * 
176
 * This method is a part of  __()
177
 *
178
 * @return string
179
 */
180
function get_user_lang(): string
181
{
182
    static $lang;
183
184
    if (!$lang) {
185
        $lang = 'en';
186
187
        // Fetch session variables.
188
        $session = get_session_instance();
189
        $panelLang = $session->get('shieldon_panel_lang');
190
        $uiLang = $session->get('shieldon_ui_lang');
191
    
192
        if (!empty($panelLang)) {
193
            $lang = $panelLang;
194
        } elseif (!empty($uiLang)) {
195
            $lang = $uiLang;
196
        }
197
    }
198
199
    return $lang;
200
}
201
202
/**
203
 * Include i18n file.
204
 * 
205
 * This method is a part of  __()
206
 *
207
 * @param string $lang     The language code.
208
 * @param string $filename The i18n language pack file.
209
 *
210
 * @return void
211
 */
212
function include_i18n_file(string $lang, string $filename): array
213
{
214
    $content = [];
215
    $lang = str_replace('-', '_', $lang);
216
217
    if (stripos($lang, 'zh_') !== false) {
218
        if (stripos($lang, 'zh_CN') !== false) {
219
            $lang = 'zh_CN';
220
        } else {
221
            $lang = 'zh';
222
        }
223
    }
224
225
    $file = __DIR__ . '/../../localization/' . $lang . '/' . $filename . '.php';
226
    
227
    if (file_exists($file)) {
228
        $content = include $file;
229
    }
230
231
    return $content;
232
}
233
234
/**
235
 * Mask strings with asterisks.
236
 *
237
 * @param string $str The text.
238
 *
239
 * @return string
240
 */
241
function mask_string($str): string
242
{
243
    if (filter_var($str, FILTER_VALIDATE_IP) !== false) {
244
        $tmp = explode('.', $str);
245
        $tmp[0] = '*';
246
        $tmp[1] = '*';
247
        $masked = implode('.', $tmp);
248
249
    } else {
250
        $masked =  str_repeat('*', strlen($str) - 6) . substr($str, -6);
251
    }
252
253
    return $masked;
254
}
255
256
/**
257
 * Get current CPU usage information.
258
 *
259
 * This function is only used in sending notifications and it is unavailable 
260
 * on Win system. If you are using shared hosting and your hosting provider 
261
 * has disabled `sys_getloadavg` and `shell_exec`, it won't work either.
262
 *
263
 * @return string
264
 */
265
function get_cpu_usage(): string
266
{
267
    $return = '';
268
269
    // This feature is not available on Windows platform.
270
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
271
        return $return;
272
    }
273
274
    $cpuLoads = @sys_getloadavg();
275
    $cpuCores = trim(@shell_exec("grep -P '^processor' /proc/cpuinfo|wc -l"));
276
277
    if (!empty($cpuCores) && !empty($cpuLoads)) {
278
        $return = round($cpuLoads[1] / ($cpuCores + 1) * 100, 0) . '%';
279
    }
280
    return $return;
281
}
282
283
/**
284
 * Get current RAM usage information. 
285
 *
286
 * If you are using shared hosting and your hosting provider has disabled 
287
 * `shell_exec`, this function may not work as expected.
288
 *
289
 * @return string
290
 */
291
function get_memory_usage(): string
292
{
293
    $return = '';
294
295
    // This feature is not available on Windows platform.
296
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
297
        return $return;
298
    }
299
300
    $freeResult = explode("\n", trim(@shell_exec('free')));
301
302
    if (!empty($freeResult)) {
303
        $parsed = preg_split("/[\s]+/", $freeResult[1]);
304
        $return = round($parsed[2] / $parsed[1] * 100, 0) . '%';
305
    }
306
    return $return;
307
}
308
309
/**
310
 * The default settings of Shieldon core.
311
 *
312
 * @return array
313
 */
314
function get_default_properties(): array
315
{
316
    return [
317
318
        'time_unit_quota' => [
319
            's' => 2,
320
            'm' => 10,
321
            'h' => 30,
322
            'd' => 60
323
        ],
324
325
        'time_reset_limit'       => 3600,
326
        'interval_check_referer' => 5,
327
        'interval_check_session' => 30,
328
        'limit_unusual_behavior' => [
329
            'cookie'  => 5,
330
            'session' => 5,
331
            'referer' => 10
332
        ],
333
334
        'cookie_name'         => 'ssjd',
335
        'cookie_domain'       => '',
336
        'cookie_value'        => '1',
337
        'display_online_info' => true,
338
        'display_user_info'   => false,
339
340
        /**
341
         * If you set this option enabled, Shieldon will record every CAPTCHA fails 
342
         * in a row, once that user have reached the limitation number, Shieldon will
343
         * put it as a blocked IP in rule table, until the new data cycle begins.
344
         * 
345
         * Once that user have been blocked, they are still access the warning page, 
346
         * it means that they are not humain for sure, so let's throw them into the 
347
         * system firewall and say goodbye to them forever.
348
         */
349
        'deny_attempt_enable' => [
350
            'data_circle'     => false,
351
            'system_firewall' => false,
352
        ],
353
354
        'deny_attempt_notify' => [
355
            'data_circle'     => false,
356
            'system_firewall' => false,
357
        ],
358
359
        'deny_attempt_buffer' => [
360
            'data_circle'     => 10,
361
            'system_firewall' => 10,
362
        ],
363
364
        /**
365
         * To prevent dropping social platform robots into iptables firewall, such 
366
         * as Facebook, Line and others who scrape snapshots from your web pages, 
367
         * you should adjust the values below to fit your needs. (unit: second)
368
         */
369
        'record_attempt_detection_period' => 5, // 5 seconds.
370
371
        // Reset the counter after n second.
372
        'reset_attempt_counter' => 1800, // 30 minutes.
373
374
        // System-layer firewall, ip6table service watches this folder to 
375
        // receive command created by Shieldon Firewall.
376
        'iptables_watching_folder' => '/tmp/',
377
    ];
378
}
379
380
/*
381
|--------------------------------------------------------------------------
382
| PSR-7 helpers.
383
|--------------------------------------------------------------------------
384
*/
385
386
/**
387
 * PSR-7 HTTP server request
388
 *
389
 * @return ServerRequestInterface
390
 */
391
function get_request(): ServerRequestInterface
392
{
393
    $request = Container::get('request');
394
395
    if (is_null($request)) {
396
        $request = HttpFactory::createRequest();
397
        Container::set('request', $request);
398
    }
399
400
    return $request;
401
}
402
403
/**
404
 * PSR-7 HTTP response.
405
 *
406
 * @return ResponseInterface
407
 */
408
function get_response(): ResponseInterface
409
{
410
    $response = Container::get('response');
411
412
    if (is_null($response)) {
413
        $response = HttpFactory::createResponse();
414
        Container::set('response', $response);
415
    }
416
417
    return $response;
418
}
419
420
/**
421
 * Set a PSR-7 HTTP server request into container.
422
 *
423
 * @param ServerRequestInterface $request The PSR-7 server request.
424
 *
425
 * @return void
426
 */
427
function set_request(ServerRequestInterface $request): void
428
{
429
    Container::set('request', $request, true);
430
}
431
432
/**
433
 * Set a PSR-7 HTTP response into container.
434
 *
435
 * @param ResponseInterface $response The PSR-7 server response.
436
 *
437
 * @return void
438
 */
439
function set_response(ResponseInterface $response): void
440
{
441
    Container::set('response', $response, true);
442
}
443
444
/*
445
|--------------------------------------------------------------------------
446
| Superglobal variables.
447
|--------------------------------------------------------------------------
448
*/
449
450
/**
451
 * Unset cookie.
452
 *
453
 * @param string|null $name The name (key) in the array of the superglobal.
454
 *
455
 * @return void
456
 */
457
function unset_global_cookie($name = null): void
458
{
459
    if (empty($name)) {
460
        $cookieParams = get_request()->getCookieParams();
461
        set_request(get_request()->withCookieParams([]));
462
463
        foreach (array_keys($cookieParams) as $name) {
464
            set_response(
465
                get_response()->withHeader(
466
                    'Set-Cookie',
467
                    "$name=; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0"
468
                )
469
            );
470
        }
471
        $_COOKIE = [];
472
473
        return;
474
    }
475
476
    $cookieParams = get_request()->getCookieParams();
477
    unset($cookieParams[$name]);
478
479
    set_request(
480
        get_request()->withCookieParams(
481
            $cookieParams
482
        )
483
    );
484
485
    set_response(
486
        get_response()->withHeader(
487
            'Set-Cookie',
488
            "$name=; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0"
489
        )
490
    );
491
    // Prevent direct access to superglobal.
492
    unset($_COOKIE[$name]);
493
}
494
495
/**
496
 * Unset post.
497
 *
498
 * @param string|null $name The name (key) in the array of the superglobal.
499
 *
500
 * @return void
501
 */
502
function unset_global_post($name = null): void
503
{
504
    if (empty($name)) {
505
        set_request(get_request()->withParsedBody([]));
506
        $_POST = [];
507
508
        return;
509
    }
510
511
    $postParams = get_request()->getParsedBody();
512
    unset($postParams[$name]);
513
    set_request(get_request()->withParsedBody($postParams));
514
    unset($_POST[$name]);
515
}
516
517
/**
518
 * Unset get.
519
 *
520
 * @param string|null $name The name (key) in the array of the superglobal.
521
 *
522
 * @return void
523
 */
524
function unset_global_get($name = null): void
525
{
526
    if (empty($name)) {
527
        set_request(get_request()->withQueryParams([]));
528
        $_GET = [];
529
530
        return;
531
    }
532
533
    $getParams = get_request()->getQueryParams();
534
    unset($getParams[$name]);
535
    set_request(get_request()->withQueryParams($getParams));
536
    unset($_GET[$name]);
537
}
538
539
/**
540
 * Unset session.
541
 *
542
 * @param string|null $name The name (key) in the array of the superglobal.
543
 *
544
 * @return void
545
 */
546
function unset_global_session($name = null): void
547
{
548
    if (empty($name)) {
549
        get_session_instance()->clear();
550
        return;
551
    }
552
553
    get_session_instance()->remove($name);
554
}
555
556
/**
557
 * Unset a variable of superglobal.
558
 *
559
 * @param string|null $name The name (key) in the array of the superglobal.
560
 *                          If $name is null that means clear all.
561
 * @param string      $type The type of the superglobal.
562
 *
563
 * @return void
564
 */
565
function unset_superglobal($name, string $type): void
566
{
567
    $types = [
568
        'get',
569
        'post',
570
        'cookie',
571
        'session',
572
    ];
573
574
    if (!in_array($type, $types)) {
575
        return;
576
    }
577
578
    $method = '\Shieldon\Firewall\unset_global_' . $type;
579
    $method($name, $type);
580
}
581
582
/*
583
|--------------------------------------------------------------------------
584
| IP address.
585
|--------------------------------------------------------------------------
586
*/
587
588
/**
589
 * Get an IP address.
590
 *
591
 * @return string
592
 */
593
function get_ip(): string
594
{
595
    $ip = Container::get('ip_address');
596
    
597
    if (empty($ip)) {
598
        $ip = $_SERVER['REMOTE_ADDR'];
599
    }
600
601
    return $ip;
602
}
603
604
/**
605
 * Set an IP address.
606
 *
607
 * @param string $ip An IP address.
608
 *
609
 * @return void
610
 */
611
function set_ip(string $ip)
612
{
613
    Container::set('ip_address', $ip, true);
614
}
615
616
/*
617
|--------------------------------------------------------------------------
618
| Time.
619
|--------------------------------------------------------------------------
620
*/
621
622
/**
623
 * Get the microtimesamp.
624
 * 
625
 * @return string
626
 */
627
function get_microtimesamp()
628
{
629
    $microtimesamp = explode(' ', microtime());
630
    $microtimesamp = $microtimesamp[1] . str_replace('0.', '', $microtimesamp[0]);
631
632
    return $microtimesamp;
633
}
634
635
/*
636
|--------------------------------------------------------------------------
637
| Session.
638
|--------------------------------------------------------------------------
639
*/
640
641
/**
642
 * Session
643
 * 
644
 * @return Session
645
 */
646
function get_session_instance(): Session
647
{
648
    $session = Container::get('session');
649
650
    if (is_null($session)) {
651
652
        // For unit testing purpose. Not use in production.
653
        if (php_sapi_name() === 'cli') {
654
            $session = get_mock_session(get_session_id());
655
        } else {
656
            $session = HttpFactory::createSession(get_session_id());
657
        }
658
659
        set_session_instance($session);
660
    }
661
662
    return $session;
663
}
664
665
/**
666
 * For unit testing purpose. Not use in production.
667
 * Create new session by specifying a session ID.
668
 * 
669
 * @param string $sessionId A session ID string.
670
 *
671
 * @return void
672
 */
673
function create_new_session_instance(string $sessionId)
674
{
675
    Container::set('session_id', $sessionId, true);
676
    $session = get_mock_session($sessionId);
677
    set_session_instance($session);
678
}
679
680
/**
681
 * For unit testing purpose. Not use in production.
682
 *
683
 * @return Session
684
 */
685
function get_mock_session($sessionId): Session
686
{
687
    Container::set('session_id', $sessionId, true);
688
689
    // Constant BOOTSTRAP_DIR is available in unit testing mode.
690
    $fileDriverStorage = BOOTSTRAP_DIR . '/../tmp/shieldon/data_driver_file';
691
    $dir = $fileDriverStorage . '/shieldon_sessions';
692
    $file = $dir . '/' . $sessionId . '.json';
693
694
    $session = HttpFactory::createSession($sessionId);
695
    $driver = new FileDriver($fileDriverStorage);
696
697
    if (!file_exists($file)) {
698
699
        // Prepare mock data.
700
        $data['id'] = $sessionId;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
701
        $data['ip'] = get_ip();
702
        $data['time'] = time();
703
        $data['microtimesamp'] = get_microtimesamp();
704
        $data['data'] = '{}';
705
    
706
        $json = json_encode($data);
707
        
708
        // Check and build the folder.
709
        $originalUmask = umask(0);
710
711
        if (!is_dir($dir)) {
712
            mkdir($dir, 0777, true);
713
        }
714
715
        umask($originalUmask);
716
717
        file_put_contents($file, $json);
718
    }
719
720
    $session->init($driver);
721
722
    return $session;
723
}
724
725
/**
726
 * Set the Session, if exists, it will be overwritten.
727
 *
728
 * @param Session $session The session instance.
729
 * 
730
 * @return void
731
 */
732
function set_session_instance(Session $session): void
733
{
734
    Container::set('session', $session, true);
735
}
736
737
/**
738
 * Get session ID.
739
 *
740
 * @return string
741
 */
742
function get_session_id(): string
743
{
744
    static $sessionId;
745
746
    if (!$sessionId) {
747
        $cookie = get_request()->getCookieParams();
748
749
        if (!empty($cookie['_shieldon'])) {
750
            $sessionId = $cookie['_shieldon'];
751
        } else {
752
            $sessionId = create_session_id();
753
        }
754
    }
755
756
    return $sessionId;
757
}
758
759
/**
760
 * Create a hash code for the Session ID.
761
 *
762
 * @return string
763
 */
764
function create_session_id(): string
765
{
766
    $hash =  rand() . 'ej;1zj47vu;3e;31g642941ek62au/41' . time();
767
768
    return md5($hash);
769
}