Remove product literal strings in "pht()", part 4
[phabricator.git] / src / applications / config / check / PhabricatorWebServerSetupCheck.php
blobcc9660325c310f93af054024ba67e33bb740095d
1 <?php
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.'))
17 ->setMessage(
18 pht(
19 'This server 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 this software. You should disable the module.'));
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
28 // anything.
29 return;
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))
42 ->setPath($send_path)
43 ->replaceQueryParam($expect_key, $expect_value);
45 $self_future = id(new HTTPSFuture($base_uri))
46 ->addHeader('X-Setup-SelfCheck', 1)
47 ->addHeader('Accept-Encoding', 'gzip')
48 ->setDisableContentDecoding(true)
49 ->setHTTPBasicAuthCredentials(
50 $expect_user,
51 new PhutilOpaqueEnvelope($expect_pass))
52 ->setTimeout(5);
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-Setup-SelfCheck', 1)
60 ->addHeader('Content-Encoding', 'gzip')
61 ->setTimeout(5)
62 ->setData($gzip_compressed);
64 } else {
65 $gzip_future = null;
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))
73 ->setTimeout(1);
75 $futures = array(
76 $self_future,
77 $ec2_future,
80 if ($gzip_future) {
81 $futures[] = $gzip_future;
84 $futures = new FutureIterator($futures);
85 foreach ($futures as $future) {
86 // Just resolve the futures here.
89 try {
90 list($body) = $ec2_future->resolvex();
91 $body = trim($body);
92 if (preg_match('/^t2/', $body)) {
93 $message = pht(
94 'This software appears to be installed on a very small EC2 instance '.
95 '(of class "%s") with burstable CPU. This is strongly discouraged. '.
96 'This software regularly needs CPU, and these instances are often '.
97 'choked to death by CPU throttling. Use an instance with a normal '.
98 'CPU instead.',
99 $body);
101 $this->newIssue('ec2.burstable')
102 ->setName(pht('Installed on Burstable CPU Instance'))
103 ->setSummary(
104 pht(
105 'Do not install this software on an instance class with '.
106 'burstable CPU.'))
107 ->setMessage($message);
109 } catch (Exception $ex) {
110 // If this fails, just continue. We're probably not running in EC2.
113 try {
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.
119 return;
122 if (BaseHTTPFuture::getHeader($headers, 'Content-Encoding') != 'gzip') {
123 $message = pht(
124 'This software sent itself a request with "Accept-Encoding: gzip", '.
125 'but received an uncompressed response.'.
126 "\n\n".
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);
136 } else {
137 if (function_exists('gzdecode')) {
138 $body = @gzdecode($body);
139 } else {
140 $body = null;
142 if (!$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.
146 return;
150 $structure = null;
151 $extra_whitespace = ($body !== trim($body));
153 try {
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) {
160 if (!$structure) {
161 $short = id(new PhutilUTF8StringTruncator())
162 ->setMaximumGlyphs(1024)
163 ->truncateString($body);
165 $message = pht(
166 'This software sent itself a test request with the '.
167 '"X-Setup-SelfCheck" header and expected to get a valid JSON '.
168 'response back. Instead, the response begins:'.
169 "\n\n".
170 '%s'.
171 "\n\n".
172 'Something is misconfigured or otherwise mangling responses.',
173 phutil_tag('pre', array(), $short));
174 } else {
175 $message = pht(
176 'This software sent itself a test request and expected to get a '.
177 'bare JSON response back. It received a JSON response, but the '.
178 'response had extra whitespace at the beginning or end.'.
179 "\n\n".
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.
193 if (!$structure) {
194 return;
198 $actual_user = idx($structure, 'user');
199 $actual_pass = idx($structure, 'pass');
200 if (($expect_user != $actual_user) || ($actual_pass != $expect_pass)) {
201 $message = pht(
202 'This software sent itself a test request with an "Authorization" '.
203 'HTTP header, and expected those credentials to be transmitted. '.
204 'However, they were absent or incorrect when received. This '.
205 'software sent username "%s" with password "%s"; received '.
206 'username "%s" and password "%s".'.
207 "\n\n".
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.',
211 $expect_user,
212 $expect_pass,
213 $actual_user,
214 $actual_pass);
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) {
224 $message = pht(
225 'This software 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.'.
228 "\n\n".
229 'This software sent a request to path "%s", and expected the '.
230 'webserver to decode and rewrite that path so that it received a '.
231 'request for "%s". However, it received a request for "%s" instead.'.
232 "\n\n".
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.'.
237 "\n\n".
238 '(This problem can be caused by a missing "B" in your RewriteRule.)',
239 $send_path,
240 $expect_path,
241 $actual_path);
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');
255 break;
259 if (($expect_key !== $actual_key) || ($expect_value !== $actual_value)) {
260 $message = pht(
261 'This software 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".'.
264 "\n\n".
265 'Your webserver is configured incorrectly and large parts of '.
266 'this software will not work until this issue is corrected.'.
267 "\n\n".
268 '(This problem can be caused by a missing "QSA" in your RewriteRule.)',
269 $expect_key,
270 $expect_value,
271 $actual_key,
272 $actual_value);
274 $this->newIssue('webserver.parameters')
275 ->setName(pht('HTTP Parameters Not Transmitting'))
276 ->setSummary(
277 pht('Your webserver is not handling GET parameters properly.'))
278 ->setMessage($message);
281 if ($gzip_future) {
282 $this->checkGzipResponse(
283 $gzip_future,
284 $gzip_uncompressed,
285 $gzip_compressed);
289 private function checkGzipResponse(
290 Future $future,
291 $uncompressed,
292 $compressed) {
294 try {
295 list($body, $headers) = $future->resolvex();
296 } catch (Exception $ex) {
297 return;
300 try {
301 $structure = phutil_json_decode(trim($body));
302 } catch (Exception $ex) {
303 return;
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) {
312 return;
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.
321 $message = array();
322 $message[] = pht(
323 'This software sent itself a test request that was compressed with '.
324 '"Content-Encoding: gzip", but received different bytes than it '.
325 'sent.');
327 $prefix_len = min(strlen($raw_body), strlen($uncompressed));
328 if ($prefix_len > 16 && !strncmp($raw_body, $uncompressed, $prefix_len)) {
329 $message[] = pht(
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.');
333 $message[] = pht(
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 '.
338 'request body.');
339 } else {
340 $message[] = pht(
341 'This suggests your webserver is configured to decompress or mangle '.
342 'compressed requests.');
344 $message[] = pht(
345 'The request body that was sent began:');
346 $message[] = $this->snipBytes($compressed);
348 $message[] = pht(
349 'The request body that was received began:');
350 $message[] = $this->snipBytes($raw_body);
353 $message[] = pht(
354 'Identify the component in your webserver configuration which is '.
355 'decompressing or mangling requests and disable it. This software '.
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'))
362 ->setSummary(
363 pht(
364 'Your webserver is not handling compressed request bodies '.
365 'properly.'))
366 ->setMessage($message);
369 private function snipBytes($raw) {
370 if (!strlen($raw)) {
371 $display = pht('<empty>');
372 } else {
373 $snip = substr($raw, 0, 24);
374 $display = phutil_loggable_string($snip);
376 if (strlen($snip) < strlen($raw)) {
377 $display .= '...';
381 return phutil_tag('tt', array(), $display);