Passed
Push — master ( a94c99...9034c4 )
by Kris
02:06
created

ApiHandler::validateCategories()   B

Complexity

Conditions 8
Paths 28

Size

Total Lines 48
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 48
rs 8.4444
cc 8
nc 28
nop 1
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.3
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->validateCategories($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
     * Check if the category(ies) given is/are valid
193
     * Check for shortname or id, and categories that can't be used alone 
194
     * 
195
     * @access protected
196
     * @param array $categories       The report categories list
197
     *
198
     * @return string               Formatted string id list ('18,2,3...')
199
     * @throws \InvalidArgumentException
200
     */
201
    protected function validateCategories(string $categories)
202
    {
203
        // the return categories string
204
        $catsString = ''; 
205
206
        // used when cat that can't be used alone
207
        $needAnother = null;
208
209
        // parse given categories
210
        $cats = explode(',', $categories);
211
212
        foreach ($cats as $cat) {
213
214
            // get index on our array of categories
215
            $catIndex    = is_numeric($cat) ? $this->getCategoryIndex($cat, 1) : $this->getCategoryIndex($cat, 0);
216
217
            // check if found
218
            if ($catIndex === false ){
219
                throw new \InvalidArgumentException('Invalid report category was given : ['. $cat .  ']');
220
            }
221
222
            // get Id
223
            $catId = $this->aipdbApiCategories[$catIndex][1];
224
225
            // need another ?
226
            if ($needAnother !== false){
227
228
                // is a standalone cat ?
229
                if ($this->aipdbApiCategories[$catIndex][3] === false) {
230
                    $needAnother = true;
231
232
                } else {
233
                    // ok, continue with other at least one given
234
                    // no need to reperform this check
235
                    $needAnother = false;
236
                }
237
            }
238
239
            // set or add to cats list 
240
            $catsString = ($catsString === '') ? $catId : $catsString .','.$catId;
241
        }
242
243
        if ($needAnother !== false){
0 ignored issues
show
introduced by
The condition $needAnother !== false is always true.
Loading history...
244
            throw new \InvalidArgumentException('Invalid report category paremeter given: some categories can\'t be used alone');
245
        }
246
247
        // if here that ok
248
        return $catsString;
249
    }
250
251
    /**
252
     * Perform a 'check' api request
253
     * 
254
     * @access public
255
     * @param string    $ip             The ip to check
256
     * @param int       $maxAge         Max age in days
257
     * @param bool      $verbose        True to get the full response. Default is false
258
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
259
     * 
260
     * @return object|array
261
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when ip value was not set. 
262
     */
263
    public function check(string $ip = null, int $maxAge = 30, bool $verbose = false, bool $returnArray = false)
264
    {
265
        // max age must be less or equal to 365
266
        if ($maxAge > 365 || $maxAge < 1){
267
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
268
        }
269
270
        // ip must be set
271
        if (empty($ip)){
272
            throw new \InvalidArgumentException('ip argument must be set (null given)');
273
        }
274
275
        // minimal data
276
        $data = [
277
            'ipAddress'     => $ip, 
278
            'maxAgeInDays'  => $maxAge,  
279
        ];
280
281
        // option
282
        if ($verbose){
283
           $data['verbose'] = true;
284
        }
285
286
        // check AbuseIPDB request
287
        $response = $this->apiRequest('check', $data, 'GET', $returnArray) ;
288
289
        return json_decode($response, $returnArray);
290
    }
291
292
    /**
293
     * Perform a 'blacklist' api request
294
     * 
295
     * @access public
296
     * @param int       $limit          The blacklist limit. Default is TODO (the api default limit) 
297
     * @param bool      $plainText      True to get the response in plain text list. Default is false
298
     * @param bool      $returnArray    True to return an indexed array instead of an object (when $plainText is set to false). Default is false. 
299
     * 
300
     * @return object|array
301
     * @throws \InvalidArgumentException    When maxAge is not a numeric value, when maxAge is less than 1 or 
302
     *                                      greater than 365, or when ip value was not set. 
303
     */
304
    public function getBlacklist(int $limit = 10000, bool $plainText = false, bool $returnArray = false)
305
    {
306
307
        if ($limit < 1){
308
            throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)');
309
        }
310
311
        // minimal data
312
        $data = [
313
            'confidenceMinimum' => 100, // The abuseConfidenceScore parameter is a subscriber feature. 
314
            'limit'             => $limit,
315
        ];
316
317
        // plaintext paremeter has no value and must be added only when true 
318
        // (set plaintext=false won't work)
319
        if ($plainText){
320
            $data['plaintext'] = $plainText;
321
        }
322
323
        $response = $this->apiRequest('blacklist', $data, 'GET');
324
325
        if ($plainText){
326
            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...
327
        } 
328
       
329
        return json_decode($response, $returnArray);
330
    }
