Skip to content

Commit eff2fb1

Browse files
feat(geolocation): Handle stream mode for country scope
1 parent 6c9327a commit eff2fb1

19 files changed

+299
-77
lines changed

.github/workflows/test-suite.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ jobs:
8181
tar -xf GeoLite2-City.tar.gz
8282
rm GeoLite2-Country.tar.gz GeoLite2-Country.tar.gz.sha256.txt GeoLite2-City.tar.gz GeoLite2-City.tar.gz.sha256.txt
8383
84-
8584
- name: Run PHP UNIT tests
8685
run: |
8786
ddev exec BOUNCER_KEY=${{ env.BOUNCER_KEY }} LAPI_URL=http://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-own-modules/crowdsec-php-lib/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-own-modules/crowdsec-php-lib/tests/IpVerificationTest.php
@@ -102,16 +101,24 @@ jobs:
102101
./test-init.sh
103102
chmod +x run-tests.sh
104103
105-
- name: Run End to end test without geolocation
104+
- name: Run End to end test (live mode without geolocation)
106105
run: |
107106
cd ${{ github.workspace }}/my-own-modules/crowdsec-php-lib/tests/end-to-end/__scripts__
108107
./run-tests.sh ci "./__tests__/1-live-mode.js"
109108
110-
- name: Run End to end test with geolocation
109+
- name: Run End to end test (live mode with geolocation)
111110
run: |
112111
cd ${{ github.workspace }}/my-own-modules/crowdsec-php-lib
113112
sed -i 's/\x27enabled\x27 => false/\x27enabled\x27 => true/g' examples/auto-prepend/settings.php
113+
sed -i 's/\x27forced_test_ip\x27 => \x27\x27/\x27forced_test_ip\x27 => \x27210.249.74.42\x27/g' examples/auto-prepend/settings.php
114114
cd ${{ github.workspace }}/my-own-modules/crowdsec-php-lib/tests/end-to-end/__scripts__
115115
./run-tests.sh ci "./__tests__/2-live-mode-with-geolocation.js"
116-
117116
117+
- name: Run End to end test (stream mode without geolocation)
118+
run: |
119+
cd ${{ github.workspace }}/my-own-modules/crowdsec-php-lib
120+
sed -i 's/\x27enabled\x27 => true/\x27enabled\x27 => false/g' examples/auto-prepend/settings.php
121+
sed -i 's/\x27forced_test_ip\x27 => \x27210.249.74.42\x27/\x27forced_test_ip\x27 => \x27\x27/g' examples/auto-prepend/settings.php
122+
sed -i 's/\x27stream_mode\x27 => false/\x27stream_mode\x27 => true/g' examples/auto-prepend/settings.php
123+
cd ${{ github.workspace }}/my-own-modules/crowdsec-php-lib/tests/end-to-end/__scripts__
124+
./run-tests.sh ci "./__tests__/3-stream-mode.js"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
require_once __DIR__.'/../../../vendor/autoload.php';
4+
require_once __DIR__.'/../settings.php';
5+
6+
use CrowdSecBouncer\StandAloneBounce;
7+
8+
if (isset($_GET['action']) && in_array($_GET['action'],['refresh', 'clear', 'prune', 'warm-up'])) {
9+
$action = $_GET['action'];
10+
$bounce = new StandAloneBounce();
11+
$bounce->setDebug($crowdSecStandaloneBouncerConfig['debug_mode']??false);
12+
$bounce->setDisplayErrors($crowdSecStandaloneBouncerConfig['display_errors'] ?? false);
13+
$bounce->init($crowdSecStandaloneBouncerConfig);
14+
$bouncer = $bounce->getBouncerInstance();
15+
switch ($action) {
16+
case 'refresh':
17+
$bouncer->refreshBlocklistCache();
18+
break;
19+
case 'clear':
20+
$bouncer->clearCache();
21+
break;
22+
case 'prune':
23+
$bouncer->pruneCache();
24+
break;
25+
case 'warm-up':
26+
$bouncer->warmBlocklistCacheUp();
27+
break;
28+
default:
29+
throw new Exception("Unknown cache action type:$action");
30+
}
31+
32+
echo "
33+
<!DOCTYPE html>
34+
<html>
35+
<head>
36+
<meta charset='utf-8'/>
37+
<title>Cache action: $action</title>
38+
</head>
39+
40+
<body>
41+
<h1>Cache action has been done: $action</h1>
42+
</body>
43+
</html>
44+
";
45+
} else {
46+
die('You must pass an "action" param (refresh or clear)' . PHP_EOL);
47+
}
48+
49+
50+
51+

examples/auto-prepend/scripts/cron-refresh-decisions.php

Lines changed: 0 additions & 14 deletions
This file was deleted.

examples/auto-prepend/settings.example.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'fs_cache_path' => __DIR__.'/.cache', // [FILL ME] Important note: be sur this path won't be publicly accessible!
1212

1313
'bouncing_level' => Constants::BOUNCING_LEVEL_NORMAL,
14+
'forced_test_ip' => '', // Set a specific for test purpose only (testing geolocation for example).
1415

1516
'stream_mode' => false,
1617

