3 final class PhabricatorWebServerSetupCheck
extends PhabricatorSetupCheck
{
5 public function getDefaultGroup() {
6 return self
::GROUP_OTHER
;
9 protected function executeChecks() {
10 // The documentation says these headers exist, but it's not clear if they
11 // are entirely reliable in practice.
12 if (isset($_SERVER['HTTP_X_MOD_PAGESPEED']) ||
13 isset($_SERVER['HTTP_X_PAGE_SPEED'])) {
14 $this->newIssue('webserver.pagespeed')
15 ->setName(pht('Disable Pagespeed'))
16 ->setSummary(pht('Pagespeed is enabled, but should be disabled.'))
19 'Phabricator received an "X-Mod-Pagespeed" or "X-Page-Speed" '.
20 'HTTP header on this request, which indicates that you have '.
21 'enabled "mod_pagespeed" on this server. This module is not '.
22 'compatible with Phabricator. You should disable it.'));
25 $base_uri = PhabricatorEnv
::getEnvConfig('phabricator.base-uri');
26 if (!strlen($base_uri)) {
27 // If `phabricator.base-uri` is not set then we can't really do
32 $expect_user = 'alincoln';
33 $expect_pass = 'hunter2';
35 $send_path = '/test-%252A/';
36 $expect_path = '/test-%2A/';
38 $expect_key = 'duck-sound';
39 $expect_value = 'quack';
41 $base_uri = id(new PhutilURI($base_uri))
43 ->replaceQueryParam($expect_key, $expect_value);
45 $self_future = id(new HTTPSFuture($base_uri))
46 ->addHeader('X-Phabricator-SelfCheck', 1)
47 ->addHeader('Accept-Encoding', 'gzip')
48 ->setDisableContentDecoding(true)
49 ->setHTTPBasicAuthCredentials(
51 new PhutilOpaqueEnvelope($expect_pass))
54 if (AphrontRequestStream
::supportsGzip()) {
55 $gzip_uncompressed = str_repeat('Quack! ', 128);
56 $gzip_compressed = gzencode($gzip_uncompressed);
58 $gzip_future = id(new HTTPSFuture($base_uri))
59 ->addHeader('X-Phabricator-SelfCheck', 1)
60 ->addHeader('Content-Encoding', 'gzip')
62 ->setData($gzip_compressed);
68 // Make a request to the metadata service available on EC2 instances,
69 // to test if we're running on a T2 instance in AWS so we can warn that
70 // this is a bad idea. Outside of AWS, this request will just fail.
71 $ec2_uri = 'http://169.254.169.254/latest/meta-data/instance-type';
72 $ec2_future = id(new HTTPSFuture($ec2_uri))
81 $futures[] = $gzip_future;
84 $futures = new FutureIterator($futures);
85 foreach ($futures as $future) {
86 // Just resolve the futures here.
90 list($body) = $ec2_future->resolvex();
92 if (preg_match('/^t2/', $body)) {
94 'Phabricator appears to be installed on a very small EC2 instance '.
95 '(of class "%s") with burstable CPU. This is strongly discouraged. '.
96 'Phabricator regularly needs CPU, and these instances are often '.
97 'choked to death by CPU throttling. Use an instance with a normal '.
101 $this->newIssue('ec2.burstable')
102 ->setName(pht('Installed on Burstable CPU Instance'))
105 'Do not install Phabricator on an instance class with '.
107 ->setMessage($message);
109 } catch (Exception
$ex) {
110 // If this fails, just continue. We're probably not running in EC2.
114 list($body, $headers) = $self_future->resolvex();
115 } catch (Exception
$ex) {
116 // If this fails for whatever reason, just ignore it. Hopefully, the
117 // error is obvious and the user can correct it on their own, but we
118 // can't do much to offer diagnostic advice.
122 if (BaseHTTPFuture
::getHeader($headers, 'Content-Encoding') != 'gzip') {
124 'Phabricator sent itself a request with "Accept-Encoding: gzip", '.
125 'but received an uncompressed response.'.
127 'This may indicate that your webserver is not configured to '.
128 'compress responses. If so, you should enable compression. '.
129 'Compression can dramatically improve performance, especially '.
130 'for clients with less bandwidth.');
132 $this->newIssue('webserver.gzip')
133 ->setName(pht('GZip Compression May Not Be Enabled'))
134 ->setSummary(pht('Your webserver may have compression disabled.'))
135 ->setMessage($message);
137 if (function_exists('gzdecode')) {
138 $body = @gzdecode
($body);
143 // For now, just bail if we can't decode the response.
144 // This might need to use the stronger magic in "AphrontRequestStream"
145 // to decode more reliably.
151 $extra_whitespace = ($body !== trim($body));
154 $structure = phutil_json_decode(trim($body));
155 } catch (Exception
$ex) {
156 // Ignore the exception, we only care if the decode worked or not.
159 if (!$structure ||
$extra_whitespace) {
161 $short = id(new PhutilUTF8StringTruncator())
162 ->setMaximumGlyphs(1024)
163 ->truncateString($body);
166 'Phabricator sent itself a test request with the '.
167 '"X-Phabricator-SelfCheck" header and expected to get a valid JSON '.
168 'response back. Instead, the response begins:'.
172 'Something is misconfigured or otherwise mangling responses.',
173 phutil_tag('pre', array(), $short));
176 'Phabricator sent itself a test request and expected to get a bare '.
177 'JSON response back. It received a JSON response, but the response '.
178 'had extra whitespace at the beginning or end.'.
180 'This usually means you have edited a file and left whitespace '.
181 'characters before the opening %s tag, or after a closing %s tag. '.
182 'Remove any leading whitespace, and prefer to omit closing tags.',
183 phutil_tag('tt', array(), '<?php'),
184 phutil_tag('tt', array(), '?>'));
187 $this->newIssue('webserver.mangle')
188 ->setName(pht('Mangled Webserver Response'))
189 ->setSummary(pht('Your webserver produced an unexpected response.'))
190 ->setMessage($message);
192 // We can't run the other checks if we could not decode the response.
198 $actual_user = idx($structure, 'user');
199 $actual_pass = idx($structure, 'pass');
200 if (($expect_user != $actual_user) ||
($actual_pass != $expect_pass)) {
202 'Phabricator sent itself a test request with an "Authorization" HTTP '.
203 'header, and expected those credentials to be transmitted. However, '.
204 'they were absent or incorrect when received. Phabricator sent '.
205 'username "%s" with password "%s"; received username "%s" and '.
208 'Your webserver may not be configured to forward HTTP basic '.
209 'authentication. If you plan to use basic authentication (for '.
210 'example, to access repositories) you should reconfigure it.',
216 $this->newIssue('webserver.basic-auth')
217 ->setName(pht('HTTP Basic Auth Not Configured'))
218 ->setSummary(pht('Your webserver is not forwarding credentials.'))
219 ->setMessage($message);
222 $actual_path = idx($structure, 'path');
223 if ($expect_path != $actual_path) {
225 'Phabricator sent itself a test request with an unusual path, to '.
226 'test if your webserver is rewriting paths correctly. The path was '.
227 'not transmitted correctly.'.
229 'Phabricator sent a request to path "%s", and expected the webserver '.
230 'to decode and rewrite that path so that it received a request for '.
231 '"%s". However, it received a request for "%s" instead.'.
233 'Verify that your rewrite rules are configured correctly, following '.
234 'the instructions in the documentation. If path encoding is not '.
235 'working properly you will be unable to access files with unusual '.
236 'names in repositories, among other issues.'.
238 '(This problem can be caused by a missing "B" in your RewriteRule.)',
243 $this->newIssue('webserver.rewrites')
244 ->setName(pht('HTTP Path Rewriting Incorrect'))
245 ->setSummary(pht('Your webserver is rewriting paths improperly.'))
246 ->setMessage($message);
249 $actual_key = pht('<none>');
250 $actual_value = pht('<none>');
251 foreach (idx($structure, 'params', array()) as $pair) {
252 if (idx($pair, 'name') == $expect_key) {
253 $actual_key = idx($pair, 'name');
254 $actual_value = idx($pair, 'value');
259 if (($expect_key !== $actual_key) ||
($expect_value !== $actual_value)) {
261 'Phabricator sent itself a test request with an HTTP GET parameter, '.
262 'but the parameter was not transmitted. Sent "%s" with value "%s", '.
263 'got "%s" with value "%s".'.
265 'Your webserver is configured incorrectly and large parts of '.
266 'Phabricator will not work until this issue is corrected.'.
268 '(This problem can be caused by a missing "QSA" in your RewriteRule.)',
274 $this->newIssue('webserver.parameters')
275 ->setName(pht('HTTP Parameters Not Transmitting'))
277 pht('Your webserver is not handling GET parameters properly.'))
278 ->setMessage($message);
282 $this->checkGzipResponse(
289 private function checkGzipResponse(
295 list($body, $headers) = $future->resolvex();
296 } catch (Exception
$ex) {
301 $structure = phutil_json_decode(trim($body));
302 } catch (Exception
$ex) {
306 $raw_body = idx($structure, 'raw.base64');
307 $raw_body = @base64_decode
($raw_body);
309 // The server received the exact compressed bytes we expected it to, so
310 // everything is working great.
311 if ($raw_body === $compressed) {
315 // If the server received a prefix of the raw uncompressed string, it
316 // is almost certainly configured to decompress responses inline. Guide
317 // users to this problem narrowly.
319 // Otherwise, something is wrong but we don't have much of a clue what.
323 'Phabricator sent itself a test request that was compressed with '.
324 '"Content-Encoding: gzip", but received different bytes than it '.
327 $prefix_len = min(strlen($raw_body), strlen($uncompressed));
328 if ($prefix_len > 16 && !strncmp($raw_body, $uncompressed, $prefix_len)) {
330 'The request body that the server received had already been '.
331 'decompressed. This strongly suggests your webserver is configured '.
332 'to decompress requests inline, before they reach PHP.');
334 'If you are using Apache, your server may be configured with '.
335 '"SetInputFilter DEFLATE". This directive destructively mangles '.
336 'requests and emits them with "Content-Length" and '.
337 '"Content-Encoding" headers that no longer match the data in the '.
341 'This suggests your webserver is configured to decompress or mangle '.
342 'compressed requests.');
345 'The request body Phabricator sent began:');
346 $message[] = $this->snipBytes($compressed);
349 'The request body Phabricator received began:');
350 $message[] = $this->snipBytes($raw_body);
354 'Identify the component in your webserver configuration which is '.
355 'decompressing or mangling requests and disable it. Phabricator '.
356 'will not work properly until you do.');
358 $message = phutil_implode_html("\n\n", $message);
360 $this->newIssue('webserver.accept-gzip')
361 ->setName(pht('Compressed Requests Not Received Properly'))
364 'Your webserver is not handling compressed request bodies '.
366 ->setMessage($message);
369 private function snipBytes($raw) {
371 $display = pht('<empty>');
373 $snip = substr($raw, 0, 24);
374 $display = phutil_loggable_string($snip);
376 if (strlen($snip) < strlen($raw)) {
381 return phutil_tag('tt', array(), $display);