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

feat: month-based rate limiter #1600

Merged
merged 3 commits into from
Oct 28, 2021
Merged
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
19 changes: 12 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ jobs:
steps:
- uses: actions/checkout@v2

- uses: php-actions/composer@v5

- name: PHPUnit Tests
uses: php-actions/phpunit@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
configuration: tests/phpunit.xml
php_version: 7.4
php_extensions: gd
php-version: 7.4
ini-values: apc.enable_cli=On
tools: pecl, phpunit
extensions: gd, apcu

- name: Install deps
run: composer install

- name: Run tests
run: composer test
49 changes: 49 additions & 0 deletions tests/CheckMonthlyRateLimitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types=1);

require_once __DIR__ . '/..' . '/www/ratelimit/check_monthly_rate_limit.php';

use PHPUnit\Framework\TestCase;

final class CheckMonthlyRateLimitTest extends TestCase {
public function testConstructorSetsDefaultValues() : void {
$ip = '127.0.0.1';
$cmrl = new CheckMonthlyRateLimit($ip);
$this->assertEquals('127.0.0.1', $cmrl->ip);
$this->assertEquals(50, $cmrl->limit);
$this->assertEquals(2678400, $cmrl->day_cycle_ttl);
$this->assertEquals('rladdr_per_month_127.0.0.1', $cmrl->cache_key);
}

public function testConstructorSetsValues() : void {
$ip = '127.0.0.0';
$cmrl = new CheckMonthlyRateLimit($ip, 40, 20);
$this->assertEquals('127.0.0.0', $cmrl->ip);
$this->assertEquals(40, $cmrl->limit);
$this->assertEquals(1728000, $cmrl->day_cycle_ttl);
$this->assertEquals('rladdr_per_month_127.0.0.0', $cmrl->cache_key);
}

/**
*
* @requires extension apcu
*/
public function testCheckFirstTime() : void {
$ip = '127.0.0.0';
$cmrl = new CheckMonthlyRateLimit($ip, 40, 20);
$passes = $cmrl->check();
$this->assertTrue($passes);
}

/**
*
* @requires extension apcu
*/
public function testCheckPastLimit() : void {
$ip = '127.0.0.0';
$cmrl = new CheckMonthlyRateLimit($ip, 2, 20);
$passes = $cmrl->check();
$passes = $cmrl->check();
$passes = $cmrl->check();
$this->assertFalse($passes);
}
}
6 changes: 3 additions & 3 deletions www/common.inc
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// Copyright 2020 Catchpoint Systems Inc.
// Use of this source code is governed by the Polyform Shield 1.0.0 license that can be
// found in the LICENSE.md file.
require_once('./common_lib.inc');
require_once('./plugins.php.inc');
require_once('./util.inc');
require_once(__DIR__ . '/common_lib.inc');
require_once(__DIR__ . '/plugins.php.inc');
require_once(__DIR__ . '/util.inc');

// Disable caching by default
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0", true);
Expand Down
37 changes: 37 additions & 0 deletions www/common/cache_utils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);

class CacheUtils {
public static function cache_store (string $key, array $value, int $ttl=0) : bool {
$key = sha1(__DIR__) . $key;
if (isset($value)) {
if (function_exists('apcu_store')) {
apcu_store($key, $value, $ttl);
return true;
} elseif (function_exists('apc_store')) {
apc_store($key, $value, $ttl);
return true;
} else {
return false;
}
}
}

public static function cache_fetch ($key) : array {
$ret = array();
$success = false;

$key = sha1(__DIR__) . $key;
if (function_exists('apcu_fetch')) {
$ret = apcu_fetch($key, $success);
if (!$success) {
$ret = array();
}
} elseif (function_exists('apc_fetch')) {
$ret = apc_fetch($key, $success);
if (!$success) {
$ret = array();
}
}
return $ret;
}
}
39 changes: 39 additions & 0 deletions www/ratelimit/check_monthly_rate_limit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types=1);

require_once(__DIR__ . '/../common/cache_utils.php');

class CheckMonthlyRateLimit {
function __construct (string $ip, int $limit = 50, int $days = 31) {
$this->ip = $ip;
$this->limit = $limit;
$this->day_cycle_ttl = $days * (24 * 60 * 60);
$this->cache_key = 'rladdr_' . 'per_month_' . $ip;
$this->bucket = array();
}

function check () : bool {
$bucket = $this->fetch_bucket();
if (count($bucket) >= $this->limit) {
return false;
} else {
$bucket[] = time();
$this->store_bucket($bucket);
}
return true;
}

private function fetch_bucket () : array {
$bucket = CacheUtils::cache_fetch($this->cache_key);
return array_filter($bucket, function ($value) {
$time_constraint = time() - $this->day_cycle_ttl;
return $time_constraint < $value;
}, ARRAY_FILTER_USE_BOTH);
}

private function store_bucket (array $bucket) : void {
$success = CacheUtils::cache_store($this->cache_key, $bucket, $this->day_cycle_ttl);
if ($success) {
$this->bucket = $bucket;
}
}
}
9 changes: 9 additions & 0 deletions www/runtest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function DealWithMagicQuotes(&$arr) {
require_once('common.inc');
require_once('./ec2/ec2.inc.php');
require_once(__DIR__ . '/include/CrUX.php');
require_once(__DIR__ . '/ratelimit/check_monthly_rate_limit.php');
set_time_limit(300);

$redirect_cache = array();
Expand Down Expand Up @@ -3086,6 +3087,14 @@ function CheckRateLimit($test, &$error) {
if (isset($USER_EMAIL) && strlen($USER_EMAIL)) {
return true;
}

$cmrl = new CheckMonthlyRateLimit($test['ip']);
$passesMonthly = $cmrl->check();

if(!$passesMonthly) {
$error = "The test has been blocked for exceeding the volume of testing allowed by anonymous users from your IP address.<br>Please log in with a registered account.";
return false;
}

// Enforce per-IP rate limits for testing
$limit = GetSetting('rate_limit_anon', null);
Expand Down