@@ -52,12 +53,11 @@
5253

5354
'geolocation' => [
5455
'save_in_session' => true, // Set to true to avoid multiple call to the geolocation database
55-
'test_public_ip' => '210.249.74.42', // Only if you test on a local network (docker, etc.)
5656
'enabled' => true, // Set to true if you want to geo-localize IP
5757
'type' => 'maxmind', // At this moment, only 'maxmind' type is available
5858
'maxmind' => [ // MaxMind settings
59-
'database_type' => 'city', // You can set 'city' or 'country'
60-
'database_path' => '/var/www/html/GeoLite2-City.mmdb' // Absolute path to the MaxMind database
59+
'database_type' => 'country', // You can set 'city' or 'country'
60+
'database_path' => '/var/www/html/GeoLite2-Country.mmdb' // Absolute path to the MaxMind database
6161
]
6262
]
6363
];

src/ApiCache.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ class ApiCache
7878
*/
7979
private $cacheKey = [];
8080

81+
/**
82+
* @var array|null
83+
*/
84+
private $scopes;
85+
8186
/**
8287
* @param LoggerInterface $logger The logger to use
8388
* @param ApiClient|null $apiClient The APIClient instance to use
@@ -140,6 +145,21 @@ public function configure(
140145
$this->apiClient->configure($apiUrl, $timeout, $userAgent, $apiKey);
141146
}
142147

148+
/**
149+
* @return array
150+
*/
151+
private function getScopes(): ?array
152+
{
153+
if($this->scopes === null){
154+
$scopes = [Constants::SCOPE_IP, Constants::SCOPE_RANGE];
155+
if (!empty($this->geolocConfig['enabled'])) {
156+
$scopes[] = Constants::SCOPE_COUNTRY;
157+
}
158+
$this->scopes = $scopes;
159+
}
160+
return $this->scopes;
161+
}
162+
143163
/**
144164
* Add remediation to a Symfony Cache Item identified by a cache key.
145165
*
@@ -556,7 +576,7 @@ public function warmUp(): array
556576
$this->clear();
557577
}
558578
$this->logger->debug('', ['type' => 'START_CACHE_WARMUP']);
559-
$decisionsDiff = $this->apiClient->getStreamedDecisions(true);
579+
$decisionsDiff = $this->apiClient->getStreamedDecisions(true, $this->getScopes());
560580
$newDecisions = $decisionsDiff['new'];
561581

562582
$nbNew = 0;
@@ -602,7 +622,7 @@ public function pullUpdates(): array
602622
}
603623

604624
$this->logger->debug('', ['type' => 'START_CACHE_UPDATE']);
605-
$decisionsDiff = $this->apiClient->getStreamedDecisions();
625+
$decisionsDiff = $this->apiClient->getStreamedDecisions(false, $this->getScopes());
606626
$newDecisions = $decisionsDiff['new'];
607627
$deletedDecisions = $decisionsDiff['deleted'];
608628

src/ApiClient.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,20 @@ public function getFilteredDecisions(array $filter): array
5656
return $this->restClient->request('/v1/decisions', $filter) ?: [];
5757
}
5858

59+
5960
/**
6061
* Request decisions using the stream mode. When the $startup flag is used, all the decisions are returned.
6162
* Else only the decisions updates (add or remove) from the last stream call are returned.
63+
* @param bool $startup
64+
* @param array $scopes
65+
* @return array
6266
*/
63-
public function getStreamedDecisions(bool $startup = false): array
67+
public function getStreamedDecisions(bool $startup = false, array $scopes = [Constants::SCOPE_IP, Constants::SCOPE_RANGE]): array
6468
{
6569
/** @var array */
66-
return $this->restClient->request('/v1/decisions/stream', $startup ? ['startup' => 'true'] : null);
70+
return $this->restClient->request(
71+
'/v1/decisions/stream',
72+
['startup' => $startup ? 'true': 'false', 'scopes' => implode(',', $scopes)]
73+
);
6774
}
6875
}