331
332
    /**
333
     * Perform a cURL request       
334
     * 
335
     * @access protected
336
     * @param string    $path           The api end path 
337
     * @param array     $data           The request data 
338
     * @param string    $method         The request method. Default is 'GET' 
339
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
340
     * 
341
     * @return mixed
342
     */
343
    protected function apiRequest(string $path, array $data, string $method = 'GET', bool $returnArray = false) 
344
    {
345
        // set api url
346
        $url = $this->aipdbApiEndpoint . $path; 
347
348
        // open curl connection
349
        $ch = curl_init(); 
350
  
351
        // set the method and data to send
352
        if ($method == 'POST') {
353
            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

353
            curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_POST, true);
Loading history...
354
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
355
        } else {
356
            $url .= '?' . http_build_query($data);
357
        }
358
         
359
        // set the url to call
360
        curl_setopt($ch, CURLOPT_URL, $url);
361
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
362
      
363
        // set the wanted format, JSON (required to prevent having full html page on error)
364
        // and the AbuseIPDB API Key as a header
365
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
366
            'Accept: application/json;',
367
            'Key: ' . $this->aipdbApiKey,
368
        ]);
369
  
370
      // execute curl call
371
      $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

371
      $result = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
372
  
373
      // close connection
374
      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

374
      curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
375
  
376
      // return response as JSON data
377
      return $result;
378
    }
379
380
    /** 
381
     * Clean message in case it comes from fail2ban <matches>
382
     * Remove backslashes and sensitive information from the report
383
     * @see https://wiki.shaunc.com/wikka.php?wakka=ReportingToAbuseIPDBWithFail2Ban
384
     * 
385
     * @access public
386
     * @param string      $message           The original message 
387
     *  
388
	 * @return string
389
     */
390
    protected function cleanMessage(string $message)
391
    {
392
        // Remove backslashes
393
        $message = str_replace('\\', '', $message);
394
395
        // Remove self ips
396
        foreach ($this->selfIps as $ip){
397
            $message = str_replace($ip, '*', $message);
398
        }
399
400
        // If we're reporting spam, further munge any email addresses in the report
401
        $emailPattern = "/[^@\s]*@[^@\s]*\.[^@\s]*/";
402
        $message = preg_replace($emailPattern, "*", $message);
403
        
404
        // Make sure message is less 1024 chars
405
        return substr($message, 0, 1024);
406
    }
407
408
    /** 
409
     * Load and returns decoded Json from given file  
410
     *
411
     * @access public
412
     * @static
413
	 * @param string    $filePath       The file's full path
414
	 * @param bool      $trowError      Throw error on true or silent process. Default is true
415
     *  
416
	 * @return object|null 
417
     * @throws \Exception
418
     * @throws \LogicException
419
     */
420
    protected static function loadJsonFile(string $filePath, bool $throwError = true)
421
    {
422
        // check file exists
423
        if (!file_exists($filePath) || !is_file($filePath)){
424
           if ($throwError) {
425
                throw new \Exception('Config file not found');
426
           }
427
           return null;  
428
        }
429
430
        // get and parse content
431
        $content = file_get_contents($filePath);
432
        $json    = json_decode(utf8_encode($content));
433
434
        // check for errors
435
        if ($json == null && json_last_error() != JSON_ERROR_NONE){
436
            if ($throwError) {
437
                throw new \LogicException(sprintf("Failed to parse config file Error: '%s'", json_last_error_msg()));
438
            }
439
        }
440
441
        return $json;        
442
    }
443
}