Passed
Push — 2.x ( da19d0...7afaed )
by Terry
02:02
created

FilterTrait::disableFilters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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