Changeset View
Changeset View
Standalone View
Standalone View
support/startup/PhabricatorStartup.php
- This file was moved from support/PhabricatorStartup.php.
Show All 40 Lines | final class PhabricatorStartup { | ||||
private static $startTime; | private static $startTime; | ||||
private static $debugTimeLimit; | private static $debugTimeLimit; | ||||
private static $accessLog; | private static $accessLog; | ||||
private static $capturingOutput; | private static $capturingOutput; | ||||
private static $rawInput; | private static $rawInput; | ||||
private static $oldMemoryLimit; | private static $oldMemoryLimit; | ||||
private static $phases; | private static $phases; | ||||
// TODO: For now, disable rate limiting entirely by default. We need to | private static $limits = array(); | ||||
// 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 )-------------------------------------- */ | /* -( Accessing Request Information )-------------------------------------- */ | ||||
/** | /** | ||||
* @task info | * @task info | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 71 Lines • ▼ Show 20 Lines | public static function didStartup($start_time) { | ||||
self::setupPHP(); | self::setupPHP(); | ||||
self::verifyPHP(); | self::verifyPHP(); | ||||
// If we've made it this far, the environment isn't completely broken so | // If we've made it this far, the environment isn't completely broken so | ||||
// we can switch over to relying on our own exception recovery mechanisms. | // we can switch over to relying on our own exception recovery mechanisms. | ||||
ini_set('display_errors', 0); | ini_set('display_errors', 0); | ||||
$rate_token = self::getRateLimitToken(); | self::connectRateLimits(); | ||||
if ($rate_token !== null) { | |||||
self::rateLimitRequest($rate_token); | |||||
} | |||||
self::normalizeInput(); | self::normalizeInput(); | ||||
self::verifyRewriteRules(); | self::verifyRewriteRules(); | ||||
self::detectPostMaxSizeTriggered(); | self::detectPostMaxSizeTriggered(); | ||||
self::beginOutputCapture(); | self::beginOutputCapture(); | ||||
Show All 35 Lines | public static function didShutdown() { | ||||
$msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf". | $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf". | ||||
"\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20". | "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20". | ||||
"\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb"; | "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb"; | ||||
self::didFatal($msg); | self::didFatal($msg); | ||||
} | } | ||||
public static function loadCoreLibraries() { | public static function loadCoreLibraries() { | ||||
$phabricator_root = dirname(dirname(__FILE__)); | $phabricator_root = dirname(dirname(dirname(__FILE__))); | ||||
$libraries_root = dirname($phabricator_root); | $libraries_root = dirname($phabricator_root); | ||||
$root = null; | $root = null; | ||||
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { | if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { | ||||
$root = $_SERVER['PHUTIL_LIBRARY_ROOT']; | $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; | ||||
} | } | ||||
ini_set( | ini_set( | ||||
▲ Show 20 Lines • Show All 473 Lines • ▼ Show 20 Lines | self::didFatal( | ||||
"'post_max_size' is set to '{$config}'."); | "'post_max_size' is set to '{$config}'."); | ||||
} | } | ||||
/* -( Rate Limiting )------------------------------------------------------ */ | /* -( Rate Limiting )------------------------------------------------------ */ | ||||
/** | /** | ||||
* Adjust the permissible rate limit score. | * Add a new client limits. | ||||
* | |||||
* By default, the limit is `1000`. You can use this method to set it to | |||||
* a larger or smaller value. If you set it to `2000`, users may make twice | |||||
* as many requests before rate limiting. | |||||
* | |||||
* @param int Maximum score before rate limiting. | |||||
* @return void | |||||
* @task ratelimit | |||||
*/ | |||||
public static function setMaximumRate($rate) { | |||||
self::$maximumRate = $rate; | |||||
} | |||||
/** | |||||
* 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. | |||||
* | * | ||||
* The key just needs to identify the user. Phabricator uses both user PHIDs | * @param PhabricatorClientLimit New limit. | ||||
* and user IPs as keys, tracking logged-in and logged-out users separately | * @return PhabricatorClientLimit The limit. | ||||
* and enforcing different limits. | |||||
* | |||||
* @param string Some key which identifies the user making the request. | |||||
* @return void If the user has exceeded the rate limit, this method | |||||
* does not return. | |||||
* @task ratelimit | |||||
*/ | */ | ||||
public static function rateLimitRequest($user_identity) { | public static function addRateLimit(PhabricatorClientLimit $limit) { | ||||
if (!self::canRateLimit()) { | self::$limits[] = $limit; | ||||
return; | return $limit; | ||||
} | |||||
$score = self::getRateLimitScore($user_identity); | |||||
$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. | |||||
$penalty = 50; | |||||
self::addRateLimitScore($user_identity, $penalty); | |||||
$score += $penalty; | |||||
self::didRateLimit($user_identity, $score, $limit); | |||||
} | |||||
} | } | ||||
/** | /** | ||||
* Add points to the rate limit score for some user. | * Apply configured rate limits. | ||||
* | * | ||||
* If users have earned more than 1000 points per minute across all the | * If any limit is exceeded, this method terminates the request. | ||||
* buckets they'll be locked out of the application, so awarding 1 point per | |||||
* request roughly corresponds to allowing 1000 requests per second, while | |||||
* awarding 50 points roughly corresponds to allowing 20 requests per second. | |||||
* | * | ||||
* @param string Some key which identifies the user making the request. | |||||
* @param float The cost for this request; more points pushes them toward | |||||
* the limit faster. | |||||
* @return void | * @return void | ||||
* @task ratelimit | * @task ratelimit | ||||
*/ | */ | ||||
public static function addRateLimitScore($user_identity, $score) { | private static function connectRateLimits() { | ||||
if (!self::canRateLimit()) { | $limits = self::$limits; | ||||
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 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(); | |||||
} | |||||
if (empty($bucket[$user_identity])) { | |||||
$bucket[$user_identity] = 0; | |||||
} | |||||
$bucket[$user_identity] += $score; | |||||
if ($is_apcu) { | |||||
apcu_store($bucket_key, $bucket); | |||||
} else { | |||||
apc_store($bucket_key, $bucket); | |||||
} | |||||
} | |||||
$reason = null; | |||||
/** | $connected = array(); | ||||
* Determine if rate limiting is available. | foreach ($limits as $limit) { | ||||
* | $reason = $limit->didConnect(); | ||||
* Rate limiting depends on APC, and isn't available unless the APC user | $connected[] = $limit; | ||||
* cache is available. | if ($reason !== null) { | ||||
* | break; | ||||
* @return bool True if rate limiting is available. | |||||
* @task ratelimit | |||||
*/ | |||||
private static function canRateLimit() { | |||||
if (!self::$maximumRate) { | |||||
return false; | |||||
} | } | ||||
if (!function_exists('apc_fetch') && !function_exists('apcu_fetch')) { | |||||
return false; | |||||
} | } | ||||
return true; | // If we're killing the request here, disconnect any limits that we | ||||
// connected to try to keep the accounting straight. | |||||
if ($reason !== null) { | |||||
foreach ($connected as $limit) { | |||||
$limit->didDisconnect(array()); | |||||
} | } | ||||
self::didRateLimit($reason); | |||||
/** | |||||
* Get the current bucket for storing rate limit scores. | |||||
* | |||||
* @return int The current bucket. | |||||
* @task ratelimit | |||||
*/ | |||||
private static function getRateLimitBucket() { | |||||
return (int)(time() / 60); | |||||
} | } | ||||
/** | |||||
* Get the total number of rate limit buckets to retain. | |||||
* | |||||
* @return int Total number of rate limit buckets to retain. | |||||
* @task ratelimit | |||||
*/ | |||||
private static function getRateLimitBucketCount() { | |||||
return 5; | |||||
} | } | ||||
/** | /** | ||||
* Get the APC key for a given bucket. | * Tear down rate limiting and allow limits to score the request. | ||||
* | * | ||||
* @param int Bucket to get the key for. | * @param map<string, wild> Additional, freeform request state. | ||||
* @return string APC key for the bucket. | * @return void | ||||
* @task ratelimit | |||||
*/ | |||||
private static function getRateLimitBucketKey($bucket) { | |||||
return 'rate:bucket:'.$bucket; | |||||
} | |||||
/** | |||||
* Get the APC key for the smallest stored bucket. | |||||
* | |||||
* @return string APC key for the smallest stored bucket. | |||||
* @task ratelimit | |||||
*/ | |||||
private static function getRateLimitMinKey() { | |||||
return 'rate:min'; | |||||
} | |||||
/** | |||||
* Get the current rate limit score for a given user. | |||||
* | |||||
* @param string Unique key identifying the user. | |||||
* @return float The user's current score. | |||||
* @task ratelimit | * @task ratelimit | ||||
*/ | */ | ||||
private static function getRateLimitScore($user_identity) { | public static function disconnectRateLimits(array $request_state) { | ||||
$is_apcu = (bool)function_exists('apcu_fetch'); | $limits = self::$limits; | ||||
$min_key = self::getRateLimitMinKey(); | |||||
// Identify the oldest bucket stored in APC. | foreach ($limits as $limit) { | ||||
$cur = self::getRateLimitBucket(); | $limit->didDisconnect($request_state); | ||||
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; | |||||
} | } | ||||
// Destroy any buckets that are older than the minimum bucket we're keeping | |||||
// track of. Under load this normally shouldn't do anything, but will clean | |||||
// up an old bucket once per minute. | |||||
$count = self::getRateLimitBucketCount(); | |||||
for ($cursor = $min; $cursor < ($cur - $count); $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_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]; | |||||
} | |||||
} | |||||
return $score; | |||||
} | |||||
/** | /** | ||||
* Emit an HTTP 429 "Too Many Requests" response (indicating that the user | * Emit an HTTP 429 "Too Many Requests" response (indicating that the user | ||||
* has exceeded application rate limits) and exit. | * has exceeded application rate limits) and exit. | ||||
* | * | ||||
* @return exit This method **does not return**. | * @return exit This method **does not return**. | ||||
* @task ratelimit | * @task ratelimit | ||||
*/ | */ | ||||
private static function didRateLimit($user_identity, $score, $limit) { | private static function didRateLimit($reason) { | ||||
$message = | |||||
"TOO MANY REQUESTS\n". | |||||
"You (\"{$user_identity}\") are issuing too many requests ". | |||||
"too quickly.\n"; | |||||
header( | header( | ||||
'Content-Type: text/plain; charset=utf-8', | 'Content-Type: text/plain; charset=utf-8', | ||||
$replace = true, | $replace = true, | ||||
$http_error = 429); | $http_error = 429); | ||||
echo $message; | echo $reason; | ||||
exit(1); | exit(1); | ||||
} | } | ||||
/* -( Startup Timers )----------------------------------------------------- */ | /* -( Startup Timers )----------------------------------------------------- */ | ||||
▲ Show 20 Lines • Show All 51 Lines • Show Last 20 Lines |