diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c78b4..94a6367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ As far as possible, we try to adhere to [Symfony guidelines](https://symfony.com --- +## [4.3.0](https://github.com/crowdsecurity/php-cs-bouncer/releases/tag/v4.3.0) - 2025-05-?? +[_Compare with previous release_](https://github.com/crowdsecurity/php-cs-bouncer/compare/v4.2.0...HEAD) + +__This release is not published yet__ + +### Added + +- Add `hasBlaasUri` to detect if the bouncer is connected to a Block As A Service Lapi +- Add `resetUsageMetrics` to reset the usage metrics cache item + +--- + ## [4.2.0](https://github.com/crowdsecurity/php-cs-bouncer/releases/tag/v4.2.0) - 2025-01-31 [_Compare with previous release_](https://github.com/crowdsecurity/php-cs-bouncer/compare/v4.1.0...v4.2.0) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index af2cf70..419e4b4 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -299,7 +299,7 @@ Below is the list of available settings: - `captcha_cache_duration`: Set the duration we keep in cache the captcha flow variables for an IP. In seconds. - Defaults to 86400.. In seconds. Defaults to 20. + Defaults to 86400. ### Geolocation diff --git a/src/AbstractBouncer.php b/src/AbstractBouncer.php index ec48f93..167742a 100644 --- a/src/AbstractBouncer.php +++ b/src/AbstractBouncer.php @@ -225,6 +225,16 @@ abstract public function getRequestUri(): string; */ abstract public function getRequestUserAgent(): string; + /** + * Check if the bouncer is connected to a "Blocklist as a service" Lapi. + */ + public function hasBlaasUri(): bool + { + $url = $this->getRemediationEngine()->getClient()->getConfig('api_url'); + + return 0 === strpos($url, Constants::BLAAS_URL); + } + /** * This method prune the cache: it removes all the expired cache items. * @@ -276,6 +286,20 @@ public function refreshBlocklistCache(): array } } + /** + * @throws InvalidArgumentException + */ + public function resetUsageMetrics(): void + { + // Retrieve metrics cache item + $metricsItem = $this->getRemediationEngine()->getCacheStorage()->getItem(AbstractCache::ORIGINS_COUNT); + if ($metricsItem->isHit()) { + // Reset the metrics + $metricsItem->set([]); + $this->getRemediationEngine()->getCacheStorage()->getAdapter()->save($metricsItem); + } + } + /** * Handle a bounce for current IP. * diff --git a/src/Constants.php b/src/Constants.php index 501aa2e..f987999 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -18,6 +18,8 @@ */ class Constants extends RemConstants { + /** @var string The URL prefix for Blocklist as a service LAPI */ + public const BLAAS_URL = 'https://admin.api.crowdsec.net'; /** @var int The duration we keep a captcha flow in cache */ public const CACHE_EXPIRATION_FOR_CAPTCHA = 86400; /** @var string The "MEMCACHED" cache system */ diff --git a/tests/Integration/AbstractBouncerTest.php b/tests/Integration/AbstractBouncerTest.php index dbec499..a1d7344 100644 --- a/tests/Integration/AbstractBouncerTest.php +++ b/tests/Integration/AbstractBouncerTest.php @@ -81,6 +81,7 @@ * @covers \CrowdSecBouncer\AbstractBouncer::shouldBounceCurrentIp * @covers \CrowdSecBouncer\AbstractBouncer::handleBounceExclusion * @covers \CrowdSecBouncer\AbstractBouncer::pushUsageMetrics + * @covers \CrowdSecBouncer\AbstractBouncer::resetUsageMetrics */ final class AbstractBouncerTest extends TestCase { @@ -1053,6 +1054,89 @@ public function testRun() ); } + public function testResetMetrics() + { + $client = new BouncerClient($this->configs, null, $this->logger); + $cache = new PhpFiles($this->configs, $this->logger); + $originCountItem = $cache->getItem(AbstractCache::ORIGINS_COUNT)->get(); + $this->assertEquals( + null, + $originCountItem, + 'The origin count for clean should be empty' + ); + + // bouncing URI + $client = new BouncerClient($this->configs, null, $this->logger); + $cache = new PhpFiles($this->configs, $this->logger); + $lapiRemediation = new LapiRemediation($this->configs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$this->configs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.3'); + $this->assertEquals(true, $bouncer->run(), 'Should bounce uri'); + $originCountItem = $cache->getItem(AbstractCache::ORIGINS_COUNT)->get(); + $this->assertEquals( + ['clean' => ['bypass' => 1]], + $originCountItem, + 'The origin count for clean should be 1' + ); + + // Test no-forward + $bouncerConfigs = array_merge( + $this->configs, + [ + 'forced_test_forwarded_ip' => Constants::X_FORWARDED_DISABLED, + ] + ); + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.7'); + + $bouncer->run(); + + $originCountItem = $cache->getItem(AbstractCache::ORIGINS_COUNT)->get(); + $this->assertEquals( + ['clean' => ['bypass' => 2]], + $originCountItem, + 'The origin count for clean should be 2' + ); + + // Test: reset metrics + $result = $bouncer->resetUsageMetrics(); + $originCountItem = $cache->getItem(AbstractCache::ORIGINS_COUNT)->get(); + $this->assertEquals( + [], + $originCountItem, + 'The origin count item should be reset' + ); + } + public function testPrivateAndProtectedMethods() { // handleCache diff --git a/tests/Unit/AbstractBouncerTest.php b/tests/Unit/AbstractBouncerTest.php index 0027add..7571ad2 100644 --- a/tests/Unit/AbstractBouncerTest.php +++ b/tests/Unit/AbstractBouncerTest.php @@ -77,6 +77,7 @@ * @uses \CrowdSecBouncer\AbstractBouncer::handleBounceExclusion * * @covers \CrowdSecBouncer\AbstractBouncer::pushUsageMetrics + * @covers \CrowdSecBouncer\AbstractBouncer::hasBlaasUri */ final class AbstractBouncerTest extends TestCase { @@ -774,6 +775,30 @@ public function testPushUsageMetricsException() $this->assertEquals('Error in unit test', $errorMessage); } + public function testHasBlockAsAServiceUri() + { + $configs = $this->configs; + $client = new BouncerClient($configs); + $cache = new PhpFiles($configs); + $lapiRemediation = new LapiRemediation($configs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $lapiRemediation]); + + $result = $bouncer->hasBlaasUri(); + $this->assertEquals(false, $result); + + $configs = array_merge($this->configs, [ + 'api_url' => 'https://admin.api.crowdsec.net', + ]); + + $client = new BouncerClient($configs); + $cache = new PhpFiles($configs); + $lapiRemediation = new LapiRemediation($configs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $lapiRemediation]); + + $result = $bouncer->hasBlaasUri(); + $this->assertEquals(true, $result); + } + public function testShouldBounceCurrentIp() { $configs = $this->configs;