Passed
Push — 2.x ( 0267a9...364dd8 )
by Terry
02:05
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
c 0
b 0
f 0
nc 28
nop 0
dl 0
loc 87
rs 7.3166

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

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