Skip to content

Commit

Permalink
cache clean ips
Browse files Browse the repository at this point in the history
  • Loading branch information
mobula9 committed Dec 3, 2020
1 parent e886d60 commit ec1431f
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 139 deletions.
3 changes: 3 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Full configuration reference
// Optional. Cap the remediation to the selected one. Select from 'bypass' (minimum remediation), 'captcha' or 'ban' (maximum remediation). Defaults to 'ban'.
'max_remediation'=> 'ban',
// Optional. Set the duration we keep in cache the fact that an IP is clean. In seconds. Defaults to 600 (10 minutes).
'cache_expiration_for_clean_ip'=> '600',
]
$cacheAdapter = (...)
$bouncer = new Bouncer();
Expand Down
111 changes: 89 additions & 22 deletions src/ApiCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class ApiCache
/** @var bool */
private $ruptureMode;

/** @var int */
private $cacheExpirationForCleanIp;

/** @var ApiClient */
private $apiClient;

Expand All @@ -39,10 +42,11 @@ public function __construct(ApiClient $apiClient = null)
/**
* Configure this instance.
*/
public function configure(AbstractAdapter $adapter, bool $ruptureMode, string $apiUrl, int $timeout, string $userAgent, string $token): void
public function configure(AbstractAdapter $adapter, bool $ruptureMode, string $apiUrl, int $timeout, string $userAgent, string $token, int $cacheExpirationForCleanIp): void
{
$this->adapter = $adapter;
$this->ruptureMode = $ruptureMode;
$this->cacheExpirationForCleanIp = $cacheExpirationForCleanIp;

$this->apiClient->configure($apiUrl, $timeout, $userAgent, $token);
}
Expand Down Expand Up @@ -85,22 +89,89 @@ private function saveDeferred(CacheItem $item, int $ip, string $type, int $expir
}
}

/*
/**
* Parse "duration" entries returned from API to a number of seconds.
*
* TODO P3 TEST
* 9999h59m56.603445s
* 10m33.3465483s
* 33.3465483s
* -285.876962ms
* 33s'// should break!;
*/
private static function parseDurationToSeconds(string $duration): int
{
$re = '/(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m';
preg_match($re, $duration, $matches);
if (!count($matches)) {
throw new BouncerException("Unable to parse the following duration: ${$duration}.");
};
$seconds = 0;
if (null !== $matches[2]) {
$seconds += ((int) $matches[1]) * 3600; // hours
}
if (null !== $matches[3]) {
$seconds += ((int) $matches[2]) * 60; // minutes
}
if (null !== $matches[4]) {
$seconds += ((int) $matches[1]); // seconds
}
if (null !== $matches[5]) { // units in milliseconds
$seconds *= 0.001;
}
if (null !== $matches[1]) { // negative
$seconds *= -1;
}
$seconds = round($seconds);

return (int)$seconds;
}



/**
* Format a remediation item of a cache item.
* This format use a minimal amount of data allowing less cache data consumption.
*
* TODO P3 TESTS
*/
private function formatRemediationFromDecision(?array $decision): array
{
if (!$decision) {
return ['clean', time() + $this->cacheExpirationForCleanIp, 0];
}

return [
$decision['type'], // ex: captcha
time() + self::parseDurationToSeconds($decision['duration']), // expiration timestamp
$decision['id'],

/*
TODO P3 useful to keep in cache?
[
$decision['origin'],// ex cscli
$decision['scenario'],//ex: "manual 'captcha' from '25b9f1216f9344b780963bd281ae5573UIxCiwc74i2mFqK4'"
$decision['scope'],// ex: IP
]
*/
];
}

