Test Failed
Push — master ( 4e8d3d...451ac1 )
by Antonio Carlos
03:57 queued 48s
created

AttackBlocker::blacklist()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 12
cts 12
cp 1
rs 8.5125
c 0
b 0
f 0
cc 5
eloc 12
nc 4
nop 1
crap 5
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 71
    public function __construct()
79
    {
80 71
        $this->loadConfig();
81 71
    }
82
83
    /**
84
     * Blacklist the IP address.
85
     *
86
     * @param $record
87
     *
88
     * @return bool
89
     */
90 5
    protected function blacklist($record)
91
    {
92 5
        if ($record['isBlacklisted']) {
93 2
            return false;
94
        }
95
96 5
        $blacklistUnknown = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_unknown");
97
98 5
        $blackWhitelisted = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_whitelisted");
99
100 5
        if ($blacklistUnknown || $blackWhitelisted) {
101 3
            $record['isBlacklisted'] = true;
102
103 3
            $ipAddress = $record['type'] == 'country' ? 'country:'.$record['country_code'] : $record['ipAddress'];
104
105 3
            $this->firewall->blacklist($ipAddress, $blackWhitelisted);
106
107 3
            $this->save($record);
108
109 3
            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 6
        $this->enabledItems->each(function ($index, $type) {
123 6
            if (($this->record[$type]['lastRequestAt']->diffInSeconds(Carbon::now())) <= ($this->getMaxSecondsForType($type))) {
124 6
                return $this->record;
125
            }
126
127
            return $this->getEmptyRecord($this->record[$type]['key'], $type);
128 6
        });
129 6
    }
130
131
    /**
132
     * Get an empty record.
133
     *
134
     * @return array
135
     */
136 6
    protected function getEmptyRecord($key, $type)
137
    {
138 6
        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 6
    protected function getExpirationTimestamp($type)
149
    {
150 6
        return Carbon::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 6
    protected function getGeo($ipAddress)
161
    {
162 6
        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 6
    protected function getMaxRequestCountForType($type = 'ip')
173
    {
174 6
        return !is_null($this->maxRequestCount)
175 6
            ? $this->maxRequestCount
176 6
            : ($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 6
    protected function getMaxSecondsForType($type)
187
    {
188 6
        return !is_null($this->maxSeconds)
189 6
            ? $this->maxSeconds
190 6
            : ($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 6
        $this->enabledItems->each(function ($index, $type) {
211 6
            $this->save($type, ['requestCount' => $this->record[$type]['requestCount'] + 1]);
212 6
        });
213 6
    }
214
215
    /**
216
     * Check if this is an attack.
217
     *
218
     * @return bool
219
     */
220
    protected function isAttack()
221
    {
222 6
        return $this->enabledItems->filter(function ($index, $type) {
223 6
            if (!$this->isWhitelisted($type) && $this->record[$type]['requestCount'] > $this->getMaxRequestCountForType($type)) {
224 5
                $this->takeAction($this->record[$type]);
225
226 5
                return true;
227
            }
228 6
        })->count() > 0;
229
    }
230
231
    /**
232
     * Check for attacks.
233
     *
234
     * @param $ipAddress
235
     *
236
     * @return bool
237
     */
238 7
    public function isBeingAttacked($ipAddress)
239
    {
240 7
        if (!$this->isEnabled()) {
241 1
            return false;
242
        }
243
244 6
        $this->loadRecord($ipAddress);
245
246 6
        return $this->isAttack();
247
    }
248
249
    /**
250
     * Get enabled state.
251
     *
252
     * @return bool
253
     */
254 7
    protected function isEnabled()
255
    {
256 7
        return count($this->enabledItems) > 0;
257
    }
258
259
    /**
260
     * Is the current user whitelisted?
261
     *
262
     * @param $type
263
     *
264
     * @return bool
265
     */
266 6
    private function isWhitelisted($type)
267
    {
268 6
        return $this->firewall->whichList($this->record[$type]['ipAddress']) == 'whitelist' &&
269 6
                $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 71
        $this->enabledItems = collect($this->config()->get('attack_blocker.enabled'))->filter(function ($item) {
280 71
            return $item === true;
281 71
        });
282 71
    }
283
284
    /**
285
     * Load a record.
286
     *
287
     * @param $ipAddress
288
     *
289
     * @return void
290
     */
291 6
    protected function loadRecord($ipAddress)
292
    {
293 6
        $this->ipAddress = $ipAddress;
294
295 6
        $this->loadRecordItems();
296
297 6
        $this->checkExpiration();
298
299 6
        $this->increment();
300 6
    }
301
302
    /**
303
     * Load all record items.
304
     *
305
     * @return void
306
     */
307
    protected function loadRecordItems()
308
    {
309 6
        $this->enabledItems->each(function ($index, $type) {
310 6
            if (is_null($this->record[$type] = $this->cache()->get($key = $this->makeKeyForType($type, $this->ipAddress)))) {
311 6
                $this->record[$type] = $this->getEmptyRecord($key, $type);
312
            }
313 6
        });
314 6
    }
315
316
    /**
317
     * Write to the log.
318
     *
319
     * @param $string
320
     *
321
     * @return void
322
     */
323 6
    protected function log($string)
324
    {
325 6
        $this->firewall->log($string);
326 6
    }
327
328
    /**
329
     * Send attack the the log.
330
     *
331
     * @param $record
332
     *
333
     * @return void
334
     */
335 5
    protected function logAttack($record)
336
    {
337 5
        $this->log("Attacker detected - IP: {$record['ipAddress']} - Request count: {$record['requestCount']}");
338 5
    }
339
340
    /**
341
     * Make a response.
342
     *
343
     * @return null|\Illuminate\Http\Response
344
     */
345 2
    public function responseToAttack()
346
    {
347 2
        if ($this->isAttack()) {
348 2
            return (new Responder())->respond($this->getResponseConfig(), $this->record);
349
        }
350 1
    }
351
352
    /**
353
     * Make a hashed key.
354
     *
355
     * @param $field
356
     *
357
     * @return string
358
     */
359 6
    public function makeHashedKey($field)
360
    {
361 6
        return hash(
362 6
            'sha256',
363 6
            $this->config()->get('attack_blocker.cache_key_prefix').'-'.$field
364
        );
365
    }
366
367
    /**
368
     * Make the cache key to record countries.
369
     *
370
     * @param $ipAddress
371
     *
372
     * @return string|null
373
     */
374 6
    protected function makeKeyForType($type, $ipAddress)
375
    {
376 6
        if ($type == 'country') {
377 5
            $geo = $this->getGeo($ipAddress);
378
379 5
            if (is_null($geo)) {
380 2
                $this->log("No GeoIp info for {$ipAddress}, is it installed?");
381
            }
382
383 5
            if (!is_null($geo) && $this->country = $geo['country_code']) {
384 3
                return $this->makeHashedKey($this->country);
385
            }
386
387 2
            unset($this->enabledItems['country']);
388
389 2
            return;
390
        }
391
392 6
        return $this->makeHashedKey($this->ipAddress = $ipAddress);
393
    }
394
395
    /**
396
     * Make a record.
397
     *
398
     * @param $key
399
     * @param $type
400
     *
401
     * @return array
402
     */
403 6
    protected function makeRecord($key, $type)
404
    {
405 6
        $geo = $this->getGeo($this->ipAddress);
406
407
        return [
408 6
            'type' => $type,
409
410 6
            'key' => $key,
411
412 6
            'ipAddress' => $this->ipAddress,
413
414 6
            'requestCount' => 0,
415
416 6
            'firstRequestAt' => Carbon::now(),
417
418 6
            'lastRequestAt' => Carbon::now(),
419
420
            'isBlacklisted' => false,
421
422
            'wasNotified' => false,
423
424 6
            'userAgent' => request()->server('HTTP_USER_AGENT'),
425
426 6
            'server' => request()->server(),
427
428 6
            'geoIp' => $geo,
429
430 6
            'country_name' => $geo ? $geo['country_name'] : null,
431
432 6
            'country_code' => $geo ? $geo['country_code'] : null,
433
434 6
            'host' => gethostbyaddr($this->ipAddress),
435
        ];
436
    }
437
438
    /**
439
     * Send notifications.
440
     *
441
     * @param $record
442
     *
443
     * @return void
444
     */
445 5
    protected function notify($record)
446
    {
447 5
        if (!$record['wasNotified'] && $this->config()->get('notifications.enabled')) {
448 4
            $this->save($record['type'], ['wasNotified' => true]);
449
450 4
            collect($this->config()->get('notifications.channels'))->filter(function ($value, $channel) use ($record) {
451
                try {
452 4
                    event(new AttackDetected($record, $channel));
453
                } catch (\Exception $exception) {
454
                    info($exception);
455
                }
456 4
            });
457
        }
458 5
    }
459
460
    /**
461
     * Renew first request timestamp, to keep the offender blocked.
462
     *
463
     * @param $record
464
     *
465
     * @return void
466
     */
467 5
    protected function renew($record)
468
    {
469 5
        $this->save($record['type'], ['lastRequestAt' => Carbon::now()]);
470 5
    }
471
472
    /**
473
     * Set firewall.
474
     *
475
     * @param Firewall $firewall
476
     *
477
     * @return void
478
     */
479 71
    public function setFirewall($firewall)
480
    {
481 71
        $this->firewall = $firewall;
482 71
    }
483
484
    /**
485
     * Store record on cache.
486
     *
487
     * @param $type
488
     * @param array $items
489
     *
490
     * @return array
491
     */
492 6
    protected function save($type, $items = [])
493
    {
494 6
        if (is_array($type)) {
495 3
            $items = $type;
496
497 3
            $type = $type['type'];
498
        }
499
500 6
        $this->record[$type] = array_merge($this->record[$type], $items);
501
502 6
        $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...
503
504 6
        return $this->record[$type];
505
    }
506
507
    /**
508
     * Take the necessary action to keep the offender blocked.
509
     *
510
     * @return void
511
     */
512 5
    protected function takeAction($record)
513
    {
514 5
        $this->renew($record);
515
516 5
        $this->blacklist($record);
517
518 5
        $this->notify($record);
519
520 5
        $this->logAttack($record);
521 5
    }
522
}
523