Compare commits

...

16 commits

Author SHA1 Message Date
kristuff
d7b635875a v1.1
- Fixed: Update email pattern in `cleanMessage()` method to handle local addresses without TLD and to allow using the caracter `@` in custom message. Close #4
2022-10-06 00:16:16 +02:00
kristuff
af62240932 Update email pattern in cleanMessage() method
Update email pattern to handle local addresses without TLD and to allow using the caracter `@` in custom message. Close #4
2022-10-06 00:14:00 +02:00
kristuff
5639f1813a v1.0 2022-01-26 22:24:24 +01:00
kristuff
dc30007818 v0.9.15
**Changes**
- **New** `ApiHandler::setTimeout(int $timeout)` method allows to change default timeout before any API request. Timeout is expressed in milliseconds. Overwrites the value passed in constructor, useful when performing multiple queries with same handler but different timeout.
2021-12-08 20:42:33 +01:00
kristuff
d1d9cf9ad0 v0.9.14
**Changes**
- **New**: the `ApiHandler` (and `QuietApiHandler`) constructor takes now a **timeout** in third parameter. The timeout is expressed in **milliseconds** .  The timeout will apply to all API request methods (will raise exception if reached out). Default is **0** (no timeout).
- Formatting
2021-12-06 21:40:39 +01:00
kristuff
3ab67950a1 v0.9.13 2021-09-18 21:42:24 +02:00
kristuff
c4b5c65598 Fix #3
Check *properly* if property exists to prevent PHP notice.
2021-09-18 21:41:42 +02:00
kristuff
37b0a2eaa9 v0.9.12
**Fixed**
- Incorrect return type in `ApiResponse::getObject()` method (bug introduced in v0.9.8 with PHP strict types)
- cleaning, typos
2021-07-04 13:09:24 +02:00
kristuff
adff27d86d v0.9.11
**Fixed**
- getCategoryIdByName and getCategoryNameById now return string (instead array)
- IoT Targeted report category now named iot (instead of oit)
2021-02-23 20:31:53 +01:00
kristuff
77c2857eb8 v0.9.10
**Changed**
- report categories methods related now static
- `SilentApiHandler` class renamed `QuietApiHandler` **break change**
- new static method `ApiResponse::createErrorResponse`
2021-01-29 20:30:12 +01:00
kristuff
3c5c7155c2 v0.9.9
**Added**
- better exception handling with new class `SilentApiHandler`: to not raise any exception during API request but instead return an `ApiResponse` with errors
- version constant
- `ApiReponse` has new `errors()` and `hasError()` methods
2021-01-17 19:09:15 +01:00
kristuff
fa42dafc36 v0.9.8
**Added**
- ApiResponse class is now the return type for all API endpoint methods **break change**
- New parameter confidenceScore in blacklist() method (suscribers feature)

**Changed**
- method loadFromConfig and json helper removed (moved to client) **break change**
- field userId removed (not used). ApiHandler constructor changed **break change**
- `clear()` and `getBlacklist()` method renamed to `clearAddress()` and `blacklist()` (all method names match now api endpoint in camel case) **break change**
- scrict types
2021-01-16 18:28:11 +01:00
kristuff
1e7e1eb87b v0.9.7
**Added**
- support for bulk-report request (send cvs file)
2021-01-09 11:26:58 +01:00
kristuff
60d42db74c Update README.md 2021-01-08 19:33:24 +01:00
kristuff
cd48341f1b v0.9.6
**Fixed**
- clear method is broken in 0.9.5.
2021-01-08 19:12:48 +01:00
kristuff
1a882c0155 v0.9.6
**Fixed**
- clear method is broken in 0.9.5.
2021-01-08 16:31:43 +01:00
10 changed files with 719 additions and 383 deletions

2
.gitignore vendored
View file

