ApiHandler::report()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 29
rs 9.7998
cc 4
nc 4
nop 3
1
<?php declare(strict_types=1);
2
3
/**
4
 *       _                 ___ ___ ___  ___
5
 *  __ _| |__ _  _ ___ ___|_ _| _ \   \| _ )
6
 * / _` | '_ \ || (_-</ -_)| ||  _/ |) | _ \
7
 * \__,_|_.__/\_,_/__/\___|___|_| |___/|___/
8
 * 
9
 * This file is part of Kristuff\AbuseIPDB.
10
 *
11
 * (c) Kristuff <[email protected]>
12
 *
13
 * For the full copyright and license information, please view the LICENSE
14
 * file that was distributed with this source code.
15
 *
16
 * @version    1.1
17
 * @copyright  2020-2022 Kristuff
18
 */
19
20
namespace Kristuff\AbuseIPDB;
21
22
/**
23
 * Class ApiHandler
24
 * 
25
 * The main class to work with the AbuseIPDB API v2 
26
 */
27
class ApiHandler extends ApiBase
28
{
29
    /**
30
     * Curl helper functions
31
     */
32
    use CurlTrait;
33
34
    /**
35
     * @var string
36
     */
37
    const VERSION = 'v1.1'; 
38
39
    /**
40
     * The ips to remove from report messages
41
     * Generally you will add to this list yours ipv4 and ipv6, hostname, domain names
42
     * 
43
     * @access protected
44
     * @var array  
45
     */
46
    protected $selfIps = []; 
47
48
    /**
49
     * The maximum number of milliseconds to allow cURL functions to execute. If libcurl is 
50
     * built to use the standard system name resolver, that portion of the connect will still 
51
     * use full-second resolution for timeouts with a minimum timeout allowed of one second. 
52
     * 
53
     * @access protected
54
     * @var int  
55
     */
56
    protected $timeout = 0; 
57
58
    /**
59
     * Constructor
60
     * 
61
     * @access public
62
     * @param string  $apiKey     The AbuseIPDB api key
63
     * @param array   $myIps      The Ips/domain name you don't want to display in report messages
64
     * @param int     $timeout    The maximum number of milliseconds to allow internal cURL functions 
65
     *                            to execute. Default is 0, no timeout
66
     * 
67
     */
68
    public function __construct(string $apiKey, array $myIps = [], int $timeout = 0)
69
    {
70
        $this->aipdbApiKey = $apiKey;
71
        $this->selfIps = $myIps;
72
        $this->timeout = $timeout;
73
    }
74
75
    /**
76
     * Sets the cURL timeout (apply then to any API request). Overwrites the value passed in 
77
     * constructor, useful when performing multiple queries with same handler but different timeout.
78
     * 
79
     * @access public
80
     * @param int     $timeout    The maximum number of milliseconds to allow internal cURL functions 
81
     *                            to execute.
82
     * 
83
     * @return void
84
     */
85
    public function setTimeout(int $timeout): void
86
    {
87
        $this->timeout = $timeout;
88
    }
89
90
    /**
91
     * Get the current configuration in a indexed array
92
     * 
93
     * @access public
94
     * 
95
     * @return array
96
     */
97
    public function getConfig(): array
98
    {
99
        return array(
100
            'apiKey'  => $this->aipdbApiKey,
101
            'selfIps' => $this->selfIps,
102
            'timeout' => $this->timeout, 
103
        );
104
    }
105
106
    /**
107
     * Performs a 'report' api request
108
     * 
109
     * Result, in json format will be something like this:
110
     *  {
111
     *       "data": {
112
     *         "ipAddress": "127.0.0.1",
113
     *         "abuseConfidenceScore": 52
114
     *       }
115
     *  }
116
     * 
117
     * @access public
118
     * @param string    $ip             The ip to report
119
     * @param string    $categories     The report category(es)
120
     * @param string    $message        The report message
121
     *
122
     * @return ApiResponse
123
     * @throws \RuntimeException
124
     * @throws \InvalidArgumentException
125
     */
126
    public function report(string $ip, string $categories, string $message): ApiResponse
127
    {
128
         // ip must be set
129
        if (empty($ip)){
130
            throw new \InvalidArgumentException('Ip was empty');
131
        }
132
133
        // categories must be set
134
        if (empty($categories)){
135
            throw new \InvalidArgumentException('Categories list was empty');
136
        }
137
138
        // message must be set
139
        if (empty($message)){
140
            throw new \InvalidArgumentException('Report message was empty');
141
        }
142
143
        // validates categories, clean message 
144
        $cats = $this->validateReportCategories($categories);
145
        $msg  = $this->cleanMessage($message);
146
147
        // AbuseIPDB request
148
        return $this->apiRequest(
149
            'report', [
150
                'ip'            => $ip,
151
                'categories'    => $cats,
152
                'comment'       => $msg
153
            ],
154
            'POST'
155
        );
156
    }
157
158
    /**
159
     * Performs a 'bulk-report' api request
160
     * 
161
     * Result, in json format will be something like this:
162
     *   {
163
     *     "data": {
164
     *       "savedReports": 60,
165
     *       "invalidReports": [
166
     *         {
167
     *           "error": "Duplicate IP",
168
     *           "input": "41.188.138.68",
169
     *           "rowNumber": 5
170
     *         },
171
     *         {
172
     *           "error": "Invalid IP",
173
     *           "input": "127.0.foo.bar",
174
     *           "rowNumber": 6
175
     *         },
176
     *         {
177
     *           "error": "Invalid Category",
178
     *           "input": "189.87.146.50",
179
     *           "rowNumber": 8
180
     *         }
181
     *       ]
182
     *     }
183
     *   }
184
     *  
185
     * @access public
186
     * @param string    $filePath       The CSV file path. Could be an absolute or relative path.
187
     *
188
     * @return ApiResponse
189
     * @throws \RuntimeException
190
     * @throws \InvalidArgumentException
191
     * @throws InvalidPermissionException
192
     */
193
    public function bulkReport(string $filePath): ApiResponse
194
    {
195
        // check file exists
196
        if (!file_exists($filePath) || !is_file($filePath)){
197
            throw new \InvalidArgumentException('The file [' . $filePath . '] does not exist.');
198
        }
199
200
        // check file is readable
201
        if (!is_readable($filePath)){
202
            throw new InvalidPermissionException('The file [' . $filePath . '] is not readable.');
203
        }
204
205
        return $this->apiRequest('bulk-report', [], 'POST', $filePath);
206
    }
207
208
    /**
209
     * Perform a 'clear-address' api request
210
     * 
211
     *  Sample response:
212
     * 
213
     *    {
214
     *      "data": {
215
     *        "numReportsDeleted": 0
216
     *      }
217
     *    }
218
     * 
219
     * @access public
220
     * @param string    $ip             The IP to clear reports
221
     * 
222
     * @return ApiResponse
223
     * @throws \RuntimeException
224
     * @throws \InvalidArgumentException    When ip value was not set. 
225
     */
226
    public function clearAddress(string $ip): ApiResponse
227
    {
228
        // ip must be set
229
        if (empty($ip)){
230
            throw new \InvalidArgumentException('IP argument must be set.');
231
        }
232
233
        return $this->apiRequest('clear-address',  ['ipAddress' => $ip ], "DELETE") ;
234
    }
235
236
    /**
237
     * Perform a 'check' api request
238
     * 
239
     * @access public
240
     * @param string    $ip             The ip to check
241
     * @param int       $maxAgeInDays   Max age in days. Default is 30.
242
     * @param bool      $verbose        True to get the full response (last reports and countryName). Default is false
243
     * 
244
     * @return ApiResponse
245
     * @throws \RuntimeException
246
     * @throws \InvalidArgumentException    when maxAge is less than 1 or greater than 365, or when ip value was not set. 
247
     */
248
    public function check(string $ip, int $maxAgeInDays = 30, bool $verbose = false): ApiResponse
249
    {
250
        // max age must be less or equal to 365
251
        if ( $maxAgeInDays > 365 || $maxAgeInDays < 1 ){
252
            throw new \InvalidArgumentException('maxAgeInDays must be between 1 and 365.');
253
        }
254
255
        // ip must be set
256
        if (empty($ip)){
257
            throw new \InvalidArgumentException('ip argument must be set (empty value given)');
258
        }
259
260
        // minimal data
261
        $data = [
262
            'ipAddress'     => $ip, 
263
            'maxAgeInDays'  => $maxAgeInDays,  
264
        ];
265
266
        // option
267
        if ($verbose){
268
           $data['verbose'] = true;
269
        }
270
271
        return $this->apiRequest('check', $data, 'GET') ;
272
    }
273
274
    /**
275
     * Perform a 'check-block' api request
276
     * 
277
     * 
278
     * Sample json response for 127.0.0.1/24
279
     * 
280
     * {
281
     *    "data": {
282
     *      "networkAddress": "127.0.0.0",
283
     *      "netmask": "255.255.255.0",
284
     *      "minAddress": "127.0.0.1",
285
     *      "maxAddress": "127.0.0.254",
286
     *      "numPossibleHosts": 254,
287
     *      "addressSpaceDesc": "Loopback",
288
     *      "reportedAddress": [
289
     *        {
290
     *          "ipAddress": "127.0.0.1",
291
     *          "numReports": 631,
292
     *          "mostRecentReport": "2019-03-21T16:35:16+00:00",
293
     *          "abuseConfidenceScore": 0,
294
     *          "countryCode": null
295
     *        },
296
     *        {
297
     *          "ipAddress": "127.0.0.2",
298
     *          "numReports": 16,
299
     *          "mostRecentReport": "2019-03-12T20:31:17+00:00",
300
     *          "abuseConfidenceScore": 0,
301
     *          "countryCode": null
302
     *        },
303
     *        ...
304
     *      ]
305
     *    }
306
     *  }
307
     * 
308
     * 
309
     * @access public
310
     * @param string    $network        The network to check
311
     * @param int       $maxAgeInDays   The Max age in days, must 
312
     * 
313
     * @return ApiResponse
314
     * @throws \RuntimeException
315
     * @throws \InvalidArgumentException    when $maxAgeInDays is less than 1 or greater than 365, or when $network value was not set. 
316
     */
317
    public function checkBlock(string $network, int $maxAgeInDays = 30): ApiResponse
318
    {
319
        // max age must be between 1 and 365
320
        if ($maxAgeInDays > 365 || $maxAgeInDays < 1){
321
            throw new \InvalidArgumentException('maxAgeInDays must be between 1 and 365 (' . $maxAgeInDays . ' was given)');
322
        }
323
324
        // ip must be set
325
        if (empty($network)){
326
            throw new \InvalidArgumentException('network argument must be set (empty value given)');
327
        }
328
329
        // minimal data
330
        $data = [
331
            'network'       => $network, 
332
            'maxAgeInDays'  => $maxAgeInDays,  
333
        ];
334
335
        return $this->apiRequest('check-block', $data, 'GET');
336
    }
337
338
    /**
339
     * Perform a 'blacklist' api request
340
     * 
341
     * @access public
342
     * @param int       $limit              The blacklist limit. Default is 10000 (the api default limit) 
343
     * @param bool      $plainText          True to get the response in plaintext list. Default is false
344
     * @param int       $confidenceMinimum  The abuse confidence score minimum (subscribers feature). Default is 100.
345
     *                                      The confidence minimum must be between 25 and 100.
346
     *                                      This parameter is a subscriber feature (not honored otherwise).
347
     * 
348
     * @return ApiResponse
349
     * @throws \RuntimeException
350
     * @throws \InvalidArgumentException    When maxAge is not a numeric value, when $limit is less than 1. 
351
     */
352
    public function blacklist(int $limit = 10000, bool $plainText = false, int $confidenceMinimum = 100): ApiResponse
353
    {
354
        if ($limit < 1){
355
            throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)');
356
        }
357
358
        // minimal data
359
        $data = [
360
            'confidenceMinimum' => $confidenceMinimum, 
361
            'limit'             => $limit,
362
        ];
363
364
        // plaintext paremeter has no value and must be added only when true 
365
        // (set plaintext=false won't work)
366
        if ($plainText){
367
            $data['plaintext'] = $plainText;
368
        }
369
370
        return $this->apiRequest('blacklist', $data, 'GET');
371
    }
