diff --git a/.gitattributes b/.gitattributes index 2e8592a..c212419 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,5 @@ 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 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 abe2257..933b544 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 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 5d1d989..d7946f1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,26 @@ # Kristuff\AbuseIPDB -> A mini library to work with the AbuseIPDB api V2 +> A wrapper for AbuseIPDB API v2 -see [kristuff/abuseipdb-cli](https://github.com/kristuff/abuseipdb-cli) for the CLI version +[![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 **✓** Requirements ------------- +------------ - PHP >= 7.1 - PHP's cURL - A valid [abuseipdb.com](https://abuseipdb.com) account with an API key @@ -18,23 +33,24 @@ Deploy with composer: ```json ... "require": { - "kristuff/abuseipdb": ">=0.1-stable" + "kristuff/abuseipdb": "^1.1-stable" }, ``` -Usage +More infos ----- -```php -echo ('TODO'); -``` +- [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) + License ------- The MIT License (MIT) -Copyright (c) 2020 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 3979348..d473e9d 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,15 @@ { "name": "kristuff/abuseipdb", - "description": "A library to work with the AbuseIPDB api V2", + "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 new file mode 100644 index 0000000..9adf14c --- /dev/null +++ b/lib/ApiBase.php @@ -0,0 +1,271 @@ + + * + * 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 ApiBase + * + * Abstract base class for ApiHanlder + * Contains main hard coded api settings + */ +abstract class ApiBase +{ + /** + * AbuseIPDB API v2 Endpoint + * @var string + */ + 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 + */ + protected static $aipdbApiCategories = [ + + // Altering DNS records resulting in improper redirection. + ['dns-c' , '1', 'DNS Compromise', true], + + // Falsifying domain server cache (cache poisoning). + ['dns-p' , '2', 'DNS Poisoning', true], + + // Fraudulent orders. + ['fraud-orders' , '3', 'Fraud Orders', true], + + // Participating in distributed denial-of-service (usually part of botnet). + ['ddos' , '4', 'DDoS Attack', true], + + // + ['ftp-bf' , '5', 'FTP Brute-Force', true], + + // Oversized IP packet. + ['pingdeath' , '6', 'Ping of Death', true], + + // Phishing websites and/or email. + ['phishing' , '7', 'Phishing', true], + + // + ['fraudvoip' , '8', 'Fraud VoIP', true], + + // Open proxy, open relay, or Tor exit node. + ['openproxy' , '9', 'Open Proxy', true], + + // Comment/forum spam, HTTP referer spam, or other CMS spam. + ['webspam' , '10', 'Web Spam', true], + + // Spam email content, infected attachments, and phishing emails. Note: Limit comments to only relevent + // information (instead of log dumps) and be sure to remove PII if you want to remain anonymous. + ['emailspam' , '11', 'Email Spam', true], + + // CMS blog comment spam. + ['blogspam' , '12', 'Blog Spam', true], + + // Conjunctive category. + ['vpnip' , '13', 'VPN IP', false], // to check alone ?? + + // Scanning for open ports and vulnerable services. + ['scan' , '14', 'Port Scan', true], + + // + ['hack' , '15', 'Hacking', true], + + // Attempts at SQL injection. + ['sql' , '16', 'SQL Injection', true], + + // Email sender spoofing. + ['spoof' , '17', 'Spoofing', true], + + // Credential brute-force attacks on webpage logins and services like SSH, FTP, SIP, SMTP, RDP, etc. + // This category is seperate from DDoS attacks. + ['brute' , '18', 'Brute-Force', true], + + // Webpage scraping (for email addresses, content, etc) and crawlers that do not honor robots.txt. + // Excessive requests and user agent spoofing can also be reported here. + ['badbot' , '19', 'Bad Web Bot', true], + + // Host is likely infected with malware and being used for other attacks or to host malicious content. + // The host owner may not be aware of the compromise. This category is often used in combination + // with other attack categories. + ['explhost' , '20', 'Exploited Host', true], + + // Attempts to probe for or exploit installed web applications such as a CMS + // like WordPress/Drupal, e-commerce solutions, forum software, phpMyAdmin and + // various other software plugins/solutions. + ['webattack' , '21', 'Web App Attack', true ], + + // Secure Shell (SSH) abuse. Use this category in combination + // with more specific categories. + ['ssh' , '22', 'SSH', false], + + // 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; + } + + /** + * Get the category id corresponding to given name + * + * @access public + * @static + * @param string $categoryName The report category name + * + * @return string|bool The category id in string format if found, otherwise false + */ + public static function getCategoryIdByName(string $categoryName) + { + foreach (self::$aipdbApiCategories as $cat){ + if ($cat[0] === $categoryName) { + return $cat[1]; + } + } + + // not found + return false; + } + + /** + * 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) + { + foreach (self::$aipdbApiCategories as $cat){ + if ($cat[1] === $categoryId) { + return $cat[0]; + } + } + + // not found + return false; + } + + /** + * 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 static function getCategoryIndex(string $value, int $index) + { + $i = 0; + foreach (self::$aipdbApiCategories as $cat){ + if ($cat[$index] === $value) { + return $i; + } + $i++; + } + + // not found + 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/ApiDefintion.php b/lib/ApiDefintion.php deleted file mode 100644 index 2571182..0000000 --- a/lib/ApiDefintion.php +++ /dev/null @@ -1,123 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * - * @version 0.1.0 - * @copyright 2020 Kristuff - */ - -namespace Kristuff\AbuseIPDB; - -/** - * Class ApiDefintion - * - * Abstract base class for ApiManager - * Contains main hard coded api settings - */ -abstract class ApiDefintion -{ - /** - * AbuseIPDB API v2 Endpoint - * @var string $api_endpoint - */ - protected $aipdbApiEndpoint = 'https://api.abuseipdb.com/api/v2/'; - - /** - * AbuseIPDB API v2 categories - * @var array $aipdbApiCategories - */ - protected $aipdbApiCategories = [ - - // Altering DNS records resulting in improper redirection. - 'dns-c' => ['1', 'DNS Compromise', true], - - // Falsifying domain server cache (cache poisoning). - 'dns-p' => ['2', 'DNS Poisoning', true], - - // Fraudulent orders. - 'fraud-orders' => ['3', 'Fraud Orders', true], - - // Participating in distributed denial-of-service (usually part of botnet). - 'ddos' => ['4', 'DDoS Attack', true], - - // - 'ftp-bf' => ['5', 'FTP Brute-Force', true], - - // Oversized IP packet. - 'pingdeath' => ['6', 'Ping of Death', true], - - // Phishing websites and/or email. - 'phishing' => ['7', 'Phishing', true], - - // - 'fraudvoip' => ['8', 'Fraud VoIP', true], - - // Open proxy, open relay, or Tor exit node. - 'openproxy' => ['9', 'Open Proxy', true], - - // Comment/forum spam, HTTP referer spam, or other CMS spam. - 'webspam' => ['10', 'Web Spam', true], - - // Spam email content, infected attachments, and phishing emails. Note: Limit comments to only relevent - // information (instead of log dumps) and be sure to remove PII if you want to remain anonymous. - 'emailspam' => ['11', 'Email Spam', true], - - // CMS blog comment spam. - 'blogspam' => ['12', 'Blog Spam', true], - - // Conjunctive category. - 'vpnip' => ['13', 'VPN IP', false], // to check alone ?? - - // Scanning for open ports and vulnerable services. - 'scan' => ['14', 'Port Scan', true], - - // seems to can't be used alone - 'hack' => ['15', 'Hacking', false], - - // Attempts at SQL injection. - 'sql' => ['16', 'SQL Injection'], true, - - // Email sender spoofing. - 'spoof' => ['17', 'Spoofing', true], - - // Credential brute-force attacks on webpage logins and services like SSH, FTP, SIP, SMTP, RDP, etc. - // This category is seperate from DDoS attacks. - 'brute' => ['18', 'Brute-Force', true], - - // Webpage scraping (for email addresses, content, etc) and crawlers that do not honor robots.txt. - // Excessive requests and user agent spoofing can also be reported here. - 'badbot' => ['19', 'Bad Web Bot', true], - - - // Host is likely infected with malware and being used for other attacks or to host malicious content. - // The host owner may not be aware of the compromise. This category is often used in combination - // with other attack categories. - 'explhost' => ['20', 'Exploited Host', true], - - // Attempts to probe for or exploit installed web applications such as a CMS - // like WordPress/Drupal, e-commerce solutions, forum software, phpMyAdmin and - // various other software plugins/solutions. - 'webattack' => ['21', 'Web App Attack', true ], - - // Secure Shell (SSH) abuse. Use this category in combination - // with more specific categories. - 'ssh' => ['22', 'SSH', false], - - // 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], - ]; - -} \ No newline at end of file diff --git a/lib/ApiHandler.php b/lib/ApiHandler.php new file mode 100644 index 0000000..2cee02c --- /dev/null +++ b/lib/ApiHandler.php @@ -0,0 +1,478 @@ + + * + * 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 deleted file mode 100644 index b90c281..0000000 --- a/lib/ApiManager.php +++ /dev/null @@ -1,304 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * - * @version 0.1.0 - * @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 you dont want to report - * - */ - 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, - ); - } - - /** - * 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 - */ - public static function fromConfigstring(string $configPath) - { - //todo check file exist - $config = self::loadJsonFile($configPath); - return new ApiManager($config->api_key, $config->user_id, $config->self_ips); - } - - /** - * 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 array $categories The report categories - * @param string $message The report message - * - * @return stdClass|array - * @throws \InvalidArgumentException - */ - public function report(string $ip = '', array $categories = [], $message = '') - { - // 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'); - } - - // TODO valider les cat / seules pas seules... - // TODO clean message ? selfips list - $cats = $this->validateCategories($categories); - - // report AbuseIPDB request - - - //TODO - return $this->apiRequest('report', 'POST', [ - 'ip' => $ip, - 'categories' => 'TODO', '21,15', - 'comment' => $message - ]); - } - - /** - * Check if the category(ies) given is/are valid - * Check for shortname or id, and categories that can't be used alone - * - * @access public - * @param array $categories The report categories list - * - * @return string Formatted string id list ('18,2,3...') - * @throws \InvalidArgumentException - */ - public function validateCategories(array $categories = []) - { - $newList = []; - $needAnother = false; - - foreach ($categories as $cat){ - - } - //todo - - } - - /** - * Perform a 'check' api request - * - * - * TODO OPTION POUR VERBOSE ;;; - * force $maxAge int as parameter ? - * - * @access public - * @param string $ip The ip to check - * @param string $maxAge Max age in days - * - * @return stdObj - * @throws \InvalidArgumentException - */ - public function check(string $ip = null, string $maxAge = '30') - { - - $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)'); - } - - // check AbuseIPDB request - return $this->apiRequest('check', 'GET', [ - 'ipAddress' => $ip, - 'maxAgeInDays' => $maxAge, - 'verbose' => true - ]); - } - - /** - * Perform a cURL request TODO: option as array - * - * @access protected - * @param string $path The api end path - * @param string $method The request method. Default is 'GET' - * @param array $data The request data - * - * @return stdObj TODO object ARRAY ;;; - */ - protected function apiRequest(string $path, string $method = 'GET', array $data) - { - // 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 json object - return json_decode($result); - } - - /** - * 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 string|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 new file mode 100644 index 0000000..3e451f0 --- /dev/null +++ b/lib/ApiResponse.php @@ -0,0 +1,137 @@ + + * + * 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 new file mode 100644 index 0000000..667fdda --- /dev/null +++ b/lib/CurlTrait.php @@ -0,0 +1,44 @@ + + * + * 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 new file mode 100644 index 0000000..8ec9bc1 --- /dev/null +++ b/lib/InvalidPermissionException.php @@ -0,0 +1,27 @@ + + * + * 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; + +/** + * Custom Exception for not readable file + */ +class InvalidPermissionException extends \Exception +{ +} \ No newline at end of file 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