Passed
Push — 2.x ( a1962c...4a3192 )
by Terry
02:33
created

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