Passed
Push — 2.x ( a336cc...b20143 )
by Terry
01:59
created

FilterTrait::filter()   B

Complexity

Conditions 11
Paths 28

Size

Total Lines 87
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 45
nc 28
nop 0
dl 0
loc 87
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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
11
declare(strict_types=1);
12
13
namespace Shieldon\Firewall\Traits\Kernel;
14
15
use Shieldon\Firewall\Kernel;
16
use function Shieldon\Firewall\get_request;
17
use function Shieldon\Firewall\get_session;
18
use function Shieldon\Firewall\unset_superglobal;
19
20
/*
21
 * This trait is used on Kernel only.
22
 */
23
trait FilterTrait
24
{
25
    /**
26
     * Enable or disable the filters.
27
     *
28
     * @var array
29
     */
30
    protected $filterStatus = [
31
        /**
32
         * Check how many pageviews an user made in a short period time.
33
         * For example, limit an user can only view 30 pages in 60 minutes.
34
         */
35
        'frequency' => true,
36
37
        /**
38
         * If an user checks any internal link on your website, the user's
39
         * browser will generate HTTP_REFERER information.
40
         * When a user view many pages without HTTP_REFERER information meaning
41
         * that the user MUST be a web crawler.
42
         */
43
        'referer' => false,
44
45
        /**
46
         * Most of web crawlers do not render JavaScript, they only get the 
47
         * content they want, so we can check whether the cookie can be created
48
         * by JavaScript or not.
49
         */
50
        'cookie' => false,
51
52
        /**
53
         * Every unique user should only has a unique session, but if a user
54
         * creates different sessions every connection... meaning that the 
55
         * user's browser doesn't support cookie.
56
         * It is almost impossible that modern browsers not support cookie,
57
         * therefore the user MUST be a web crawler.
58
         */
59
        'session' => false,
60
    ];
61
62
    /**
63
     * The status for Filters to reset.
64
     *
65
     * @var array
66
     */
67
    protected $filterResetStatus = [
68
        's' => false, // second.
69
        'm' => false, // minute.
70
        'h' => false, // hour.
71
        'd' => false, // day.
72
    ];
73
74
    /**
75
     * Detect and analyze an user's behavior.
76
     *
77
     * @return int The response code.
78
     */
79
    protected function filter(): int
80
    {
81
        $now = time();
82
        $isFlagged = false;
83
84
        // Fetch an IP data from Shieldon log table.
85
        $ipDetail = $this->driver->get($this->ip, 'filter');
86
87
        $ipDetail = $this->driver->parseData($ipDetail, 'filter');
88
        $logData = $ipDetail;
89
90
        // Counting user pageviews.
91
        foreach (array_keys($this->filterResetStatus) as $unit) {
92
93
            // Each time unit will increase by 1.
94
            $logData['pageviews_' . $unit] = $ipDetail['pageviews_' . $unit] + 1;
95
            $logData['first_time_' . $unit] = $ipDetail['first_time_' . $unit];
96
        }
97
98
        $logData['first_time_flag'] = $ipDetail['first_time_flag'];
99
100
        if (!empty($ipDetail['ip'])) {
101
            $logData['ip'] = $this->ip;
102
            $logData['session'] = get_session()->get('id');
103
            $logData['hostname'] = $this->rdns;
104
            $logData['last_time'] = $now;
105
106
            // Filter: HTTP referrer information.
107
            $filterReferer = $this->filterReferer($logData, $ipDetail, $isFlagged);
108
            $isFlagged = $filterReferer['is_flagged'];
109
            $logData = $filterReferer['log_data'];
110
111
            if ($filterReferer['is_reject']) {
112
                return kernel::RESPONSE_TEMPORARILY_DENY;
113
            }
114
115
            // Filter: Session.
116
            $filterSession = $this->filterSession($logData, $ipDetail, $isFlagged);
117
            $isFlagged = $filterSession['is_flagged'];
118
            $logData = $filterSession['log_data'];
119
120
            if ($filterSession['is_reject']) {
121
                return kernel::RESPONSE_TEMPORARILY_DENY;
122
            }
123
124
            // Filter: JavaScript produced cookie.
125
            $filterCookie = $this->filterCookie($logData, $ipDetail, $isFlagged);
126
            $isFlagged = $filterCookie['is_flagged'];
127
            $logData = $filterCookie['log_data'];
128
129
            if ($filterCookie['is_reject']) {
130
                return kernel::RESPONSE_TEMPORARILY_DENY;
131
            }
132
133
            // Filter: frequency.
134
            $filterFrequency = $this->filterFrequency($logData, $ipDetail, $isFlagged);
135
            $isFlagged = $filterFrequency['is_flagged'];
136
            $logData = $filterFrequency['log_data'];
137
138
            if ($filterFrequency['is_reject']) {
139
                return kernel::RESPONSE_TEMPORARILY_DENY;
140
            }
141
142
            // Is fagged as unusual beavior? Count the first time.
143
            if ($isFlagged) {
144
                $logData['first_time_flag'] = (!empty($logData['first_time_flag'])) ? $logData['first_time_flag'] : $now;
145
            }
146
147
            // Reset the flagged factor check.
148
            if (!empty($ipDetail['first_time_flag'])) {
149
                if ($now - $ipDetail['first_time_flag'] >= $this->properties['time_reset_limit']) {
150
                    $logData['flag_multi_session'] = 0;
151
                    $logData['flag_empty_referer'] = 0;
152
                    $logData['flag_js_cookie'] = 0;
153
                }
154
            }
155
156
            $this->driver->save($this->ip, $logData, 'filter');
157
158
        } else {
159
160
            // If $ipDetail[ip] is empty.
161
            // It means that the user is first time visiting our webiste.
162
            $this->InitializeFirstTimeFilter($logData);
163
        }
164
165
        return kernel::RESPONSE_ALLOW;
166
    }
167
168
    /**
169
     * When the user is first time visiting our webiste.
170
     * Initialize the log data.
171
     * 
172
     * @param array $logData The user's log data.
173
     *
174
     * @return void
175
     */
176
    protected function InitializeFirstTimeFilter($logData)
177
    {
178
        $now = time();
179
180
        $logData['ip']        = $this->ip;
181
        $logData['session']   = get_session()->get('id');
182
        $logData['hostname']  = $this->rdns;
183
        $logData['last_time'] = $now;
184
185
        foreach (array_keys($this->filterResetStatus) as $unit) {
186
            $logData['first_time_' . $unit] = $now;
187
        }
188
189
        $this->driver->save($this->ip, $logData, 'filter');
190
    }
191
192
    /**
193
     * Filter - Referer.
194
     *
195
     * @param array $logData   IP data from Shieldon log table.
196
     * @param array $ipData    The IP log data.
197
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
198
     *
199
     * @return array
200
     */
201
    protected function filterReferer(array $logData, array $ipDetail, bool $isFlagged): array
202
    {
203
        $isReject = false;
204
205
        if ($this->filterStatus['referer']) {
206
207
            if ($logData['last_time'] - $ipDetail['last_time'] > $this->properties['interval_check_referer']) {
208
209
                // Get values from data table. We will count it and save it back to data table.
210
                // If an user is already in your website, it is impossible no referer when he views other pages.
211
                $logData['flag_empty_referer'] = $ipDetail['flag_empty_referer'] ?? 0;
212
213
                if (empty(get_request()->getHeaderLine('referer'))) {
214
                    $logData['flag_empty_referer']++;
215
                    $isFlagged = true;
216
                }
217
218
                // Ban this IP if they reached the limit.
219
                if ($logData['flag_empty_referer'] > $this->properties['limit_unusual_behavior']['referer']) {
220
                    $this->action(
0 ignored issues
show
Bug introduced by
It seems like action() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

220
                    $this->/** @scrutinizer ignore-call */ 
221
                           action(
Loading history...
221
                        kernel::ACTION_TEMPORARILY_DENY,
222
                        kernel::REASON_EMPTY_REFERER
223
                    );
224
                    $isReject = true;
225
                }
226
            }
227
        }
228
229
        return [
230
            'is_flagged' => $isFlagged,
231
            'is_reject' => $isReject,
232
            'log_data' => $logData,
233
        ];
234
    }
235
236
    /**
237
     * Filter - Session
238
     *
239
     * @param array $logData   IP data from Shieldon log table.
240
     * @param array $ipData    The IP log data.
241
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
242
     *
243
     * @return array
244
     */
245
    protected function filterSession(array $logData, array $ipDetail, bool $isFlagged): array
246
    {
247
        $isReject = false;
248
249
        if ($this->filterStatus['session']) {
250
251
            if ($logData['last_time'] - $ipDetail['last_time'] > $this->properties['interval_check_session']) {
252
253
                // Get values from data table. We will count it and save it back to data table.
254
                $logData['flag_multi_session'] = $ipDetail['flag_multi_session'] ?? 0;
255
                
256
                if (get_session()->get('id') !== $ipDetail['session']) {
257
258
                    // Is is possible because of direct access by the same user many times.
259
                    // Or they don't have session cookie set.
260
                    $logData['flag_multi_session']++;
261
                    $isFlagged = true;
262
                }
263
264
                // Ban this IP if they reached the limit.
265
                if ($logData['flag_multi_session'] > $this->properties['limit_unusual_behavior']['session']) {
266
                    $this->action(
267
                        kernel::ACTION_TEMPORARILY_DENY,
268
                        kernel::REASON_TOO_MANY_SESSIONS
269
                    );
270
                    $isReject = true;
271
                }
272
            }
273
        }
274
275
276
        return [
277
            'is_flagged' => $isFlagged,
278
            'is_reject' => $isReject,
279
            'log_data' => $logData,
280
        ];
281
    }
282
283
    /**
284
     * Filter - Cookie
285
     *
286
     * @param array $logData   IP data from Shieldon log table.
287
     * @param array $ipData    The IP log data.
288
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
289
     *
290
     * @return array
291
     */
292
    protected function filterCookie(array $logData, array $ipDetail, bool $isFlagged): array
293
    {
294
        $isReject = false;
295
296
        // Let's checking cookie created by javascript..
297
        if ($this->filterStatus['cookie']) {
298
299
            // Get values from data table. We will count it and save it back to data table.
300
            $logData['flag_js_cookie'] = $ipDetail['flag_js_cookie'] ?? 0;
301
            $logData['pageviews_cookie'] = $ipDetail['pageviews_cookie'] ?? 0;
302
303
            $c = $this->properties['cookie_name'];
304
305
            $jsCookie = get_request()->getCookieParams()[$c] ?? 0;
306
307
            // Checking if a cookie is created by JavaScript.
308
            if (!empty($jsCookie)) {
309
310
                if ($jsCookie == '1') {
311
                    $logData['pageviews_cookie']++;
312
313
                } else {
314
                    // Flag it if the value is not 1.
315
                    $logData['flag_js_cookie']++;
316
                    $isFlagged = true;
317
                }
318
            } else {
319
                // If we cannot find the cookie, flag it.
320
                $logData['flag_js_cookie']++;
321
                $isFlagged = true;
322
            }
323
324
            if ($logData['flag_js_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
325
326
                // Ban this IP if they reached the limit.
327
                $this->action(
328
                    kernel::ACTION_TEMPORARILY_DENY,
329
                    kernel::REASON_EMPTY_JS_COOKIE
330
                );
331
                $isReject = true;
332
            }
333
334
            // Remove JS cookie and reset.
335
            if ($logData['pageviews_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
336
                $logData['pageviews_cookie'] = 0; // Reset to 0.
337
                $logData['flag_js_cookie'] = 0;
338
                unset_superglobal($c, 'cookie');
339
            }
340
        }
341
342
        return [
343
            'is_flagged' => $isFlagged,
344
            'is_reject' => $isReject,
345
            'log_data' => $logData,
346
        ];
347
    }
348
349
    /**
350
     * Filter - Frequency
351
     *
352
     * @param array $logData   IP data from Shieldon log table.
353
     * @param array $ipData    The IP log data.
354
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
355
     *
356
     * @return array
357
     */
358
    protected function filterFrequency(array $logData, array $ipDetail, bool $isFlagged): array
359
    {
360
        $isReject = false;
361
362
        if ($this->filterStatus['frequency']) {
363
            $timeSecond = [];
364
            $timeSecond['s'] = 1;
365
            $timeSecond['m'] = 60;
366
            $timeSecond['h'] = 3600;
367
            $timeSecond['d'] = 86400;
368
369
            foreach (array_keys($this->properties['time_unit_quota']) as $unit) {
370
371
                if (($logData['last_time'] - $ipDetail['first_time_' . $unit]) >= ($timeSecond[$unit] + 1)) {
372
373
                    // For example:
374
                    // (1) minutely: now > first_time_m about 61, (2) hourly: now > first_time_h about 3601, 
375
                    // Let's prepare to rest the the pageview count.
376
                    $this->filterResetStatus[$unit] = true;
377
378
                } else {
379
380
                    // If an user's pageview count is more than the time period limit
381
                    // He or she will get banned.
382
                    if ($logData['pageviews_' . $unit] > $this->properties['time_unit_quota'][$unit]) {
383
384
                        if ($unit === 's') {
385
                            $this->action(
386
                                kernel::ACTION_TEMPORARILY_DENY,
387
                                kernel::REASON_REACHED_LIMIT_SECOND
388
                            );
389
                        }
390
391
                        if ($unit === 'm') {
392
                            $this->action(
393
                                kernel::ACTION_TEMPORARILY_DENY,
394
                                kernel::REASON_REACHED_LIMIT_MINUTE
395
                            );
396
                        }
397
398
                        if ($unit === 'h') {
399
                            $this->action(
400
                                kernel::ACTION_TEMPORARILY_DENY,
401
                                kernel::REASON_REACHED_LIMIT_HOUR
402
                            );
403
                        }
404
405
                        if ($unit === 'd') {
406
                            $this->action(
407
                                kernel::ACTION_TEMPORARILY_DENY,
408
                                kernel::REASON_REACHED_LIMIT_DAY
409
                            );
410
                        }
411
412
                        $isReject = true;
413
                    }
414
                }
415
            }
416
417
            foreach ($this->filterResetStatus as $unit => $status) {
418
                // Reset the pageview check for specfic time unit.
419
                if ($status) {
420
                    $logData['first_time_' . $unit] = $logData['last_time'];
421
                    $logData['pageviews_' . $unit] = 0;
422
                }
423
            }
424
        }
425
426
        return [
427
            'is_flagged' => $isFlagged,
428
            'is_reject' => $isReject,
429
            'log_data' => $logData,
430
        ];
431
    }
432
}
433