Test Failed
Push — master ( def136...222fba )
by Antonio Carlos
08:21
created

AttackBlocker::isBeingAttacked()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

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