Passed
Push — master ( e8f6e3...bf4671 )
by Antonio Carlos
21:13 queued 14:51
created

AttackBlocker::getEmptyRecord()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
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\Support\CacheManager;
9
use PragmaRX\Support\Config;
10
11
class AttackBlocker
12
{
13
    /**
14
     * The config.
15
     *
16
     * @var Config
17
     */
18
    protected $config;
19
20
    /**
21
     * The request record.
22
     *
23
     * @var Config
24
     */
25
    protected $record = [
26
        'ip' => null,
27
28
        'country' => null,
29
    ];
30
31
    /**
32
     * The cache.
33
     *
34
     * @var CacheManager
35
     */
36
    protected $cache;
37
38
    /**
39
     * The ip address.
40
     *
41
     * @var string
42
     */
43
    protected $ipAddress;
44
45
    /**
46
     * The cache key.
47
     *
48
     * @var string
49
     */
50
    protected $key;
51
52
    /**
53
     * The max request count.
54
     *
55
     * @var int
56
     */
57
    protected $maxRequestCount;
58
59
    /**
60
     * The max request count.
61
     *
62
     * @var int
63
     */
64
    protected $maxSeconds;
65
66
    /**
67
     * The firewall instance.
68
     *
69
     * @var Firewall
70
     */
71
    protected $firewall;
72
73
    /**
74
     * The country.
75
     *
76
     * @var string
77
     */
78
    protected $country;
79
80
    /**
81
     * The enabled items.
82
     *
83
     * @var array
84
     */
85
    protected $enabledItems;
86
87
    /**
88
     * AttackBlocker constructor.
89
     *
90
     * @param Config       $config
91
     * @param CacheManager $cache
92
     */
93
    public function __construct(Config $config, CacheManager $cache)
94
    {
95
        $this->config = $config;
96
97
        $this->cache = $cache;
98
99
        $this->loadConfig();
100
    }
101
102
    /**
103
     * Blacklist the IP address.
104
     *
105
     * @param $record
106
     *
107
     * @return bool
108
     */
109
    protected function blacklist($record)
110
    {
111
        if ($record['isBlacklisted']) {
112
            return false;
113
        }
114
115
        $blacklistUnknown = $this->config->get("attack_blocker.action.{$record['type']}.blacklist_unknown");
116
117
        $blackWhitelisted = $this->config->get("attack_blocker.action.{$record['type']}.blacklist_whitelisted");
118
119
        if ($blacklistUnknown || $blackWhitelisted) {
120
            $record['isBlacklisted'] = true;
121
122
            $ipAddress = $record['type'] == 'country'
123
                        ? 'country:'.$record['country_code']
124
                        : $record['ipAddress'];
125
126
            $this->firewall->blacklist($ipAddress, $blackWhitelisted);
127
128
            $this->save($record);
129
        }
130
    }
131
132
    /**
133
     * Check for expiration.
134
     *
135
     * @return mixed
136
     */
137
    protected function checkExpiration()
138
    {
139
        $this->enabledItems->each(function ($index, $type) {
140
            if (($this->record[$type]['lastRequestAt']->diffInSeconds(Carbon::now())) <= ($this->getMaxSecondsForType($type))) {
141
                return $this->record;
142
            }
143
144
            return $this->getEmptyRecord($this->record[$type]['key'], $type);
145
        });
146
    }
147
148
    /**
149
     * Get an empty record.
150
     *
151
     * @return array
152
     */
153
    protected function getEmptyRecord($key, $type)
154
    {
155
        return $this->makeRecord($key, $type);
156
    }
157
158
    /**
159
     * Get a timestamp for the time the cache should expire.
160
     *
161
     * @param $type
162
     *
163
     * @return float|int
164
     */
165
    protected function getExpirationTimestamp($type)
166
    {
167
        return Carbon::now()->addSeconds($this->getMaxSecondsForType($type));
0 ignored issues
show
Bug Best Practice introduced by
The expression return Carbon\Carbon::no...xSecondsForType($type)) returns the type Carbon\Carbon which is incompatible with the documented return type integer|double.
Loading history...
168
    }
169
170
    /**
171
     * Get firewall.
172
     *
173
     * @return Firewall
174
     */
175
    public function getFirewall()
176
    {
177
        return $this->firewall;
178
    }
179
180
    /**
181
     * @param $ipAddress
182
     *
183
     * @return array|null|void
184
     */
185
    protected function getGeo($ipAddress)
186
    {
187
        return $this->firewall->getGeoIp()->searchAddr($ipAddress);
188
    }
189
190
    /**
191
     * Get the cache key.
192
     *
193
     * @return string
194
     */
195
    public function getKey()
196
    {
197
        return $this->key;
198
    }
