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 | |||||