372
  
373
    /**
374
     * Perform a cURL request       
375
     * 
376
     * @access protected
377
     * @param string    $path           The api end path 
378
     * @param array     $data           The request data 
379
     * @param string    $method         The request method. Default is 'GET' 
380
     * @param string    $csvFilePath    The file path for csv file. When not empty, $data parameter is ignored and in place,
381
     *                                  the content of the given file if passed as csv. Default is empty string. 
382
     * 
383
     * @return ApiResponse
384
     * @throws \RuntimeException
385
     */
386
    protected function apiRequest(string $path, array $data, string $method = 'GET', string $csvFilePath = ''): ApiResponse
387
    {
388
        $curlErrorNumber = -1;                   // will be used later to check curl execution
0 ignored issues
show
Unused Code introduced by
The assignment to $curlErrorNumber is dead and can be removed.
Loading history...
389
        $curlErrorMessage = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $curlErrorMessage is dead and can be removed.
Loading history...
390
        $url = $this->aipdbApiEndpoint . $path;  // api url
391
        
392
        // set the wanted format, JSON (required to prevent having full html page on error)
393
        // and the AbuseIPDB API Key as a header
394
        $headers = [
395
            'Accept: application/json;',
396
            'Key: ' . $this->aipdbApiKey,
397
        ];
398
399
        // open curl connection
400
        $ch = curl_init(); 
401
  
402
        // for csv
403
        if (!empty($csvFilePath)){
404
            $cfile = new \CurlFile($csvFilePath,  'text/csv', 'csv');
405
            //curl file itself return the realpath with prefix of @
406
            $data = array('csv' => $cfile);
407
        }
408
409
        // set the method and data to send
410
        if ($method == 'POST') {
411
            $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

411
            $this->setCurlOption(/** @scrutinizer ignore-type */ $ch, CURLOPT_POST, true);
Loading history...
412
            $this->setCurlOption($ch, CURLOPT_POSTFIELDS, $data);
413
        
414
        } else {
415
            $this->setCurlOption($ch, CURLOPT_CUSTOMREQUEST, $method);
416
            $url .= '?' . http_build_query($data);
417
        }
418
419
        // set url and options
420
        $this->setCurlOption($ch, CURLOPT_URL, $url);
421
        $this->setCurlOption($ch, CURLOPT_RETURNTRANSFER, 1); 
422
        $this->setCurlOption($ch, CURLOPT_HTTPHEADER, $headers);
423
        
424
        /**
425
         * set timeout  
426
         * 
427
         * @see https://curl.se/libcurl/c/CURLOPT_TIMEOUT_MS.html
428
         * @see https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT_MS.html
429
         *  If libcurl is built to use the standard system name resolver, that portion of the transfer 
430
         *  will still use full-second resolution for timeouts with a minimum timeout allowed of one second. 
431
         *  In unix-like systems, this might cause signals to be used unless CURLOPT_NOSIGNAL is set. 
432
         */
433
        $this->setCurlOption($ch, CURLOPT_NOSIGNAL, 1);
434
        $this->setCurlOption($ch, CURLOPT_TIMEOUT_MS, $this->timeout);
435
436
        // execute curl call
437
        $result = curl_exec($ch);
438
        $curlErrorNumber = curl_errno($ch);
439
        $curlErrorMessage = curl_error($ch);
440
441
        // close connection
442
        curl_close($ch);
443
444
        if ($curlErrorNumber !== 0){
445
            throw new \RuntimeException($curlErrorMessage);
446
        }
447
448
        return new ApiResponse($result !== false ? $result : '');
0 ignored issues
show
Bug introduced by
It seems like $result !== false ? $result : '' can also be of type true; however, parameter $plaintext of Kristuff\AbuseIPDB\ApiResponse::__construct() does only seem to accept null|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

448
        return new ApiResponse(/** @scrutinizer ignore-type */ $result !== false ? $result : '');
Loading history...
449
    }
450
451
    /** 
452
     * Clean message in case it comes from fail2ban <matches>
453
     * Remove backslashes and sensitive information from the report
454
     * @see https://wiki.shaunc.com/wikka.php?wakka=ReportingToAbuseIPDBWithFail2Ban
455
     * 
456
     * @access public
457
     * @param string      $message           The original message 
458
     *  
459
	 * @return string
460
     */
461
    public function cleanMessage(string $message): string
462
    {
463
        // Remove backslashes
464
        $message = str_replace('\\', '', $message);
465
466
        // Remove self ips
467
        foreach ($this->selfIps as $ip){
468
            $message = str_replace($ip, '*', $message);
469
        }
470
471
        // If we're reporting spam, further munge any email addresses in the report
472
        $emailPattern   = "/\b[A-Z0-9!#$%&'*`\/?^{|}~=+_.-]+@[A-Z0-9.-]+\b/i";
473
        $message        = preg_replace($emailPattern, "*", $message);
474
        
475
        // Make sure message is less 1024 chars
476
        return substr($message, 0, 1024);
477
    }
478
}