FilterTrait::filterFrequency()   B
last analyzed

Complexity

Conditions 7
Paths 2

Size

Total Lines 51
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 7

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 29
c 2
b 0
f 0
dl 0
loc 51
ccs 23
cts 23
cp 1
rs 8.5226
cc 7
nc 2
nop 3
crap 7

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Kernel;
24
25
use Shieldon\Firewall\Kernel\Enum;
26
use function Shieldon\Firewall\get_request;
27
use function Shieldon\Firewall\get_session_instance;
28
use function Shieldon\Firewall\unset_superglobal;
29
use function time;
30
use function array_keys;
31
32
/*
33
 * This trait is used on Kernel only.
34
 */
35
trait FilterTrait
36
{
37
    /**
38
     *   Public methods       | Desctiotion
39
     *  ----------------------|---------------------------------------------
40
     *   setFilters           | Set the filters.
41
     *   setFilter            | Set a filter.
42
     *   disableFilters       | Disable all filters.
43
     *  ----------------------|---------------------------------------------
44
     */
45
46
    /**
47
     * Enable or disable the filters.
48
     *
49
     * @var array
50
     */
51
    protected $filterStatus = [
52
        /**
53
         * Check how many pageviews an user made in a short period time.
54
         * For example, limit an user can only view 30 pages in 60 minutes.
55
         */
56
        'frequency' => true,
57
58
        /**
59
         * If an user checks any internal link on your website, the user's
60
         * browser will generate HTTP_REFERER information.
61
         * When a user view many pages without HTTP_REFERER information meaning
62
         * that the user MUST be a web crawler.
63
         */
64
        'referer' => false,
65
66
        /**
67
         * Most of web crawlers do not render JavaScript, they only get the
68
         * content they want, so we can check whether the cookie can be created
69
         * by JavaScript or not.
70
         */
71
        'cookie' => false,
72
73
        /**
74
         * Every unique user should only has a unique session, but if a user
75
         * creates different sessions every connection... meaning that the
76
         * user's browser doesn't support cookie.
77
         * It is almost impossible that modern browsers not support cookie,
78
         * therefore the user MUST be a web crawler.
79
         */
80
        'session' => false,
81
    ];
82
83
    /**
84
     * The status for Filters to reset.
85
     *
86
     * @var array
87
     */
88
    protected $filterResetStatus = [
89
        's' => false, // second.
90
        'm' => false, // minute.
91
        'h' => false, // hour.
92
        'd' => false, // day.
93
    ];
94
95
    /**
96
     * Start an action for this IP address, allow or deny, and give a reason for it.
97
     *
98
     * @param int    $actionCode The action code. - 0: deny, 1: allow, 9: unban.
99
     * @param string $reasonCode The response code.
100
     * @param string $assignIp   The IP address.
101
     *
102
     * @return void
103
     */
104
    abstract public function action(int $actionCode, int $reasonCode, string $assignIp = ''): void;
105
106
    /**
107
     * Set the filters.
108
     *
109
     * @param array $settings filter settings.
110
     *
111
     * @return void
112
     */
113 108
    public function setFilters(array $settings)
114
    {
115 108
        foreach (array_keys($this->filterStatus) as $k) {
116 108
            if (isset($settings[$k])) {
117 108
                $this->filterStatus[$k] = $settings[$k] ?? false;
118
            }
119
        }
120
    }
121
122
    /**
123
     * Set a filter.
124
     *
125
     * @param string $filterName The filter's name.
126
     * @param bool   $value      True for enabling the filter, overwise.
127
     *
128
     * @return void
129
     */
130 5
    public function setFilter(string $filterName, bool $value): void
131
    {
132 5
        if (isset($this->filterStatus[$filterName])) {
133 5
            $this->filterStatus[$filterName] = $value;
134
        }
135
    }
136
137
    /**
138
     * Disable filters.
139
     *
140
     * @return void
141
     */
142 78
    public function disableFilters(): void
143
    {
144 78
        $this->setFilters(
145 78
            [
146 78
                'session'   => false,
147 78
                'cookie'    => false,
148 78
                'referer'   => false,
149 78
                'frequency' => false,
150 78
            ]
151 78
        );
152
    }
153
154
    /*
155
    |--------------------------------------------------------------------------
156
    | Stage in Kernel
157
    |--------------------------------------------------------------------------
158
    | The below methods are used in "process" method in Kernel.
159
    */
160
161
    /**
162
     * Detect and analyze an user's behavior.
163
     *
164
     * @return int The response code.
165
     */
166 54
    protected function filter(): int
167
    {
168 54
        $now = time();
169 54
        $isFlagged = false;
170
171
        // Fetch an IP data from Shieldon log table.
172 54
        $ipDetail = $this->driver->get($this->ip, 'filter');
173
174 54
        $ipDetail = $this->driver->parseData($ipDetail, 'filter');
175 54
        $logData = $ipDetail;
176
177
        // Counting user pageviews.
178 54
        foreach (array_keys($this->filterResetStatus) as $unit) {
179
            // Each time unit will increase by 1.
180
            $logData['pageviews_' . $unit] = $ipDetail['pageviews_' . $unit] + 1;
181 54
            $logData['first_time_' . $unit] = $ipDetail['first_time_' . $unit];
182 54
        }
183
184
        $logData['first_time_flag'] = $ipDetail['first_time_flag'];
185 54
186
        if (!empty($ipDetail['ip'])) {
187 54
            $logData['ip'] = $this->ip;
188 31
            $logData['session'] = get_session_instance()->getId();
189 31
            $logData['hostname'] = $this->rdns;
190 31
            $logData['last_time'] = $now;
191 31
192
            // Start checking...
193
            foreach (array_keys($this->filterStatus) as $filter) {
194 31
                // For example: filterSession
195
                $method = 'filter' . ucfirst($filter);
196
197 31
                // For example: call $this->filterSession
198
                $filterReturnData = $this->{$method}($logData, $ipDetail, $isFlagged);
199
200 31
                // The log data will be updated by the filter.
201
                $logData = $filterReturnData['log_data'];
202
203 31
                // The flag will be passed to the next Filter.
204
                $isFlagged = $filterReturnData['is_flagged'];
205
206 31
                // If we find this session reached the filter limit, reject it.
207
                $isReject = $filterReturnData['is_reject'];
208
209 31
                if ($isReject) {
210
                    return Enum::RESPONSE_TEMPORARILY_DENY;
211 31
                }
212 31
            }
213
214
            // Is fagged as unusual beavior? Count the first time.
215
            if ($isFlagged) {
216
                $logData['first_time_flag'] = !empty($logData['first_time_flag'])
217 31
                    ? $logData['first_time_flag']
218 21
                    : $now;
219
            }
220
221
            // Reset the flagged factor check.
222 31
            if (!empty($ipDetail['first_time_flag'])) {
223 17
                if ($now - $ipDetail['first_time_flag'] >= $this->properties['time_reset_limit']) {
224 4
                    $logData['flag_multi_session'] = 0;
225 4
                    $logData['flag_empty_referer'] = 0;
226 4
                    $logData['flag_js_cookie'] = 0;
227
                }
228
            }
229
230 31
            $this->driver->save($this->ip, $logData, 'filter');
231
        } else {
232
            // If $ipDetail[ip] is empty.
233
            // It means that the user is first time visiting our webiste.
234
            $this->InitializeFirstTimeFilter($logData);
235
        }
236 54
237
        return Enum::RESPONSE_ALLOW;
238
    }
239 54
240
    /*
241
    |--------------------------------------------------------------------------
242
    | The below methods are used only in "filter" method in current Trait.
243
    | See "Start checking..."
244
    |--------------------------------------------------------------------------
245
    */
246
247
    /**
248
     * When the user is first time visiting our webiste.
249
     * Initialize the log data.
250
     *
251
     * @param array $logData The user's log data.
252
     *
253
     * @return void
254
     */
255
    protected function InitializeFirstTimeFilter($logData): void
256
    {
257 54
        $now = time();
258
259 54
        $logData['ip']        = $this->ip;
260
        $logData['session']   = get_session_instance()->getId();
261 54
        $logData['hostname']  = $this->rdns;
262 54
        $logData['last_time'] = $now;
263 54
264 54
        foreach (array_keys($this->filterResetStatus) as $unit) {
265
            $logData['first_time_' . $unit] = $now;
266 54
        }
267 54
268
        $this->driver->save($this->ip, $logData, 'filter');
269
    }
270 54
271
    /**
272
     * Filter - Referer.
273
     *
274
     * @param array $logData   IP data from Shieldon log table.
275
     * @param array $ipDetail  The IP log data.
276
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
277
     *
278
     * @return array
279
     */
280
    protected function filterReferer(array $logData, array $ipDetail, bool $isFlagged): array
281
    {
282 31
        $isReject = false;
283
284 31
        if ($this->filterStatus['referer']) {
285
            if ($logData['last_time'] - $ipDetail['last_time'] > $this->properties['interval_check_referer']) {
286 31
                // Get values from data table. We will count it and save it back to data table.
287 12
                // If an user is already in your website, it is impossible no referer when he views other pages.
288
                $logData['flag_empty_referer'] = $ipDetail['flag_empty_referer'];
289
290
                if (empty(get_request()->getHeaderLine('referer'))) {
291 8
                    $logData['flag_empty_referer']++;
292
                    $isFlagged = true;
293 8
                }
294 8
295 8
                // Ban this IP if they reached the limit.
296
                if ($logData['flag_empty_referer'] > $this->properties['limit_unusual_behavior']['referer']) {
297
                    $this->action(
298
                        Enum::ACTION_TEMPORARILY_DENY,
299 8
                        Enum::REASON_EMPTY_REFERER_DENIED
300 4
                    );
301 4
                    $isReject = true;
302 4
                }
303 4
            }
304 4
        }
305
306
        return [
307
            'is_flagged' => $isFlagged,
308
            'is_reject' => $isReject,
309 31
            'log_data' => $logData,
310 31
        ];
311 31
    }
312 31
313 31
    /**
314
     * Filter - Session
315
     *
316
     * @param array $logData   IP data from Shieldon log table.
317
     * @param array $ipDetail  The IP log data.
318
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
319
     *
320
     * @return array
321
     */
322
    protected function filterSession(array $logData, array $ipDetail, bool $isFlagged): array
323
    {
324
        $isReject = false;
325 31
        $sessionId = get_session_instance()->getId();
326
327 31
        if ($this->filterStatus['session']) {
328 31
            // Get values from data table. We will count it and save it back to data table.
329
            $logData['flag_multi_session'] = $ipDetail['flag_multi_session'];
330 31
331
            if ($sessionId !== $ipDetail['session']) {
332
                // Is is possible because of direct access by the same user many times.
333 9
                // Or they don't have session cookie set.
334
                $logData['flag_multi_session']++;
335 9
                $isFlagged = true;
336
            }
337
338
            // Ban this IP if they reached the limit.
339 7
            if ($logData['flag_multi_session'] > $this->properties['limit_unusual_behavior']['session']) {
340 7
                $this->action(
341
                    Enum::ACTION_TEMPORARILY_DENY,
342
                    Enum::REASON_TOO_MANY_SESSIONS_DENIED
343
                );
344 9
                $isReject = true;
345 5
            }
346 5
        }
347 5
348 5
        return [
349 5
            'is_flagged' => $isFlagged,
350
            'is_reject' => $isReject,
351
            'log_data' => $logData,
352
        ];
353 31
    }
354 31
355 31
    /**
356 31
     * Filter - Cookie
357 31
     *
358
     * @param array $logData   IP data from Shieldon log table.
359
     * @param array $ipDetail  The IP log data.
360
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
361
     *
362
     * @return array
363
     */
364
    protected function filterCookie(array $logData, array $ipDetail, bool $isFlagged): array
365
    {
366
        $isReject = false;
367
368
        // Let's checking cookie created by javascript..
369 31
        if ($this->filterStatus['cookie']) {
370
            // Get values from data table. We will count it and save it back to data table.
371 31
            $logData['flag_js_cookie'] = $ipDetail['flag_js_cookie'];
372
            $logData['pageviews_cookie'] = $ipDetail['pageviews_cookie'];
373
374 31
            $c = $this->properties['cookie_name'];
375
376
            $jsCookie = get_request()->getCookieParams()[$c] ?? 0;
377 8
378 8
            // Checking if a cookie is created by JavaScript.
379
            if (!empty($jsCookie)) {
380 8
                if ($jsCookie == '1') {
381
                    $logData['pageviews_cookie']++;
382 8
                } else {
383
                    // Flag it if the value is not 1.
384
                    $logData['flag_js_cookie']++;
385 8
                    $isFlagged = true;
386 4
                }
387 4
            } else {
388
                // If we cannot find the cookie, flag it.
389
                $logData['flag_js_cookie']++;
390
                $isFlagged = true;
391 4
            }
392 4
393
            if ($logData['flag_js_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
394
                // Ban this IP if they reached the limit.
395
                $this->action(
396 8
                    Enum::ACTION_TEMPORARILY_DENY,
397 8
                    Enum::REASON_EMPTY_JS_COOKIE_DENIED
398
                );
399
                $isReject = true;
400 8
            }
401
402
            // Remove JS cookie and reset.
403 4
            if ($logData['pageviews_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
404 4
                $logData['pageviews_cookie'] = 0; // Reset to 0.
405 4
                $logData['flag_js_cookie'] = 0;
406 4
                unset_superglobal($c, 'cookie');
407 4
            }
408
        }
409
410
        return [
411 8
            'is_flagged' => $isFlagged,
412 4
            'is_reject' => $isReject,
413 4
            'log_data' => $logData,
414 4
        ];
415
    }
416
417
    /**
418 31
     * Filter - Frequency
419 31
     *
420 31
     * @param array $logData   IP data from Shieldon log table.
421 31
     * @param array $ipDetail  The IP log data.
422 31
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
423
     *
424
     * @return array
425
     */
426
    protected function filterFrequency(array $logData, array $ipDetail, bool $isFlagged): array
427
    {
428
        $isReject = false;
429
430
        if ($this->filterStatus['frequency']) {
431
            $timeSecond = [];
432
            $timeSecond['s'] = 1;
433
            $timeSecond['m'] = 60;
434 31
            $timeSecond['h'] = 3600;
435
            $timeSecond['d'] = 86400;
436 31
437
            foreach (array_keys($this->properties['time_unit_quota']) as $unit) {
438 31
                if (($logData['last_time'] - $ipDetail['first_time_' . $unit]) >= ($timeSecond[$unit] + 1)) {
439 14
                    // For example:
440 14
                    // (1) minutely: now > first_time_m about 61, (2) hourly: now > first_time_h about 3601,
441 14
                    // Let's prepare to rest the the pageview count.
442 14
                    $this->filterResetStatus[$unit] = true;
443 14
                } else {
444
                    // If an user's pageview count is more than the time period limit
445 14
                    // He or she will get banned.
446
                    if ($logData['pageviews_' . $unit] > $this->properties['time_unit_quota'][$unit]) {
447 14
                        $actionReason = [
448
                            's' => Enum::REASON_REACH_SECONDLY_LIMIT_DENIED,
449
                            'm' => Enum::REASON_REACH_MINUTELY_LIMIT_DENIED,
450
                            'h' => Enum::REASON_REACH_HOURLY_LIMIT_DENIED,
451
                            'd' => Enum::REASON_REACH_DAILY_LIMIT_DENIED,
452 8
                        ];
453
454
                        $this->action(
455
                            Enum::ACTION_TEMPORARILY_DENY,
456
                            $actionReason[$unit]
457
                        );
458 14
459
                        $isReject = true;
460 8
                    }
461 8
                }
462 8
            }
463 8
464 8
            foreach ($this->filterResetStatus as $unit => $status) {
465 8
                // Reset the pageview check for specfic time unit.
466
                if ($status) {
467 8
                    $logData['first_time_' . $unit] = $logData['last_time'];
468 8
                    $logData['pageviews_' . $unit] = 0;
469 8
                }
470 8
            }
471
        }
472 14
473
        return [
474
            'is_flagged' => $isFlagged,
475
            'is_reject' => $isReject,
476
            'log_data' => $logData,
477 14
        ];
478
    }
479
}
480