Skip to content

Instantly share code, notes, and snippets.

@NoMan2000
Created June 22, 2016 16:07
Show Gist options
  • Save NoMan2000/a41879fd4608b77f5b30f0d7db6bd6cf to your computer and use it in GitHub Desktop.
Save NoMan2000/a41879fd4608b77f5b30f0d7db6bd6cf to your computer and use it in GitHub Desktop.
PHP and Redis Throttler
<?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;
}
}
<?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);
}
}
<?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;
}
}
<?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