4 * Handle request startup, before loading the environment or libraries. This
5 * class bootstraps the request state up to the point where we can enter
8 * NOTE: This class MUST NOT have any dependencies. It runs before libraries
14 * Phabricator limits the rate at which clients can request pages, and issues
15 * HTTP 429 "Too Many Requests" responses if clients request too many pages too
16 * quickly. Although this is not a complete defense against high-volume attacks,
17 * it can protect an install against aggressive crawlers, security scanners,
18 * and some types of malicious activity.
20 * To perform rate limiting, each page increments a score counter for the
21 * requesting user's IP. The page can give the IP more points for an expensive
22 * request, or fewer for an authetnicated request.
24 * Score counters are kept in buckets, and writes move to a new bucket every
25 * minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
26 * the oldest bucket is discarded. This provides a simple mechanism for keeping
27 * track of scores without needing to store, access, or read very much data.
29 * Users are allowed to accumulate up to 1000 points per minute, averaged across
30 * all of the tracked buckets.
32 * @task info Accessing Request Information
33 * @task hook Startup Hooks
34 * @task apocalypse In Case Of Apocalypse
35 * @task validation Validation
36 * @task ratelimit Rate Limiting
37 * @task phases Startup Phase Timers
38 * @task request-path Request Path
40 final class PhabricatorStartup
{
42 private static $startTime;
43 private static $debugTimeLimit;
44 private static $accessLog;
45 private static $capturingOutput;
46 private static $rawInput;
47 private static $oldMemoryLimit;
48 private static $phases;
50 private static $limits = array();
51 private static $requestPath;
54 /* -( Accessing Request Information )-------------------------------------- */
60 public static function getStartTime() {
61 return self
::$startTime;
68 public static function getMicrosecondsSinceStart() {
69 // This is the same as "phutil_microseconds_since()", but we may not have
70 // loaded libraries yet.
71 return (int)(1000000 * (microtime(true) - self
::getStartTime()));
78 public static function setAccessLog($access_log) {
79 self
::$accessLog = $access_log;
86 public static function getRawInput() {
87 if (self
::$rawInput === null) {
88 $stream = new AphrontRequestStream();
90 if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
91 $encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
92 $stream->setEncoding($encoding);
97 $bytes = $stream->readData();
98 if ($bytes === null) {
104 self
::$rawInput = $input;
107 return self
::$rawInput;
111 /* -( Startup Hooks )------------------------------------------------------ */
115 * @param float Request start time, from `microtime(true)`.
118 public static function didStartup($start_time) {
119 self
::$startTime = $start_time;
121 self
::$phases = array();
123 self
::$accessLog = null;
124 self
::$requestPath = null;
128 // NOTE: This protects us against multiple calls to didStartup() in the
129 // same request, but also against repeated requests to the same
130 // interpreter state, which we may implement in the future.
131 register_shutdown_function(array(__CLASS__
, 'didShutdown'));
138 // If we've made it this far, the environment isn't completely broken so
139 // we can switch over to relying on our own exception recovery mechanisms.
140 ini_set('display_errors', 0);
142 self
::connectRateLimits();
144 self
::normalizeInput();
146 self
::readRequestPath();
148 self
::beginOutputCapture();
155 public static function didShutdown() {
156 // Disconnect any active rate limits before we shut down. If we don't do
157 // this, requests which exit early will lock a slot in any active
158 // connection limits, and won't count for rate limits.
159 self
::disconnectRateLimits(array());
161 $event = error_get_last();
167 switch ($event['type']) {
170 case E_COMPILE_ERROR
:
176 $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
178 // Even though we should be emitting this as text-plain, escape things
179 // just to be sure since we can't really be sure what the program state
180 // is when we get here.
181 $msg .= htmlspecialchars(
182 $event['message']."\n\n".$event['file'].':'.$event['line'],
189 $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
190 "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
191 "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
193 self
::didFatal($msg);
196 public static function loadCoreLibraries() {
197 $phabricator_root = dirname(dirname(dirname(__FILE__
)));
198 $libraries_root = dirname($phabricator_root);
201 if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
202 $root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
207 $libraries_root.PATH_SEPARATOR
.ini_get('include_path'));
209 $ok = @include_once
$root.'arcanist/src/init/init-library.php';
212 'Unable to load the "Arcanist" library. Put "arcanist/" next to '.
213 '"phabricator/" on disk.');
216 // Load Phabricator itself using the absolute path, so we never end up doing
217 // anything surprising (loading index.php and libraries from different
219 phutil_load_library($phabricator_root.'/src');
222 /* -( Output Capture )----------------------------------------------------- */
225 public static function beginOutputCapture() {
226 if (self
::$capturingOutput) {
227 self
::didFatal('Already capturing output!');
229 self
::$capturingOutput = true;
234 public static function endOutputCapture() {
235 if (!self
::$capturingOutput) {
238 self
::$capturingOutput = false;
239 return ob_get_clean();
243 /* -( Debug Time Limit )--------------------------------------------------- */
247 * Set a time limit (in seconds) for the current script. After time expires,
250 * This works like `max_execution_time`, but prints out a useful stack trace
251 * when the time limit expires. This is primarily intended to make it easier
252 * to debug pages which hang by allowing extraction of a stack trace: set a
253 * short debug limit, then use the trace to figure out what's happening.
255 * The limit is implemented with a tick function, so enabling it implies
256 * some accounting overhead.
258 * @param int Time limit in seconds.
261 public static function setDebugTimeLimit($limit) {
262 self
::$debugTimeLimit = $limit;
267 register_tick_function(array(__CLASS__
, 'onDebugTick'));
273 * Callback tick function used by @{method:setDebugTimeLimit}.
275 * Fatals with a useful stack trace after the time limit expires.
279 public static function onDebugTick() {
280 $limit = self
::$debugTimeLimit;
285 $elapsed = (microtime(true) - self
::getStartTime());
286 if ($elapsed > $limit) {
288 foreach (debug_backtrace() as $frame) {
289 $file = isset($frame['file']) ?
$frame['file'] : '-';
290 $file = basename($file);
292 $line = isset($frame['line']) ?
$frame['line'] : '-';
293 $class = isset($frame['class']) ?
$frame['class'].'->' : null;
294 $func = isset($frame['function']) ?
$frame['function'].'()' : '?';
296 $frames[] = "{$file}:{$line} {$class}{$func}";
300 "Request aborted by debug time limit after {$limit} seconds.\n\n".
302 implode("\n", $frames));
307 /* -( In Case of Apocalypse )---------------------------------------------- */
311 * Fatal the request completely in response to an exception, sending a plain
312 * text message to the client. Calls @{method:didFatal} internally.
314 * @param string Brief description of the exception context, like
315 * `"Rendering Exception"`.
316 * @param Throwable The exception itself.
317 * @param bool True if it's okay to show the exception's stack trace
318 * to the user. The trace will always be logged.
319 * @return exit This method **does not return**.
323 public static function didEncounterFatalException(
328 $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
330 $full_message = $message;
331 $full_message .= "\n\n";
332 $full_message .= $ex->getTraceAsString();
335 $message = $full_message;
338 self
::didFatal($message, $full_message);
343 * Fatal the request completely, sending a plain text message to the client.
345 * @param string Plain text message to send to the client.
346 * @param string Plain text message to send to the error log. If not
347 * provided, the client message is used. You can pass a more
348 * detailed message here (e.g., with stack traces) to avoid
349 * showing it to users.
350 * @return exit This method **does not return**.
354 public static function didFatal($message, $log_message = null) {
355 if ($log_message === null) {
356 $log_message = $message;
359 self
::endOutputCapture();
360 $access_log = self
::$accessLog;
362 // We may end up here before the access log is initialized, e.g. from
364 $access_log->setData(
368 $access_log->write();
372 'Content-Type: text/plain; charset=utf-8',
376 error_log($log_message);
383 /* -( Validation )--------------------------------------------------------- */
389 private static function setupPHP() {
390 error_reporting(E_ALL | E_STRICT
);
391 self
::$oldMemoryLimit = ini_get('memory_limit');
392 ini_set('memory_limit', -1);
394 // If we have libxml, disable the incredibly dangerous entity loader.
395 // PHP 8 deprecates this function and disables this by default; remove once
396 // PHP 7 is no longer supported or a future version has removed the function
398 if (function_exists('libxml_disable_entity_loader')) {
399 @libxml_disable_entity_loader
(true);
402 // See T13060. If the locale for this process (the parent process) is not
403 // a UTF-8 locale we can encounter problems when launching subprocesses
404 // which receive UTF-8 parameters in their command line argument list.
405 @setlocale
(LC_ALL
, 'en_US.UTF-8');
408 // See PHI1894. Keep "args" in exception backtraces.
409 'zend.exception_ignore_args' => 0,
411 // See T13100. We'd like the regex engine to fail, rather than segfault,
412 // if handed a pathological regular expression.
413 'pcre.backtrack_limit' => 10000,
414 'pcre.recusion_limit' => 10000,
416 // NOTE: Arcanist applies a similar set of startup options for CLI
417 // environments in "init-script.php". Changes here may also be
418 // appropriate to apply there.
421 foreach ($config_map as $config_key => $config_value) {
422 ini_set($config_key, $config_value);
430 public static function getOldMemoryLimit() {
431 return self
::$oldMemoryLimit;
437 private static function normalizeInput() {
438 // Replace superglobals with unfiltered versions, disrespect php.ini (we
439 // filter ourselves).
441 // NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
442 // changes made in "preamble.php".
444 // NOTE: WE don't filter INPUT_POST because we may be constructing it
445 // lazily if "enable_post_data_reading" is disabled.
452 foreach ($filter as $type) {
453 $filtered = filter_input_array($type, FILTER_UNSAFE_RAW
);
454 if (!is_array($filtered)) {
459 $_GET = array_merge($_GET, $filtered);
462 $_COOKIE = array_merge($_COOKIE, $filtered);
465 $env = array_merge($_ENV, $filtered);
466 $_ENV = self
::filterEnvSuperglobal($env);
471 self
::rebuildRequest();
477 public static function rebuildRequest() {
478 // Rebuild $_REQUEST, respecting order declared in ".ini" files.
479 $order = ini_get('request_order');
482 $order = ini_get('variables_order');
486 // $_REQUEST will be empty, so leave it alone.
491 for ($ii = 0; $ii < strlen($order); $ii++
) {
492 switch ($order[$ii]) {
494 $_REQUEST = array_merge($_REQUEST, $_GET);
497 $_REQUEST = array_merge($_REQUEST, $_POST);
500 $_REQUEST = array_merge($_REQUEST, $_COOKIE);
503 // $_ENV and $_SERVER never go into $_REQUEST.
511 * Adjust `$_ENV` before execution.
513 * Adjustments here primarily impact the environment as seen by subprocesses.
514 * The environment is forwarded explicitly by @{class:ExecFuture}.
516 * @param map<string, wild> Input `$_ENV`.
517 * @return map<string, string> Suitable `$_ENV`.
520 private static function filterEnvSuperglobal(array $env) {
522 // In some configurations, we may get "argc" and "argv" set in $_ENV.
523 // These are not real environmental variables, and "argv" may have an array
524 // value which can not be forwarded to subprocesses. Remove these from the
525 // environment if they are present.
536 private static function verifyPHP() {
537 $required_version = '5.2.3';
538 if (version_compare(PHP_VERSION
, $required_version) < 0) {
540 "You are running PHP version '".PHP_VERSION
."', which is older than ".
541 "the minimum version, '{$required_version}'. Update to at least ".
542 "'{$required_version}'.");
545 if (function_exists('get_magic_quotes_gpc')) {
546 if (@get_magic_quotes_gpc
()) {
548 'Your server is configured with the PHP language feature '.
549 '"magic_quotes_gpc" enabled.'.
551 'This feature is "highly discouraged" by PHP\'s developers, and '.
552 'has been removed entirely in PHP8.'.
554 'You must disable "magic_quotes_gpc" to run Phabricator. Consult '.
555 'the PHP manual for instructions.');
559 if (extension_loaded('apc')) {
560 $apc_version = phpversion('apc');
564 '3.1.15-dev' => true,
566 if (isset($known_bad[$apc_version])) {
568 "You have APC {$apc_version} installed. This version of APC is ".
569 "known to be bad, and does not work with Phabricator (it will ".
570 "cause Phabricator to fatal unrecoverably with nonsense errors). ".
571 "Downgrade to version 3.1.13.");
575 if (isset($_SERVER['HTTP_PROXY'])) {
577 'This HTTP request included a "Proxy:" header, poisoning the '.
578 'environment (CVE-2016-5385 / httpoxy). Declining to process this '.
579 'request. For details, see: https://phurl.io/u/httpoxy');
587 private static function readRequestPath() {
589 // See T13575. The request path may be provided in:
591 // - the "$_GET" parameter "__path__" (normal for Apache and nginx); or
592 // - the "$_SERVER" parameter "REQUEST_URI" (normal for the PHP builtin
595 // Locate it wherever it is, and store it for later use. Note that writing
596 // to "$_REQUEST" here won't always work, because later code may rebuild
597 // "$_REQUEST" from other sources.
599 if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
600 self
::setRequestPath($_REQUEST['__path__']);
604 // Compatibility with PHP 5.4+ built-in web server.
605 if (php_sapi_name() == 'cli-server') {
606 $path = parse_url($_SERVER['REQUEST_URI']);
607 self
::setRequestPath($path['path']);
611 if (!isset($_REQUEST['__path__'])) {
613 "Request parameter '__path__' is not set. Your rewrite rules ".
614 "are not configured correctly.");
617 if (!strlen($_REQUEST['__path__'])) {
619 "Request parameter '__path__' is set, but empty. Your rewrite rules ".
620 "are not configured correctly. The '__path__' should always ".
621 "begin with a '/'.");
628 public static function getRequestPath() {
629 $path = self
::$requestPath;
631 if ($path === null) {
633 'Request attempted to access request path, but no request path is '.
634 'available for this request. You may be calling web request code '.
635 'from a non-request context, or your webserver may not be passing '.
636 'a request path to Phabricator in a format that it understands.');
645 public static function setRequestPath($path) {
646 self
::$requestPath = $path;
650 /* -( Rate Limiting )------------------------------------------------------ */
654 * Add a new client limits.
656 * @param PhabricatorClientLimit New limit.
657 * @return PhabricatorClientLimit The limit.
659 public static function addRateLimit(PhabricatorClientLimit
$limit) {
660 self
::$limits[] = $limit;
666 * Apply configured rate limits.
668 * If any limit is exceeded, this method terminates the request.
673 private static function connectRateLimits() {
674 $limits = self
::$limits;
677 $connected = array();
678 foreach ($limits as $limit) {
679 $reason = $limit->didConnect();
680 $connected[] = $limit;
681 if ($reason !== null) {
686 // If we're killing the request here, disconnect any limits that we
687 // connected to try to keep the accounting straight.
688 if ($reason !== null) {
689 foreach ($connected as $limit) {
690 $limit->didDisconnect(array());
693 self
::didRateLimit($reason);
699 * Tear down rate limiting and allow limits to score the request.
701 * @param map<string, wild> Additional, freeform request state.
705 public static function disconnectRateLimits(array $request_state) {
706 $limits = self
::$limits;
708 // Remove all limits before disconnecting them so this works properly if
709 // it runs twice. (We run this automatically as a shutdown handler.)
710 self
::$limits = array();
712 foreach ($limits as $limit) {
713 $limit->didDisconnect($request_state);
720 * Emit an HTTP 429 "Too Many Requests" response (indicating that the user
721 * has exceeded application rate limits) and exit.
723 * @return exit This method **does not return**.
726 private static function didRateLimit($reason) {
728 'Content-Type: text/plain; charset=utf-8',
738 /* -( Startup Timers )----------------------------------------------------- */
742 * Record the beginning of a new startup phase.
744 * For phases which occur before @{class:PhabricatorStartup} loads, save the
745 * time and record it with @{method:recordStartupPhase} after the class is
748 * @param string Phase name.
751 public static function beginStartupPhase($phase) {
752 self
::recordStartupPhase($phase, microtime(true));
757 * Record the start time of a previously executed startup phase.
759 * For startup phases which occur after @{class:PhabricatorStartup} loads,
760 * use @{method:beginStartupPhase} instead. This method can be used to
761 * record a time before the class loads, then hand it over once the class
764 * @param string Phase name.
765 * @param float Phase start time, from `microtime(true)`.
768 public static function recordStartupPhase($phase, $time) {
769 self
::$phases[$phase] = $time;
774 * Get information about startup phase timings.
776 * Sometimes, performance problems can occur before we start the profiler.
777 * Since the profiler can't examine these phases, it isn't useful in
778 * understanding their performance costs.
780 * Instead, the startup process marks when it enters various phases using
781 * @{method:beginStartupPhase}. A later call to this method can retrieve this
782 * information, which can be examined to gain greater insight into where
783 * time was spent. The output is still crude, but better than nothing.
787 public static function getPhases() {
788 return self
::$phases;