Passed
Push — master ( 2e21e7...ed9e4b )
by Kris
01:49 queued 14s
created

ApiHandler::clear()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 15
rs 10
cc 2
nc 2
nop 2
1
<?php
2
3
/**
4
 *     _    _                    ___ ____  ____  ____
5
 *    / \  | |__  _   _ ___  ___|_ _|  _ \|  _ \| __ )
6
 *   / _ \ | '_ \| | | / __|/ _ \| || |_) | | | |  _ \
7
 *  / ___ \| |_) | |_| \__ \  __/| ||  __/| |_| | |_) |
8
 * /_/   \_\_.__/ \__,_|___/\___|___|_|   |____/|____/
9
 *
10
 * This file is part of Kristuff\AbsuseIPDB.
11
 *
12
 * (c) Kristuff <[email protected]>
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 *
17
 * @version    0.9.5
18
 * @copyright  2020-2021 Kristuff
19
 */
20
21
namespace Kristuff\AbuseIPDB;
22
23
/**
24
 * Class ApiHandler
25
 * 
26
 * The main class to work with the AbuseIPDB API v2 
27
 */
28
class ApiHandler extends ApiDefintion
29
{
30
    /**
31
     * AbuseIPDB API key
32
     *  
33
     * @access protected
34
     * @var string $aipdbApiKey  
35
     */
36
    protected $aipdbApiKey = null; 
37
38
    /**
39
     * AbuseIPDB user id 
40
     * 
41
     * @access protected
42
     * @var string $aipdbUserId  
43
     */
44
    protected $aipdbUserId = null; 
45
46
    /**
47
     * The ips to remove from message
48
     * Generally you will add to this list yours ipv4 and ipv6, and the hostname
49
     * 
50
     * @access protected
51
     * @var array $selfIps  
52
     */
53
    protected $selfIps = []; 
54
55
    /**
56
     * Constructor
57
     * 
58
     * @access public
59
     * @param string  $apiKey     The AbuseIPDB api key
60
     * @param string  $userId     The AbuseIPDB user's id
61
     * @param array   $myIps      The Ips/domain name you dont want to display in report messages
62
     * 
63
     */
64
    public function __construct(string $apiKey, string $userId, array $myIps = [])
65
    {
66
        $this->aipdbApiKey = $apiKey;
67
        $this->aipdbUserId = $userId;
68
        $this->selfIps = $myIps;
69
    }
70
71
    /**
72
     * Get the current configuration in a indexed array
73
     * 
74
     * @access public 
75
     * @return array
76
     */
77
    public function getConfig()
78
    {
79
        return array(
80
            'userId'  => $this->aipdbUserId,
81
            'apiKey'  => $this->aipdbApiKey,
82
            'selfIps' => $this->selfIps,
83
            
84
            // TODO  default report cat 
85
        );
86
    }
87
88
    /**
89
     * Get a new instance of ApiHandler with config stored in a Json file
90
     * 
91
     * @access public 
92
     * @static
93
     * @param string    $configPath     The configuration file path
94
     * 
95
     * @return \Kristuff\AbuseIPDB\ApiHandler
96
     * @throws \InvalidArgumentException                        If the given file does not exist
97
     * @throws \Kristuff\AbuseIPDB\InvalidPermissionException   If the given file is not readable 
98
     */
99
    public static function fromConfigFile(string $configPath)
100
    {
101
102
        // check file exists
103
        if (!file_exists($configPath) || !is_file($configPath)){
104
            throw new \InvalidArgumentException('The file [' . $configPath . '] does not exist.');
105
        }
106
107
        // check file is readable
108
        if (!is_readable($configPath)){
109
            throw new InvalidPermissionException('The file [' . $configPath . '] is not readable.');
110
        }
111
112
        $keyConfig = self::loadJsonFile($configPath);
113
        $selfIps = [];
114
        
115
        // Look for other optional config files in the same directory 
116
        $selfIpsConfigPath = pathinfo($configPath, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . 'self_ips.json';
117
        if (file_exists($selfIpsConfigPath)){
118
            $selfIps = self::loadJsonFile($selfIpsConfigPath)->self_ips;
119
        }
120
121
        $app = new self($keyConfig->api_key, $keyConfig->user_id, $selfIps);
122
        
123
        return $app;
124
    }
125
126
    /**
127
     * Performs a 'report' api request
128
     * 
129
     * Result, in json format will be something like this:
130
     *  {
131
     *       "data": {
132
     *         "ipAddress": "127.0.0.1",
133
     *         "abuseConfidenceScore": 52
134
     *       }
135
     *  }
136
     * 
137
     * @access public
138
     * @param string    $ip             The ip to report
139
     * @param string    $categories     The report categories
140
     * @param string    $message        The report message
141
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
142
     *
143
     * @return object|array
144
     * @throws \InvalidArgumentException
145
     */
146
    public function report(string $ip = '', string $categories = '', string $message = '', bool $returnArray = false)
147
    {
148
         // ip must be set
149
        if (empty($ip)){
150
            throw new \InvalidArgumentException('Ip was empty');
151
        }
152
153
        // categories must be set
154
        if (empty($categories)){
155
            throw new \InvalidArgumentException('categories list was empty');
156
        }
157
158
        // message must be set
159
          if (empty($message)){
160
            throw new \InvalidArgumentException('report message was empty');
161
        }
162
163
        // validates categories, clean message 
164
        $cats = $this->validateReportCategories($categories);
165
        $msg = $this->cleanMessage($message);
166
167
        // report AbuseIPDB request
168
        $response = $this->apiRequest(
169
            'report', [
170
                'ip' => $ip,
171
                'categories' => $cats,
172
                'comment' => $msg
173
            ],
174
            'POST', $returnArray
175
        );
176
177
        return json_decode($response, $returnArray);
178
    }
179
180
    /**
181
     * Perform a 'check-block' api request
182
     * 
183
     * 
184
     * Sample json response for 127.0.0.1/24
185
     * 
186
     * {
187
     *    "data": {
188
     *      "networkAddress": "127.0.0.0",
189
     *      "netmask": "255.255.255.0",
190
     *      "minAddress": "127.0.0.1",
191
     *      "maxAddress": "127.0.0.254",
192
     *      "numPossibleHosts": 254,
193
     *      "addressSpaceDesc": "Loopback",
194
     *      "reportedAddress": [
195
     *        {
196
     *          "ipAddress": "127.0.0.1",
197
     *          "numReports": 631,
198
     *          "mostRecentReport": "2019-03-21T16:35:16+00:00",
199
     *          "abuseConfidenceScore": 0,
200
     *          "countryCode": null
201
     *        },
202
     *        {
203
     *          "ipAddress": "127.0.0.2",
204
     *          "numReports": 16,
205
     *          "mostRecentReport": "2019-03-12T20:31:17+00:00",
206
     *          "abuseConfidenceScore": 0,
207
     *          "countryCode": null
208
     *        },
209
     *        ...
210
     *      ]
211
     *    }
212
     *  }
213
     * 
214
     * 
215
     * @access public
216
     * @param string    $network        The network to check
217
     * @param int       $maxAge         Max age in days
218
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
219
     * 
220
     * @return object|array
221
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when network value was not set. 
222
     */
223
    public function checkBlock(string $network = null, int $maxAge = 30, bool $returnArray = false)
224
    {
225
        // max age must be less or equal to 365
226
        if ($maxAge > 365 || $maxAge < 1){
227
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
228
        }
229
230
        // ip must be set
231
        if (empty($network)){
232
            throw new \InvalidArgumentException('network argument must be set (null given)');
233
        }
234
235
        // minimal data
236
        $data = [
237
            'network'       => $network, 
238
            'maxAgeInDays'  => $maxAge,  
239
        ];
240
241
        $response = $this->apiRequest('check-block', $data, 'GET', $returnArray) ;
242
243
        return json_decode($response, $returnArray);
244
    }
245
   
246
    /**
247
     * Perform a 'clear-address' api request
248
     * 
249
     *  Sample response:
250
     * 
251
     *    {
252
     *      "data": {
253
     *        "numReportsDeleted": 0
254
     *      }
255
     *    }
256
     * 
257
     * 
258
     * @access public
259
     * @param string    $ip             The ip to check
260
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
261
     * 
262
     * @return object|array
263
     * @throws \InvalidArgumentException    When ip value was not set. 
264
     */
265
    public function clear(string $ip = null, bool $returnArray = false)
266
    {
267
        // ip must be set
268
        if (empty($ip)){
269
            throw new \InvalidArgumentException('ip argument must be set (null given)');
270
        }
271
272
        // minimal data
273
        $data = [
274
            'ipAddress'     => $ip, 
275
        ];
276
277
        $response = $this->apiRequest('check', $data, 'DELETE', $returnArray) ;
278
279
        return json_decode($response, $returnArray);
280
    }
281
282
    /**
283
     * Perform a 'check' api request
284
     * 
285
     * @access public
286
     * @param string    $ip             The ip to check
287
     * @param int       $maxAge         Max age in days
288
     * @param bool      $verbose        True to get the full response. Default is false
289
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
290
     * 
291
     * @return object|array
292
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when ip value was not set. 
293
     */
294
    public function check(string $ip = null, int $maxAge = 30, bool $verbose = false, bool $returnArray = false)
295
    {
296
        // max age must be less or equal to 365
297
        if ($maxAge > 365 || $maxAge < 1){
298
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
299
        }
300
301
        // ip must be set
302
        if (empty($ip)){
303
            throw new \InvalidArgumentException('ip argument must be set (null given)');
304
        }
305
306
        // minimal data
307
        $data = [
308
            'ipAddress'     => $ip, 
309
            'maxAgeInDays'  => $maxAge,  
310
        ];
311
312
        // option
313
        if ($verbose){
314
           $data['verbose'] = true;
315
        }
316
317
        $response = $this->apiRequest('check', $data, 'GET', $returnArray) ;
318
319
        return json_decode($response, $returnArray);
320
    }
321
322
    /**
323
     * Perform a 'blacklist' api request
324
     * 
325
     * @access public
326
     * @param int       $limit          The blacklist limit. Default is TODO (the api default limit) 
327
     * @param bool      $plainText      True to get the response in plain text list. Default is false
328
     * @param bool      $returnArray    True to return an indexed array instead of object (when $plainText is set to false). Default is false. 
329
     * 
330
     * @return object|array
331
     * @throws \InvalidArgumentException    When maxAge is not a numeric value, when maxAge is less than 1 or 
332
     *                                      greater than 365, or when ip value was not set. 
333
     */
334
    public function getBlacklist(int $limit = 10000, bool $plainText = false, bool $returnArray = false)
335
    {
336
337
        if ($limit < 1){
338
            throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)');
339
        }
340
341
        // minimal data
342
        $data = [
343
            'confidenceMinimum' => 100, // The abuseConfidenceScore parameter is a subscriber feature. 
344
            'limit'             => $limit,
345
        ];
346
347
        // plaintext paremeter has no value and must be added only when true 
348
        // (set plaintext=false won't work)
349
        if ($plainText){
350
            $data['plaintext'] = $plainText;
351
        }
352
353
        $response = $this->apiRequest('blacklist', $data, 'GET');
354
355
        if ($plainText){
356
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type boolean|string which is incompatible with the documented return type array|object.
Loading history...
357
        } 
358
       
359
        return json_decode($response, $returnArray);
360
    }
361
362
    /**
363
     * Check if the category(ies) given is/are valid
364
     * Check for shortname or id, and categories that can't be used alone 
365
     * 
366
     * @access protected
367
     * @param array $categories       The report categories list
368
     *
369
     * @return string               Formatted string id list ('18,2,3...')
370
     * @throws \InvalidArgumentException
371
     */
372
    protected function validateReportCategories(string $categories)
373
    {
374
        // the return categories string
375
        $catsString = ''; 
376
377
        // used when cat that can't be used alone
378
        $needAnother = null;
379
380
        // parse given categories
381
        $cats = explode(',', $categories);
382
383
        foreach ($cats as $cat) {
384
385
            // get index on our array of categories
386
            $catIndex    = is_numeric($cat) ? $this->getCategoryIndex($cat, 1) : $this->getCategoryIndex($cat, 0);
387
388
            // check if found
389
            if ($catIndex === false ){
390
                throw new \InvalidArgumentException('Invalid report category was given : ['. $cat .  ']');
391
            }
392
393
            // get Id
394
            $catId = $this->aipdbApiCategories[$catIndex][1];
395
396
            // need another ?
397
            if ($needAnother !== false){
398
399
                // is a standalone cat ?
400
                if ($this->aipdbApiCategories[$catIndex][3] === false) {
401
                    $needAnother = true;
402
403
                } else {
404
                    // ok, continue with other at least one given
405
                    // no need to reperform this check
406
                    $needAnother = false;
407
                }
408
            }
409
410
            // set or add to cats list 
411
            $catsString = ($catsString === '') ? $catId : $catsString .','.$catId;
412
        }
413
414
        if ($needAnother !== false){
0 ignored issues
show
introduced by
The condition $needAnother !== false is always true.
Loading history...
415
            throw new \InvalidArgumentException('Invalid report category paremeter given: some categories can\'t be used alone');
416
        }
417
418
        // if here that ok
419
        return $catsString;
420
    }
421
422
    /**
423
     * Perform a cURL request       
424
     * 
425
     * @access protected
426
     * @param string    $path           The api end path 
427
     * @param array     $data           The request data 
428
     * @param string    $method         The request method. Default is 'GET' 
429
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
430
     * 
431
     * @return mixed
432
     */
433
    protected function apiRequest(string $path, array $data, string $method = 'GET', bool $returnArray = false) 
434
    {
435
        // set api url
436
        $url = $this->aipdbApiEndpoint . $path; 
437
438
        // open curl connection
439
        $ch = curl_init(); 
440
  
441
        // set the method and data to send
442
        if ($method == 'POST') {
443
            curl_setopt($ch, CURLOPT_POST, true);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

443
            curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_POST, true);
Loading history...
444
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
445
        } else {
446
            $url .= '?' . http_build_query($data);
447
        }
448
         
449
        // set the url to call
450
        curl_setopt($ch, CURLOPT_URL, $url);
451
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
452
      
453
        // set the wanted format, JSON (required to prevent having full html page on error)
454
        // and the AbuseIPDB API Key as a header
455
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
456
            'Accept: application/json;',
457
            'Key: ' . $this->aipdbApiKey,
458
        ]);
