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