diff --git a/.gitattributes b/.gitattributes index c212419..2e8592a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,5 +3,6 @@ vendor/ export-ignore .gitattributes export-ignore .gitignore export-ignore .travis.yml export-ignore +composer.* export-ignore phpunit.xml export-ignore README.* export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 208a599..25d109d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +_* +.old* vendor/* \ No newline at end of file diff --git a/LICENSE b/LICENSE index 933b544..abe2257 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2022 kristuff +Copyright (c) 2020 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 d7946f1..f8540d7 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,20 @@ # Kristuff\AbuseIPDB -> A wrapper for AbuseIPDB API v2 +> A mini library to work with the AbuseIPDB api V2 [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/kristuff/abuseipdb/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/kristuff/abuseipdb/?branch=master) [![Build Status](https://scrutinizer-ci.com/g/kristuff/abuseipdb/badges/build.png?b=master)](https://scrutinizer-ci.com/g/kristuff/abuseipdb/build-status/master) [![Latest Stable Version](https://poser.pugx.org/kristuff/abuseipdb/v/stable)](https://packagist.org/packages/kristuff/abuseipdb) [![License](https://poser.pugx.org/kristuff/abuseipdb/license)](https://packagist.org/packages/kristuff/abuseipdb) + ***see also [kristuff/abuseipdb-cli](https://github.com/kristuff/abuseipdb-cli) for the `CLI` version*** Features -------- -- Single IP check request **✓** -- IP block check request **✓** -- Blacklist request **✓** -- 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) **✓** -- Define timeout for cURL internal requests **✓** +- **✓** Single check request +- **✓** Single report request +- *\[TODO\]* Check block request +- *\[TODO\]* Bulk report request Requirements ------------ @@ -33,24 +30,25 @@ Deploy with composer: ```json ... "require": { - "kristuff/abuseipdb": "^1.1-stable" + "kristuff/abuseipdb": ">=0.1-stable" }, ``` -More infos +Usage ----- -- [Project website](https://kristuff.fr/projects/abuseipdb) -- [Api documentation](https://kristuff.fr/projects/abuseipdb/doc) -- CLI version: [github](https://github.com/kristuff/abuseipdb-cli) | [website](https://kristuff.fr/projects/abuseipdbcli) +```php + + * This file is part of Kristuff\AbsuseIPDB. + * + * (c) Kristuff * * 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 + * @version 0.9.1 + * @copyright 2020 Kristuff */ namespace Kristuff\AbuseIPDB; /** - * Class ApiBase + * Class ApiDefintion * - * Abstract base class for ApiHanlder + * Abstract base class for ApiManager * Contains main hard coded api settings */ -abstract class ApiBase +abstract class ApiDefintion { - /** + /** * AbuseIPDB API v2 Endpoint - * @var string + * @var string $api_endpoint */ protected $aipdbApiEndpoint = 'https://api.abuseipdb.com/api/v2/'; - /** - * AbuseIPDB API key - * - * @access protected - * @var string $aipdbApiKey - */ - protected $aipdbApiKey = null; - /** * AbuseIPDB API v2 categories - * shortname, id (string), long name - * last parameter is false when the category can't be used alone - * - * @static - * @var array + * @var array $aipdbApiCategories */ - protected static $aipdbApiCategories = [ + protected $aipdbApiCategories = [ // Altering DNS records resulting in improper redirection. ['dns-c' , '1', 'DNS Compromise', true], @@ -127,36 +116,22 @@ 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. - ['iot' , '23', 'IoT Targeted', true], - ]; - - /** - * Get the list of report categories - * - * @access public - * @static - * - * @return array - */ - public static function getCategories(): array - { - return self::$aipdbApiCategories; - } + ['oit' , '23', 'IoT Targeted', true], + ]; /** * Get the category id corresponding to given name * * @access public - * @static - * @param string $categoryName The report category name + * @param string $categoryName The report categoriy name * * @return string|bool The category id in string format if found, otherwise false */ - public static function getCategoryIdByName(string $categoryName) + public function getCategoryIdbyName(string $categoryName) { - foreach (self::$aipdbApiCategories as $cat){ + foreach ($this->aipdbApiCategories as $cat){ if ($cat[0] === $categoryName) { - return $cat[1]; + return $cat; } } @@ -168,16 +143,15 @@ 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 static function getCategoryNameById(string $categoryId) + public function getCategoryNameById(string $categoryId) { - foreach (self::$aipdbApiCategories as $cat){ + foreach ($this->aipdbApiCategories as $cat){ if ($cat[1] === $categoryId) { - return $cat[0]; + return $cat; } } @@ -189,16 +163,15 @@ 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 + * @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 + * @return int|bool The category index if found, otherwise false */ - protected static function getCategoryIndex(string $value, int $index) + protected function getCategoryIndex(string $value, int $index) { $i = 0; - foreach (self::$aipdbApiCategories as $cat){ + foreach ($this->aipdbApiCategories as $cat){ if ($cat[$index] === $value) { return $i; } @@ -209,63 +182,4 @@ abstract class ApiBase return false; } - /** - * Check if the category(ies) given is/are valid - * Check for shortname or id, and categories that can't be used alone - * - * @access protected - * @param array $categories The report categories list - * - * @return string Formatted string id list ('18,2,3...') - * @throws \InvalidArgumentException - */ - protected function validateReportCategories(string $categories) - { - // the return categories string - $catsString = ''; - - // used when cat that can't be used alone - $needAnother = null; - - // parse given categories - $cats = explode(',', $categories); - - foreach ($cats as $cat) { - - // get index on our array of categories - $catIndex = is_numeric($cat) ? self::getCategoryIndex($cat, 1) : self::getCategoryIndex($cat, 0); - - // check if found - if ($catIndex === false ){ - throw new \InvalidArgumentException('Invalid report category was given.'); - } - - // get Id - $catId = self::$aipdbApiCategories[$catIndex][1]; - - // need another ? - if ($needAnother !== false){ - - // is a standalone cat ? - if (self::$aipdbApiCategories[$catIndex][3] === false) { - $needAnother = true; - - } else { - // ok, continue with other at least one given - // no need to reperform this check - $needAnother = false; - } - } - - // set or add to cats list - $catsString = ($catsString === '') ? $catId : $catsString .','.$catId; - } - - if ($needAnother !== false){ - throw new \InvalidArgumentException('Invalid report category parameter given: this category can\'t be used alone.'); - } - - // if here that ok - return $catsString; - } } \ No newline at end of file diff --git a/lib/ApiHandler.php b/lib/ApiHandler.php deleted file mode 100644 index 2cee02c..0000000 --- a/lib/ApiHandler.php +++ /dev/null @@ -1,478 +0,0 @@ - - * - * 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 ApiHandler - * - * The main class to work with the AbuseIPDB API v2 - */ -class ApiHandler extends ApiBase -{ - /** - * Curl helper functions - */ - 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 - * - * @access protected - * @var array - */ - 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 = [], 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; - } - - /** - * Get the current configuration in a indexed array - * - * @access public - * - * @return array - */ - public function getConfig(): array - { - return array( - 'apiKey' => $this->aipdbApiKey, - 'selfIps' => $this->selfIps, - 'timeout' => $this->timeout, - ); - } - - /** - * Performs a 'report' api request - * - * Result, in json format will be something like this: - * { - * "data": { - * "ipAddress": "127.0.0.1", - * "abuseConfidenceScore": 52 - * } - * } - * - * @access public - * @param string $ip The ip to report - * @param string $categories The report category(es) - * @param string $message The report message - * - * @return ApiResponse - * @throws \RuntimeException - * @throws \InvalidArgumentException - */ - public function report(string $ip, string $categories, string $message): ApiResponse - { - // ip must be set - if (empty($ip)){ - throw new \InvalidArgumentException('Ip was empty'); - } - - // categories must be set - if (empty($categories)){ - throw new \InvalidArgumentException('Categories list was empty'); - } - - // message must be set - if (empty($message)){ - throw new \InvalidArgumentException('Report message was empty'); - } - - // validates categories, clean message - $cats = $this->validateReportCategories($categories); - $msg = $this->cleanMessage($message); - - // AbuseIPDB request - return $this->apiRequest( - 'report', [ - 'ip' => $ip, - 'categories' => $cats, - 'comment' => $msg - ], - 'POST' - ); - } - - /** - * Performs a 'bulk-report' api request - * - * Result, in json format will be something like this: - * { - * "data": { - * "savedReports": 60, - * "invalidReports": [ - * { - * "error": "Duplicate IP", - * "input": "41.188.138.68", - * "rowNumber": 5 - * }, - * { - * "error": "Invalid IP", - * "input": "127.0.foo.bar", - * "rowNumber": 6 - * }, - * { - * "error": "Invalid Category", - * "input": "189.87.146.50", - * "rowNumber": 8 - * } - * ] - * } - * } - * - * @access public - * @param string $filePath The CSV file path. Could be an absolute or relative path. - * - * @return ApiResponse - * @throws \RuntimeException - * @throws \InvalidArgumentException - * @throws InvalidPermissionException - */ - public function bulkReport(string $filePath): ApiResponse - { - // check file exists - if (!file_exists($filePath) || !is_file($filePath)){ - throw new \InvalidArgumentException('The file [' . $filePath . '] does not exist.'); - } - - // check file is readable - if (!is_readable($filePath)){ - throw new InvalidPermissionException('The file [' . $filePath . '] is not readable.'); - } - - return $this->apiRequest('bulk-report', [], 'POST', $filePath); - } - - /** - * Perform a 'clear-address' api request - * - * Sample response: - * - * { - * "data": { - * "numReportsDeleted": 0 - * } - * } - * - * @access public - * @param string $ip The IP to clear reports - * - * @return ApiResponse - * @throws \RuntimeException - * @throws \InvalidArgumentException When ip value was not set. - */ - public function clearAddress(string $ip): ApiResponse - { - // ip must be set - if (empty($ip)){ - throw new \InvalidArgumentException('IP argument must be set.'); - } - - return $this->apiRequest('clear-address', ['ipAddress' => $ip ], "DELETE") ; - } - - /** - * Perform a 'check' api request - * - * @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 - * @throws \RuntimeException - * @throws \InvalidArgumentException when maxAge is less than 1 or greater than 365, or when ip value was not set. - */ - 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.'); - } - - // ip must be set - if (empty($ip)){ - throw new \InvalidArgumentException('ip argument must be set (empty value given)'); - } - - // minimal data - $data = [ - 'ipAddress' => $ip, - 'maxAgeInDays' => $maxAgeInDays, - ]; - - // option - if ($verbose){ - $data['verbose'] = true; - } - - return $this->apiRequest('check', $data, 'GET') ; - } - - /** - * Perform a 'check-block' api request - * - * - * Sample json response for 127.0.0.1/24 - * - * { - * "data": { - * "networkAddress": "127.0.0.0", - * "netmask": "255.255.255.0", - * "minAddress": "127.0.0.1", - * "maxAddress": "127.0.0.254", - * "numPossibleHosts": 254, - * "addressSpaceDesc": "Loopback", - * "reportedAddress": [ - * { - * "ipAddress": "127.0.0.1", - * "numReports": 631, - * "mostRecentReport": "2019-03-21T16:35:16+00:00", - * "abuseConfidenceScore": 0, - * "countryCode": null - * }, - * { - * "ipAddress": "127.0.0.2", - * "numReports": 16, - * "mostRecentReport": "2019-03-12T20:31:17+00:00", - * "abuseConfidenceScore": 0, - * "countryCode": null - * }, - * ... - * ] - * } - * } - * - * - * @access public - * @param string $network The network to check - * @param int $maxAgeInDays The Max age in days, must - * - * @return ApiResponse - * @throws \RuntimeException - * @throws \InvalidArgumentException when $maxAgeInDays is less than 1 or greater than 365, or when $network value was not set. - */ - public function checkBlock(string $network, int $maxAgeInDays = 30): ApiResponse - { - // max age must be between 1 and 365 - if ($maxAgeInDays > 365 || $maxAgeInDays < 1){ - throw new \InvalidArgumentException('maxAgeInDays must be between 1 and 365 (' . $maxAgeInDays . ' was given)'); - } - - // ip must be set - if (empty($network)){ - throw new \InvalidArgumentException('network argument must be set (empty value given)'); - } - - // minimal data - $data = [ - 'network' => $network, - 'maxAgeInDays' => $maxAgeInDays, - ]; - - return $this->apiRequest('check-block', $data, 'GET'); - } - - /** - * Perform a 'blacklist' api request - * - * @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 - * @throws \RuntimeException - * @throws \InvalidArgumentException When maxAge is not a numeric value, when $limit is less than 1. - */ - public function blacklist(int $limit = 10000, bool $plainText = false, int $confidenceMinimum = 100): ApiResponse - { - if ($limit < 1){ - throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)'); - } - - // minimal data - $data = [ - 'confidenceMinimum' => $confidenceMinimum, - 'limit' => $limit, - ]; - - // plaintext paremeter has no value and must be added only when true - // (set plaintext=false won't work) - if ($plainText){ - $data['plaintext'] = $plainText; - } - - return $this->apiRequest('blacklist', $data, 'GET'); - } - - /** - * Perform a cURL request - * - * @access protected - * @param string $path The api end path - * @param array $data The request data - * @param string $method The request method. Default is 'GET' - * @param string $csvFilePath The file path for csv file. When not empty, $data parameter is ignored and in place, - * the content of the given file if passed as csv. Default is empty string. - * - * @return ApiResponse - * @throws \RuntimeException - */ - protected function apiRequest(string $path, array $data, string $method = 'GET', string $csvFilePath = ''): ApiResponse - { - $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 = [ - 'Accept: application/json;', - 'Key: ' . $this->aipdbApiKey, - ]; - - // open curl connection - $ch = curl_init(); - - // for csv - if (!empty($csvFilePath)){ - $cfile = new \CurlFile($csvFilePath, 'text/csv', 'csv'); - //curl file itself return the realpath with prefix of @ - $data = array('csv' => $cfile); - } - - // set the method and data to send - if ($method == 'POST') { - $this->setCurlOption($ch, CURLOPT_POST, true); - $this->setCurlOption($ch, CURLOPT_POSTFIELDS, $data); - - } else { - $this->setCurlOption($ch, CURLOPT_CUSTOMREQUEST, $method); - $url .= '?' . http_build_query($data); - } - - // 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 : ''); - } - - /** - * Clean message in case it comes from fail2ban - * Remove backslashes and sensitive information from the report - * @see https://wiki.shaunc.com/wikka.php?wakka=ReportingToAbuseIPDBWithFail2Ban - * - * @access public - * @param string $message The original message - * - * @return string - */ - public function cleanMessage(string $message): string - { - // Remove backslashes - $message = str_replace('\\', '', $message); - - // Remove self ips - foreach ($this->selfIps as $ip){ - $message = str_replace($ip, '*', $message); - } - - // If we're reporting spam, further munge any email addresses in the report - $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); - } -} \ No newline at end of file diff --git a/lib/ApiManager.php b/lib/ApiManager.php new file mode 100644 index 0000000..ad4f501 --- /dev/null +++ b/lib/ApiManager.php @@ -0,0 +1,406 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @version 0.9.1 + * @copyright 2020 Kristuff + */ + +namespace Kristuff\AbuseIPDB; + +/** + * Class ApiManager + * + * The main class to work with the AbuseIPDB API v2 + */ +class ApiManager extends ApiDefintion +{ + /** + * AbuseIPDB API key + * + * @access protected + * @var string $aipdbApiKey + */ + protected $aipdbApiKey = null; + + /** + * AbuseIPDB user id + * + * @access protected + * @var string $aipdbUserId + */ + protected $aipdbUserId = null; + + /** + * The ips to remove from message + * Generally you will add to this list yours ipv4 and ipv6, and the hostname + * + * @access protected + * @var array $selfIps + */ + protected $selfIps = []; + + /** + * Constructor + * + * @access public + * @param string $apiKey The AbuseIPDB api key + * @param string $userId The AbuseIPDB user's id + * @param array $myIps The Ips/domain name you dont want to display in report messages + * + */ + public function __construct(string $apiKey, string $userId, array $myIps = []) + { + $this->aipdbApiKey = $apiKey; + $this->aipdbUserId = $userId; + $this->selfIps = $myIps; + } + + /** + * Get the current configuration in a indexed array + * + * @access public + * @return array + */ + public function getConfig() + { + return array( + 'userId' => $this->aipdbUserId, + 'apiKey' => $this->aipdbApiKey, + 'selfIps' => $this->selfIps, + + // TODO default report cat + ); + } + + /** + * Get a new instance of ApiManager with config stored in a Json file + * + * @access public + * @static + * @param string $configPath The configuration file path + * + * @return \Kristuff\AbuseIPDB\ApiManager + * @throws \InvalidArgumentException If the given file does not exist + * @throws \Kristuff\AbuseIPDB\InvalidPermissionException If the given file is not readable + */ + public static function fromConfigFile(string $configPath) + { + + // check file exists + if (!file_exists($configPath) || !is_file($configPath)){ + throw new \InvalidArgumentException('The file [' . $configPath . '] does not exist.'); + } + + // check file is readable + if (!is_readable($configPath)){ + throw new InvalidPermissionException('The file [' . $configPath . '] is not readable.'); + } + + // todo check file exist + $keyConfig = self::loadJsonFile($configPath); + $selfIps = []; + + // Look for other optional config files in the same directory + $selfIpsConfigPath = pathinfo($configPath, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . 'self_ips.json'; + if (file_exists($selfIpsConfigPath)){ + $selfIps = self::loadJsonFile($selfIpsConfigPath)->self_ips; + } + + $app = new ApiManager($keyConfig->api_key, $keyConfig->user_id, $selfIps); + + return $app; + } + + /** + * Get the list of report categories + * + * @access public + * @return array + */ + public function getCategories() + { + return $this->aipdbApiCategories; + } + + /** + * Performs a 'report' api request + * + * Result, in json format will be something like this: + * { + * "data": { + * "ipAddress": "127.0.0.1", + * "abuseConfidenceScore": 52 + * } + * } + * + * @access public + * @param string $ip The ip to report + * @param string $categories The report categories + * @param string $message The report message + * @param bool $returnArray True to return an indexed array instead of an object. Default is false. + * + * @return object|array + * @throws \InvalidArgumentException + */ + public function report(string $ip = '', string $categories = '', string $message = '', bool $returnArray = false) + { + // ip must be set + if (empty($ip)){ + throw new \InvalidArgumentException('Ip was empty'); + } + + // categories must be set + if (empty($categories)){ + throw new \InvalidArgumentException('categories list was empty'); + } + + // message must be set + if (empty($message)){ + throw new \InvalidArgumentException('report message was empty'); + } + + // validates categories, clean message + $cats = $this->validateCategories($categories); + $msg = $this->cleanMessage($message); + + // report AbuseIPDB request + return $this->apiRequest('report', [ + 'ip' => $ip, + 'categories' => $cats, + 'comment' => $msg + ], + 'POST', $returnArray + ); + } + + /** + * Check if the category(ies) given is/are valid + * Check for shortname or id, and categories that can't be used alone + * + * @access protected + * @param array $categories The report categories list + * + * @return string Formatted string id list ('18,2,3...') + * @throws \InvalidArgumentException + */ + protected function validateCategories(string $categories) + { + // the return categories string + $catsString = ''; + + // used when cat that can't be used alone + $needAnother = null; + + // parse given categories + $cats = explode(',', $categories); + + foreach ($cats as $cat) { + + // get index on our array of categories + $catIndex = is_numeric($cat) ? $this->getCategoryIndex($cat, 1) : $this->getCategoryIndex($cat, 0); + + // check if found + if ($catIndex === false ){ + throw new \InvalidArgumentException('Invalid report category was given : ['. $cat . ']'); + } + + // get Id + $catId = $this->aipdbApiCategories[$catIndex][1]; + + // need another ? + if ($needAnother !== false){ + + // is a standalone cat ? + if ($this->aipdbApiCategories[$catIndex][3] === false) { + $needAnother = true; + + } else { + // ok, continue with other at least one given + // no need to reperform this check + $needAnother = false; + } + } + + // set or add to cats list + $catsString = ($catsString === '') ? $catId : $catsString .','.$catId; + } + + if ($needAnother !== false){ + throw new \InvalidArgumentException('Invalid report category paremeter given: some categories can\'t be used alone'); + } + + // if here that ok + return $catsString; + } + + /** + * Perform a 'check' api request + * + * @access public + * @param string $ip The ip to check + * @param string $maxAge Max age in days + * @param bool $verbose True to get the full response. Default is false + * @param bool $returnArray True to return an indexed array instead of an object. Default is false. + * + * @return object|array + * @throws \InvalidArgumentException When maxAge is not a numeric value, when maxAge is less than 1 or + * greater than 365, or when ip value was not set. + */ + public function check(string $ip = null, string $maxAge = '30', bool $verbose = false, bool $returnArray = false) + { + + if (!is_numeric($maxAge)){ + throw new \InvalidArgumentException('maxAge must be a numeric value (' . $maxAge . ' was given)'); + } + $maxAge = intval($maxAge); + + // max age must less or equal to 365 + if ($maxAge > 365 || $maxAge < 1){ + throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)'); + } + + //ip must be set + if (empty($ip)){ + throw new \InvalidArgumentException('ip argument must be set (null given)'); + } + + // minimal data + $data = [ + 'ipAddress' => $ip, + 'maxAgeInDays' => $maxAge, + ]; + + // option + if ($verbose){ + $data['verbose'] = true; + } + + // check AbuseIPDB request + return $this->apiRequest('check', $data, 'GET', $returnArray) ; + } + + /** + * Perform a cURL request + * + * @access protected + * @param string $path The api end path + * @param array $data The request data + * @param string $method The request method. Default is 'GET' + * @param bool $returnArray True to return an indexed array instead of an object. Default is false. + * + * @return object|array + */ + protected function apiRequest(string $path, array $data, string $method = 'GET', bool $returnArray = false) + { + + + // set api url + $url = $this->aipdbApiEndpoint . $path; + + // open curl connection + $ch = curl_init(); + + // set the method and data to send + if ($method == 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } else { + $url .= '?' . http_build_query($data); + } + + // set the url to call + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + // set the AbuseIPDB API Key as a header + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Accept: application/json;', + 'Key: ' . $this->aipdbApiKey, + ]); + + // execute curl call + $result = curl_exec($ch); + + // close connection + curl_close($ch); + + // return response as object / array + return json_decode($result, $returnArray); + } + + /** + * Clean message in case it comes from fail2ban + * https://wiki.shaunc.com/wikka.php?wakka=ReportingToAbuseIPDBWithFail2Ban + * + * @access public + * @param string $message The original message + * + * @return string + */ + protected function cleanMessage(string $message) + { + // Remove backslashes and sensitive information from the report + $message = str_replace('\\', '', $message); + + // Remove self ips + foreach ($this->selfIps as $ip){ + $message = str_replace($ip, '[MUNGED]', $message); + } + + // If we're reporting spam, further munge any email addresses in the report + $emailPattern = "/[^@\s]*@[^@\s]*\.[^@\s]*/"; + $emailRemplacement = "[MUNGED]"; + preg_replace($emailPattern, $emailRemplacement, $message); + + return $message; + } + + /** + * Load and returns decoded Json from given file + * + * @access public + * @static + * @param string $filePath The file's full path + * @param bool $trowError Throw error on true or silent process. Default is true + * + * @return object|null + * @throws \Exception + * @throws \LogicException + */ + protected static function loadJsonFile(string $filePath, bool $throwError = true) + { + // check file exists + if (!file_exists($filePath) || !is_file($filePath)){ + if ($throwError) { + throw new \Exception('Config file not found'); + } + return null; + } + + // get and parse content + $content = file_get_contents($filePath); + $json = json_decode(utf8_encode($content)); + + // check for errors + if ($json == null && json_last_error() != JSON_ERROR_NONE){ + if ($throwError) { + throw new \LogicException(sprintf("Failed to parse config file Error: '%s'", json_last_error_msg())); + } + } + + return $json; + } +} \ No newline at end of file diff --git a/lib/ApiResponse.php b/lib/ApiResponse.php deleted file mode 100644 index 3e451f0..0000000 --- a/lib/ApiResponse.php +++ /dev/null @@ -1,137 +0,0 @@ - - * - * 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 ApiResponse - * - */ -class ApiResponse -{ - /** - * - * @access protected - * @var string - */ - protected $curlResponse; - - /** - * - * @access protected - * @var object - */ - protected $decodedResponse; - - /** - * Constructor - * - * @access public - * @param string $plaintext AbuseIPDB response in plaintext - * - */ - public function __construct(?string $plaintext = null) - { - $this->curlResponse = $plaintext; - $this->decodedResponse = !empty($plaintext) ? json_decode($plaintext, false) : null; - } - - /** - * Get response as array. May return null - * - * @access public - * - * @return array|null - */ - public function getArray(): ?array - { - return json_decode($this->curlResponse, true); - } - - /** - * Get response as object. May return null - * - * @access public - * - * @return \stdClass|null - */ - public function getObject(): ?\stdClass - { - return $this->decodedResponse; - } - - /** - * Get response as plaintext. May return null - * - * @access public - * - * @return string|null - */ - public function getPlaintext(): ?string - { - 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 deleted file mode 100644 index 667fdda..0000000 --- a/lib/CurlTrait.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * 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; - -/** - * cURL helper functions - */ -trait CurlTrait -{ - /** - * helper to configure cURL option - * - * @access protected - * @param resource $ch - * @param int $option - * @param mixed $value - * - * @return void - * @throws \RuntimeException - */ - protected function setCurlOption($ch, int $option, $value): void - { - if(!curl_setopt($ch,$option,$value)){ - throw new \RuntimeException('curl_setopt failed! '.curl_error($ch)); - } - } -} diff --git a/lib/InvalidPermissionException.php b/lib/InvalidPermissionException.php index 8ec9bc1..d9a6c25 100644 --- a/lib/InvalidPermissionException.php +++ b/lib/InvalidPermissionException.php @@ -1,27 +1,29 @@ - + * This file is part of Kristuff\AbsuseIPDB. + * + * (c) Kristuff * * 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 + * @version 0.9.1 + * @copyright 2020 Kristuff */ namespace Kristuff\AbuseIPDB; /** - * Custom Exception for not readable file + * Custom Exception for non redable file */ class InvalidPermissionException extends \Exception { + } \ No newline at end of file diff --git a/lib/QuietApiHandler.php b/lib/QuietApiHandler.php deleted file mode 100644 index 632b140..0000000 --- a/lib/QuietApiHandler.php +++ /dev/null @@ -1,141 +0,0 @@ - - * - * 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