199
200
    /**
201
     * Get max request count from config.
202
     *
203
     * @param string $type
204
     * @return int|mixed
205
     */
206
    protected function getMaxRequestCountForType($type = 'ip')
207
    {
208
        return !is_null($this->maxRequestCount)
209
            ? $this->maxRequestCount
210
            : ($this->maxRequestCount = $this->config->get("attack_blocker.allowed_frequency.{$type}.requests"));
211
    }
212
213
    /**
214
     * Get max seconds from config.
215
     *
216
     * @param $type
217
     *
218
     * @return mixed
219
     */
220
    protected function getMaxSecondsForType($type)
221
    {
222
        return !is_null($this->maxSeconds)
223
            ? $this->maxSeconds
224
            : ($this->maxSeconds = $this->config->get("attack_blocker.allowed_frequency.{$type}.seconds"));
225
    }
226
227
    /**
228
     * Get attack records.
229
     *
230
     * @return array
231
     */
232
    public function getRecord()
233
    {
234
        return $this->record;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->record returns the type PragmaRX\Support\Config which is incompatible with the documented return type array.
Loading history...
235
    }
236
237
    /**
238
     * Get the response configuration.
239
     *
240
     * @return mixed
241
     */
242
    protected function getResponseConfig()
243
    {
244
        return $this->config->get('attack_blocker.response');
245
    }
246
247
    /**
248
     * Increment request count.
249
     *
250
     * @return mixed
251
     */
252
    protected function increment()
253
    {
254
        $this->enabledItems->each(function ($index, $type) {
255
            $this->save($type, ['requestCount' => $this->record[$type]['requestCount'] + 1]);
256
        });
257
    }
258
259
    /**
260
     * Check if this is an attack.
261
     *
262
     * @return mixed
263
     */
264
    protected function isAttack()
265
    {
266
        return $this->enabledItems->filter(function ($index, $type) {
267
            if (!$this->isWhitelisted($type) && $isAttack = $this->record[$type]['requestCount'] > $this->getMaxRequestCountForType($type)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $isAttack is dead and can be removed.
Loading history...
268
                // $this->takeAction($this->record[$type]);
269
270
                return true;
271
            }
272
        })->count() > 0;
273
    }
274
275
    /**
276
     * Check for attacks.
277
     *
278
     * @param $ipAddress
279
     *
280
     * @return mixed
281
     */
282
    public function isBeingAttacked($ipAddress)
283
    {
284
        if (!$this->isEnabled()) {
285
            return false;
286
        }
287
288
        $this->loadRecord($ipAddress);
289
290
        return $this->isAttack();
291
    }
292
293
    /**
294
     * Get enabled state.
295
     */
296
    protected function isEnabled()
297
    {
298
        return count($this->enabledItems) > 0;
299
    }
300
301
    /**
302
     * Is the current user whitelisted?
303
     *
304
     * @param $type
305
     *
306
     * @return bool
307
     */
308
    private function isWhitelisted($type)
309
    {
310
        return $this->firewall->whichList($this->record[$type]['ipAddress']) == 'whitelist' &&
311
                $this->config->get("attack_blocker.action.{$this->record[$type]['type']}.blacklist_whitelisted");
312
    }
313
314
    /**
315
     * Load the configuration.
316
     */
317
    private function loadConfig()
318
    {
319
        $this->enabledItems = collect($this->config->get('attack_blocker.enabled'))->filter(function ($item) {
0 ignored issues
show
Documentation Bug introduced by
It seems like collect($this->config->g...ion(...) { /* ... */ }) of type Illuminate\Support\Collection is incompatible with the declared type array of property $enabledItems.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
320
            return $item === true;
321
        });
322
    }
323
324
    /**
325
     * Load a record.
326
     *
327
     * @param $ipAddress
328
     *
329
     * @return array|\Illuminate\Contracts\Cache\Repository
330
     */
331
    protected function loadRecord($ipAddress)
332
    {
333
        $this->ipAddress = $ipAddress;
334
335
        $this->loadRecordItems();
336
337
        $this->checkExpiration();
338
339
        $this->increment();
340
    }
341
342
    /**
343
     * Load all record items.
344
     */
345
    protected function loadRecordItems()
346
    {
347
        $this->enabledItems->each(function ($index, $type) {
348
            if (is_null($this->record[$type] = $this->cache->get($key = $this->makeKeyForType($type, $this->ipAddress)))) {
349
                $this->record[$type] = $this->getEmptyRecord($key, $type);
350
            }
351
        });
352
    }
353
354
    /**
355
     * Write to the log.
356
     *
357
     * @param $string
358
     */
359
    protected function log($string)
360
    {
361
        $this->firewall->log($string);
362
    }