459
  
460
      // execute curl call
461
      $result = curl_exec($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

461
      $result = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
462
  
463
      // close connection
464
      curl_close($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

464
      curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
465
  
466
      // return response as JSON data
467
      return $result;
468
    }
469
470
    /** 
471
     * Clean message in case it comes from fail2ban <matches>
472
     * Remove backslashes and sensitive information from the report
473
     * @see https://wiki.shaunc.com/wikka.php?wakka=ReportingToAbuseIPDBWithFail2Ban
474
     * 
475
     * @access public
476
     * @param string      $message           The original message 
477
     *  
478
	 * @return string
479
     */
480
    protected function cleanMessage(string $message)
481
    {
482
        // Remove backslashes
483
        $message = str_replace('\\', '', $message);
484
485
        // Remove self ips
486
        foreach ($this->selfIps as $ip){
487
            $message = str_replace($ip, '*', $message);
488
        }
489
490
        // If we're reporting spam, further munge any email addresses in the report
491
        $emailPattern = "/[^@\s]*@[^@\s]*\.[^@\s]*/";
492
        $message = preg_replace($emailPattern, "*", $message);
493
        
494
        // Make sure message is less 1024 chars
495
        return substr($message, 0, 1024);
496
    }
497
498
    /** 
499
     * Load and returns decoded Json from given file  
500
     *
501
     * @access public
502
     * @static
503
	 * @param string    $filePath       The file's full path
504
	 * @param bool      $trowError      Throw error on true or silent process. Default is true
505
     *  
506
	 * @return object|null 
507
     * @throws \Exception
508
     * @throws \LogicException
509
     */
510
    protected static function loadJsonFile(string $filePath, bool $throwError = true)
511
    {
512
        // check file exists
513
        if (!file_exists($filePath) || !is_file($filePath)){
514
           if ($throwError) {
515
                throw new \Exception('Config file not found');
516
           }
517
           return null;  
518
        }
519
520
        // get and parse content
521
        $content = file_get_contents($filePath);
522
        $json    = json_decode(utf8_encode($content));
523
524
        // check for errors
525
        if ($json == null && json_last_error() != JSON_ERROR_NONE){
526
            if ($throwError) {
527
                throw new \LogicException(sprintf("Failed to parse config file Error: '%s'", json_last_error_msg()));
528
            }
529
        }
530
531
        return $json;        
532
    }
533
}