Update the cached remediations from these new decisions.
/**
* Update the cached remediations from these new decisions.
TODO P2 WRITE TESTS
0 decisions
3 known remediation type
3 decisions but 1 unknown remediation type
3 unknown remediation type
* TODO P2 WRITE TESTS
* 0 decisions
* 3 known remediation type
* 3 decisions but 1 unknown remediation type
* 3 unknown remediation type
*/
private function saveRemediations(array $decisions): bool
{
foreach ($decisions as $decision) {
$ipRange = range($decision['start_ip'], $decision['end_ip']);
foreach ($ipRange as $ip) {
$remediation = Remediation::formatFromDecision($decision);
$remediation = $this->formatRemediationFromDecision($decision);
$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
}
Expand All @@ -114,8 +185,14 @@ private function saveRemediations(array $decisions): bool
*/
private function saveRemediationsForIp(array $decisions, int $ip): void
{
foreach ($decisions as $decision) {
$remediation = Remediation::formatFromDecision($decision);
if (\count($decisions)) {
foreach ($decisions as $decision) {
$remediation = $this->formatRemediationFromDecision($decision);
$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
}
} else {
$remediation = $this->formatRemediationFromDecision(null);
$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);
}
Expand Down Expand Up @@ -164,17 +241,7 @@ public function pullUpdates(): void
private function miss(int $ip): string
{
$decisions = $this->apiClient->getFilteredDecisions(['ip' => long2ip($ip)]);

if (!\count($decisions)) {
// TODO P1 cache also the clean IP.
//$item = $this->buildRemediationCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
//$this->saveDeferred($item, $ip, $remediation[0], $remediation[1], $remediation[2]);

return Remediation::formatFromDecision(null)[0];
}

$this->saveRemediationsForIp($decisions, $ip);

return $this->hit($ip);
}

Expand Down Expand Up @@ -213,6 +280,6 @@ public function get(int $ip): ?string
return $this->miss($ip);
}

return Remediation::formatFromDecision(null)[0];
return $this->formatRemediationFromDecision(null)[0];
}
}
3 changes: 2 additions & 1 deletion src/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public function configure(array $config, AbstractAdapter $cacheAdapter): void
$this->config['api_url'],
$this->config['api_timeout'],
$this->config['api_user_agent'],
$this->config['api_token']
$this->config['api_token'],
$this->config['cache_expiration_for_clean_ip']
);
}

Expand Down
1 change: 1 addition & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function getConfigTreeBuilder()
->integerNode('api_timeout')->defaultValue(Constants::API_TIMEOUT)->end()
->booleanNode('rupture_mode')->defaultValue(true)->end()
->enumNode('max_remediation')->values(['bypass', 'captcha', 'ban'])->defaultValue('ban')->end()
->integerNode('cache_expiration_for_clean_ip')->defaultValue(Constants::CACHE_EXPIRATION_FOR_CLEAN_IP)->end()
->end();

return $treeBuilder;
Expand Down
3 changes: 3 additions & 0 deletions src/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class Constants
/** @var int The timeout when calling LAPI or CAPI */
const API_TIMEOUT = 1; // TODO P2 get the correct one

/** @var int The duration we keep a clean IP in cache 600s = 10m */
const CACHE_EXPIRATION_FOR_CLEAN_IP = 600; // TODO P2 get the correct one

/** @var array The list of each known remediation, sorted by priority */
const ORDERED_REMEDIATIONS = ['ban', 'captcha']; // TODO P2 get the correct one
}
67 changes: 0 additions & 67 deletions src/Remediation.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,71 +59,4 @@ public static function sortRemediationByPriority(array $remediations): array

return $remediationsWithPriorities;
}

/**
* Parse "duration" entries returned from API to a number of seconds.
*
* TODO P3 TEST
* 9999h59m56.603445s
* 10m33.3465483s
* 33.3465483s
* -285.876962ms
* 33s'// should break!;
*/
private static function parseDurationToSeconds(string $duration): int
{
$re = '/(-?)(?:(?:(\d+)h)?(\d+)m)?(\d+).\d+(m?)s/m';
preg_match($re, $duration, $matches);
if (!count($matches)) {
throw new BouncerException("Unable to parse the following duration: ${$duration}.");
};
$seconds = 0;
if (null !== $matches[2]) {
$seconds += ((int) $matches[1]) * 3600;// hours
}
if (null !== $matches[3]) {
$seconds += ((int) $matches[2]) * 60;// minutes
}
if (null !== $matches[4]) {
$seconds += ((int) $matches[1]);// seconds
}
if (null !== $matches[5]) {// units in milliseconds
$seconds *= 0.001;
}
if (null !== $matches[1]) {// negative
$seconds *= -1;
}
$seconds = round($seconds);

return $seconds;
}

/**
* Format a remediation item of a cache item.
* This format use a minimal amount of data allowing less cache data consumption.
*
* TODO P3 TESTS
*/
public static function formatFromDecision(?array $decision): array
{
if (!$decision) {
return ['clear', 0, null];
}

return [
$decision['type'], // ex: captcha
time() + self::parseDurationToSeconds($decision['duration']), // expiration
$decision['id'],

/*
TODO P3 useful to keep in cache?
[
$decision['id'],// id from API
$decision['origin'],// ex cscli
$decision['scenario'],//ex: "manual 'captcha' from '25b9f1216f9344b780963bd281ae5573UIxCiwc74i2mFqK4'"
$decision['scope'],// ex: IP
]
*/
];
}
}
Loading

0 comments on commit ec1431f

Please sign in to comment.