Passed
Push — master ( 3d0d87...075b69 )
by Antonio Carlos
09:13
created

AttackBlocker::blacklist()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

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