src/Bouncer.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public function __construct(AbstractAdapter $cacheAdapter = null, LoggerInterfac
5353
* Configure this instance.
5454
*
5555
* @param array $config An array with all configuration parameters
56+
* @throws \Psr\Cache\InvalidArgumentException
5657
*/
5758
public function configure(array $config): void
5859
{
@@ -107,12 +108,14 @@ private function capRemediationLevel(string $remediation): string
107108
* @param string $ip The IP to check
108109
*
109110
* @return string the remediation to apply (ex: 'ban', 'captcha', 'bypass')
111+
* @throws \Psr\Cache\InvalidArgumentException
110112
*/
111113
public function getRemediationForIp(string $ip): string
112114
{
113-
$address = Factory::parseAddressString($ip, ParseStringFlag::MAY_INCLUDE_ZONEID);
115+
$ipToCheck = !empty($this->config['forced_test_ip']) ? $this->config['forced_test_ip'] : $ip;
116+
$address = Factory::parseAddressString($ipToCheck, ParseStringFlag::MAY_INCLUDE_ZONEID);
114117
if (null === $address) {
115-
throw new BouncerException("IP $ip format is invalid.");
118+
throw new BouncerException("IP $ipToCheck format is invalid.");
116119
}
117120
$remediation = $this->apiCache->get($address);
118121

@@ -166,6 +169,7 @@ public static function getCaptchaHtmlTemplate(bool $error, string $captchaImageS
166169
* This method should be called only to force a cache warm up.
167170
*
168171
* @return array "count": number of decisions added, "errors": decisions not added
172+
* @throws \Psr\Cache\InvalidArgumentException
169173
*/
170174
public function warmBlocklistCacheUp(): array
171175
{
@@ -177,6 +181,7 @@ public function warmBlocklistCacheUp(): array
177181
* This method should be called periodically (ex: crontab) in an asynchronous way to update the bouncer cache.
178182
*
179183
* @return array Number of deleted and new decisions, and errors when processing decisions
184+
* @throws \Psr\Cache\InvalidArgumentException
180185
*/
181186
public function refreshBlocklistCache(): array
182187
{
@@ -187,6 +192,7 @@ public function refreshBlocklistCache(): array
187192
* This method clear the full data in cache.
188193
*
189194
* @return bool If the cache has been successfully cleared or not
195+
* @throws \Psr\Cache\InvalidArgumentException
190196
*/
191197
public function clearCache(): bool
192198
{
@@ -218,7 +224,7 @@ public function getLogger(): LoggerInterface
218224
*
219225
* @return array an array composed of two items, a "phrase" string representing the phrase and a "inlineImage" representing the image data
220226
*/
221-
public static function buildCaptchaCouple()
227+
public static function buildCaptchaCouple(): array
222228
{
223229
$captchaBuilder = new CaptchaBuilder();
224230

@@ -238,7 +244,7 @@ public static function buildCaptchaCouple()
238244
*
239245
* @return bool If the captcha input was correct or not
240246
*/
241-
public function checkCaptcha(string $expected, string $try, string $ip)
247+
public function checkCaptcha(string $expected, string $try, string $ip): bool
242248
{
243249
$solved = PhraseBuilder::comparePhrases($expected, $try);
244250
$this->logger->warning('', [
@@ -255,7 +261,7 @@ public function checkCaptcha(string $expected, string $try, string $ip)
255261
*
256262
* @return void If the connection was successful or not
257263
*
258-
* @throws BouncerException if the connection was not successful
264+
* @throws BouncerException|\Psr\Cache\InvalidArgumentException if the connection was not successful
259265
* */
260266
public function testConnection()
261267
{

src/Configuration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function getConfigTreeBuilder(): TreeBuilder
3333
->scalarNode('api_user_agent')->defaultValue(Constants::BASE_USER_AGENT)->end()
3434
->integerNode('api_timeout')->defaultValue(Constants::API_TIMEOUT)->end()
3535
->booleanNode('live_mode')->defaultValue(true)->end()
36+
->scalarNode('forced_test_ip')->end()
3637
->enumNode('max_remediation_level')
3738
->values(Constants::ORDERED_REMEDIATIONS)
3839
->defaultValue(Constants::REMEDIATION_BAN)
@@ -50,7 +51,6 @@ public function getConfigTreeBuilder(): TreeBuilder
5051
->arrayNode('geolocation')
5152
->addDefaultsIfNotSet()
5253
->children()
53-
->scalarNode('test_public_ip')->end()
5454
->booleanNode('save_in_session')
5555
->defaultTrue()
5656
->end()

src/Geolocation.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,11 @@ public function getCountryResult(array $geolocConfig, string $ip): array
8585

8686
return $result;
8787
}
88-
$currentIp = empty($geolocConfig['test_public_ip']) ? $ip : $geolocConfig['test_public_ip'];
8988
switch ($geolocConfig['type']) {
9089
case Constants::GEOLOCATION_TYPE_MAXMIND:
9190
$configPath = $geolocConfig[Constants::GEOLOCATION_TYPE_MAXMIND];
9291
$result =
93-
$this->getMaxMindCountry($currentIp, $configPath['database_type'], $configPath['database_path']);
92+
$this->getMaxMindCountry($ip, $configPath['database_type'], $configPath['database_path']);
9493
break;
9594
default:
9695
throw new Exception('Unknown Geolocation type:'.$geolocConfig['type']);

src/StandAloneBounce.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public function getBouncerInstance(): Bouncer
9999
$fallbackRemediation = $this->getStringSettings('fallback_remediation');
100100
$bouncingLevel = $this->getStringSettings('bouncing_level');
101101
$geolocation = $this->getArraySettings('geolocation');
102+
$forcedTestIp = $this->getStringSettings('forced_test_ip');
102103

103104
// Init Bouncer instance
104105

@@ -136,6 +137,7 @@ public function getBouncerInstance(): Bouncer
136137
'cache_expiration_for_clean_ip' => $cleanIpCacheDuration,
137138
'cache_expiration_for_bad_ip' => $badIpCacheDuration,
138139
'geolocation' => $geolocation,
140+
'forced_test_ip' => $forcedTestIp
139141
]);
140142

141143
return $this->bouncer;

0 commit comments

Comments
 (0)