diff --git a/.gitignore b/.gitignore index 25d109d..208a599 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -_* -.old* vendor/* \ No newline at end of file diff --git a/LICENSE b/LICENSE index 007d465..933b544 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2021 kristuff +Copyright (c) 2020-2022 kristuff Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f300358..d7946f1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Features - Single IP report request **✓** - Bulk report request (send `csv` file) **✓** - Clear IP address request (remove your own reports) **✓** -- Auto cleaning report comments from sensitive data (email, custom ip/domain names list) **✓** +- Auto cleaning report comments from sensitive data (email, custom ip/domain names list) **✓** +- Define timeout for cURL internal requests **✓** Requirements ------------ @@ -32,7 +33,7 @@ Deploy with composer: ```json ... "require": { - "kristuff/abuseipdb": ">=0.9.8-stable" + "kristuff/abuseipdb": "^1.1-stable" }, ``` @@ -41,8 +42,7 @@ More infos - [Project website](https://kristuff.fr/projects/abuseipdb) - [Api documentation](https://kristuff.fr/projects/abuseipdb/doc) -- [Config/Install guide](https://kristuff.fr/projects/abuseipdb/technical#configuration) -- [CLI version](https://kristuff.fr/projects/abuseipdbcli) +- CLI version: [github](https://github.com/kristuff/abuseipdb-cli) | [website](https://kristuff.fr/projects/abuseipdbcli) License @@ -50,7 +50,7 @@ License The MIT License (MIT) -Copyright (c) 2020-2021 Kristuff +Copyright (c) 2020-2022 Kristuff Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index 61b0af6..d473e9d 100644 --- a/composer.json +++ b/composer.json @@ -2,11 +2,14 @@ "name": "kristuff/abuseipdb", "description": "A PHP wrapper for AbuseIPDB API v2", "type": "library", + "keywords": ["abuseIPDB", "API"], "license": "MIT", "authors": [ { "name": "Kristuff", - "homepage": "https://github.com/kristuff" + "homepage": "https://github.com/kristuff", + "email": "kristuff@kristuff.fr", + "role": "Developer" } ], "require": { diff --git a/lib/ApiBase.php b/lib/ApiBase.php index 0a4edd2..9adf14c 100644 --- a/lib/ApiBase.php +++ b/lib/ApiBase.php @@ -1,21 +1,20 @@ + * (c) Kristuff * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * - * @version 0.9.8 - * @copyright 2020-2021 Kristuff + * @version 1.1 + * @copyright 2020-2022 Kristuff */ namespace Kristuff\AbuseIPDB; @@ -44,12 +43,13 @@ abstract class ApiBase /** * AbuseIPDB API v2 categories - * shorname, id (string), long name - * last paramter is false when the category cant' be used alone + * shortname, id (string), long name + * last parameter is false when the category can't be used alone * + * @static * @var array */ - protected $aipdbApiCategories = [ + protected static $aipdbApiCategories = [ // Altering DNS records resulting in improper redirection. ['dns-c' , '1', 'DNS Compromise', true], @@ -127,34 +127,36 @@ abstract class ApiBase // Abuse was targeted at an "Internet of Things" type device. Include // information about what type of device was targeted in the comments. - ['oit' , '23', 'IoT Targeted', true], + ['iot' , '23', 'IoT Targeted', true], ]; /** * Get the list of report categories * * @access public + * @static * * @return array */ - public function getCategories() + public static function getCategories(): array { - return $this->aipdbApiCategories; + return self::$aipdbApiCategories; } /** * Get the category id corresponding to given name * * @access public - * @param string $categoryName The report categoriy name + * @static + * @param string $categoryName The report category name * * @return string|bool The category id in string format if found, otherwise false */ - public function getCategoryIdbyName(string $categoryName) + public static function getCategoryIdByName(string $categoryName) { - foreach ($this->aipdbApiCategories as $cat){ + foreach (self::$aipdbApiCategories as $cat){ if ($cat[0] === $categoryName) { - return $cat; + return $cat[1]; } } @@ -166,15 +168,16 @@ abstract class ApiBase * Get the category name corresponding to given id * * @access public + * @static * @param string $categoryId The report category id * * @return string|bool The category name if found, otherwise false */ - public function getCategoryNameById(string $categoryId) + public static function getCategoryNameById(string $categoryId) { - foreach ($this->aipdbApiCategories as $cat){ + foreach (self::$aipdbApiCategories as $cat){ if ($cat[1] === $categoryId) { - return $cat; + return $cat[0]; } } @@ -186,15 +189,16 @@ abstract class ApiBase * Get the index of category corresponding to given value * * @access protected + * @static * @param string $value The report category id or name * @param string $index The index in value array * * @return int|bool The category index if found, otherwise false */ - protected function getCategoryIndex(string $value, int $index) + protected static function getCategoryIndex(string $value, int $index) { $i = 0; - foreach ($this->aipdbApiCategories as $cat){ + foreach (self::$aipdbApiCategories as $cat){ if ($cat[$index] === $value) { return $i; } @@ -229,7 +233,7 @@ abstract class ApiBase foreach ($cats as $cat) { // get index on our array of categories - $catIndex = is_numeric($cat) ? $this->getCategoryIndex($cat, 1) : $this->getCategoryIndex($cat, 0); + $catIndex = is_numeric($cat) ? self::getCategoryIndex($cat, 1) : self::getCategoryIndex($cat, 0); // check if found if ($catIndex === false ){ @@ -237,13 +241,13 @@ abstract class ApiBase } // get Id - $catId = $this->aipdbApiCategories[$catIndex][1]; + $catId = self::$aipdbApiCategories[$catIndex][1]; // need another ? if ($needAnother !== false){ // is a standalone cat ? - if ($this->aipdbApiCategories[$catIndex][3] === false) { + if (self::$aipdbApiCategories[$catIndex][3] === false) { $needAnother = true; } else { diff --git a/lib/ApiHandler.php b/lib/ApiHandler.php index b8eadd2..2cee02c 100644 --- a/lib/ApiHandler.php +++ b/lib/ApiHandler.php @@ -1,21 +1,20 @@ + * (c) Kristuff * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * - * @version 0.9.8 - * @copyright 2020-2021 Kristuff + * @version 1.1 + * @copyright 2020-2022 Kristuff */ namespace Kristuff\AbuseIPDB; @@ -32,6 +31,11 @@ class ApiHandler extends ApiBase */ use CurlTrait; + /** + * @var string + */ + const VERSION = 'v1.1'; + /** * The ips to remove from report messages * Generally you will add to this list yours ipv4 and ipv6, hostname, domain names @@ -41,18 +45,46 @@ class ApiHandler extends ApiBase */ protected $selfIps = []; + /** + * The maximum number of milliseconds to allow cURL functions to execute. If libcurl is + * built to use the standard system name resolver, that portion of the connect will still + * use full-second resolution for timeouts with a minimum timeout allowed of one second. + * + * @access protected + * @var int + */ + protected $timeout = 0; + /** * Constructor * * @access public * @param string $apiKey The AbuseIPDB api key * @param array $myIps The Ips/domain name you don't want to display in report messages + * @param int $timeout The maximum number of milliseconds to allow internal cURL functions + * to execute. Default is 0, no timeout * */ - public function __construct(string $apiKey, array $myIps = []) + public function __construct(string $apiKey, array $myIps = [], int $timeout = 0) { $this->aipdbApiKey = $apiKey; $this->selfIps = $myIps; + $this->timeout = $timeout; + } + + /** + * Sets the cURL timeout (apply then to any API request). Overwrites the value passed in + * constructor, useful when performing multiple queries with same handler but different timeout. + * + * @access public + * @param int $timeout The maximum number of milliseconds to allow internal cURL functions + * to execute. + * + * @return void + */ + public function setTimeout(int $timeout): void + { + $this->timeout = $timeout; } /** @@ -67,6 +99,7 @@ class ApiHandler extends ApiBase return array( 'apiKey' => $this->aipdbApiKey, 'selfIps' => $this->selfIps, + 'timeout' => $this->timeout, ); } @@ -155,6 +188,7 @@ class ApiHandler extends ApiBase * @return ApiResponse * @throws \RuntimeException * @throws \InvalidArgumentException + * @throws InvalidPermissionException */ public function bulkReport(string $filePath): ApiResponse { @@ -214,8 +248,8 @@ class ApiHandler extends ApiBase public function check(string $ip, int $maxAgeInDays = 30, bool $verbose = false): ApiResponse { // max age must be less or equal to 365 - if ($maxAgeInDays > 365 || $maxAgeInDays < 1){ - throw new \InvalidArgumentException('maxAgeInDays must be between 1 and 365 (' . $maxAgeInDays . ' was given)'); + if ( $maxAgeInDays > 365 || $maxAgeInDays < 1 ){ + throw new \InvalidArgumentException('maxAgeInDays must be between 1 and 365.'); } // ip must be set @@ -309,7 +343,7 @@ class ApiHandler extends ApiBase * @param bool $plainText True to get the response in plaintext list. Default is false * @param int $confidenceMinimum The abuse confidence score minimum (subscribers feature). Default is 100. * The confidence minimum must be between 25 and 100. - * This parameter is subscriber feature (not honored otherwise). + * This parameter is a subscriber feature (not honored otherwise). * * @return ApiResponse * @throws \RuntimeException @@ -351,9 +385,10 @@ class ApiHandler extends ApiBase */ protected function apiRequest(string $path, array $data, string $method = 'GET', string $csvFilePath = ''): ApiResponse { - // set api url - $url = $this->aipdbApiEndpoint . $path; - + $curlErrorNumber = -1; // will be used later to check curl execution + $curlErrorMessage = ''; + $url = $this->aipdbApiEndpoint . $path; // api url + // set the wanted format, JSON (required to prevent having full html page on error) // and the AbuseIPDB API Key as a header $headers = [ @@ -381,17 +416,35 @@ class ApiHandler extends ApiBase $url .= '?' . http_build_query($data); } - // set the url to call + // set url and options $this->setCurlOption($ch, CURLOPT_URL, $url); $this->setCurlOption($ch, CURLOPT_RETURNTRANSFER, 1); $this->setCurlOption($ch, CURLOPT_HTTPHEADER, $headers); - + + /** + * set timeout + * + * @see https://curl.se/libcurl/c/CURLOPT_TIMEOUT_MS.html + * @see https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT_MS.html + * If libcurl is built to use the standard system name resolver, that portion of the transfer + * will still use full-second resolution for timeouts with a minimum timeout allowed of one second. + * In unix-like systems, this might cause signals to be used unless CURLOPT_NOSIGNAL is set. + */ + $this->setCurlOption($ch, CURLOPT_NOSIGNAL, 1); + $this->setCurlOption($ch, CURLOPT_TIMEOUT_MS, $this->timeout); + // execute curl call $result = curl_exec($ch); - + $curlErrorNumber = curl_errno($ch); + $curlErrorMessage = curl_error($ch); + // close connection curl_close($ch); - + + if ($curlErrorNumber !== 0){ + throw new \RuntimeException($curlErrorMessage); + } + return new ApiResponse($result !== false ? $result : ''); } @@ -416,8 +469,8 @@ class ApiHandler extends ApiBase } // If we're reporting spam, further munge any email addresses in the report - $emailPattern = "/[^@\s]*@[^@\s]*\.[^@\s]*/"; - $message = preg_replace($emailPattern, "*", $message); + $emailPattern = "/\b[A-Z0-9!#$%&'*`\/?^{|}~=+_.-]+@[A-Z0-9.-]+\b/i"; + $message = preg_replace($emailPattern, "*", $message); // Make sure message is less 1024 chars return substr($message, 0, 1024); diff --git a/lib/ApiResponse.php b/lib/ApiResponse.php index 1b9a232..3e451f0 100644 --- a/lib/ApiResponse.php +++ b/lib/ApiResponse.php @@ -1,21 +1,20 @@ + * (c) Kristuff * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * - * @version 0.9.8 - * @copyright 2020-2021 Kristuff + * @version 1.1 + * @copyright 2020-2022 Kristuff */ namespace Kristuff\AbuseIPDB; @@ -33,6 +32,13 @@ class ApiResponse */ protected $curlResponse; + /** + * + * @access protected + * @var object + */ + protected $decodedResponse; + /** * Constructor * @@ -43,6 +49,7 @@ class ApiResponse public function __construct(?string $plaintext = null) { $this->curlResponse = $plaintext; + $this->decodedResponse = !empty($plaintext) ? json_decode($plaintext, false) : null; } /** @@ -62,11 +69,11 @@ class ApiResponse * * @access public * - * @return object|null + * @return \stdClass|null */ - public function getObject(): ?object + public function getObject(): ?\stdClass { - return json_decode($this->curlResponse, false); + return $this->decodedResponse; } /** @@ -80,4 +87,51 @@ class ApiResponse { return $this->curlResponse; } + + /** + * Get whether the response contains error(s) + * + * @access public + * + * @return bool + */ + public function hasError(): bool + { + return count($this->errors()) > 0; + } + + /** + * Get an array of errors (object) contained is response + * + * @access public + * + * @return array + */ + public function errors(): array + { + return ($this->decodedResponse && property_exists($this->decodedResponse, 'errors')) ? $this->decodedResponse->errors : []; + } + + /** + * Get an internal error message in an ApiResponse object + * + * @access public + * @static + * @param string $message The error message + * + * @return ApiResponse + */ + public static function createErrorResponse(string $message): ApiResponse + { + $response = [ + "errors" => [ + [ + "title" => "Internal Error", + "detail" => $message + ] + ] + ]; + + return new ApiResponse(json_encode($response)); + } } \ No newline at end of file diff --git a/lib/CurlTrait.php b/lib/CurlTrait.php index 53758d6..667fdda 100644 --- a/lib/CurlTrait.php +++ b/lib/CurlTrait.php @@ -1,21 +1,20 @@ + * (c) Kristuff * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * - * @version 0.9.8 - * @copyright 2020-2021 Kristuff + * @version 1.1 + * @copyright 2020-2022 Kristuff */ namespace Kristuff\AbuseIPDB; diff --git a/lib/InvalidPermissionException.php b/lib/InvalidPermissionException.php index 7290792..8ec9bc1 100644 --- a/lib/InvalidPermissionException.php +++ b/lib/InvalidPermissionException.php @@ -1,21 +1,20 @@ + * (c) Kristuff * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * - * @version 0.9.8 - * @copyright 2020-2021 Kristuff + * @version 1.1 + * @copyright 2020-2022 Kristuff */ namespace Kristuff\AbuseIPDB; diff --git a/lib/QuietApiHandler.php b/lib/QuietApiHandler.php new file mode 100644 index 0000000..632b140 --- /dev/null +++ b/lib/QuietApiHandler.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @version 1.1 + * @copyright 2020-2022 Kristuff + */ + +namespace Kristuff\AbuseIPDB; + +/** + * Class QuietApiHandler + * + * Overwrite ApiHandler with Exception handling + * Instead of Exception, all methods return an ApiResponse that may + * contains errors from the AbuseIPDB API, or internal errors + */ +class QuietApiHandler extends ApiHandler +{ + /** + * Performs a 'report' api request, with Exception handling + * + * @access public + * @param string $ip The ip to report + * @param string $categories The report category(es) + * @param string $message The report message + * + * @return ApiResponse + */ + public function report(string $ip, string $categories, string $message): ApiResponse + { + try { + return parent::report($ip,$categories,$message); + } catch (\Exception $e) { + return ApiResponse::createErrorResponse($e->getMessage()); + } + } + + /** + * Performs a 'bulk-report' api request, with Exception handling + * + * @access public + * @param string $filePath The CSV file path. Could be an absolute or relative path. + * + * @return ApiResponse + */ + public function bulkReport(string $filePath): ApiResponse + { + try { + return parent::bulkReport($filePath); + } catch (\Exception $e) { + return ApiResponse::createErrorResponse($e->getMessage()); + } + } + + /** + * Perform a 'clear-address' api request, with Exception handling + * + * @access public + * @param string $ip The IP to clear reports + * + * @return ApiResponse + */ + public function clearAddress(string $ip): ApiResponse + { + try { + return parent::clearAddress($ip); + } catch (\Exception $e) { + return ApiResponse::createErrorResponse($e->getMessage()); + } + } + + /** + * Perform a 'check' api request, with Exception handling + * + * @access public + * @param string $ip The ip to check + * @param int $maxAgeInDays Max age in days. Default is 30. + * @param bool $verbose True to get the full response (last reports and countryName). Default is false + * + * @return ApiResponse + */ + public function check(string $ip, int $maxAgeInDays = 30, bool $verbose = false): ApiResponse + { + try { + return parent::check($ip, $maxAgeInDays, $verbose); + } catch (\Exception $e) { + return ApiResponse::createErrorResponse($e->getMessage()); + } + } + + /** + * Perform a 'check-block' api request, with Exception handling + * + * @access public + * @param string $network The network to check + * @param int $maxAgeInDays The Max age in days, must + * + * @return ApiResponse + */ + public function checkBlock(string $network, int $maxAgeInDays = 30): ApiResponse + { + try { + return parent::checkBlock($network, $maxAgeInDays); + } catch (\Exception $e) { + return ApiResponse::createErrorResponse($e->getMessage()); + } + } + + /** + * Perform a 'blacklist' api request, with Exception handling + * + * @access public + * @param int $limit The blacklist limit. Default is 10000 (the api default limit) + * @param bool $plainText True to get the response in plaintext list. Default is false + * @param int $confidenceMinimum The abuse confidence score minimum (subscribers feature). Default is 100. + * The confidence minimum must be between 25 and 100. + * This parameter is a subscriber feature (not honored otherwise). + * + * @return ApiResponse + */ + public function blacklist(int $limit = 10000, bool $plainText = false, int $confidenceMinimum = 100): ApiResponse + { + try { + return parent::blacklist($limit, $plainText, $confidenceMinimum); + } catch (\Exception $e) { + return ApiResponse::createErrorResponse($e->getMessage()); + } + } +} \ No newline at end of file