Skip to content

Commit 01c3f7b

Browse files
authored
Merge pull request #8 from crowdsecurity/feature/bouncer-prunable
cache is now prunable
2 parents 99a1b48 + 00bca35 commit 01c3f7b

File tree

7 files changed

+70
-25
lines changed

7 files changed

+70
-25
lines changed

.github/workflows/tests.yml

-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ jobs:
4545
- name: Add a bouncer to run phpunit tests
4646
run: docker exec crowdsec cscli bouncers add bouncer-php-library -o raw > .bouncer-key
4747

48-
# TODO P2 Move values to env vars
4948
- name: Add a machine to pilot crowdsec state
5049
run: docker exec crowdsec cscli machines add PhpUnitTestMachine --password PhpUnitTestMachinePassword
5150

src/ApiCache.php

+33-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace CrowdSecBouncer;
66

77
use Symfony\Component\Cache\Adapter\AbstractAdapter;
8+
use Symfony\Component\Cache\PruneableInterface;
89
use Psr\Log\LoggerInterface;
910

1011
/**
@@ -98,7 +99,7 @@ private function addRemediationToCacheItem(string $ip, string $type, int $expira
9899
// Build the item lifetime in cache and sort remediations by priority
99100
$maxLifetime = max(array_column($remediations, 1));
100101
$prioritizedRemediations = Remediation::sortRemediationByPriority($remediations);
101-
102+
102103
$item->set($prioritizedRemediations);
103104
$item->expiresAfter($maxLifetime);
104105

@@ -205,7 +206,7 @@ private function formatRemediationFromDecision(?array $decision): array
205206
}
206207

207208
return [
208-
$decision['type'], // ex: captcha
209+
$decision['type'], // ex: ban, captcha
209210
time() + self::parseDurationToSeconds($decision['duration']), // expiration timestamp
210211
$decision['id'],
211212
];
@@ -222,12 +223,6 @@ private function defferUpdateCacheConfig(array $config): void
222223

223224
/**
224225
* Update the cached remediations from these new decisions.
225-
226-
* TODO P2 WRITE TESTS
227-
* 0 decisions
228-
* 3 known remediation type
229-
* 3 decisions but 1 unknown remediation type
230-
* 3 unknown remediation type
231226
*/
232227
private function saveRemediations(array $decisions): bool
233228
{
@@ -278,6 +273,12 @@ private function saveRemediationsForIp(array $decisions, string $ip): void
278273
{
279274
if (\count($decisions)) {
280275
foreach ($decisions as $decision) {
276+
if (!in_array($decision['type'], Constants::ORDERED_REMEDIATIONS)) {
277+
$highestRemediationLevel = Constants::ORDERED_REMEDIATIONS[0];
278+
// TODO P1 test the case of unknown remediation type
279+
$this->logger->warning("The remediation type " . $decision['type'] . " is unknown by this CrowdSec Bouncer version. Fallback to highest remedition level: " . $highestRemediationLevel);
280+
$decision['type'] = $highestRemediationLevel;
281+
}
281282
$remediation = $this->formatRemediationFromDecision($decision);
282283
$this->addRemediationToCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
283284
}
@@ -288,12 +289,17 @@ private function saveRemediationsForIp(array $decisions, string $ip): void
288289
$this->adapter->commit();
289290
}
290291

292+
public function clear(): bool
293+
{
294+
return $this->adapter->clear();
295+
}
296+
291297
/**
292298
* Used in stream mode only.
293299
* Warm the cache up.
294300
* Used when the stream mode has just been activated.
295301
*
296-
* TODO P2 test for overlapping decisions strategy (max expires, remediation ordered by priorities)
302+
* TODO P2 test for overlapping decisions strategy (ex: max expires)
297303
*/
298304
public function warmUp(): void
299305
{
@@ -302,7 +308,7 @@ public function warmUp(): void
302308
$decisionsDiff = $this->apiClient->getStreamedDecisions($startup);
303309
$newDecisions = $decisionsDiff['new'];
304310

305-
$this->adapter->clear();
311+
$this->clear();
306312

307313
if ($newDecisions) {
308314
$this->warmedUp = $this->saveRemediations($newDecisions);
@@ -366,7 +372,8 @@ private function miss(string $ip): string
366372
private function hit(string $ip): string
367373
{
368374
$remediations = $this->adapter->getItem($ip)->get();
369-
// TODO P2 control before if date is not expired and if true, update cache item.
375+
// TODO P1 foreach $remediations, control if exp date is not expired.
376+
// If true, update cache item by removing this expired remediation.
370377

371378
// We apply array values first because keys are ids.
372379
$firstRemediation = array_values($remediations)[0];
@@ -398,4 +405,19 @@ public function get(string $ip): string
398405
return $this->miss($ip);
399406
}
400407
}
408+
409+
public function prune(): bool
410+
{
411+
$isPrunable = ($this->adapter instanceof PruneableInterface);
412+
if (!$isPrunable) {
413+
throw new BouncerException("Cache Adapter" . get_class($this->adapter) . " is not prunable.");
414+
}
415+
/** @var PruneableInterface */
416+
$adapter = $this->adapter;
417+
$pruned = $adapter->prune();
418+
$this->logger->info('Cached adapter pruned');
419+
420+
// TODO P3 Prune remediation inside cache items.
421+
return $pruned;
422+
}
401423
}

src/ApiClient.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function configure(string $baseUri, int $timeout, string $userAgent, stri
5151
*/
5252
public function getFilteredDecisions(array $filter): array
5353
{
54-
// TODO P2 keep results filtered for scope=ip or scope=range (we can't do anything with other scopes)
54+
// TODO P1 keep results filtered for scope=ip or scope=range (we can't do anything with other scopes)
5555
$decisions = $this->restClient->request('/v1/decisions', $filter);
5656
$decisions = $decisions ?: [];
5757

@@ -64,7 +64,7 @@ public function getFilteredDecisions(array $filter): array
6464
*/
6565
public function getStreamedDecisions(bool $startup = false): array
6666
{
67-
// TODO P2 keep results filtered for scope=ip or scope=range (we can't do anything with other scopes)
67+
// TODO P1 keep results filtered for scope=ip or scope=range (we can't do anything with other scopes)
6868
/** @var array */
6969
$decisionsDiff = $this->restClient->request('/v1/decisions/stream', $startup ? ['startup' => 'true'] : null);
7070

src/Bouncer.php

+18
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ public function refreshBlocklistCache(): void
129129
$this->apiCache->pullUpdates();
130130
}
131131

132+
/**
133+
* This method clear the full data in cache.
134+
*/
135+
public function clearCache(): bool
136+
{
137+
return $this->apiCache->clear();
138+
}
139+
140+
/**
141+
* This method prune the cache: it removes all the expired cache items.
142+
*/
143+
public function pruneCache(): bool
144+
{
145+
return $this->apiCache->clear();
146+
}
147+
148+
149+
132150
/**
133151
* Browse the remediations cache.
134152
*/

src/Constants.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@
1515
class Constants
1616
{
1717
/** @var string The URL of the CrowdSec Central API */
18-
public const CAPI_URL = 'https://api.crowdsec.net/v2/'; // TODO P2 get the correct one
18+
public const CAPI_URL = 'https://api.crowdsec.net/v2/';
1919

2020
/** @var string The user agent used to send request to LAPI or CAPI */
21-
public const BASE_USER_AGENT = 'CrowdSec PHP Library/1.0.0'; // TODO P3 get the correct version
21+
public const BASE_USER_AGENT = 'PHP CrowdSec Bouncer/1.0.0'; // TODO P3 get the correct version
2222

2323
/** @var int The timeout when calling LAPI or CAPI */
24-
public const API_TIMEOUT = 1; // TODO P2 get the correct one
24+
public const API_TIMEOUT = 1;
2525

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

2929
/** @var string The ban remediation */
3030
public const REMEDIATION_BAN = 'ban';

tests/IpVerificationTest.php

+11-5
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@
1111
use Symfony\Component\Cache\Adapter\AbstractAdapter;
1212
use PHPUnit\Framework\MockObject\MockObject;
1313
use Psr\Log\LoggerInterface;
14+
use Symfony\Component\Cache\PruneableInterface;
1415

1516
define("HOST_IS_UP", true);
1617
define("HOST_IS_DOWN", false);
1718

1819
/*
1920
TODO P2 Instanciate all the configuration
20-
TODO P2 testThrowErrorWhenMissAndApiIsNotReachable()
21+
TODO P2 testThrowErrorWhenMissAndApiIsNotReachable() https://stackoverflow.com/questions/5683592/phpunit-assert-that-an-exception-was-thrown
2122
TODO P2 testThrowErrorWhenMissAndApiTimeout()
22-
TODO P2 testCanVerifyCaptchableIp()
2323
TODO P2 testCanHandleCacheSaturation()
24-
TODO P2 testCanNotUseCapiInLiveMode()
25-
TODO P2 testCanVerifyIpInStreamModeWithCacheSystemBeforeWarmingTheCacheUp() https://stackoverflow.com/questions/5683592/phpunit-assert-that-an-exception-was-thrown
24+
TODO P3 testCanNotUseCapiInLiveMode()
25+
TODO P2 testCanVerifyIpInStreamModeWithCacheSystemBeforeWarmingTheCacheUp()
2626
*/
2727

2828
final class IpVerificationTest extends TestCase
@@ -65,6 +65,7 @@ public function testCanVerifyIpInLiveModeWithoutCacheSystem(): void
6565
$remediation = $bouncer->getRemediationForIp($badIp);
6666
$this->assertEquals($remediation, 'ban');
6767
}*/
68+
6869
/**
6970
* @group integration
7071
* @covers Bouncer
@@ -122,8 +123,13 @@ public function testCanVerifyIpInLiveModeWithCacheSystem(AbstractAdapter $cacheA
122123
$cleanRemediation2ndCall = $bouncer->getRemediationForIp($cleanIp);
123124
$this->assertEquals('bypass', $cleanRemediation2ndCall);
124125

126+
// Prune cache
127+
if ($cacheAdapter instanceof PruneableInterface) {
128+
$this->assertTrue($bouncer->pruneCache(), 'The cache should be prunable');
129+
}
130+
125131
// Clear cache
126-
$cacheAdapter->clear();
132+
$this->assertTrue($bouncer->clearCache(), 'The cache should be clearable');
127133

128134
// Call one more time (should miss as the cache has been cleared)
129135

tests/Template403Test.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ final class Template403Test extends TestCase
99
{
1010
/**
1111
* @group integration
12-
* @covers \CrowdSecBouncer\Bouncer
12+
* @covers Bouncer
1313
*/
1414
public function testCanGetDefault403Template(): void
1515
{
16-
// TODO P2 update these tests
16+
// TODO P1 update these tests
1717
//$bouncer = new Bouncer();
1818
//$bouncer->configure($config, $cacheAdapter);
1919
//$this->assertIsString($bouncer->getdefault403Template());

0 commit comments

Comments
 (0)