Passed
Push — master ( 9034c4...6140da )
by Kris
01:44
created

ApiHandler::loadJsonFile()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 22
rs 8.8333
cc 7
nc 5
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.4
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 ApiManager 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\ApiManager
0 ignored issues
show
Bug introduced by
The type Kristuff\AbuseIPDB\ApiManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $app returns the type Kristuff\AbuseIPDB\ApiHandler which is incompatible with the documented return type Kristuff\AbuseIPDB\ApiManager.
Loading history...
124
    }
125
126
    /**
127
     * Get the list of report categories
128
     * 
129
     * @access public 
130
     * @return array
131
     */
132
    public function getCategories()
133
    {
134
        return $this->aipdbApiCategories;
135
    }
136
137
    /**
138
     * Performs a 'report' api request
139
     * 
140
     * Result, in json format will be something like this:
141
     *  {
142
     *       "data": {
143
     *         "ipAddress": "127.0.0.1",
144
     *         "abuseConfidenceScore": 52
145
     *       }
146
     *  }
147
     * 
148
     * @access public
149
     * @param string    $ip             The ip to report
150
     * @param string    $categories     The report categories
151
     * @param string    $message        The report message
152
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
153
     *
154
     * @return object|array
155
     * @throws \InvalidArgumentException
156
     */
157
    public function report(string $ip = '', string $categories = '', string $message = '', bool $returnArray = false)
158
    {
159
         // ip must be set
160
        if (empty($ip)){
161
            throw new \InvalidArgumentException('Ip was empty');
162
        }
163
164
        // categories must be set
165
        if (empty($categories)){
166
            throw new \InvalidArgumentException('categories list was empty');
167
        }
168
169
        // message must be set
170
          if (empty($message)){
171
            throw new \InvalidArgumentException('report message was empty');
172
        }
173
174
        // validates categories, clean message 
175
        $cats = $this->validateReportCategories($categories);
176
        $msg = $this->cleanMessage($message);
177
178
        // report AbuseIPDB request
179
        $response = $this->apiRequest(
180
            'report', [
181
                'ip' => $ip,
182
                'categories' => $cats,
183
                'comment' => $msg
184
            ],
185
            'POST', $returnArray
186
        );
187
188
        return json_decode($response, $returnArray);
189
    }
190
191
    /**
192
     * Perform a 'check-block' api request
193
     * 
194
     * 
195
     * Sample json response for 127.0.0.1/24
196
     * 
197
     * {
198
     *    "data": {
199
     *      "networkAddress": "127.0.0.0",
200
     *      "netmask": "255.255.255.0",
201
     *      "minAddress": "127.0.0.1",
202
     *      "maxAddress": "127.0.0.254",
203
     *      "numPossibleHosts": 254,
204
     *      "addressSpaceDesc": "Loopback",
205
     *      "reportedAddress": [
206
     *        {
207
     *          "ipAddress": "127.0.0.1",
208
     *          "numReports": 631,
209
     *          "mostRecentReport": "2019-03-21T16:35:16+00:00",
210
     *          "abuseConfidenceScore": 0,
211
     *          "countryCode": null
212
     *        },
213
     *        {
214
     *          "ipAddress": "127.0.0.2",
215
     *          "numReports": 16,
216
     *          "mostRecentReport": "2019-03-12T20:31:17+00:00",
217
     *          "abuseConfidenceScore": 0,
218
     *          "countryCode": null
219
     *        },
220
     *        ...
221
     *      ]
222
     *    }
223
     *  }
224
     * 
225
     * 
226
     * @access public
227
     * @param string    $network        The network to check
228
     * @param int       $maxAge         Max age in days
229
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
230
     * 
231
     * @return object|array
232
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when network value was not set. 
233
     */
234
    public function checkBlock(string $network = null, int $maxAge = 30, bool $returnArray = false)
235
    {
236
        // max age must be less or equal to 365
237
        if ($maxAge > 365 || $maxAge < 1){
238
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
239
        }
240
241
        // ip must be set
242
        if (empty($network)){
243
            throw new \InvalidArgumentException('network argument must be set (null given)');
244
        }
245
246
        // minimal data
247
        $data = [
248
            'network'       => $network, 
249
            'maxAgeInDays'  => $maxAge,  
250
        ];
251
252
        $response = $this->apiRequest('check-block', $data, 'GET', $returnArray) ;
253
254
        return json_decode($response, $returnArray);
255
    }
256
   
257
    /**
258
     * Perform a 'check' api request
259
     * 
260
     * @access public
261
     * @param string    $ip             The ip to check
262
     * @param int       $maxAge         Max age in days
263
     * @param bool      $verbose        True to get the full response. Default is false
264
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
265
     * 
266
     * @return object|array
267
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when ip value was not set. 
268
     */
269
    public function check(string $ip = null, int $maxAge = 30, bool $verbose = false, bool $returnArray = false)
