@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
4
5 public function getDefaultGroup() {
6 return self::GROUP_OTHER;
7 }
8
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.'));
23 }
24
25 $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
26 if (!$base_uri) {
27 // If `phabricator.base-uri` is not set then we can't really do
28 // anything.
29 return;
30 }
31
32 $expect_user = 'alincoln';
33 $expect_pass = 'hunter2';
34
35 $send_path = '/test-%252A/';
36 $expect_path = '/test-%2A/';
37
38 $expect_key = 'duck-sound';
39 $expect_value = 'quack';
40
41 $base_uri = id(new PhutilURI($base_uri))
42 ->setPath($send_path)
43 ->replaceQueryParam($expect_key, $expect_value);
44
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);
53
54 if (AphrontRequestStream::supportsGzip()) {
55 $gzip_uncompressed = str_repeat('Quack! ', 128);
56 $gzip_compressed = gzencode($gzip_uncompressed);
57
58 $gzip_future = id(new HTTPSFuture($base_uri))
59 ->addHeader('X-Setup-SelfCheck', '1')
60 ->addHeader('Content-Encoding', 'gzip')
61 ->setMethod('POST')
62 ->setTimeout(5)
63 ->setData($gzip_compressed);
64
65 } else {
66 $gzip_future = null;
67 }
68
69 // Make a request to the metadata service available on EC2 instances,
70 // to test if we're running on a T2 instance in AWS so we can warn that
71 // this is a bad idea. Outside of AWS, this request will just fail.
72 $ec2_uri = 'http://169.254.169.254/latest/meta-data/instance-type';
73 $ec2_future = id(new HTTPSFuture($ec2_uri))
74 ->setTimeout(1);
75
76 $futures = array(
77 $self_future,
78 $ec2_future,
79 );
80
81 if ($gzip_future) {
82 $futures[] = $gzip_future;
83 }
84
85 $futures = new FutureIterator($futures);
86 foreach ($futures as $future) {
87 // Just resolve the futures here.
88 }
89
90 try {
91 list($body) = $ec2_future->resolvex();
92 $body = trim($body);
93 if (preg_match('/^t2/', $body)) {
94 $message = pht(
95 'This software appears to be installed on a very small EC2 instance '.
96 '(of class "%s") with burstable CPU. This is strongly discouraged. '.
97 'This software regularly needs CPU, and these instances are often '.
98 'choked to death by CPU throttling. Use an instance with a normal '.
99 'CPU instead.',
100 $body);
101
102 $this->newIssue('ec2.burstable')
103 ->setName(pht('Installed on Burstable CPU Instance'))
104 ->setSummary(
105 pht(
106 'Do not install this software on an instance class with '.
107 'burstable CPU.'))
108 ->setMessage($message);
109 }
110 } catch (Exception $ex) {
111 // If this fails, just continue. We're probably not running in EC2.
112 }
113
114 try {
115 list($body, $headers) = $self_future->resolvex();
116 } catch (Exception $ex) {
117 // If this fails for whatever reason, just ignore it. Hopefully, the
118 // error is obvious and the user can correct it on their own, but we
119 // can't do much to offer diagnostic advice.
120 return;
121 }
122
123 if (BaseHTTPFuture::getHeader($headers, 'Content-Encoding') != 'gzip') {
124 $message = pht(
125 'This software sent itself a request with "Accept-Encoding: gzip", '.
126 'but received an uncompressed response.'.
127 "\n\n".
128 'This may indicate that your webserver is not configured to '.
129 'compress responses. If so, you should enable compression. '.
130 'Compression can dramatically improve performance, especially '.
131 'for clients with less bandwidth.');
132
133 $this->newIssue('webserver.gzip')
134 ->setName(pht('GZip Compression May Not Be Enabled'))
135 ->setSummary(pht('Your webserver may have compression disabled.'))
136 ->setMessage($message);
137 } else {
138 if (function_exists('gzdecode')) {
139 $body = @gzdecode($body);
140 } else {
141 $body = null;
142 }
143 if (!$body) {
144 // For now, just bail if we can't decode the response.
145 // This might need to use the stronger magic in "AphrontRequestStream"
146 // to decode more reliably.
147 return;
148 }
149 }
150
151 $structure = null;
152 $extra_whitespace = ($body !== trim($body));
153
154 try {
155 $structure = phutil_json_decode(trim($body));
156 } catch (Exception $ex) {
157 // Ignore the exception, we only care if the decode worked or not.
158 }
159
160 if (!$structure || $extra_whitespace) {
161 if (!$structure) {
162 $short = id(new PhutilUTF8StringTruncator())
163 ->setMaximumGlyphs(1024)
164 ->truncateString($body);
165
166 $message = pht(
167 'This software sent itself a test request with the '.
168 '"X-Setup-SelfCheck" header and expected to get a valid JSON '.
169 'response back. Instead, the response begins:'.
170 "\n\n".
171 '%s'.
172 "\n\n".
173 'Something is misconfigured or otherwise mangling responses.',
174 phutil_tag('pre', array(), $short));
175 } else {
176 $message = pht(
177 'This software sent itself a test request and expected to get a '.
178 'bare JSON response back. It received a JSON response, but the '.
179 'response had extra whitespace at the beginning or end.'.
180 "\n\n".
181 'This usually means you have edited a file and left whitespace '.
182 'characters before the opening %s tag, or after a closing %s tag. '.
183 'Remove any leading whitespace, and prefer to omit closing tags.',
184 phutil_tag('tt', array(), '<?php'),
185 phutil_tag('tt', array(), '?>'));
186 }
187
188 $this->newIssue('webserver.mangle')
189 ->setName(pht('Mangled Webserver Response'))
190 ->setSummary(pht('Your webserver produced an unexpected response.'))
191 ->setMessage($message);
192
193 // We can't run the other checks if we could not decode the response.
194 if (!$structure) {
195 return;
196 }
197 }
198
199 $actual_user = idx($structure, 'user');
200 $actual_pass = idx($structure, 'pass');
201 if (($expect_user != $actual_user) || ($actual_pass != $expect_pass)) {
202 $message = pht(
203 'This software sent itself a test request with an "Authorization" '.
204 'HTTP header, and expected those credentials to be transmitted. '.
205 'However, they were absent or incorrect when received. This '.
206 'software sent username "%s" with password "%s"; received '.
207 'username "%s" and password "%s".'.
208 "\n\n".
209 'Your webserver may not be configured to forward HTTP basic '.
210 'authentication. If you plan to use basic authentication (for '.
211 'example, to access repositories) you should reconfigure it.',
212 $expect_user,
213 $expect_pass,
214 $actual_user,
215 $actual_pass);
216
217 $this->newIssue('webserver.basic-auth')
218 ->setName(pht('HTTP Basic Auth Not Configured'))
219 ->setSummary(pht('Your webserver is not forwarding credentials.'))
220 ->setMessage($message);
221 }
222
223 $actual_path = idx($structure, 'path');
224 if ($expect_path != $actual_path) {
225 $message = pht(
226 'This software sent itself a test request with an unusual path, to '.
227 'test if your webserver is rewriting paths correctly. The path was '.
228 'not transmitted correctly.'.
229 "\n\n".
230 'This software sent a request to path "%s", and expected the '.
231 'webserver to decode and rewrite that path so that it received a '.
232 'request for "%s". However, it received a request for "%s" instead.'.
233 "\n\n".
234 'Verify that your rewrite rules are configured correctly, following '.
235 'the instructions in the documentation. If path encoding is not '.
236 'working properly you will be unable to access files with unusual '.
237 'names in repositories, among other issues.'.
238 "\n\n".
239 '(This problem can be caused by a missing "B" in your RewriteRule.)',
240 $send_path,
241 $expect_path,
242 $actual_path);
243
244 $this->newIssue('webserver.rewrites')
245 ->setName(pht('HTTP Path Rewriting Incorrect'))
246 ->setSummary(pht('Your webserver is rewriting paths improperly.'))
247 ->setMessage($message);
248 }
249
250 $actual_key = pht('<none>');
251 $actual_value = pht('<none>');
252 foreach (idx($structure, 'params', array()) as $pair) {
253 if (idx($pair, 'name') == $expect_key) {
254 $actual_key = idx($pair, 'name');
255 $actual_value = idx($pair, 'value');
256 break;
257 }
258 }
259
260 if (($expect_key !== $actual_key) || ($expect_value !== $actual_value)) {
261 $message = pht(
262 'This software sent itself a test request with an HTTP GET parameter, '.
263 'but the parameter was not transmitted. Sent "%s" with value "%s", '.
264 'got "%s" with value "%s".'.
265 "\n\n".
266 'Your webserver is configured incorrectly and large parts of '.
267 'this software will not work until this issue is corrected.'.
268 "\n\n".
269 '(This problem can be caused by a missing "QSA" in your RewriteRule.)',
270 $expect_key,
271 $expect_value,
272 $actual_key,
273 $actual_value);
274
275 $this->newIssue('webserver.parameters')
276 ->setName(pht('HTTP Parameters Not Transmitting'))
277 ->setSummary(
278 pht('Your webserver is not handling GET parameters properly.'))
279 ->setMessage($message);
280 }
281
282 if ($gzip_future) {
283 $this->checkGzipResponse(
284 $gzip_future,
285 $gzip_uncompressed,
286 $gzip_compressed);
287 }
288 }
289
290 private function checkGzipResponse(
291 Future $future,
292 $uncompressed,
293 $compressed) {
294
295 try {
296 list($body, $headers) = $future->resolvex();
297 } catch (Exception $ex) {
298 return;
299 }
300
301 try {
302 $structure = phutil_json_decode(trim($body));
303 } catch (Exception $ex) {
304 return;
305 }
306
307 $raw_body = idx($structure, 'raw.base64');
308 $raw_body = @base64_decode($raw_body);
309
310 // The server received the exact compressed bytes we expected it to, so
311 // everything is working great.
312 if ($raw_body === $compressed) {
313 return;
314 }
315
316 // If the server received a prefix of the raw uncompressed string, it
317 // is almost certainly configured to decompress responses inline. Guide
318 // users to this problem narrowly.
319
320 // Otherwise, something is wrong but we don't have much of a clue what.
321
322 $message = array();
323 $message[] = pht(
324 'This software sent itself a test request that was compressed with '.
325 '"Content-Encoding: gzip", but received different bytes than it '.
326 'sent.');
327
328 $prefix_len = min(strlen($raw_body), strlen($uncompressed));
329 if ($prefix_len > 16 && !strncmp($raw_body, $uncompressed, $prefix_len)) {
330 $message[] = pht(
331 'The request body that the server received had already been '.
332 'decompressed. This strongly suggests your webserver is configured '.
333 'to decompress requests inline, before they reach PHP.');
334 $message[] = pht(
335 'If you are using Apache, your server may be configured with '.
336 '"SetInputFilter DEFLATE". This directive destructively mangles '.
337 'requests and emits them with "Content-Length" and '.
338 '"Content-Encoding" headers that no longer match the data in the '.
339 'request body.');
340 } else {
341 $message[] = pht(
342 'This suggests your webserver is configured to decompress or mangle '.
343 'compressed requests.');
344
345 $message[] = pht(
346 'The request body that was sent began:');
347 $message[] = $this->snipBytes($compressed);
348
349 $message[] = pht(
350 'The request body that was received began:');
351 $message[] = $this->snipBytes($raw_body);
352 }
353
354 $message[] = pht(
355 'Identify the component in your webserver configuration which is '.
356 'decompressing or mangling requests and disable it. This software '.
357 'will not work properly until you do.');
358
359 $message = phutil_implode_html("\n\n", $message);
360
361 $this->newIssue('webserver.accept-gzip')
362 ->setName(pht('Compressed Requests Not Received Properly'))
363 ->setSummary(
364 pht(
365 'Your webserver is not handling compressed request bodies '.
366 'properly.'))
367 ->setMessage($message);
368 }
369
370 private function snipBytes($raw) {
371 if (!strlen($raw)) {
372 $display = pht('<empty>');
373 } else {
374 $snip = substr($raw, 0, 24);
375 $display = phutil_loggable_string($snip);
376
377 if (strlen($snip) < strlen($raw)) {
378 $display .= '...';
379 }
380 }
381
382 return phutil_tag('tt', array(), $display);
383 }
384
385}