diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -771,12 +771,21 @@ ); } + $raw_input = @file_get_contents('php://input'); + if ($raw_input !== false) { + $base64_input = base64_encode($raw_input); + } else { + $base64_input = null; + } + $result = array( 'path' => $path, 'params' => $params, 'user' => idx($_SERVER, 'PHP_AUTH_USER'), 'pass' => idx($_SERVER, 'PHP_AUTH_PW'), + 'raw.base64' => $base64_input, + // This just makes sure that the response compresses well, so reasonable // algorithms should want to gzip or deflate it. 'filler' => str_repeat('Q', 1024 * 16), diff --git a/src/aphront/requeststream/AphrontRequestStream.php b/src/aphront/requeststream/AphrontRequestStream.php --- a/src/aphront/requeststream/AphrontRequestStream.php +++ b/src/aphront/requeststream/AphrontRequestStream.php @@ -89,4 +89,24 @@ return $stream; } + public static function supportsGzip() { + if (!function_exists('gzencode') || !function_exists('gzdecode')) { + return false; + } + + $has_zlib = false; + + // NOTE: At least locally, this returns "zlib.*", which is not terribly + // reassuring. We care about "zlib.inflate". + + $filters = stream_get_filters(); + foreach ($filters as $filter) { + if (preg_match('/^zlib\\./', $filter)) { + $has_zlib = true; + } + } + + return $has_zlib; + } + } diff --git a/src/applications/config/check/PhabricatorWebServerSetupCheck.php b/src/applications/config/check/PhabricatorWebServerSetupCheck.php --- a/src/applications/config/check/PhabricatorWebServerSetupCheck.php +++ b/src/applications/config/check/PhabricatorWebServerSetupCheck.php @@ -50,6 +50,20 @@ new PhutilOpaqueEnvelope($expect_pass)) ->setTimeout(5); + if (AphrontRequestStream::supportsGzip()) { + $gzip_uncompressed = str_repeat('Quack! ', 128); + $gzip_compressed = gzencode($gzip_uncompressed); + + $gzip_future = id(new HTTPSFuture($base_uri)) + ->addHeader('X-Phabricator-SelfCheck', 1) + ->addHeader('Content-Encoding', 'gzip') + ->setTimeout(5) + ->setData($gzip_compressed); + + } else { + $gzip_future = null; + } + // Make a request to the metadata service available on EC2 instances, // to test if we're running on a T2 instance in AWS so we can warn that // this is a bad idea. Outside of AWS, this request will just fail. @@ -61,12 +75,16 @@ $self_future, $ec2_future, ); + + if ($gzip_future) { + $futures[] = $gzip_future; + } + $futures = new FutureIterator($futures); foreach ($futures as $future) { // Just resolve the futures here. } - try { list($body) = $ec2_future->resolvex(); $body = trim($body); @@ -259,6 +277,107 @@ ->setMessage($message); } + if ($gzip_future) { + $this->checkGzipResponse( + $gzip_future, + $gzip_uncompressed, + $gzip_compressed); + } + } + + private function checkGzipResponse( + Future $future, + $uncompressed, + $compressed) { + + try { + list($body, $headers) = $future->resolvex(); + } catch (Exception $ex) { + return; + } + + try { + $structure = phutil_json_decode(trim($body)); + } catch (Exception $ex) { + return; + } + + $raw_body = idx($structure, 'raw.base64'); + $raw_body = base64_decode($raw_body); + + // The server received the exact compressed bytes we expected it to, so + // everything is working great. + if ($raw_body === $compressed) { + return; + } + + // If the server received a prefix of the raw uncompressed string, it + // is almost certainly configured to decompress responses inline. Guide + // users to this problem narrowly. + + // Otherwise, something is wrong but we don't have much of a clue what. + + $message = array(); + $message[] = pht( + 'Phabricator sent itself a test request that was compressed with '. + '"Content-Encoding: gzip", but received different bytes than it '. + 'sent.'); + + $prefix_len = min(strlen($raw_body), strlen($uncompressed)); + if ($prefix_len > 16 && !strncmp($raw_body, $uncompressed, $prefix_len)) { + $message[] = pht( + 'The request body that the server received had already been '. + 'decompressed. This strongly suggests your webserver is configured '. + 'to decompress requests inline, before they reach PHP.'); + $message[] = pht( + 'If you are using Apache, your server may be configured with '. + '"SetInputFilter DEFLATE". This directive destructively mangles '. + 'requests and emits them with "Content-Length" and '. + '"Content-Encoding" headers that no longer match the data in the '. + 'request body.'); + } else { + $message[] = pht( + 'This suggests your webserver is configured to decompress or mangle '. + 'compressed requests.'); + + $message[] = pht( + 'The request body Phabricator sent began:'); + $message[] = $this->snipBytes($compressed); + + $message[] = pht( + 'The request body Phabricator received began:'); + $message[] = $this->snipBytes($raw_body); + } + + $message[] = pht( + 'Identify the component in your webserver configuration which is '. + 'decompressing or mangling requests and disable it. Phabricator '. + 'will not work properly until you do.'); + + $message = phutil_implode_html("\n\n", $message); + + $this->newIssue('webserver.accept-gzip') + ->setName(pht('Compressed Requests Not Received Properly')) + ->setSummary( + pht( + 'Your webserver is not handling compressed request bodies '. + 'properly.')) + ->setMessage($message); + } + + private function snipBytes($raw) { + if (!strlen($raw)) { + $display = pht(''); + } else { + $snip = substr($raw, 0, 24); + $display = phutil_loggable_string($snip); + + if (strlen($snip) < strlen($raw)) { + $display .= '...'; + } + } + + return phutil_tag('tt', array(), $display); } }