Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RateLimiter: Allow grouping of IP addresses to share a quota #4064

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions config/vufind/RateLimiter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ Storage:
# query Query string parameters
# post POST request parameters
#
# groupByIpv4Octets Share a single quota of tokens between a group of related IP
# addresses, defined by truncating the client IP. Useful for
# botnets that control a range of IPs. This config defines the
# group by truncating the IP address, if it is IPv4, to the
# given number of octets. Valid values are 1-3. For example,
# setting this to 3 would truncate 1.2.3.4 to 1.2.3 for the
# purpose of grouping.
#
# groupByIpv6Hextets Same as groupByIpv6Hextets, but this truncates IPv6 addresses.
# Valid values are 1-7. For example, setting this to 3 would
# truncate 2001:0db8:0000:0000:0000:ff00:0042:8329 to
# 2001:0db8:0000 for the purpose of grouping.
#
# rateLimiterSettings Rate Limiter settings.
# See https://symfony.com/doc/current/rate_limiter.html#rate-limiting-policies
# for more information and policy settings.
Expand Down
31 changes: 31 additions & 0 deletions module/VuFind/src/VuFind/Net/IpAddressUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

namespace VuFind\Net;

use function array_slice;
use function count;
use function defined;

Expand Down Expand Up @@ -124,4 +125,34 @@ public function isInRange($ip, $ranges)
}
return false;
}

/**
* Truncate an IP address to the given number of IPv4 octets
* or IPv6 hextets, depending what kind of IP address it is.
*
* @param string $ip IP address to truncate
* @param int $ipv4Octets Number of octets to return if it is IPv4
* @param int $ipv6Hextets Number of hextets to return if it is IPv6
*
* @return string The possibly truncated IP address
*/
public function truncate($ip, $ipv4Octets = null, $ipv6Hextets = null)
maccabeelevine marked this conversation as resolved.
Show resolved Hide resolved
{
if (!str_contains($ip, ':') || !defined('AF_INET6')) {
maccabeelevine marked this conversation as resolved.
Show resolved Hide resolved
// IPv4 address
if ($ipv4Octets) {
$ipComponents = explode('.', $ip);
$ipComponents = array_slice($ipComponents, 0, $ipv4Octets);
$ip = implode('.', $ipComponents);
}
} else {
maccabeelevine marked this conversation as resolved.
Show resolved Hide resolved
// IPv6 address
if ($ipv6Hextets) {
$ipComponents = explode(':', $ip);
$ipComponents = array_slice($ipComponents, 0, $ipv6Hextets);
$ip = implode(':', $ipComponents);
}
}
return $ip;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ protected function getRateLimiter(
?string $userId
): LimiterInterface {
$policy = $config['Policies'][$policyId] ?? [];

// Truncate IP if configured, to share a quota among related IPs.
$ipv4Octets = $policy['groupByIpv4Octets'] ?? null;
$ipv6Hextets = $policy['groupByIpv6Hextets'] ?? null;
if ($ipv4Octets || $ipv6Hextets) {
$ipUtils = $this->serviceLocator->get(\VuFind\Net\IpAddressUtils::class);
$clientIp = $ipUtils->truncate($clientIp, $ipv4Octets, $ipv6Hextets);
}

$rateLimiterConfig = $policy['rateLimiterSettings'] ?? [];
$rateLimiterConfig['id'] = $policyId;
if (null !== $userId && !($policy['preferIPAddress'] ?? false)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,44 @@ public function testIsInRange()
)
);
}

/**
* Test truncate()
*
* @return void
*/
public function testTruncate()
{
$utils = new IpAddressUtils();

// IPv4 address
$address = '123.234.432.321';
$this->assertEquals(
'123.234',
$utils->truncate($address, 2, 1)
);
$this->assertEquals(
'123.234.432',
$utils->truncate($address, 3, 1)
);
$this->assertEquals(
'123.234.432.321',
$utils->truncate($address)
);

// IPv6 address
$address = '2001:0db8:0000:0000:0000:ff00:0042:8329';
$this->assertEquals(
'2001:0db8',
$utils->truncate($address, 1, 2)
);
$this->assertEquals(
'2001:0db8:0000',
$utils->truncate($address, 1, 3)
);
$this->assertEquals(
'2001:0db8:0000:0000:0000:ff00:0042:8329',
$utils->truncate($address)
);
}
}
Loading