270
    {
271
        // max age must be less or equal to 365
272
        if ($maxAge > 365 || $maxAge < 1){
273
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
274
        }
275
276
        // ip must be set
277
        if (empty($ip)){
278
            throw new \InvalidArgumentException('ip argument must be set (null given)');
279
        }
280
281
        // minimal data
282
        $data = [
283
            'ipAddress'     => $ip, 
284
            'maxAgeInDays'  => $maxAge,  
285
        ];
286
287
        // option
288
        if ($verbose){
289
           $data['verbose'] = true;
290
        }
291
292
        // check AbuseIPDB request
293
        $response = $this->apiRequest('check', $data, 'GET', $returnArray) ;
294
295
        return json_decode($response, $returnArray);
296
    }
297
298
    /**
299
     * Perform a 'blacklist' api request
300
     * 
301
     * @access public
302
     * @param int       $limit          The blacklist limit. Default is TODO (the api default limit) 
303
     * @param bool      $plainText      True to get the response in plain text list. Default is false
304
     * @param bool      $returnArray    True to return an indexed array instead of an object (when $plainText is set to false). Default is false. 
305
     * 
306
     * @return object|array
307
     * @throws \InvalidArgumentException    When maxAge is not a numeric value, when maxAge is less than 1 or 
308
     *                                      greater than 365, or when ip value was not set. 
309
     */
310
    public function getBlacklist(int $limit = 10000, bool $plainText = false, bool $returnArray = false)
311
    {
312
313
        if ($limit < 1){
314
            throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)');
315
        }
316
317
        // minimal data
318
        $data = [
319
            'confidenceMinimum' => 100, // The abuseConfidenceScore parameter is a subscriber feature. 
320
            'limit'             => $limit,
321
        ];
322
323
        // plaintext paremeter has no value and must be added only when true 
324
        // (set plaintext=false won't work)
325
        if ($plainText){
326
            $data['plaintext'] = $plainText;
327
        }
328
329
        $response = $this->apiRequest('blacklist', $data, 'GET');
330
331
        if ($plainText){
332
            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...
333
        } 
334
       
335
        return json_decode($response, $returnArray);
336
    }
337
338
    /**
339
     * Check if the category(ies) given is/are valid
340
     * Check for shortname or id, and categories that can't be used alone 
341
     * 
342
     * @access protected
343
     * @param array $categories       The report categories list
344
     *
345
     * @return string               Formatted string id list ('18,2,3...')
346
     * @throws \InvalidArgumentException
347
     */
348
    protected function validateReportCategories(string $categories)
349
    {
350
        // the return categories string
351
        $catsString = ''; 
352
353
        // used when cat that can't be used alone
354
        $needAnother = null;
355
356
        // parse given categories
357
        $cats = explode(',', $categories);
358
359
        foreach ($cats as $cat) {
360
361
            // get index on our array of categories
362
            $catIndex    = is_numeric($cat) ? $this->getCategoryIndex($cat, 1) : $this->getCategoryIndex($cat, 0);
363
364
            // check if found
365
            if ($catIndex === false ){
366
                throw new \InvalidArgumentException('Invalid report category was given : ['. $cat .  ']');
367
            }
368
369
            // get Id
370
            $catId = $this->aipdbApiCategories[$catIndex][1];
371
372
            // need another ?
373
            if ($needAnother !== false){
374
375
                // is a standalone cat ?
376
                if ($this->aipdbApiCategories[$catIndex][3] === false) {
377
                    $needAnother = true;
378
379
                } else {
380
                    // ok, continue with other at least one given
381
                    // no need to reperform this check
382
                    $needAnother = false;
383
                }
384
            }
385
386
            // set or add to cats list 
387
            $catsString = ($catsString === '') ? $catId : $catsString .','.$catId;
388
        }
389
390
        if ($needAnother !== false){
0 ignored issues
show
introduced by
The condition $needAnother !== false is always true.
Loading history...
391
            throw new \InvalidArgumentException('Invalid report category paremeter given: some categories can\'t be used alone');
392
        }
393
394
        // if here that ok
395
        return $catsString;
396
    }
397
398
    /**
399
     * Perform a cURL request       
400
     * 
401
     * @access protected
402
     * @param string    $path           The api end path 
403
     * @param array     $data           The request data 
404
     * @param string    $method         The request method. Default is 'GET' 
405
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
406
     * 
407
     * @return mixed
408
     */
409
    protected function apiRequest(string $path, array $data, string $method = 'GET', bool $returnArray = false) 
410
    {
411
        // set api url
412
        $url = $this->aipdbApiEndpoint . $path; 
413
414
        // open curl connection
415
        $ch = curl_init(); 
416
  
417
        // set the method and data to send
418
        if ($method == 'POST') {
419
            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

419
            curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_POST, true);
Loading history...
420
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
421
        } else {
422
            $url .= '?' . http_build_query($data);
423
        }
424
         
425
        // set the url to call
426
        curl_setopt($ch, CURLOPT_URL, $url);
427
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
428
      
429
        // set the wanted format, JSON (required to prevent having full html page on error)
430
        // and the AbuseIPDB API Key as a header
431
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
432
            'Accept: application/json;',
433
            'Key: ' . $this->aipdbApiKey,
434
        ]);
435
  
436
      // execute curl call
437
      $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

437
      $result = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
438
  
439
      // close connection
440
      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

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