Completed
Push — master ( 85b241...3d718b )
by Antonio Carlos
05:58 queued 02:30
created

AttackBlocker::getGeo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
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 66
    public function __construct()
79
    {
80 66
        $this->loadConfig();
81 66
    }
82
83
    /**
84
     * Blacklist the IP address.
85
     *
86
     * @param $record
87
     *
88
     * @return bool
89
     */
90 2
    protected function blacklist($record)
91
    {
92 2
        if ($record['isBlacklisted']) {
93 1
            return false;
94
        }
95
96 2
        $blacklistUnknown = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_unknown");
97
98 2
        $blackWhitelisted = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_whitelisted");
99
100 2
        if ($blacklistUnknown || $blackWhitelisted) {
101 2
            $record['isBlacklisted'] = true;
102
103 2
            $ipAddress = $record['type'] == 'country' ? 'country:'.$record['country_code'] : $record['ipAddress'];
104
105 2
            $this->firewall->blacklist($ipAddress, $blackWhitelisted);
106
107 2
            $this->save($record);
108
109 2
            return true;
110
        }
111
112
        return false;
113
    }
114
115
    /**
116
     * Check for expiration.
117
     *
118
     * @return void
119
     */
120
    protected function checkExpiration()
121
    {
122 2
        $this->enabledItems->each(function ($index, $type) {
123 2
            $now = (string) $this->now();
0 ignored issues
show
Unused Code introduced by
$now is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
124 2
            $last = (string) $this->record[$type]['lastRequestAt'];
0 ignored issues
show
Unused Code introduced by
$last is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

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