1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-18 11:30:55 +01:00

Update rate limiting for APCu and X-Forwarded-For

Summary:
Ref T12612. This updates the rate limiting code to:

  - Support a customizable token, like the client's X-Forwarded-For address, rather than always using `REMOTE_ADDR`.
  - Support APCu.
  - Report a little more rate limiting information.
  - Not reference nonexistent documentation (removed in D16403).

I'm planning to put this into production on `secure` for now and then we can deploy it more broadly if things work well.

Test Plan:
 - Enabled it locally, used `ab -n 100` to hit the limit, saw the limit enforced.
 - Waited a while, was allowed to browse again.

Reviewers: chad, amckinley

Reviewed By: amckinley

Maniphest Tasks: T12612

Differential Revision: https://secure.phabricator.com/D17758
This commit is contained in:
epriestley 2017-04-21 12:41:53 -07:00
parent 03d0e3fdbc
commit 3698e4a14f
3 changed files with 94 additions and 27 deletions

View file

@ -205,9 +205,8 @@ abstract class AphrontApplicationConfiguration extends Phobject {
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
// Add points to the rate limits for this request.
if (isset($_SERVER['REMOTE_ADDR'])) {
$user_ip = $_SERVER['REMOTE_ADDR'];
$rate_token = PhabricatorStartup::getRateLimitToken();
if ($rate_token !== null) {
// The base score for a request allows users to make 30 requests per
// minute.
$score = (1000 / 30);
@ -217,7 +216,7 @@ abstract class AphrontApplicationConfiguration extends Phobject {
$score = $score / 5;
}
PhabricatorStartup::addRateLimitScore($user_ip, $score);
PhabricatorStartup::addRateLimitScore($rate_token, $score);
}
if ($processing_exception) {

View file

@ -1,8 +1,7 @@
@title Configuring a Preamble Script
@group config
Adjust environmental settings (SSL, remote IP, rate limiting) using a preamble
script.
Adjust environmental settings (SSL, remote IPs) using a preamble script.
Overview
========

View file

@ -50,6 +50,7 @@ final class PhabricatorStartup {
// iterate on it a bit for Conduit, some of the specific score levels, and
// to deal with NAT'd offices.
private static $maximumRate = 0;
private static $rateLimitToken;
/* -( Accessing Request Information )-------------------------------------- */
@ -137,8 +138,9 @@ final class PhabricatorStartup {
// we can switch over to relying on our own exception recovery mechanisms.
ini_set('display_errors', 0);
if (isset($_SERVER['REMOTE_ADDR'])) {
self::rateLimitRequest($_SERVER['REMOTE_ADDR']);
$rate_token = self::getRateLimitToken();
if ($rate_token !== null) {
self::rateLimitRequest($rate_token);
}
self::normalizeInput();
@ -680,6 +682,36 @@ final class PhabricatorStartup {
}
/**
* Set a token to identify the client for purposes of rate limiting.
*
* By default, the `REMOTE_ADDR` is used. If your install is behind a load
* balancer, you may want to parse `X-Forwarded-For` and use that address
* instead.
*
* @param string Client identity for rate limiting.
*/
public static function setRateLimitToken($token) {
self::$rateLimitToken = $token;
}
/**
* Get the current client identity for rate limiting.
*/
public static function getRateLimitToken() {
if (self::$rateLimitToken !== null) {
return self::$rateLimitToken;
}
if (isset($_SERVER['REMOTE_ADDR'])) {
return $_SERVER['REMOTE_ADDR'];
}
return null;
}
/**
* Check if the user (identified by `$user_identity`) has issued too many
* requests recently. If they have, end the request with a 429 error code.
@ -699,13 +731,18 @@ final class PhabricatorStartup {
}
$score = self::getRateLimitScore($user_identity);
if ($score > (self::$maximumRate * self::getRateLimitBucketCount())) {
$limit = self::$maximumRate * self::getRateLimitBucketCount();
if ($score > $limit) {
// Give the user some bonus points for getting rate limited. This keeps
// bad actors who keep slamming the 429 page locked out completely,
// instead of letting them get a burst of requests through every minute
// after a bucket expires.
self::addRateLimitScore($user_identity, 50);
self::didRateLimit($user_identity);
$penalty = 50;
self::addRateLimitScore($user_identity, $penalty);
$score += $penalty;
self::didRateLimit($user_identity, $score, $limit);
}
}
@ -729,15 +766,21 @@ final class PhabricatorStartup {
return;
}
$is_apcu = (bool)function_exists('apcu_fetch');
$current = self::getRateLimitBucket();
// There's a bit of a race here, if a second process reads the bucket before
// this one writes it, but it's fine if we occasionally fail to record a
// user's score. If they're making requests fast enough to hit rate
// limiting, we'll get them soon.
// There's a bit of a race here, if a second process reads the bucket
// before this one writes it, but it's fine if we occasionally fail to
// record a user's score. If they're making requests fast enough to hit
// rate limiting, we'll get them soon enough.
$bucket_key = self::getRateLimitBucketKey($current);
if ($is_apcu) {
$bucket = apcu_fetch($bucket_key);
} else {
$bucket = apc_fetch($bucket_key);
}
if (!is_array($bucket)) {
$bucket = array();
}
@ -747,8 +790,13 @@ final class PhabricatorStartup {
}
$bucket[$user_identity] += $score;
if ($is_apcu) {
apcu_store($bucket_key, $bucket);
} else {
apc_store($bucket_key, $bucket);
}
}
/**
@ -761,11 +809,12 @@ final class PhabricatorStartup {
* @task ratelimit
*/
private static function canRateLimit() {
if (!self::$maximumRate) {
return false;
}
if (!function_exists('apc_fetch')) {
if (!function_exists('apc_fetch') && !function_exists('apcu_fetch')) {
return false;
}
@ -826,16 +875,26 @@ final class PhabricatorStartup {
* @task ratelimit
*/
private static function getRateLimitScore($user_identity) {
$is_apcu = (bool)function_exists('apcu_fetch');
$min_key = self::getRateLimitMinKey();
// Identify the oldest bucket stored in APC.
$cur = self::getRateLimitBucket();
if ($is_apcu) {
$min = apcu_fetch($min_key);
} else {
$min = apc_fetch($min_key);
}
// If we don't have any buckets stored yet, store the current bucket as
// the oldest bucket.
if (!$min) {
if ($is_apcu) {
apcu_store($min_key, $cur);
} else {
apc_store($min_key, $cur);
}
$min = $cur;
}
@ -844,14 +903,25 @@ final class PhabricatorStartup {
// up an old bucket once per minute.
$count = self::getRateLimitBucketCount();
for ($cursor = $min; $cursor < ($cur - $count); $cursor++) {
apc_delete(self::getRateLimitBucketKey($cursor));
$bucket_key = self::getRateLimitBucketKey($cursor);
if ($is_apcu) {
apcu_delete($bucket_key);
apcu_store($min_key, $cursor + 1);
} else {
apc_delete($bucket_key);
apc_store($min_key, $cursor + 1);
}
}
// Now, sum up the user's scores in all of the active buckets.
$score = 0;
for (; $cursor <= $cur; $cursor++) {
$bucket = apc_fetch(self::getRateLimitBucketKey($cursor));
$bucket_key = self::getRateLimitBucketKey($cursor);
if ($is_apcu) {
$bucket = apcu_fetch($bucket_key);
} else {
$bucket = apc_fetch($bucket_key);
}
if (isset($bucket[$user_identity])) {
$score += $bucket[$user_identity];
}
@ -868,12 +938,11 @@ final class PhabricatorStartup {
* @return exit This method **does not return**.
* @task ratelimit
*/
private static function didRateLimit() {
private static function didRateLimit($user_identity, $score, $limit) {
$message =
"TOO MANY REQUESTS\n".
"You are issuing too many requests too quickly.\n".
"To adjust limits, see \"Configuring a Preamble Script\" in the ".
"documentation.";
"You (\"{$user_identity}\") are issuing too many requests ".
"too quickly.\n";
header(
'Content-Type: text/plain; charset=utf-8',