Completed
Push — master ( 42b6f8...140d5a )
by Antonio Carlos
06:38
created

AttackBlocker::isBeingAttacked()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
crap 2
1
<?php
2
3
namespace PragmaRX\Firewall\Support;
4
5
use Carbon\Carbon;
6
use PragmaRX\Firewall\Events\AttackDetected;
7
use PragmaRX\Firewall\Firewall;
8
use PragmaRX\Firewall\Repositories\Cache\Cache;
9
use PragmaRX\Support\Config;
10
11
class AttackBlocker
12
{
13
    use ServiceInstances;
14
15
    /**
16
     * The request record.
17
     *
18
     * @var array
19
     */
20
    protected $record = [
21
        'ip' => null,
22
23
        'country' => null,
24
    ];
25
26
    /**
27
     * The ip address.
28
     *
29
     * @var string
30
     */
31
    protected $ipAddress;
32
33
    /**
34
     * The cache key.
35
     *
36
     * @var string
37
     */
38
    protected $key;
39
40
    /**
41
     * The max request count.
42
     *
43
     * @var int
44
     */
45
    protected $maxRequestCount;
46
47
    /**
48
     * The max request count.
49
     *
50
     * @var int
51
     */
52
    protected $maxSeconds;
53
54
    /**
55
     * The firewall instance.
56
     *
57
     * @var Firewall
58
     */
59
    protected $firewall;
60
61
    /**
62
     * The country.
63
     *
64
     * @var string
65
     */
66
    protected $country;
67
68
    /**
69
     * The enabled items.
70
     *
71
     * @var \Illuminate\Support\Collection
72
     */
73
    protected $enabledItems;
74
75
    /**
76
     * AttackBlocker constructor.
77
     */
78 72
    public function __construct()
79
    {
80 72
        $this->loadConfig();
81 72
    }
82
83
    /**
84
     * Blacklist the IP address.
85
     *
86
     * @param $record
87
     *
88
     * @return bool
89
     */
90 6
    protected function blacklist($record)
91
    {
92 6
        if ($record['isBlacklisted']) {
93 2
            return false;
94
        }
95
96 6
        $blacklistUnknown = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_unknown");
97
98 6
        $blackWhitelisted = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_whitelisted");
99
100 6
        if ($blacklistUnknown || $blackWhitelisted) {
101 4
            $record['isBlacklisted'] = true;
102
103 4
            $ipAddress = $record['type'] == 'country' ? 'country:'.$record['country_code'] : $record['ipAddress'];
104
105 4
            $this->firewall->blacklist($ipAddress, $blackWhitelisted);
106
107 4
            $this->save($record);
108
109 4
            return true;
110
        }
111
112 3
        return false;
113
    }
114
115
    /**
116
     * Check for expiration.
117
     *
118
     * @return void
119
     */
120
    protected function checkExpiration()
121
    {
122 7
        $this->enabledItems->each(function ($index, $type) {
123 7
            if (($this->now()->diffInSeconds($this->record[$type]['lastRequestAt'])) <= ($this->getMaxSecondsForType($type))) {
124 7
                return $this->record;
125
            }
126
127 1
            return $this->record[$type] = $this->getEmptyRecord($this->record[$type]['key'], $type);
128 7
        });
129 7
    }
130
131
    /**
132
     * Get an empty record.
133
     *
134
     * @return array
135
     */
136 7
    protected function getEmptyRecord($key, $type)
137
    {
138 7
        return $this->makeRecord($key, $type);
139
    }
140
141
    /**
142
     * Get a timestamp for the time the cache should expire.
143
     *
144
     * @param $type
145
     *
146
     * @return \Carbon\Carbon
147
     */
148 7
    protected function getExpirationTimestamp($type)
149
    {
150 7
        return $this->now()->addSeconds($this->getMaxSecondsForType($type));
151
    }
152
153
    /**
154
     * Search geo localization by ip.
155
     *
156
     * @param $ipAddress
157
     *
158
     * @return array|null
159
     */
160 7
    protected function getGeo($ipAddress)
161
    {
162 7
        return $this->firewall->getGeoIp()->searchAddr($ipAddress);
163
    }
164
165
    /**
166
     * Get max request count from config.
167
     *
168
     * @param string $type
169
     *
170
     * @return int
171
     */
172 7
    protected function getMaxRequestCountForType($type = 'ip')
173
    {
174 7
        return !is_null($this->maxRequestCount)
175 7
            ? $this->maxRequestCount
176 7
            : ($this->maxRequestCount = $this->config()->get("attack_blocker.allowed_frequency.{$type}.requests"));
177
    }
178
179
    /**
180
     * Get max seconds from config.
181
     *
182
     * @param $type
183
     *
184
     * @return int
185
     */
186 7
    protected function getMaxSecondsForType($type)
187
    {
188 7
        return !is_null($this->maxSeconds)
189 7
            ? $this->maxSeconds
190 7
            : ($this->maxSeconds = $this->config()->get("attack_blocker.allowed_frequency.{$type}.seconds"));
191
    }
192
193
    /**
194
     * Get the response configuration.
195
     *
196
     * @return array
197
     */
198 2
    protected function getResponseConfig()
199
    {
200 2
        return $this->config()->get('attack_blocker.response');
201
    }
202
203
    /**
204
     * Increment request count.
205
     *
206
     * @return void
207
     */
208
    protected function increment()
209
    {
210 7
        $this->enabledItems->each(function ($index, $type) {
211 7
            $this->save($type, ['requestCount' => $this->record[$type]['requestCount'] + 1]);
212 7
        });
213 7
    }
214
215
    /**
216
     * Check if this is an attack.
217
     *
218
     * @return bool
219
     */
220
    protected function isAttack()
221
    {
222 7
        return $this->enabledItems->filter(function ($index, $type) {
223 7
            if (!$this->isWhitelisted($type) && $this->record[$type]['requestCount'] > $this->getMaxRequestCountForType($type)) {
224 6
                $this->takeAction($this->record[$type]);
225
226 6
                return true;
227
            }
228 7
        })->count() > 0;
229
    }
230
231
    /**
232
     * Check for attacks.
233
     *
234
     * @param $ipAddress
235
     *
236
     * @return bool
237
     */
238 8
    public function isBeingAttacked($ipAddress)
239
    {
240 8
        if (!$this->isEnabled()) {
241 1
            return false;
242
        }
243
244 7
        $this->loadRecord($ipAddress);
245
246 7
        return $this->isAttack();
247
    }
248
249
    /**
250
     * Get enabled state.
251
     *
252
     * @return bool
253
     */
254 8
    protected function isEnabled()
255
    {
256 8
        return count($this->enabledItems) > 0;
257
    }
258
259
    /**
260
     * Is the current user whitelisted?
261
     *
262
     * @param $type
263
     *
264
     * @return bool
265
     */
266 7
    private function isWhitelisted($type)
267
    {
268 7
        return $this->firewall->whichList($this->record[$type]['ipAddress']) == 'whitelist' &&
269 7
                $this->config()->get("attack_blocker.action.{$this->record[$type]['type']}.blacklist_whitelisted");
270
    }
271
272
    /**
273
     * Load the configuration.
274
     *
275
     * @return void
276
     */
277
    private function loadConfig()
278
    {
279 72
        $this->enabledItems = collect($this->config()->get('attack_blocker.enabled'))->filter(function ($item) {
280 72
            return $item === true;
281 72
        });
282 72
    }
283
284
    /**
285
     * Load a record.
286
     *
287
     * @param $ipAddress
288
     *
289
     * @return void
290
     */
291 7
    protected function loadRecord($ipAddress)
292
    {
293 7
        $this->ipAddress = $ipAddress;
294
295 7
        $this->loadRecordItems();
296
297 7
        $this->checkExpiration();
298
299 7
        $this->increment();
300 7
    }
301
302
    /**
303
     * Load all record items.
304
     *
305
     * @return void
306
     */
307
    protected function loadRecordItems()
308
    {
309 7
        $this->enabledItems->each(function ($index, $type) {
310 7
            if (is_null($this->record[$type] = $this->cache()->get($key = $this->makeKeyForType($type, $this->ipAddress)))) {
311 7
                $this->record[$type] = $this->getEmptyRecord($key, $type);
312
            }
313 7
        });
314 7
    }
315
316
    /**
317
     * Write to the log.
318
     *
319
     * @param $string
320
     *
321
     * @return void
322
     */
323 7
    protected function log($string)
324
    {
325 7
        $this->firewall->log($string);
326 7
    }
327
328
    /**
329
     * Send attack the the log.
330
     *
331
     * @param $record
332
     *
333
     * @return void
334
     */
335 6
    protected function logAttack($record)
336
    {
337 6
        $this->log("Attacker detected - IP: {$record['ipAddress']} - Request count: {$record['requestCount']}");
338 6
    }
339
340
    /**
341
     * Get the current date time.
342
     *
343
     * @return Carbon
344
     */
345 7
    private function now()
346
    {
347 7
        Carbon::setTestNow();
348
349 7
        return Carbon::now();
350
    }
351
352
    /**
353
     * Make a response.
354
     *
355
     * @return null|\Illuminate\Http\Response
356
     */
357 3
    public function responseToAttack()
358
    {
359 3
        if ($this->isAttack()) {
360 2
            return (new Responder())->respond($this->getResponseConfig(), $this->record);
361
        }
362 2
    }
363
364
    /**
365
     * Make a hashed key.
366
     *
367
     * @param $field
368
     *
369
     * @return string
370
     */
371 7
    public function makeHashedKey($field)
372
    {
373 7
        return hash(
374 7
            'sha256',
375 7
            $this->config()->get('attack_blocker.cache_key_prefix').'-'.$field
376
        );
377
    }
378
379
    /**
380
     * Make the cache key to record countries.
381
     *
382
     * @param $ipAddress
383
     *
384
     * @return string|null
385
     */
386 7
    protected function makeKeyForType($type, $ipAddress)
387
    {
388 7
        if ($type == 'country') {
389 6
            $geo = $this->getGeo($ipAddress);
390
391 6
            if (is_null($geo)) {
392 3
                $this->log("No GeoIp info for {$ipAddress}, is it installed?");
393
            }
394
395 6
            if (!is_null($geo) && $this->country = $geo['country_code']) {
396 3
                return $this->makeHashedKey($this->country);
397
            }
398
399 3
            unset($this->enabledItems['country']);
400
401 3
            return;
402
        }
403
404 7
        return $this->makeHashedKey($this->ipAddress = $ipAddress);
405
    }
406
407
    /**
408
     * Make a record.
409
     *
410
     * @param $key
411
     * @param $type
412
     *
413
     * @return array
414
     */
415 7
    protected function makeRecord($key, $type)
416
    {
417 7
        $geo = $this->getGeo($this->ipAddress);
418
419
        return [
420 7
            'type' => $type,
421
422 7
            'key' => $key,
423
424 7
            'ipAddress' => $this->ipAddress,
425
426 7
            'requestCount' => 0,
427
428 7
            'firstRequestAt' => $this->now(),
429
430 7
            'lastRequestAt' => $this->now(),
431
432
            'isBlacklisted' => false,
433
434
            'wasNotified' => false,
435
436 7
            'userAgent' => request()->server('HTTP_USER_AGENT'),
437
438 7
            'server' => request()->server(),
439
440 7
            'geoIp' => $geo,
441
442 7
            'country_name' => $geo ? $geo['country_name'] : null,
443
444 7
            'country_code' => $geo ? $geo['country_code'] : null,
445
446 7
            'host' => gethostbyaddr($this->ipAddress),
447
        ];
448
    }
449
450
    /**
451
     * Send notifications.
452
     *
453
     * @param $record
454
     *
455
     * @return void
456
     */
457 6
    protected function notify($record)
458
    {
459 6
        if (!$record['wasNotified'] && $this->config()->get('notifications.enabled')) {
460 5
            $this->save($record['type'], ['wasNotified' => true]);
461
462 5
            collect($this->config()->get('notifications.channels'))->filter(function ($value, $channel) use ($record) {
463
                try {
464 5
                    event(new AttackDetected($record, $channel));
465
                } catch (\Exception $exception) {
466
                    info($exception);
467
                }
468 5
            });
469
        }
470 6
    }
471
472
    /**
473
     * Renew first request timestamp, to keep the offender blocked.
474
     *
475
     * @param $record
476
     *
477
     * @return void
478
     */
479 6
    protected function renew($record)
480
    {
481 6
        $this->save($record['type'], ['lastRequestAt' => $this->now()]);
482 6
    }
483
484
    /**
485
     * Set firewall.
486
     *
487
     * @param Firewall $firewall
488
     *
489
     * @return void
490
     */
491 72
    public function setFirewall($firewall)
492
    {
493 72
        $this->firewall = $firewall;
494 72
    }
495
496
    /**
497
     * Store record on cache.
498
     *
499
     * @param $type
500
     * @param array $items
501
     *
502
     * @return array
503
     */
504 7
    protected function save($type, $items = [])
505
    {
506 7
        if (is_array($type)) {
507 4
            $items = $type;
508
509 4
            $type = $type['type'];
510
        }
511
512 7
        $this->record[$type] = array_merge($this->record[$type], $items);
513
514 7
        $this->cache()->put($this->record[$type]['key'], $this->record[$type], $this->getExpirationTimestamp($type));
0 ignored issues
show
Documentation introduced by
$this->getExpirationTimestamp($type) is of type object<Carbon\Carbon>, but the function expects a integer|null|boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
515
516 7
        return $this->record[$type];
517
    }
518
519
    /**
520
     * Take the necessary action to keep the offender blocked.
521
     *
522
     * @return void
523
     */
524 6
    protected function takeAction($record)
525
    {
526 6
        $this->renew($record);
527
528 6
        $this->blacklist($record);
529
530 6
        $this->notify($record);
531
532 6
        $this->logAttack($record);
533 6
    }
534
}
535