363
364
    /**
365
     * Send attack the the log.
366
     *
367
     * @param $record
368
     */
369
    protected function logAttack($record)
370
    {
371
        $this->log("Attacker detected - IP: {$record['ipAddress']} - Request count: {$record['requestCount']}");
372
    }
373
374
    /**
375
     * Make a response.
376
     *
377
     */
378
    public function responseToAttack()
379
    {
380
        if ($this->isAttack()) {
381
            return (new Responder())->respond($this->getResponseConfig(), $this->record);
382
        }
383
384
        return null;
385
    }
386
387
    /**
388
     * Make a hashed key.
389
     *
390
     * @param $field
391
     *
392
     * @return string
393
     */
394
    public function makeHashedKey($field)
395
    {
396
        return hash(
397
            'sha256',
398
            $this->config->get('attack_blocker.cache_key_prefix').'-'.$field
399
        );
400
    }
401
402
    /**
403
     * Make the cache key to record countries.
404
     *
405
     * @param $ipAddress
406
     *
407
     * @return string
408
     */
409
    protected function makeKeyForType($type, $ipAddress)
410
    {
411
        if ($type == 'country') {
412
            $geo = $this->getGeo($ipAddress);
413
414
            if (is_null($geo)) {
415
                $this->log("No GeoIp info for {$ipAddress}, is it installed?");
416
            }
417
418
            if (!is_null($geo) && $this->country = $geo['country_code']) {
419
                return $this->makeHashedKey($this->country);
420
            }
421
422
            unset($this->enabledItems['country']);
423
424
            return;
425
        }
426
427
        return $this->makeHashedKey($this->ipAddress = $ipAddress);
428
    }
429
430
    /**
431
     * Make a record.
432
     *
433
     * @param $key
434
     * @param $type
435
     *
436
     * @return array
437
     */
438
    protected function makeRecord($key, $type)
439
    {
440
        $geo = $this->getGeo($this->ipAddress);
441
442
        return [
443
            'type' => $type,
444
445
            'key' => $key,
446
447
            'ipAddress' => $this->ipAddress,
448
449
            'requestCount' => 0,
450
451
            'firstRequestAt' => Carbon::now(),
452
453
            'lastRequestAt' => Carbon::now(),
454
455
            'isBlacklisted' => false,
456
457
            'wasNotified' => false,
458
459
            'userAgent' => request()->server('HTTP_USER_AGENT'),
460
461
            'server' => request()->server(),
462
463
            'geoIp' => $geo,
464
465
            'country_name' => $geo ? $geo['country_name'] : null,
466
467
            'country_code' => $geo ? $geo['country_code'] : null,
468
469
            'host' => gethostbyaddr($this->ipAddress),
470
        ];
471
    }
472
473
    /**
474
     * Send notifications.
475
     *
476
     * @param $record
477
     */
478
    protected function notify($record)
479
    {
480
        if (!$record['wasNotified'] && $this->config->get('notifications.enabled')) {
481
            $this->save($record['type'], ['wasNotified' => true]);
482
483
            collect($this->config->get('notifications.channels'))->filter(function ($value, $channel) use ($record) {
484
                try {
485
                    event(new AttackDetected($record, $channel));
486
                } catch (\Exception $exception) {
487
                    info($exception);
488
                }
489
            });
490
        }
491
    }
492
493
    /**
494
     * Renew first request timestamp, to keep the offender blocked.
495
     *
496
     * @param $record
497
     */
498
    protected function renew($record)
499
    {
500
        $this->save($record['type'], ['lastRequestAt' => Carbon::now()]);
501
    }
502
503
    /**
504
     * Set firewall.
505
     *
506
     * @param Firewall $firewall
507
     */
508
    public function setFirewall($firewall)
509
    {
510
        $this->firewall = $firewall;
511
    }
512
513
    /**
514
     * Store record on cache.
515
     *
516
     * @param $type
517
     * @param array $items
518
     *
519
     * @return array
520
     */
521
    protected function save($type, $items = [])
522
    {
523
        if (is_array($type)) {
524
            $items = $type;
525
526
            $type = $type['type'];
527
        }
528
529
        $this->record[$type] = array_merge($this->record[$type], $items);
530
531
        $this->cache->put($this->record[$type]['key'], $this->record[$type], $this->getExpirationTimestamp($type));
532
533
        return $this->record[$type];
534
    }
535
536
    /**
537
     * Take the necessary action to keep the offender blocked.
538
     */
539
    protected function takeAction($record)
540
    {
541
        $this->renew($record);
542
543
        $this->blacklist($record);
544
545
        $this->notify($record);
546
547
        $this->logAttack($record);
548
    }
549
}
550