Created
June 22, 2016 16:07
-
-
Save NoMan2000/a41879fd4608b77f5b30f0d7db6bd6cf to your computer and use it in GitHub Desktop.
PHP and Redis Throttler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace cellControl\SecurityThrottle; | |
require_once dirname(dirname(__DIR__)) . '/init.php'; | |
use \Redis; | |
use cellControl\RedisSingleton; | |
use cellControl\IPFilter\IPFilter; | |
use cellControl\Traits\Environment; | |
use cellControl\Interfaces\Timer\TimeInterface; | |
/** | |
* Class BlackLister | |
* @package cellControl\SecurityThrottle | |
*/ | |
class BlackLister implements TimeInterface | |
{ | |
use Environment; | |
/** | |
* @const int | |
*/ | |
const MAX_SLEEP_TIME = 25; | |
/** | |
* @const int | |
*/ | |
const MAX_REQUESTS_PER_DAY_TO_BAN = 20; | |
/** | |
* @var IPFilter | |
*/ | |
protected $ipFilter; | |
/** | |
* @var Redis | |
*/ | |
protected $redis; | |
/** | |
* BlackLister constructor. | |
* @param Redis|null $redisInstance | |
*/ | |
public function __construct(Redis $redis = null, IPFilter $ipFilter = null) | |
{ | |
$this->redis = $redis ?: new RedisSingleton(); | |
$this->ipFilter = $ipFilter ?: new IPFilter(); | |
} | |
/** | |
* @return $this | |
*/ | |
public function blackList() | |
{ | |
$redis = $this->getRedis(); | |
$sleepAmount = 0; | |
foreach ($this->getIpAddress() as $ipBan) { | |
$redis->incrBy("BANIP:{$ipBan}", 1); | |
$redis->expire("BANIP:{$ipBan}", self::DAY); | |
$sleepAmount = $redis->get("BANIP:{$ipBan}"); | |
} | |
if ($sleepAmount > self::MAX_SLEEP_TIME) { | |
$sleepAmount = self::MAX_SLEEP_TIME; | |
} | |
return $this->sleep($sleepAmount); | |
} | |
/** | |
* Using sleep as a throttler has a security problem, a malicious user can curl request | |
* multiple times to the same file and sleep will halt execution, crashing the server. | |
* | |
* @param $seconds | |
* @return bool | |
*/ | |
protected function sleep($seconds) | |
{ | |
$sleepAmount = time() + $seconds; | |
for (;;) { | |
if (time() > $sleepAmount) { | |
break; | |
} | |
} | |
return $this; | |
} | |
/** | |
* @return array | |
*/ | |
public function getIpAddress() | |
{ | |
return $this->getIpFilter()->getAddresses(); | |
} | |
/** | |
* @return IPFilter | |
*/ | |
public function getIpFilter() | |
{ | |
return $this->ipFilter; | |
} | |
/** | |
* @return RedisSingleton|Redis | |
*/ | |
public function getRedis() | |
{ | |
return $this->redis; | |
} | |
/** | |
* @param $key | |
* @return mixed | |
*/ | |
public function getSleeper($key) | |
{ | |
return $this->redis->get($key); | |
} | |
/** | |
* This is not the best way to deal with the problem. | |
* Hackers use Botnets or change Tor Exit Nodes | |
* to mask their activity. Banning an IP only works on the most basic of attacks. | |
* | |
* @return bool | |
*/ | |
public function isBlackListed() | |
{ | |
$redis = $this->getRedis(); | |
foreach ($this->getIpAddress() as $ip) { | |
$ban = $redis->get("BANIP:{$ip}"); | |
if ($ban >= self::MAX_REQUESTS_PER_DAY_TO_BAN) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* @return $this | |
*/ | |
public function removeBlackList() | |
{ | |
$redis = $this->getRedis(); | |
foreach ($this->getIpAddress() as $ipBan) { | |
$redis->set("BANIP:{$ipBan}", 0); | |
} | |
return $this; | |
} | |
/** | |
* @param $key | |
*/ | |
public function removeSleeper($key) | |
{ | |
$this->getRedis()->del($key); | |
} | |
/** | |
* @param IPFilter $ipFilter | |
* @return BlackLister | |
*/ | |
public function setIpFilter($ipFilter) | |
{ | |
$this->ipFilter = $ipFilter; | |
return $this; | |
} | |
/** | |
* @param $key | |
* @param null $value | |
* @return int | |
*/ | |
public function setSleeper($key, $value = null) | |
{ | |
$value = $value ?: 1; | |
$sleep = $this->getRedis()->incrBy($key, $value); | |
if ($sleep > self::MAX_SLEEP_TIME) { | |
$sleep = self::MAX_SLEEP_TIME; | |
} | |
return $this->sleep($sleep); | |
} | |
/** | |
* @param Redis $redis | |
* @return $this | |
*/ | |
protected function setRedis(Redis $redis) | |
{ | |
$this->redis = $redis; | |
return $this; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace cellControl\IPFilter; | |
require_once dirname(dirname(__DIR__)) . '/init.php'; | |
use Illuminate\Support\Collection; | |
use cellControl\Exceptions\IPException; | |
/** | |
* Class IPFilter | |
* @package cellControl\IPFilter | |
*/ | |
class IPFilter | |
{ | |
/** | |
* This is 4294967040 | |
*/ | |
const CONVERSION_NUMBER = 0xFFFFFF00; | |
/** | |
* | |
*/ | |
const IP4_BYTES = 4; | |
/** | |
* | |
*/ | |
const IP6_BYTES = 16; | |
/** | |
* @var array | |
*/ | |
protected $baseIPKey = [ | |
'REMOTE_ADDR', | |
]; | |
/** | |
* @var array | |
*/ | |
protected $forwardedIPKeys = [ | |
'HTTP_X_FORWARDED_FOR', | |
'HTTP_X_FORWARDED', | |
'HTTP_FORWARDED_FOR', | |
'HTTP_FORWARDED', | |
'HTTP_CLIENT_IP', | |
'HTTP_X_CLUSTER_CLIENT_IP', | |
]; | |
/** | |
* Converts an unpacked binary string into a printable IP | |
* @param string $str | |
* @return string $ip | |
* @throws IPException | |
*/ | |
public function dtrNtop($str) | |
{ | |
if (strlen($str) === self::IP6_BYTES || strlen($str) === self::IP4_BYTES) { | |
return inet_ntop(pack("A" . strlen($str), $str)); | |
} | |
throw new IPException("Please provide a 4 or 16 byte string"); | |
} | |
/** | |
* Converts a printable IP into an unpacked binary string | |
* | |
* @param string $ip | |
* @return string $bin | |
* @throws IPException | |
*/ | |
public function dtrPton($ip) | |
{ | |
if ($this->isIP4($ip)) { | |
return current(unpack("A4", inet_pton($ip))); | |
} | |
if ($this->canTranslateIP6() && $this->isIP6($ip)) { | |
return current(unpack("A16", inet_pton($ip))); | |
} | |
throw new IPException("Please supply a valid IPv4 or IPv6 address"); | |
} | |
/** | |
* @return array | |
*/ | |
public function getAddresses() | |
{ | |
$list = $this->getFilteredList(); | |
if (!$this->canTranslateIP6()) { | |
return $list; | |
} | |
return $this->convertIPs($list); | |
} | |
/** | |
* @param array $ipKeys | |
* @return IPFilter | |
*/ | |
public function setIpKeys($ipKeys) | |
{ | |
$this->forwardedIPKeys = $ipKeys; | |
return $this; | |
} | |
/** | |
* @return bool | |
*/ | |
protected function canTranslateIP6() | |
{ | |
return defined('AF_INET6'); | |
} | |
/** | |
* $convertIpFilter will change | |
* 2001:0db8:0a0b:12f0:0000:0000:0000:0001 | |
* into 2001:db8:a0b:12f0::1 | |
* | |
* @param array $ipList | |
* @return array | |
*/ | |
protected function convertIPs(array $ipList) | |
{ | |
$ipList = new Collection($ipList); | |
return $ipList->filter($filterInvalidIPs = function ($ip) { | |
return filter_var($ip, FILTER_VALIDATE_IP); | |
})->map($convertIpFilter = function ($ip) { | |
return $this->dtrNtop($this->dtrPton($ip)); | |
})->toArray(); | |
} | |
/** | |
* @return array | |
*/ | |
protected function getFilteredList() | |
{ | |
$ipKeys = new Collection($this->getIpKeys()); | |
return $ipKeys->filter($removeEmptyServer = function ($key) { | |
return !empty($_SERVER[$key]); | |
})->map($getKeys = function ($keyName) { | |
return $_SERVER[$keyName]; | |
})->toArray(); | |
} | |
/** | |
* @return array | |
*/ | |
protected function getIpKeys() | |
{ | |
return array_merge($this->forwardedIPKeys, $this->baseIPKey); | |
} | |
/** | |
* @param $ip | |
* @return mixed | |
*/ | |
protected function isIP4($ip) | |
{ | |
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); | |
} | |
/** | |
* @param $ip | |
* @return mixed | |
*/ | |
protected function isIP6($ip) | |
{ | |
return $this->canTranslateIP6() && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace cellControl\IPFilter; | |
require_once dirname(dirname(__DIR__)) . '/init.php'; | |
use cellControl\IPFilter\IPFilter; | |
use GeoIp2\Database\Reader; | |
use Illuminate\Support\Collection; | |
use GeoIp2\Exception\AddressNotFoundException; | |
/** | |
* Class IPLookup | |
* @package cellControl\IPFilter | |
*/ | |
class IPLookup | |
{ | |
/** | |
* @var IPFilter | |
*/ | |
protected $filter; | |
/** | |
* @var Reader | |
*/ | |
protected $reader; | |
/** | |
* IPLookup constructor. | |
* @param Reader|null $reader | |
* @param \cellControl\IPFilter\IPFilter|null $filter | |
*/ | |
public function __construct( | |
Reader $reader = null, | |
IPFilter $filter = null | |
) { | |
$this->reader = $reader ?: new Reader(__DIR__ . '/Database/GeoLite2-City.mmdb'); | |
$this->filter = $filter ?: new IPFilter(); | |
} | |
/** | |
* @return array | |
*/ | |
public function getCities() | |
{ | |
$recordList = new Collection($this->getFilter()->getAddresses()); | |
return $recordList->filter($checkIpInDatabase = function ($ip) { | |
try { | |
$this->getReader()->city($ip); | |
} catch (AddressNotFoundException $e) { | |
return false; | |
} | |
return true; | |
})->map($getCityNames = function ($ip) { | |
return $this->getReader()->city($ip); | |
})->toArray(); | |
} | |
/** | |
* @param $ip | |
* @return \GeoIp2\Model\City | |
*/ | |
public function getCityFromIP($ip) | |
{ | |
return $this->getReader()->city($ip); | |
} | |
/** | |
* @return \cellControl\IPFilter\IPFilter | |
*/ | |
public function getFilter() | |
{ | |
return $this->filter; | |
} | |
/** | |
* @return Reader | |
*/ | |
public function getReader() | |
{ | |
return $this->reader; | |
} | |
/** | |
* @param \cellControl\IPFilter\IPFilter $filter | |
* @return IPLookup | |
*/ | |
public function setFilter(IPFilter $filter) | |
{ | |
$this->filter = $filter; | |
return $this; | |
} | |
/** | |
* @param Reader $reader | |
* @return IPLookup | |
*/ | |
public function setReader(Reader $reader) | |
{ | |
$this->reader = $reader; | |
return $this; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace cellControl\Interfaces\Timer; | |
require_once dirname(dirname(dirname(__DIR__))) . '/init.php'; | |
/** | |
* Interface TimeInterface | |
* @package cellControl\Interfaces\Timer | |
*/ | |
interface TimeInterface | |
{ | |
/** | |
* | |
*/ | |
const DAY = 86400; | |
/** | |
* | |
*/ | |
const FIVE_HOURS_IN_SECONDS = self::LONG_RESTART_TIMER; | |
/** | |
* | |
*/ | |
const FIVE_YEARS_IN_MINUTES = 2628000; | |
/** | |
* | |
*/ | |
const FORTY_FIVE_MINUTES = 45; | |
/** | |
* | |
*/ | |
const FORTY_FIVE_MINUTES_IN_SECONDS = 45 * 60; | |
/** | |
* | |
*/ | |
const HOUR = 3600; | |
/** | |
* | |
*/ | |
const LONG_RESTART_TIMER = 60 * 60 * 5; | |
/** | |
* | |
*/ | |
const MINUTE = 60; | |
/** | |
* | |
*/ | |
const SHORT_RESTART_TIMER = self::FORTY_FIVE_MINUTES_IN_SECONDS; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment