Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/support/startup/PhabricatorStartup.php
12240 views
1
<?php
2
3
/**
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
6
* Phabricator code.
7
*
8
* NOTE: This class MUST NOT have any dependencies. It runs before libraries
9
* load.
10
*
11
* Rate Limiting
12
* =============
13
*
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.
19
*
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.
23
*
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.
28
*
29
* Users are allowed to accumulate up to 1000 points per minute, averaged across
30
* all of the tracked buckets.
31
*
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
39
*/
40
final class PhabricatorStartup {
41
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;
49
50
private static $limits = array();
51
private static $requestPath;
52
53
54
/* -( Accessing Request Information )-------------------------------------- */
55
56
57
/**
58
* @task info
59
*/
60
public static function getStartTime() {
61
return self::$startTime;
62
}
63
64
65
/**
66
* @task info
67
*/
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()));
72
}
73
74
75
/**
76
* @task info
77
*/
78
public static function setAccessLog($access_log) {
79
self::$accessLog = $access_log;
80
}
81
82
83
/**
84
* @task info
85
*/
86
public static function getRawInput() {
87
if (self::$rawInput === null) {
88
$stream = new AphrontRequestStream();
89
90
if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
91
$encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
92
$stream->setEncoding($encoding);
93
}
94
95
$input = '';
96
do {
97
$bytes = $stream->readData();
98
if ($bytes === null) {
99
break;
100
}
101
$input .= $bytes;
102
} while (true);
103
104
self::$rawInput = $input;
105
}
106
107
return self::$rawInput;
108
}
109
110
111
/* -( Startup Hooks )------------------------------------------------------ */
112
113
114
/**
115
* @param float Request start time, from `microtime(true)`.
116
* @task hook
117
*/
118
public static function didStartup($start_time) {
119
self::$startTime = $start_time;
120
121
self::$phases = array();
122
123
self::$accessLog = null;
124
self::$requestPath = null;
125
126
static $registered;
127
if (!$registered) {
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'));
132
$registered = true;
133
}
134
135
self::setupPHP();
136
self::verifyPHP();
137
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);
141
142
self::connectRateLimits();
143
144
self::normalizeInput();
145
146
self::readRequestPath();
147
148
self::beginOutputCapture();
149
}
150
151
152
/**
153
* @task hook
154
*/
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());
160
161
$event = error_get_last();
162
163
if (!$event) {
164
return;
165
}
166
167
switch ($event['type']) {
168
case E_ERROR:
169
case E_PARSE:
170
case E_COMPILE_ERROR:
171
break;
172
default:
173
return;
174
}
175
176
$msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
177
if ($event) {
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'],
183
ENT_QUOTES,
184
'UTF-8');
185
}
186
187
// flip dem tables
188
$msg .= "\n\n\n";
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";
192
193
self::didFatal($msg);
194
}
195
196
public static function loadCoreLibraries() {
197
$phabricator_root = dirname(dirname(dirname(__FILE__)));
198
$libraries_root = dirname($phabricator_root);
199
200
$root = null;
201
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
202
$root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
203
}
204
205
ini_set(
206
'include_path',
207
$libraries_root.PATH_SEPARATOR.ini_get('include_path'));
208
209
$ok = @include_once $root.'arcanist/src/init/init-library.php';
210
if (!$ok) {
211
self::didFatal(
212
'Unable to load the "Arcanist" library. Put "arcanist/" next to '.
213
'"phabricator/" on disk.');
214
}
215
216
// Load Phabricator itself using the absolute path, so we never end up doing
217
// anything surprising (loading index.php and libraries from different
218
// directories).
219
phutil_load_library($phabricator_root.'/src');
220
}
221
222
/* -( Output Capture )----------------------------------------------------- */
223
224
225
public static function beginOutputCapture() {
226
if (self::$capturingOutput) {
227
self::didFatal('Already capturing output!');
228
}
229
self::$capturingOutput = true;
230
ob_start();
231
}
232
233
234
public static function endOutputCapture() {
235
if (!self::$capturingOutput) {
236
return null;
237
}
238
self::$capturingOutput = false;
239
return ob_get_clean();
240
}
241
242
243
/* -( Debug Time Limit )--------------------------------------------------- */
244
245
246
/**
247
* Set a time limit (in seconds) for the current script. After time expires,
248
* the script fatals.
249
*
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.
254
*
255
* The limit is implemented with a tick function, so enabling it implies
256
* some accounting overhead.
257
*
258
* @param int Time limit in seconds.
259
* @return void
260
*/
261
public static function setDebugTimeLimit($limit) {
262
self::$debugTimeLimit = $limit;
263
264
static $initialized;
265
if (!$initialized) {
266
declare(ticks=1);
267
register_tick_function(array(__CLASS__, 'onDebugTick'));
268
}
269
}
270
271
272
/**
273
* Callback tick function used by @{method:setDebugTimeLimit}.
274
*
275
* Fatals with a useful stack trace after the time limit expires.
276
*
277
* @return void
278
*/
279
public static function onDebugTick() {
280
$limit = self::$debugTimeLimit;
281
if (!$limit) {
282
return;
283
}
284
285
$elapsed = (microtime(true) - self::getStartTime());
286
if ($elapsed > $limit) {
287
$frames = array();
288
foreach (debug_backtrace() as $frame) {
289
$file = isset($frame['file']) ? $frame['file'] : '-';
290
$file = basename($file);
291
292
$line = isset($frame['line']) ? $frame['line'] : '-';
293
$class = isset($frame['class']) ? $frame['class'].'->' : null;
294
$func = isset($frame['function']) ? $frame['function'].'()' : '?';
295
296
$frames[] = "{$file}:{$line} {$class}{$func}";
297
}
298
299
self::didFatal(
300
"Request aborted by debug time limit after {$limit} seconds.\n\n".
301
"STACK TRACE\n".
302
implode("\n", $frames));
303
}
304
}
305
306
307
/* -( In Case of Apocalypse )---------------------------------------------- */
308
309
310
/**
311
* Fatal the request completely in response to an exception, sending a plain
312
* text message to the client. Calls @{method:didFatal} internally.
313
*
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**.
320
*
321
* @task apocalypse
322
*/
323
public static function didEncounterFatalException(
324
$note,
325
$ex,
326
$show_trace) {
327
328
$message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
329
330
$full_message = $message;
331
$full_message .= "\n\n";
332
$full_message .= $ex->getTraceAsString();
333
334
if ($show_trace) {
335
$message = $full_message;
336
}
337
338
self::didFatal($message, $full_message);
339
}
340
341
342
/**
343
* Fatal the request completely, sending a plain text message to the client.
344
*
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**.
351
*
352
* @task apocalypse
353
*/
354
public static function didFatal($message, $log_message = null) {
355
if ($log_message === null) {
356
$log_message = $message;
357
}
358
359
self::endOutputCapture();
360
$access_log = self::$accessLog;
361
if ($access_log) {
362
// We may end up here before the access log is initialized, e.g. from
363
// verifyPHP().
364
$access_log->setData(
365
array(
366
'c' => 500,
367
));
368
$access_log->write();
369
}
370
371
header(
372
'Content-Type: text/plain; charset=utf-8',
373
$replace = true,
374
$http_error = 500);
375
376
error_log($log_message);
377
echo $message."\n";
378
379
exit(1);
380
}
381
382
383
/* -( Validation )--------------------------------------------------------- */
384
385
386
/**
387
* @task validation
388
*/
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);
393
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
397
// entirely.
398
if (function_exists('libxml_disable_entity_loader')) {
399
@libxml_disable_entity_loader(true);
400
}
401
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');
406
407
$config_map = array(
408
// See PHI1894. Keep "args" in exception backtraces.
409
'zend.exception_ignore_args' => 0,
410
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,
415
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.
419
);
420
421
foreach ($config_map as $config_key => $config_value) {
422
ini_set($config_key, $config_value);
423
}
424
}
425
426
427
/**
428
* @task validation
429
*/
430
public static function getOldMemoryLimit() {
431
return self::$oldMemoryLimit;
432
}
433
434
/**
435
* @task validation
436
*/
437
private static function normalizeInput() {
438
// Replace superglobals with unfiltered versions, disrespect php.ini (we
439
// filter ourselves).
440
441
// NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
442
// changes made in "preamble.php".
443
444
// NOTE: WE don't filter INPUT_POST because we may be constructing it
445
// lazily if "enable_post_data_reading" is disabled.
446
447
$filter = array(
448
INPUT_GET,
449
INPUT_ENV,
450
INPUT_COOKIE,
451
);
452
foreach ($filter as $type) {
453
$filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
454
if (!is_array($filtered)) {
455
continue;
456
}
457
switch ($type) {
458
case INPUT_GET:
459
$_GET = array_merge($_GET, $filtered);
460
break;
461
case INPUT_COOKIE:
462
$_COOKIE = array_merge($_COOKIE, $filtered);
463
break;
464
case INPUT_ENV;
465
$env = array_merge($_ENV, $filtered);
466
$_ENV = self::filterEnvSuperglobal($env);
467
break;
468
}
469
}
470
471
self::rebuildRequest();
472
}
473
474
/**
475
* @task validation
476
*/
477
public static function rebuildRequest() {
478
// Rebuild $_REQUEST, respecting order declared in ".ini" files.
479
$order = ini_get('request_order');
480
481
if (!$order) {
482
$order = ini_get('variables_order');
483
}
484
485
if (!$order) {
486
// $_REQUEST will be empty, so leave it alone.
487
return;
488
}
489
490
$_REQUEST = array();
491
for ($ii = 0; $ii < strlen($order); $ii++) {
492
switch ($order[$ii]) {
493
case 'G':
494
$_REQUEST = array_merge($_REQUEST, $_GET);
495
break;
496
case 'P':
497
$_REQUEST = array_merge($_REQUEST, $_POST);
498
break;
499
case 'C':
500
$_REQUEST = array_merge($_REQUEST, $_COOKIE);
501
break;
502
default:
503
// $_ENV and $_SERVER never go into $_REQUEST.
504
break;
505
}
506
}
507
}
508
509
510
/**
511
* Adjust `$_ENV` before execution.
512
*
513
* Adjustments here primarily impact the environment as seen by subprocesses.
514
* The environment is forwarded explicitly by @{class:ExecFuture}.
515
*
516
* @param map<string, wild> Input `$_ENV`.
517
* @return map<string, string> Suitable `$_ENV`.
518
* @task validation
519
*/
520
private static function filterEnvSuperglobal(array $env) {
521
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.
526
unset($env['argc']);
527
unset($env['argv']);
528
529
return $env;
530
}
531
532
533
/**
534
* @task validation
535
*/
536
private static function verifyPHP() {
537
$required_version = '5.2.3';
538
if (version_compare(PHP_VERSION, $required_version) < 0) {
539
self::didFatal(
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}'.");
543
}
544
545
if (function_exists('get_magic_quotes_gpc')) {
546
if (@get_magic_quotes_gpc()) {
547
self::didFatal(
548
'Your server is configured with the PHP language feature '.
549
'"magic_quotes_gpc" enabled.'.
550
"\n\n".
551
'This feature is "highly discouraged" by PHP\'s developers, and '.
552
'has been removed entirely in PHP8.'.
553
"\n\n".
554
'You must disable "magic_quotes_gpc" to run Phabricator. Consult '.
555
'the PHP manual for instructions.');
556
}
557
}
558
559
if (extension_loaded('apc')) {
560
$apc_version = phpversion('apc');
561
$known_bad = array(
562
'3.1.14' => true,
563
'3.1.15' => true,
564
'3.1.15-dev' => true,
565
);
566
if (isset($known_bad[$apc_version])) {
567
self::didFatal(
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.");
572
}
573
}
574
575
if (isset($_SERVER['HTTP_PROXY'])) {
576
self::didFatal(
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');
580
}
581
}
582
583
584
/**
585
* @task request-path
586
*/
587
private static function readRequestPath() {
588
589
// See T13575. The request path may be provided in:
590
//
591
// - the "$_GET" parameter "__path__" (normal for Apache and nginx); or
592
// - the "$_SERVER" parameter "REQUEST_URI" (normal for the PHP builtin
593
// webserver).
594
//
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.
598
599
if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
600
self::setRequestPath($_REQUEST['__path__']);
601
return;
602
}
603
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']);
608
return;
609
}
610
611
if (!isset($_REQUEST['__path__'])) {
612
self::didFatal(
613
"Request parameter '__path__' is not set. Your rewrite rules ".
614
"are not configured correctly.");
615
}
616
617
if (!strlen($_REQUEST['__path__'])) {
618
self::didFatal(
619
"Request parameter '__path__' is set, but empty. Your rewrite rules ".
620
"are not configured correctly. The '__path__' should always ".
621
"begin with a '/'.");
622
}
623
}
624
625
/**
626
* @task request-path
627
*/
628
public static function getRequestPath() {
629
$path = self::$requestPath;
630
631
if ($path === null) {
632
self::didFatal(
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.');
637
}
638
639
return $path;
640
}
641
642
/**
643
* @task request-path
644
*/
645
public static function setRequestPath($path) {
646
self::$requestPath = $path;
647
}
648
649
650
/* -( Rate Limiting )------------------------------------------------------ */
651
652
653
/**
654
* Add a new client limits.
655
*
656
* @param PhabricatorClientLimit New limit.
657
* @return PhabricatorClientLimit The limit.
658
*/
659
public static function addRateLimit(PhabricatorClientLimit $limit) {
660
self::$limits[] = $limit;
661
return $limit;
662
}
663
664
665
/**
666
* Apply configured rate limits.
667
*
668
* If any limit is exceeded, this method terminates the request.
669
*
670
* @return void
671
* @task ratelimit
672
*/
673
private static function connectRateLimits() {
674
$limits = self::$limits;
675
676
$reason = null;
677
$connected = array();
678
foreach ($limits as $limit) {
679
$reason = $limit->didConnect();
680
$connected[] = $limit;
681
if ($reason !== null) {
682
break;
683
}
684
}
685
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());
691
}
692
693
self::didRateLimit($reason);
694
}
695
}
696
697
698
/**
699
* Tear down rate limiting and allow limits to score the request.
700
*
701
* @param map<string, wild> Additional, freeform request state.
702
* @return void
703
* @task ratelimit
704
*/
705
public static function disconnectRateLimits(array $request_state) {
706
$limits = self::$limits;
707
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();
711
712
foreach ($limits as $limit) {
713
$limit->didDisconnect($request_state);
714
}
715
}
716
717
718
719
/**
720
* Emit an HTTP 429 "Too Many Requests" response (indicating that the user
721
* has exceeded application rate limits) and exit.
722
*
723
* @return exit This method **does not return**.
724
* @task ratelimit
725
*/
726
private static function didRateLimit($reason) {
727
header(
728
'Content-Type: text/plain; charset=utf-8',
729
$replace = true,
730
$http_error = 429);
731
732
echo $reason;
733
734
exit(1);
735
}
736
737
738
/* -( Startup Timers )----------------------------------------------------- */
739
740
741
/**
742
* Record the beginning of a new startup phase.
743
*
744
* For phases which occur before @{class:PhabricatorStartup} loads, save the
745
* time and record it with @{method:recordStartupPhase} after the class is
746
* available.
747
*
748
* @param string Phase name.
749
* @task phases
750
*/
751
public static function beginStartupPhase($phase) {
752
self::recordStartupPhase($phase, microtime(true));
753
}
754
755
756
/**
757
* Record the start time of a previously executed startup phase.
758
*
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
762
* becomes available.
763
*
764
* @param string Phase name.
765
* @param float Phase start time, from `microtime(true)`.
766
* @task phases
767
*/
768
public static function recordStartupPhase($phase, $time) {
769
self::$phases[$phase] = $time;
770
}
771
772
773
/**
774
* Get information about startup phase timings.
775
*
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.
779
*
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.
784
*
785
* @task phases
786
*/
787
public static function getPhases() {
788
return self::$phases;
789
}
790
791
}
792
793