Skip to content

Commit c820d37

Browse files
committed
use an ipv6 compatible lib
1 parent e8e1461 commit c820d37

File tree

5 files changed

+153
-23
lines changed

5 files changed

+153
-23
lines changed

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@
3333
"symfony/config": "^5.2",
3434
"symfony/cache": "^5.2",
3535
"monolog/monolog": "^2.1",
36-
"gregwar/captcha": "^1.1"
36+
"gregwar/captcha": "^1.1",
37+
"mlocati/ip-lib": "^1.14"
3738
},
3839
"require-dev": {
3940
"bramus/monolog-colored-line-formatter": "^3.0",
4041
"symfony/var-dumper": "^5.2",
4142
"phpunit/phpunit": "8.5.13",
4243
"clean/phpdoc-md": "^0.19.1"
4344
}
44-
}
45+
}

composer.lock

Lines changed: 73 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ApiCache.php

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace CrowdSecBouncer;
66

77
use DateTime;
8+
use IPLib\Factory;
9+
use IPLib\Range\Subnet;
810
use Psr\Log\LoggerInterface;
911
use Symfony\Component\Cache\Adapter\AbstractAdapter;
1012
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
@@ -248,12 +250,29 @@ private function defferUpdateCacheConfig(array $config): void
248250
private function saveRemediations(array $decisions): bool
249251
{
250252
foreach ($decisions as $decision) {
251-
if (\is_int($decision['start_ip']) && \is_int($decision['end_ip'])) {
252-
$ipRange = array_map('long2ip', range($decision['start_ip'], $decision['end_ip']));
253-
$remediation = $this->formatRemediationFromDecision($decision);
254-
foreach ($ipRange as $ip) {
255-
$this->addRemediationToCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
256-
}
253+
$remediation = $this->formatRemediationFromDecision($decision);
254+
$type = $remediation[0];
255+
$exp = $remediation[1];
256+
$id = $remediation[2];
257+
258+
if ('Ip' === $decision['scope']) {
259+
$address = Factory::addressFromString($decision['value']);
260+
$this->addRemediationToCacheItem($address->toString(), $type, $exp, $id);
261+
} elseif ('Range' === $decision['scope']) {
262+
$range = Subnet::fromString($decision['value']);
263+
264+
$comparableEndAddress = $range->getEndAddress()->getComparableString();
265+
$address = $range->getStartAddress();
266+
$this->addRemediationToCacheItem($address->toString(), $type, $exp, $id);
267+
$ipCount = 1;
268+
do {
269+
$address = $address->getNextAddress();
270+
$this->addRemediationToCacheItem($address->toString(), $type, $exp, $id);
271+
++$ipCount;
272+
if ($ipCount >= Constants::MAX_ALLOWED_IP_RANGE_WIDTH) {
273+
throw new BouncerException("Unable to store the decision ${$decision['id']}, the IP range: ${$decision['value']} is too large and can cause storage problem. Decision ignored.");
274+
}
275+
} while (0 !== strcmp($address->getComparableString(), $comparableEndAddress));
257276
}
258277
}
259278

@@ -264,18 +283,44 @@ private function removeRemediations(array $decisions): int
264283
{
265284
$count = 0;
266285
foreach ($decisions as $decision) {
267-
if (\is_int($decision['start_ip']) && \is_int($decision['end_ip'])) {
268-
$ipRange = array_map('long2ip', range($decision['start_ip'], $decision['end_ip']));
269-
$this->logger->debug('', [
270-
'type' => 'DECISION_REMOVED', 'decision' => $decision['id'], 'ips' => $ipRange,
271-
]);
286+
if ('Ip' === $decision['scope']) {
287+
$address = Factory::addressFromString($decision['value']);
288+
if (!$this->removeDecisionFromRemediationItem($address->toString(), $decision['id'])) {
289+
$this->logger->debug('', ['type' => 'DECISION_TO_REMOVE_NOT_FOUND_IN_CACHE', 'decision' => $decision['id']]);
290+
} else {
291+
$this->logger->debug('', [
292+
'type' => 'DECISION_REMOVED',
293+
'decision' => $decision['id'],
294+
'value' => $decision['value'],
295+
]);
296+
}
297+
} elseif ('Range' === $decision['scope']) {
298+
$range = Subnet::fromString($decision['value']);
299+
300+
$comparableEndAddress = $range->getEndAddress()->getComparableString();
301+
$address = $range->getStartAddress();
302+
if (!$this->removeDecisionFromRemediationItem($address->toString(), $decision['id'])) {
303+
$this->logger->debug('', ['type' => 'DECISION_TO_REMOVE_NOT_FOUND_IN_CACHE', 'decision' => $decision['id']]);
304+
}
305+
$ipCount = 1;
272306
$success = true;
273-
foreach ($ipRange as $ip) {
274-
if (!$this->removeDecisionFromRemediationItem($ip, $decision['id'])) {
307+
do {
308+
$address = $address->getNextAddress();
309+
if (!$this->removeDecisionFromRemediationItem($address->toString(), $decision['id'])) {
275310
$success = false;
276311
}
277-
}
312+
++$ipCount;
313+
if ($ipCount >= Constants::MAX_ALLOWED_IP_RANGE_WIDTH) {
314+
throw new BouncerException("Unable to store the decision ${$decision['id']}, the IP range: ${$decision['value']} is too large and can cause storage problem. Decision ignored.");
315+
}
316+
} while (0 !== strcmp($address->getComparableString(), $comparableEndAddress));
317+
278318
if ($success) {
319+
$this->logger->debug('', [
320+
'type' => 'DECISION_REMOVED',
321+
'decision' => $decision['id'],
322+
'value' => $decision['value'],
323+
]);
279324
++$count;
280325
} else {
281326
// The API may return stale deletion events due to API design.
@@ -303,11 +348,17 @@ private function saveRemediationsForIp(array $decisions, string $ip): string
303348
$decision['type'] = $this->fallbackRemediation;
304349
}
305350
$remediation = $this->formatRemediationFromDecision($decision);
306-
$remediationResult = $this->addRemediationToCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
351+
$type = $remediation[0];
352+
$exp = $remediation[1];
353+
$id = $remediation[2];
354+
$remediationResult = $this->addRemediationToCacheItem($ip, $type, $exp, $id);
307355
}
308356
} else {
309357
$remediation = $this->formatRemediationFromDecision(null);
310-
$remediationResult = $this->addRemediationToCacheItem($ip, $remediation[0], $remediation[1], $remediation[2]);
358+
$type = $remediation[0];
359+
$exp = $remediation[1];
360+
$id = $remediation[2];
361+
$remediationResult = $this->addRemediationToCacheItem($ip, $type, $exp, $id);
311362
}
312363
$this->commit();
313364

src/Bouncer.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use Gregwar\Captcha\CaptchaBuilder;
99
use Gregwar\Captcha\PhraseBuilder;
10+
use IPLib\Factory;
1011
use Monolog\Handler\NullHandler;
1112
use Monolog\Logger;
1213
use Psr\Log\LoggerInterface;
@@ -99,11 +100,11 @@ private function capRemediationLevel(string $remediation): string
99100
*/
100101
public function getRemediationForIp(string $ip): string
101102
{
102-
$intIp = ip2long($ip);
103-
if (false === $intIp) {
104-
throw new BouncerException("IP $ip should looks like x.x.x.x, with x in 0-255. Ex: 1.2.3.4");
103+
$ip = Factory::addressFromString($ip, false);
104+
if (null === $ip) {
105+
throw new BouncerException("IP $ip format is invalid.");
105106
}
106-
$remediation = $this->apiCache->get(long2ip($intIp));
107+
$remediation = $this->apiCache->get($ip->toString());
107108

108109
return $this->capRemediationLevel($remediation);
109110
}

src/Constants.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace CrowdSecBouncer;
46

57
/**
@@ -43,4 +45,7 @@ class Constants
4345

4446
/** @var array<string> The list of each known remediation, sorted by priority */
4547
public const ORDERED_REMEDIATIONS = [self::REMEDIATION_BAN, self::REMEDIATION_CAPTCHA, self::REMEDIATION_BYPASS];
48+
49+
/** @var int The maximum allowed range width */
50+
public const MAX_ALLOWED_IP_RANGE_WIDTH = 1000000;
4651
}

0 commit comments

Comments
 (0)