@ -1,3 +1 @@
_*
.old*
vendor/* vendor/*

View file

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -13,12 +13,11 @@ Features
- Single IP check request **✓** - Single IP check request **✓**
- IP block check request **✓** - IP block check request **✓**
- Blacklist request **✓** - Blacklist request **✓**
- Single report request **✓** - Single IP report request **✓**
- Clear address request (remove own reports) **✓** - Bulk report request (send `csv` file) **✓**
- Auto cleaning report comment from sensitive data **✓** - Clear IP address request (remove your own reports) **✓**
- Auto cleaning report comments from sensitive data (email, custom ip/domain names list) **✓**
**Not implemented:** - Define timeout for cURL internal requests **✓**
- *\[TODO\] Bulk report Api request*
Requirements Requirements
------------ ------------
@ -34,25 +33,24 @@ Deploy with composer:
```json ```json
... ...
"require": { "require": {
"kristuff/abuseipdb": ">=0.9.5-stable" "kristuff/abuseipdb": "^1.1-stable"
}, },
``` ```
Usage More infos
----- -----
```php - [Project website](https://kristuff.fr/projects/abuseipdb)
<?php - [Api documentation](https://kristuff.fr/projects/abuseipdb/doc)
- CLI version: [github](https://github.com/kristuff/abuseipdb-cli) | [website](https://kristuff.fr/projects/abuseipdbcli)
echo ('TODO');
```
License License
------- -------
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,12 +1,15 @@
{ {
"name": "kristuff/abuseipdb", "name": "kristuff/abuseipdb",
"description": "A wrapper for AbuseIPDB API v2", "description": "A PHP wrapper for AbuseIPDB API v2",
"type": "library", "type": "library",
"keywords": ["abuseIPDB", "API"],
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
"name": "Kristuff", "name": "Kristuff",
"homepage": "https://github.com/kristuff" "homepage": "https://github.com/kristuff",
"email": "kristuff@kristuff.fr",
"role": "Developer"
} }
], ],
"require": { "require": {

View file

@ -1,32 +1,31 @@
<?php <?php declare(strict_types=1);
/** /**
* _ _ ___ ____ ____ ____ * _ ___ ___ ___ ___
* / \ | |__ _ _ ___ ___|_ _| _ \| _ \| __ ) * __ _| |__ _ _ ___ ___|_ _| _ \ \| _ )
* / _ \ | '_ \| | | / __|/ _ \| || |_) | | | | _ \ * / _` | '_ \ || (_-</ -_)| || _/ |) | _ \
* / ___ \| |_) | |_| \__ \ __/| || __/| |_| | |_) | * \__,_|_.__/\_,_/__/\___|___|_| |___/|___/
* /_/ \_\_.__/ \__,_|___/\___|___|_| |____/|____/
* *
* This file is part of Kristuff\AbsuseIPDB. * This file is part of Kristuff\AbuseIPDB.
* *
* (c) Kristuff <contact@kristuff.fr> * (c) Kristuff <kristuff@kristuff.fr>
* *
* For the full copyright and license information, please view the LICENSE * For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code. * file that was distributed with this source code.
* *
* @version 0.9.5 * @version 1.1
* @copyright 2020-2021 Kristuff * @copyright 2020-2022 Kristuff
*/ */
namespace Kristuff\AbuseIPDB; namespace Kristuff\AbuseIPDB;
/** /**
* Class ApiDefintion * Class ApiBase
* *
* Abstract base class for ApiManager * Abstract base class for ApiHanlder
* Contains main hard coded api settings * Contains main hard coded api settings
*/ */
abstract class ApiDefintion abstract class ApiBase
{ {
/** /**
* AbuseIPDB API v2 Endpoint * AbuseIPDB API v2 Endpoint
@ -35,13 +34,22 @@ abstract class ApiDefintion
protected $aipdbApiEndpoint = 'https://api.abuseipdb.com/api/v2/'; protected $aipdbApiEndpoint = 'https://api.abuseipdb.com/api/v2/';
/** /**
* AbuseIPDB API v2 categories * AbuseIPDB API key
* shorname, id (string), long name
* last paramter is false when the category cant' be used alone
* *
* @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
*/ */
protected $aipdbApiCategories = [ protected static $aipdbApiCategories = [
// Altering DNS records resulting in improper redirection. // Altering DNS records resulting in improper redirection.
['dns-c' , '1', 'DNS Compromise', true], ['dns-c' , '1', 'DNS Compromise', true],
@ -119,33 +127,36 @@ abstract class ApiDefintion
// Abuse was targeted at an "Internet of Things" type device. Include // Abuse was targeted at an "Internet of Things" type device. Include
// information about what type of device was targeted in the comments. // 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 * Get the list of report categories
* *
* @access public * @access public
* @static
*
* @return array * @return array
*/ */
public function getCategories() public static function getCategories(): array
{ {
return $this->aipdbApiCategories; return self::$aipdbApiCategories;
} }
/** /**
* Get the category id corresponding to given name * Get the category id corresponding to given name
* *
* @access public * @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 * @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) { if ($cat[0] === $categoryName) {
return $cat; return $cat[1];
} }
} }
@ -157,15 +168,16 @@ abstract class ApiDefintion
* Get the category name corresponding to given id * Get the category name corresponding to given id
* *
* @access public * @access public
* @static
* @param string $categoryId The report category id * @param string $categoryId The report category id
* *
* @return string|bool The category name if found, otherwise false * @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) { if ($cat[1] === $categoryId) {
return $cat; return $cat[0];
} }
} }
@ -177,15 +189,16 @@ abstract class ApiDefintion
* Get the index of category corresponding to given value * Get the index of category corresponding to given value
* *
* @access protected * @access protected
* @static
* @param string $value The report category id or name * @param string $value The report category id or name
* @param string $index The index in value array * @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 function getCategoryIndex(string $value, int $index) protected static function getCategoryIndex(string $value, int $index)
{ {
$i = 0; $i = 0;
foreach ($this->aipdbApiCategories as $cat){ foreach (self::$aipdbApiCategories as $cat){
if ($cat[$index] === $value) { if ($cat[$index] === $value) {
return $i; return $i;
} }
@ -196,4 +209,63 @@ abstract class ApiDefintion
return false; 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;
}
} }

View file

@ -1,21 +1,20 @@
<?php <?php declare(strict_types=1);
/** /**
* _ _ ___ ____ ____ ____ * _ ___ ___ ___ ___
* / \ | |__ _ _ ___ ___|_ _| _ \| _ \| __ ) * __ _| |__ _ _ ___ ___|_ _| _ \ \| _ )
* / _ \ | '_ \| | | / __|/ _ \| || |_) | | | | _ \ * / _` | '_ \ || (_-</ -_)| || _/ |) | _ \
* / ___ \| |_) | |_| \__ \ __/| || __/| |_| | |_) | * \__,_|_.__/\_,_/__/\___|___|_| |___/|___/
* /_/ \_\_.__/ \__,_|___/\___|___|_| |____/|____/
* *
* This file is part of Kristuff\AbsuseIPDB. * This file is part of Kristuff\AbuseIPDB.
* *
* (c) Kristuff <contact@kristuff.fr> * (c) Kristuff <kristuff@kristuff.fr>
* *
* For the full copyright and license information, please view the LICENSE * For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code. * file that was distributed with this source code.
* *
* @version 0.9.5 * @version 1.1
* @copyright 2020-2021 Kristuff * @copyright 2020-2022 Kristuff
*/ */
namespace Kristuff\AbuseIPDB; namespace Kristuff\AbuseIPDB;
@ -25,104 +24,85 @@ namespace Kristuff\AbuseIPDB;
* *
* The main class to work with the AbuseIPDB API v2 * The main class to work with the AbuseIPDB API v2
*/ */
class ApiHandler extends ApiDefintion class ApiHandler extends ApiBase
{ {
/** /**
* AbuseIPDB API key * Curl helper functions
*
* @access protected
* @var string $aipdbApiKey
*/ */
protected $aipdbApiKey = null; use CurlTrait;
/** /**
* AbuseIPDB user id * @var string
*
* @access protected
* @var string $aipdbUserId
*/ */
protected $aipdbUserId = null; const VERSION = 'v1.1';
/** /**
* The ips to remove from message * The ips to remove from report messages
* Generally you will add to this list yours ipv4 and ipv6, and the hostname * Generally you will add to this list yours ipv4 and ipv6, hostname, domain names
* *
* @access protected * @access protected
* @var array $selfIps * @var array
*/ */
protected $selfIps = []; 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 * Constructor
* *
* @access public * @access public
* @param string $apiKey The AbuseIPDB api key * @param string $apiKey The AbuseIPDB api key
* @param string $userId The AbuseIPDB user's id * @param array $myIps The Ips/domain name you don't want to display in report messages
* @param array $myIps The Ips/domain name you dont 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, string $userId, array $myIps = []) public function __construct(string $apiKey, array $myIps = [], int $timeout = 0)
{ {
$this->aipdbApiKey = $apiKey; $this->aipdbApiKey = $apiKey;
$this->aipdbUserId = $userId;
$this->selfIps = $myIps; $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 * Get the current configuration in a indexed array
* *
* @access public * @access public
*
* @return array * @return array
*/ */
public function getConfig() public function getConfig(): array
{ {
return array( return array(
'userId' => $this->aipdbUserId,
'apiKey' => $this->aipdbApiKey, 'apiKey' => $this->aipdbApiKey,
'selfIps' => $this->selfIps, 'selfIps' => $this->selfIps,
'timeout' => $this->timeout,
// TODO default report cat
); );
} }
/**
* Get a new instance of ApiHandler with config stored in a Json file
*
* @access public
* @static
* @param string $configPath The configuration file path
*
* @return \Kristuff\AbuseIPDB\ApiHandler
* @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.');
}
$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 self($keyConfig->api_key, $keyConfig->user_id, $selfIps);
return $app;
}
/** /**
* Performs a 'report' api request * Performs a 'report' api request
* *
@ -136,14 +116,14 @@ class ApiHandler extends ApiDefintion
* *
* @access public * @access public
* @param string $ip The ip to report * @param string $ip The ip to report
* @param string $categories The report categories * @param string $categories The report category(es)
* @param string $message The report message * @param string $message The report message
* @param bool $returnArray True to return an indexed array instead of object. Default is false.
* *
* @return object|array * @return ApiResponse
* @throws \RuntimeException
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function report(string $ip = '', string $categories = '', string $message = '', bool $returnArray = false) public function report(string $ip, string $categories, string $message): ApiResponse
{ {
// ip must be set // ip must be set
if (empty($ip)){ if (empty($ip)){
@ -152,29 +132,143 @@ class ApiHandler extends ApiDefintion
// categories must be set // categories must be set
if (empty($categories)){ if (empty($categories)){
throw new \InvalidArgumentException('categories list was empty'); throw new \InvalidArgumentException('Categories list was empty');
} }
// message must be set // message must be set
if (empty($message)){ if (empty($message)){
throw new \InvalidArgumentException('report message was empty'); throw new \InvalidArgumentException('Report message was empty');
} }
// validates categories, clean message // validates categories, clean message
$cats = $this->validateReportCategories($categories); $cats = $this->validateReportCategories($categories);
$msg = $this->cleanMessage($message); $msg = $this->cleanMessage($message);
// report AbuseIPDB request // AbuseIPDB request
$response = $this->apiRequest( return $this->apiRequest(
'report', [ 'report', [
'ip' => $ip, 'ip' => $ip,
'categories' => $cats, 'categories' => $cats,
'comment' => $msg 'comment' => $msg
], ],
'POST', $returnArray 'POST'
); );
}
return json_decode($response, $returnArray); /**
* 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') ;
} }
/** /**
@ -214,133 +308,56 @@ class ApiHandler extends ApiDefintion
* *
* @access public * @access public
* @param string $network The network to check * @param string $network The network to check
* @param int $maxAge Max age in days * @param int $maxAgeInDays The Max age in days, must
* @param bool $returnArray True to return an indexed array instead of object. Default is false.
* *
* @return object|array * @return ApiResponse
* @throws \InvalidArgumentException when maxAge is less than 1 or greater than 365, or when network value was not set. * @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 = null, int $maxAge = 30, bool $returnArray = false) public function checkBlock(string $network, int $maxAgeInDays = 30): ApiResponse
{ {
// max age must be less or equal to 365 // max age must be between 1 and 365
if ($maxAge > 365 || $maxAge < 1){ if ($maxAgeInDays > 365 || $maxAgeInDays < 1){
throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)'); throw new \InvalidArgumentException('maxAgeInDays must be between 1 and 365 (' . $maxAgeInDays . ' was given)');
} }
// ip must be set // ip must be set
if (empty($network)){ if (empty($network)){
throw new \InvalidArgumentException('network argument must be set (null given)'); throw new \InvalidArgumentException('network argument must be set (empty value given)');
} }
// minimal data // minimal data
$data = [ $data = [
'network' => $network, 'network' => $network,
'maxAgeInDays' => $maxAge, 'maxAgeInDays' => $maxAgeInDays,
]; ];
$response = $this->apiRequest('check-block', $data, 'GET', $returnArray) ; return $this->apiRequest('check-block', $data, 'GET');
return json_decode($response, $returnArray);
}
/**
* Perform a 'clear-address' api request
*
* Sample response:
*
* {
* "data": {
* "numReportsDeleted": 0
* }
* }
*
*
* @access public
* @param string $ip The ip to check
* @param bool $returnArray True to return an indexed array instead of object. Default is false.
*
* @return object|array
* @throws \InvalidArgumentException When ip value was not set.
*/
public function clear(string $ip = null, bool $returnArray = false)
{
// ip must be set
if (empty($ip)){
throw new \InvalidArgumentException('ip argument must be set (null given)');
}
// minimal data
$data = [
'ipAddress' => $ip,
];
$response = $this->apiRequest('check', $data, 'DELETE', $returnArray) ;
return json_decode($response, $returnArray);
}
/**
* Perform a 'check' api request
*
* @access public
* @param string $ip The ip to check
* @param int $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 object. Default is false.
*
* @return object|array
* @throws \InvalidArgumentException when maxAge is less than 1 or greater than 365, or when ip value was not set.
*/
public function check(string $ip = null, int $maxAge = 30, bool $verbose = false, bool $returnArray = false)
{
// max age must be 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;
}
$response = $this->apiRequest('check', $data, 'GET', $returnArray) ;
return json_decode($response, $returnArray);
} }
/** /**
* Perform a 'blacklist' api request * Perform a 'blacklist' api request
* *
* @access public * @access public
* @param int $limit The blacklist limit. Default is TODO (the api default limit) * @param int $limit The blacklist limit. Default is 10000 (the api default limit)
* @param bool $plainText True to get the response in plain text list. Default is false * @param bool $plainText True to get the response in plaintext list. Default is false
* @param bool $returnArray True to return an indexed array instead of object (when $plainText is set to false). 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 object|array * @return ApiResponse
* @throws \InvalidArgumentException When maxAge is not a numeric value, when maxAge is less than 1 or * @throws \RuntimeException
* greater than 365, or when ip value was not set. * @throws \InvalidArgumentException When maxAge is not a numeric value, when $limit is less than 1.
*/ */
public function getBlacklist(int $limit = 10000, bool $plainText = false, bool $returnArray = false) public function blacklist(int $limit = 10000, bool $plainText = false, int $confidenceMinimum = 100): ApiResponse
{ {
if ($limit < 1){ if ($limit < 1){
throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)'); throw new \InvalidArgumentException('limit must be at least 1 (' . $limit . ' was given)');
} }
// minimal data // minimal data
$data = [ $data = [
'confidenceMinimum' => 100, // The abuseConfidenceScore parameter is a subscriber feature. 'confidenceMinimum' => $confidenceMinimum,
'limit' => $limit, 'limit' => $limit,
]; ];
@ -350,73 +367,7 @@ class ApiHandler extends ApiDefintion
$data['plaintext'] = $plainText; $data['plaintext'] = $plainText;
} }
$response = $this->apiRequest('blacklist', $data, 'GET'); return $this->apiRequest('blacklist', $data, 'GET');
if ($plainText){
return $response;
}
return json_decode($response, $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 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) ? $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;
} }
/** /**
@ -426,45 +377,75 @@ class ApiHandler extends ApiDefintion
* @param string $path The api end path * @param string $path The api end path
* @param array $data The request data * @param array $data The request data
* @param string $method The request method. Default is 'GET' * @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. * @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 mixed * @return ApiResponse
* @throws \RuntimeException
*/ */
protected function apiRequest(string $path, array $data, string $method = 'GET', bool $returnArray = false) protected function apiRequest(string $path, array $data, string $method = 'GET', string $csvFilePath = ''): ApiResponse
{ {
// set api url $curlErrorNumber = -1; // will be used later to check curl execution
$url = $this->aipdbApiEndpoint . $path; $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 // open curl connection
$ch = curl_init(); $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 // set the method and data to send
if ($method == 'POST') { if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true); $this->setCurlOption($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); $this->setCurlOption($ch, CURLOPT_POSTFIELDS, $data);
} else { } else {
$this->setCurlOption($ch, CURLOPT_CUSTOMREQUEST, $method);
$url .= '?' . http_build_query($data); $url .= '?' . http_build_query($data);
} }
// set the url to call // set url and options
curl_setopt($ch, CURLOPT_URL, $url); $this->setCurlOption($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $this->setCurlOption($ch, CURLOPT_RETURNTRANSFER, 1);
$this->setCurlOption($ch, CURLOPT_HTTPHEADER, $headers);
// set the wanted format, JSON (required to prevent having full html page on error) /**
// and the AbuseIPDB API Key as a header * set timeout
curl_setopt($ch, CURLOPT_HTTPHEADER, [ *
'Accept: application/json;', * @see https://curl.se/libcurl/c/CURLOPT_TIMEOUT_MS.html
'Key: ' . $this->aipdbApiKey, * @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 // execute curl call
$result = curl_exec($ch); $result = curl_exec($ch);
$curlErrorNumber = curl_errno($ch);
$curlErrorMessage = curl_error($ch);
// close connection // close connection
curl_close($ch); curl_close($ch);
// return response as JSON data if ($curlErrorNumber !== 0){
return $result; throw new \RuntimeException($curlErrorMessage);
}
return new ApiResponse($result !== false ? $result : '');
} }
/** /**
@ -477,7 +458,7 @@ class ApiHandler extends ApiDefintion
* *
* @return string * @return string
*/ */
protected function cleanMessage(string $message) public function cleanMessage(string $message): string
{ {
// Remove backslashes // Remove backslashes
$message = str_replace('\\', '', $message); $message = str_replace('\\', '', $message);
@ -488,46 +469,10 @@ class ApiHandler extends ApiDefintion
} }
// If we're reporting spam, further munge any email addresses in the report // If we're reporting spam, further munge any email addresses in the report
$emailPattern = "/[^@\s]*@[^@\s]*\.[^@\s]*/"; $emailPattern = "/\b[A-Z0-9!#$%&'*`\/?^{|}~=+_.-]+@[A-Z0-9.-]+\b/i";
$message = preg_replace($emailPattern, "*", $message); $message = preg_replace($emailPattern, "*", $message);
// Make sure message is less 1024 chars // Make sure message is less 1024 chars
return substr($message, 0, 1024); return substr($message, 0, 1024);
} }
/**
* 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;
}
} }

137
lib/ApiResponse.php Normal file
View file

@ -0,0 +1,137 @@
<?php declare(strict_types=1);
/**
* _ ___ ___ ___ ___
* __ _| |__ _ _ ___ ___|_ _| _ \ \| _ )
* / _` | '_ \ || (_-</ -_)| || _/ |) | _ \
* \__,_|_.__/\_,_/__/\___|___|_| |___/|___/
*
* This file is part of Kristuff\AbuseIPDB.
*
* (c) Kristuff <kristuff@kristuff.fr>
*
* 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));
}
}

44
lib/CurlTrait.php Normal file
View file

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
/**
* _ ___ ___ ___ ___
* __ _| |__ _ _ ___ ___|_ _| _ \ \| _ )
* / _` | '_ \ || (_-</ -_)| || _/ |) | _ \
* \__,_|_.__/\_,_/__/\___|___|_| |___/|___/
*
* This file is part of Kristuff\AbuseIPDB.
*
* (c) Kristuff <kristuff@kristuff.fr>
*
* 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));
}
}
}

View file

@ -1,29 +1,27 @@
<?php <?php declare(strict_types=1);
/** /**
* _ _ ___ ____ ____ ____ * _ ___ ___ ___ ___
* / \ | |__ _ _ ___ ___|_ _| _ \| _ \| __ ) * __ _| |__ _ _ ___ ___|_ _| _ \ \| _ )
* / _ \ | '_ \| | | / __|/ _ \| || |_) | | | | _ \ * / _` | '_ \ || (_-</ -_)| || _/ |) | _ \
* / ___ \| |_) | |_| \__ \ __/| || __/| |_| | |_) | * \__,_|_.__/\_,_/__/\___|___|_| |___/|___/
* /_/ \_\_.__/ \__,_|___/\___|___|_| |____/|____/
* *
* This file is part of Kristuff\AbsuseIPDB. * This file is part of Kristuff\AbuseIPDB.
* *
* (c) Kristuff <contact@kristuff.fr> * (c) Kristuff <kristuff@kristuff.fr>
* *
* For the full copyright and license information, please view the LICENSE * For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code. * file that was distributed with this source code.
* *
* @version 0.9.5 * @version 1.1
* @copyright 2020-2021 Kristuff * @copyright 2020-2022 Kristuff
*/ */
namespace Kristuff\AbuseIPDB; namespace Kristuff\AbuseIPDB;
/** /**
* Custom Exception for non redable file * Custom Exception for not readable file
*/ */
class InvalidPermissionException extends \Exception class InvalidPermissionException extends \Exception
{ {
} }

141
lib/QuietApiHandler.php Normal file
View file

@ -0,0 +1,141 @@
<?php declare(strict_types=1);
/**
* _ ___ ___ ___ ___
* __ _| |__ _ _ ___ ___|_ _| _ \ \| _ )
* / _` | '_ \ || (_-</ -_)| || _/ |) | _ \
* \__,_|_.__/\_,_/__/\___|___|_| |___/|___/
*
* This file is part of Kristuff\AbuseIPDB.
*
* (c) Kristuff <kristuff@kristuff.fr>
*
* 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());
}
}
}