Test Failed
Push — master ( 4b7b93...a5f8f7 )
by Antonio Carlos
03:28
created

AttackBlocker::logAttack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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