Passed
Branch master (1e7e1e)
by Kris
01:37
created

ApiHandler::getConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 0
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.7
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 ApiBase
29
{
30
    /**
31
     * Curl helper functions
32
     */
33
    use CurlTrait;
34
35
    /**
36
     * The ips to remove from message
37
     * Generally you will add to this list yours ipv4 and ipv6, and the hostname
38
     * 
39
     * @access protected
40
     * @var array $selfIps  
41
     */
42
    protected $selfIps = []; 
43
44
    /**
45
     * Constructor
46
     * 
47
     * @access public
48
     * @param string  $apiKey     The AbuseIPDB api key
49
     * @param string  $userId     The AbuseIPDB user's id
50
     * @param array   $myIps      The Ips/domain name you dont want to display in report messages
51
     * 
52
     */
53
    public function __construct(string $apiKey, string $userId, array $myIps = [])
54
    {
55
        $this->aipdbApiKey = $apiKey;
56
        $this->aipdbUserId = $userId;
57
        $this->selfIps = $myIps;
58
    }
59
60
    /**
61
     * Get the current configuration in a indexed array
62
     * 
63
     * @access public 
64
     * @return array
65
     */
66
    public function getConfig()
67
    {
68
        return array(
69
            'userId'  => $this->aipdbUserId,
70
            'apiKey'  => $this->aipdbApiKey,
71
            'selfIps' => $this->selfIps,
72
            // TODO  default report cat 
73
        );
74
    }
75
76
    /**
77
     * Get a new instance of ApiHandler with config stored in a Json file
78
     * 
79
     * @access public 
80
     * @static
81
     * @param string    $configPath     The configuration file path
82
     * 
83
     * @return \Kristuff\AbuseIPDB\ApiHandler
84
     * @throws \InvalidArgumentException                        If the given file does not exist
85
     * @throws \Kristuff\AbuseIPDB\InvalidPermissionException   If the given file is not readable 
86
     */
87
    public static function fromConfigFile(string $configPath)
88
    {
89
90
        // check file exists
91
        if (!file_exists($configPath) || !is_file($configPath)){
92
            throw new \InvalidArgumentException('The file [' . $configPath . '] does not exist.');
93
        }
94
95
        // check file is readable
96
        if (!is_readable($configPath)){
97
            throw new InvalidPermissionException('The file [' . $configPath . '] is not readable.');
98
        }
99
100
        $keyConfig = self::loadJsonFile($configPath);
101
        $selfIps = [];
102
        
103
        // Look for other optional config files in the same directory 
104
        $selfIpsConfigPath = pathinfo($configPath, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . 'self_ips.json';
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($configPath, Kr...eIPDB\PATHINFO_DIRNAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

104
        $selfIpsConfigPath = /** @scrutinizer ignore-type */ pathinfo($configPath, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . 'self_ips.json';
Loading history...
105
        if (file_exists($selfIpsConfigPath)){
106
            $selfIps = self::loadJsonFile($selfIpsConfigPath)->self_ips;
107
        }
108
109
        $app = new self($keyConfig->api_key, $keyConfig->user_id, $selfIps);
110
        
111
        return $app;
112
    }
113
114
    /**
115
     * Performs a 'report' api request
116
     * 
117
     * Result, in json format will be something like this:
118
     *  {
119
     *       "data": {
120
     *         "ipAddress": "127.0.0.1",
121
     *         "abuseConfidenceScore": 52
122
     *       }
123
     *  }
124
     * 
125
     * @access public
126
     * @param string    $ip             The ip to report
127
     * @param string    $categories     The report categories
128
     * @param string    $message        The report message
129
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
130
     *
131
     * @return object|array
132
     * @throws \InvalidArgumentException
133
     */
134
    public function report(string $ip = '', string $categories = '', string $message = '', bool $returnArray = false)
135
    {
136
         // ip must be set
137
        if (empty($ip)){
138
            throw new \InvalidArgumentException('Ip was empty');
139
        }
140
141
        // categories must be set
142
        if (empty($categories)){
143
            throw new \InvalidArgumentException('categories list was empty');
144
        }
145
146
        // message must be set
147
          if (empty($message)){
148
            throw new \InvalidArgumentException('report message was empty');
149
        }
150
151
        // validates categories, clean message 
152
        $cats = $this->validateReportCategories($categories);
153
        $msg = $this->cleanMessage($message);
154
155
        // AbuseIPDB request
156
        $response = $this->apiRequest(
157
            'report', [
158
                'ip' => $ip,
159
                'categories' => $cats,
160
                'comment' => $msg
161
            ],
162
            'POST'
163
        );
164
165
        return json_decode($response, $returnArray);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, 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

165
        return json_decode(/** @scrutinizer ignore-type */ $response, $returnArray);
Loading history...
166
    }
167
168
    /**
169
     * Performs a 'bulk-report' api request
170
     * 
171
     * Result, in json format will be something like this:
172
     *   {
173
     *     "data": {
174
     *       "savedReports": 60,
175
     *       "invalidReports": [
176
     *         {
177
     *           "error": "Duplicate IP",
178
     *           "input": "41.188.138.68",
179
     *           "rowNumber": 5
180
     *         },
181
     *         {
182
     *           "error": "Invalid IP",
183
     *           "input": "127.0.foo.bar",
184
     *           "rowNumber": 6
185
     *         },
186
     *         {
187
     *           "error": "Invalid Category",
188
     *           "input": "189.87.146.50",
189
     *           "rowNumber": 8
190
     *         }
191
     *       ]
192
     *     }
193
     *   }
194
     *  
195
     * @access public
196
     * @param string    $ip             The ip to report
197
     * @param string    $categories     The report categories
198
     * @param string    $message        The report message
199
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
200
     *
201
     * @return object|array
202
     * @throws \InvalidArgumentException
203
     */
204
    public function bulkReport(string $filePath, bool $returnArray = false)
205
    {
206
        // check file exists
207
        if (!file_exists($filePath) || !is_file($filePath)){
208
            throw new \InvalidArgumentException('The file [' . $filePath . '] does not exist.');
209
        }
210
211
        // check file is readable
212
        if (!is_readable($filePath)){
213
            throw new InvalidPermissionException('The file [' . $filePath . '] is not readable.');
214
        }
215
216
        // AbuseIPDB request
217
        $response = $this->apiRequest('bulk-report', [], 'POST', $filePath);
218
219
        return json_decode($response, $returnArray);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, 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

219
        return json_decode(/** @scrutinizer ignore-type */ $response, $returnArray);
Loading history...
220
    }
221
222
    /**
223
     * Perform a 'clear-address' api request
224
     * 
225
     *  Sample response:
226
     * 
227
     *    {
228
     *      "data": {
229
     *        "numReportsDeleted": 0
230
     *      }
231
     *    }
232
     * 
233
     * @access public
234
     * @param string    $ip             The ip to check
235
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
236
     * 
237
     * @return object|array
238
     * @throws \InvalidArgumentException    When ip value was not set. 
239
     */
240
    public function clear(string $ip = null, bool $returnArray = false)
241
    {
242
        // ip must be set
243
        if (empty($ip)){
244
            throw new \InvalidArgumentException('ip argument must be set (null given)');
245
        }
246
247
        // minimal data
248
        $data = [
249
            'ipAddress'     => $ip, 
250
        ];
251
252
        $response = $this->apiRequest('clear-address', $data, "DELETE") ;
253
        return json_decode($response, $returnArray);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, 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

253
        return json_decode(/** @scrutinizer ignore-type */ $response, $returnArray);
Loading history...
254
    }
255
256
    /**
257
     * Perform a 'check' api request
258
     * 
259
     * @access public
260
     * @param string    $ip             The ip to check
261
     * @param int       $maxAge         Max age in days
262
     * @param bool      $verbose        True to get the full response. Default is false
263
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
264
     * 
265
     * @return object|array
266
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when ip value was not set. 
267
     */
268
    public function check(string $ip = null, int $maxAge = 30, bool $verbose = false, bool $returnArray = false)
269
    {
270
        // max age must be less or equal to 365
271
        if ($maxAge > 365 || $maxAge < 1){
272
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
273
        }
274
275
        // ip must be set
276
        if (empty($ip)){
277
            throw new \InvalidArgumentException('ip argument must be set (null given)');
278
        }
279
280
        // minimal data
281
        $data = [
282
            'ipAddress'     => $ip, 
283
            'maxAgeInDays'  => $maxAge,  
284
        ];
285
286
        // option
287
        if ($verbose){
288
           $data['verbose'] = true;
289
        }
290
291
        $response = $this->apiRequest('check', $data, 'GET') ;
292
293
        return json_decode($response, $returnArray);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, 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

293
        return json_decode(/** @scrutinizer ignore-type */ $response, $returnArray);
Loading history...
294
    }
295
296
    /**
297
     * Perform a 'check-block' api request
298
     * 
299
     * 
300
     * Sample json response for 127.0.0.1/24
301
     * 
302
     * {
303
     *    "data": {
304
     *      "networkAddress": "127.0.0.0",
305
     *      "netmask": "255.255.255.0",
306
     *      "minAddress": "127.0.0.1",
307
     *      "maxAddress": "127.0.0.254",
308
     *      "numPossibleHosts": 254,
309
     *      "addressSpaceDesc": "Loopback",
310
     *      "reportedAddress": [
311
     *        {
312
     *          "ipAddress": "127.0.0.1",
313
     *          "numReports": 631,
314
     *          "mostRecentReport": "2019-03-21T16:35:16+00:00",
315
     *          "abuseConfidenceScore": 0,
316
     *          "countryCode": null
317
     *        },
318
     *        {
319
     *          "ipAddress": "127.0.0.2",
320
     *          "numReports": 16,
321
     *          "mostRecentReport": "2019-03-12T20:31:17+00:00",
322
     *          "abuseConfidenceScore": 0,
323
     *          "countryCode": null
324
     *        },
325
     *        ...
326
     *      ]
327
     *    }
328
     *  }
329
     * 
330
     * 
331
     * @access public
332
     * @param string    $network        The network to check
333
     * @param int       $maxAge         Max age in days
334
     * @param bool      $returnArray    True to return an indexed array instead of object. Default is false. 
335
     * 
336
     * @return object|array
337
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when network value was not set. 
338
     */
339
    public function checkBlock(string $network = null, int $maxAge = 30, bool $returnArray = false)
340
    {
341
        // max age must be less or equal to 365
342
        if ($maxAge > 365 || $maxAge < 1){
343
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
344
        }
345
346
        // ip must be set
347
        if (empty($network)){
348
            throw new \InvalidArgumentException('network argument must be set (null given)');
349
        }
350
351
        // minimal data
352
        $data = [
353
            'network'       => $network, 
354
            'maxAgeInDays'  => $maxAge,  
355
        ];
356
357
        $response = $this->apiRequest('check-block', $data, 'GET') ;
358
359
        return json_decode($response, $returnArray);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, 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

359
        return json_decode(/** @scrutinizer ignore-type */ $response, $returnArray);
Loading history...
360
    }
361
362
    /**
363
     * Perform a 'blacklist' api request
364
     * 
365
     * @access public
366
     * @param int       $limit          The blacklist limit. Default is TODO (the api default limit) 
367
     * @param bool      $plainText      True to get the response in plain text list. Default is false
368
     * @param bool      $returnArray    True to return an indexed array instead of object (when $plainText is set to false). Default is false. 
369
     * 
370
     * @return object|array|string
371
     * @throws \InvalidArgumentException    When maxAge is not a numeric value, when maxAge is less than 1 or 
372
     *                                      greater than 365, or when ip value was not set. 
373
     */
374
    public function getBlacklist(int $limit = 10000, bool $plainText = false, bool $returnArray = false)
375
    {
376
377
        if ($limit < 1){
378
            throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)');
379
        }
380
381
        // minimal data
382
        $data = [
383
            'confidenceMinimum' => 100, // The abuseConfidenceScore parameter is a subscriber feature. 
384
            'limit'             => $limit,
385
        ];
386
387
        // plaintext paremeter has no value and must be added only when true 
388
        // (set plaintext=false won't work)
389
        if ($plainText){
390
            $data['plaintext'] = $plainText;
391
        }
392
393
        $response = $this->apiRequest('blacklist', $data, 'GET');
394
395
        if ($plainText){
396
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response also could return the type true which is incompatible with the documented return type array|object|string.
Loading history...
397
        } 
398
       
399
        return json_decode($response, $returnArray);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, 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

399
        return json_decode(/** @scrutinizer ignore-type */ $response, $returnArray);
Loading history...
400
    }
401
  
402
    /**
403
     * Perform a cURL request       
404
     * 
405
     * @access protected
406
     * @param string    $path           The api end path 
407
     * @param array     $data           The request data 
408
     * @param string    $method         The request method. Default is 'GET' 
409
     * @param bool      $csvFilePath    The file path for csv file. When not empty, $data parameter is ignored and in place,
410
     *                                  the content of the given file if passed as csv. Default is empty string. 
411
     * 
412
     * @return mixed
413
     * @throws \RuntimeException
414
     */
415
    protected function apiRequest(string $path, array $data, string $method = 'GET', string $csvFilePath = '') 
416
    {
417
        // set api url
418
        $url = $this->aipdbApiEndpoint . $path; 
419
420
        // set the wanted format, JSON (required to prevent having full html page on error)
421
        // and the AbuseIPDB API Key as a header
422
        $headers = [
423
            'Accept: application/json;',
424
            'Key: ' . $this->aipdbApiKey,
425
        ];
426
427
        // open curl connection
428
        $ch = curl_init(); 
429
  
430
        // for csv
431
        if (!empty($csvFilePath)){
432
            $cfile = new \CurlFile($csvFilePath,  'text/csv', 'csv');
433
            //curl file itself return the realpath with prefix of @
434
            $data = array('csv' => $cfile);
435
        }
436
437
        // set the method and data to send
438
        if ($method == 'POST') {
439
            $this->setCurlOption($ch, CURLOPT_POST, true);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type CurlHandle; however, parameter $ch of Kristuff\AbuseIPDB\ApiHandler::setCurlOption() 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

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