Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index c24d59ac90..bb4fbefa41 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,874 +1,883 @@
<?php
/**
* @task routing URI Routing
* @task response Response Handling
* @task exception Exception Handling
*/
final class AphrontApplicationConfiguration
extends Phobject {
private $request;
private $host;
private $path;
private $console;
public function buildRequest() {
$parser = new PhutilQueryStringParser();
$data = array();
$data += $_POST;
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($data);
$request->setApplicationConfiguration($this);
$request->setCookiePrefix($cookie_prefix);
return $request;
}
public function build404Controller() {
return array(new Phabricator404Controller(), array());
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function getConsole() {
return $this->console;
}
public function setConsole($console) {
$this->console = $console;
return $this;
}
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function runHTTPRequest(AphrontHTTPSink $sink) {
if (isset($_SERVER['HTTP_X_PHABRICATOR_SELFCHECK'])) {
$response = self::newSelfCheckResponse();
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('multimeter');
$multimeter = MultimeterControl::newInstance();
$multimeter->setEventContext('<http-init>');
$multimeter->setEventViewer('<none>');
// Build a no-op write guard for the setup phase. We'll replace this with a
// real write guard later on, but we need to survive setup and build a
// request object first.
$write_guard = new AphrontWriteGuard('id');
PhabricatorStartup::beginStartupPhase('preflight');
$response = PhabricatorSetupCheck::willPreflightRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('env.init');
self::readHTTPPOSTData();
try {
PhabricatorEnv::initializeWebEnvironment();
$database_exception = null;
} catch (PhabricatorClusterStrandedException $ex) {
$database_exception = $ex;
}
// If we're in developer mode, set a flag so that top-level exception
// handlers can add more information.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$sink->setShowStackTraces(true);
}
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception,
true);
$response = PhabricatorSetupCheck::newIssueResponse($issue);
return self::writeResponse($sink, $response);
}
$multimeter->setSampleRate(
PhabricatorEnv::getEnvConfig('debug.sample-rate'));
$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
if ($debug_time_limit) {
PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
}
// This is the earliest we can get away with this, we need env config first.
PhabricatorStartup::beginStartupPhase('log.access');
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
PhabricatorStartup::setAccessLog($access_log);
$address = PhabricatorEnv::getRemoteAddress();
if ($address) {
$address_string = $address->getAddress();
} else {
$address_string = '-';
}
$access_log->setData(
array(
'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
'r' => $address_string,
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
DarkConsoleXHProfPluginAPI::hookProfiler();
// We just activated the profiler, so we don't need to keep track of
// startup phases anymore: it can take over from here.
PhabricatorStartup::beginStartupPhase('startup.done');
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
$response = PhabricatorSetupCheck::willProcessRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
$host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__'];
$application = new self();
$application->setHost($host);
$application->setPath($path);
$request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which
// actually checks CSRF tokens.
$write_guard->dispose();
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
// Build the server URI implied by the request headers. If an administrator
// has not configured "phabricator.base-uri" yet, we'll use this to generate
// links.
$request_protocol = ($request->isHTTPS() ? 'https' : 'http');
$request_base_uri = "{$request_protocol}://{$host}/";
PhabricatorEnv::setRequestBaseURI($request_base_uri);
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
));
$processing_exception = null;
try {
$response = $application->processRequest(
$request,
$access_log,
$sink,
$multimeter);
$response_code = $response->getHTTPResponseCode();
} catch (Exception $ex) {
$processing_exception = $ex;
$response_code = 500;
}
$write_guard->dispose();
$access_log->setData(
array(
'c' => $response_code,
'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
));
$multimeter->newEvent(
MultimeterEvent::TYPE_REQUEST_TIME,
$multimeter->getEventContext(),
PhabricatorStartup::getMicrosecondsSinceStart());
$access_log->write();
$multimeter->saveEvents();
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
PhabricatorStartup::disconnectRateLimits(
array(
'viewer' => $request->getUser(),
));
if ($processing_exception) {
throw $processing_exception;
}
}
public function processRequest(
AphrontRequest $request,
PhutilDeferredLog $access_log,
AphrontHTTPSink $sink,
MultimeterControl $multimeter) {
$this->setRequest($request);
list($controller, $uri_data) = $this->buildController();
$controller_class = get_class($controller);
$access_log->setData(
array(
'C' => $controller_class,
));
$multimeter->setEventContext('web.'.$controller_class);
$request->setController($controller);
$request->setURIMap($uri_data);
$controller->setRequest($request);
// If execution throws an exception and then trying to render that
// exception throws another exception, we want to show the original
// exception, as it is likely the root cause of the rendering exception.
$original_exception = null;
try {
$response = $controller->willBeginExecution();
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
'P' => $request->getUser()->getPHID(),
));
$multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->handleRequest($request);
$this->validateControllerResponse($controller, $response);
}
} catch (Exception $ex) {
$original_exception = $ex;
} catch (Throwable $ex) {
$original_exception = $ex;
}
$response_exception = null;
try {
if ($original_exception) {
$response = $this->handleThrowable($original_exception);
}
$response = $this->produceResponse($request, $response);
$response = $controller->willSendResponse($response);
$response->setRequest($request);
self::writeResponse($sink, $response);
} catch (Exception $ex) {
$response_exception = $ex;
} catch (Throwable $ex) {
$response_exception = $ex;
}
if ($response_exception) {
// If we encountered an exception while building a normal response, then
// encountered another exception while building a response for the first
// exception, throw an aggregate exception that will be unpacked by the
// higher-level handler. This is above our pay grade.
if ($original_exception) {
throw new PhutilAggregateException(
pht(
'Encountered a processing exception, then another exception when '.
'trying to build a response for the first exception.'),
array(
$response_exception,
$original_exception,
));
}
// If we built a response successfully and then ran into an exception
// trying to render it, try to handle and present that exception to the
// user using the standard handler.
// The problem here might be in rendering (more common) or in the actual
// response mechanism (less common). If it's in rendering, we can likely
// still render a nice exception page: the majority of rendering issues
// are in main page content, not content shared with the exception page.
$handling_exception = null;
try {
$response = $this->handleThrowable($response_exception);
$response = $this->produceResponse($request, $response);
$response = $controller->willSendResponse($response);
$response->setRequest($request);
self::writeResponse($sink, $response);
} catch (Exception $ex) {
$handling_exception = $ex;
} catch (Throwable $ex) {
$handling_exception = $ex;
}
// If we didn't have any luck with that, raise the original response
// exception. As above, this is the root cause exception and more likely
// to be useful. This will go to the fallback error handler at top
// level.
if ($handling_exception) {
throw $response_exception;
}
}
return $response;
}
private static function writeResponse(
AphrontHTTPSink $sink,
AphrontResponse $response) {
$unexpected_output = PhabricatorStartup::endOutputCapture();
if ($unexpected_output) {
$unexpected_output = pht(
"Unexpected output:\n\n%s",
$unexpected_output);
phlog($unexpected_output);
if ($response instanceof AphrontWebpageResponse) {
$response->setUnexpectedOutput($unexpected_output);
}
}
$sink->writeResponse($response);
}
/* -( URI Routing )-------------------------------------------------------- */
/**
* Build a controller to respond to the request.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
private function buildController() {
$request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which
// were not received on a cluster interface.
//
// For example, a host may have an internal address like "170.0.0.1", and
// also have a public address like "51.23.95.16". Assuming the cluster
// is configured on a range like "170.0.0.0/16", we want to reject the
// requests received on the public interface.
//
// Ideally, nodes in a cluster should only be listening on internal
// interfaces, but they may be configured in such a way that they also
// listen on external interfaces, since this is easy to forget about or
// get wrong. As a broad security measure, reject requests received on any
// interfaces which aren't on the whitelist.
$cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
if ($cluster_addresses) {
$server_addr = idx($_SERVER, 'SERVER_ADDR');
if (!$server_addr) {
if (php_sapi_name() == 'cli') {
// This is a command line script (probably something like a unit
// test) so it's fine that we don't have SERVER_ADDR defined.
} else {
throw new AphrontMalformedRequestException(
pht('No %s', 'SERVER_ADDR'),
pht(
'Phabricator is configured to operate in cluster mode, but '.
'%s is not defined in the request context. Your webserver '.
'configuration needs to forward %s to PHP so Phabricator can '.
'reject requests received on external interfaces.',
'SERVER_ADDR',
'SERVER_ADDR'));
}
} else {
if (!PhabricatorEnv::isClusterAddress($server_addr)) {
throw new AphrontMalformedRequestException(
pht('External Interface'),
pht(
'Phabricator is configured in cluster mode and the address '.
'this request was received on ("%s") is not whitelisted as '.
'a cluster address.',
$server_addr));
}
}
}
$site = $this->buildSiteForRequest($request);
if ($site->shouldRequireHTTPS()) {
if (!$request->isHTTPS()) {
// Don't redirect intracluster requests: doing so drops headers and
// parameters, imposes a performance penalty, and indicates a
// misconfiguration.
if ($request->isProxiedClusterRequest()) {
throw new AphrontMalformedRequestException(
pht('HTTPS Required'),
pht(
'This request reached a site which requires HTTPS, but the '.
'request is not marked as HTTPS.'));
}
$https_uri = $request->getRequestURI();
$https_uri->setDomain($request->getHost());
$https_uri->setProtocol('https');
// In this scenario, we'll be redirecting to HTTPS using an absolute
// URI, so we need to permit an external redirect.
return $this->buildRedirectController($https_uri, true);
}
}
$maps = $site->getRoutingMaps();
$path = $request->getPath();
$result = $this->routePath($maps, $path);
if ($result) {
return $result;
}
// If we failed to match anything but don't have a trailing slash, try
// to add a trailing slash and issue a redirect if that resolves.
// NOTE: We only do this for GET, since redirects switch to GET and drop
// data like POST parameters.
if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {
$result = $this->routePath($maps, $path.'/');
if ($result) {
$target_uri = $request->getAbsoluteRequestURI();
// We need to restore URI encoding because the webserver has
// interpreted it. For example, this allows us to redirect a path
// like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
// resolved meaningfully by an application.
$target_path = phutil_escape_uri($path.'/');
$target_uri->setPath($target_path);
$target_uri = (string)$target_uri;
return $this->buildRedirectController($target_uri, true);
}
}
$result = $site->new404Controller($request);
if ($result) {
return array($result, array());
}
return $this->build404Controller();
}
/**
* Map a specific path to the corresponding controller. For a description
* of routing, see @{method:buildController}.
*
* @param list<AphrontRoutingMap> List of routing maps.
* @param string Path to route.
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
private function routePath(array $maps, $path) {
foreach ($maps as $map) {
$result = $map->routePath($path);
if ($result) {
return array($result->getController(), $result->getURIData());
}
}
}
private function buildSiteForRequest(AphrontRequest $request) {
$sites = PhabricatorSite::getAllSites();
$site = null;
foreach ($sites as $candidate) {
$site = $candidate->newSiteForRequest($request);
if ($site) {
break;
}
}
if (!$site) {
$path = $request->getPath();
$host = $request->getHost();
throw new AphrontMalformedRequestException(
pht('Site Not Found'),
pht(
'This request asked for "%s" on host "%s", but no site is '.
'configured which can serve this request.',
$path,
$host),
true);
}
$request->setSite($site);
return $site;
}
/* -( Response Handling )-------------------------------------------------- */
/**
* Tests if a response is of a valid type.
*
* @param wild Supposedly valid response.
* @return bool True if the object is of a valid type.
* @task response
*/
private function isValidResponseObject($response) {
if ($response instanceof AphrontResponse) {
return true;
}
if ($response instanceof AphrontResponseProducerInterface) {
return true;
}
return false;
}
/**
* Verifies that the return value from an @{class:AphrontController} is
* of an allowed type.
*
* @param AphrontController Controller which returned the response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateControllerResponse(
AphrontController $controller,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Controller "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($controller),
'handleRequest()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontResponseProducerInterface} is of an allowed type.
*
* @param AphrontResponseProducerInterface Object which produced
* this response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateProducerResponse(
AphrontResponseProducerInterface $producer,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Producer "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($producer),
'produceAphrontResponse()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontRequestExceptionHandler} is of an allowed type.
*
* @param AphrontRequestExceptionHandler Object which produced this
* response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateErrorHandlerResponse(
AphrontRequestExceptionHandler $handler,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Exception handler "%s" returned an invalid response from call to '.
'"%s". This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($handler),
'handleRequestException()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Resolves a response object into an @{class:AphrontResponse}.
*
* Controllers are permitted to return actual responses of class
* @{class:AphrontResponse}, or other objects which implement
* @{interface:AphrontResponseProducerInterface} and can produce a response.
*
* If a controller returns a response producer, invoke it now and produce
* the real response.
*
* @param AphrontRequest Request being handled.
* @param AphrontResponse|AphrontResponseProducerInterface Response, or
* response producer.
* @return AphrontResponse Response after any required production.
* @task response
*/
private function produceResponse(AphrontRequest $request, $response) {
$original = $response;
// Detect cycles on the exact same objects. It's still possible to produce
// infinite responses as long as they're all unique, but we can only
// reasonably detect cycles, not guarantee that response production halts.
$seen = array();
while (true) {
// NOTE: It is permissible for an object to be both a response and a
// response producer. If so, being a producer is "stronger". This is
// used by AphrontProxyResponse.
// If this response is a valid response, hand over the request first.
if ($response instanceof AphrontResponse) {
$response->setRequest($request);
}
// If this isn't a producer, we're all done.
if (!($response instanceof AphrontResponseProducerInterface)) {
break;
}
$hash = spl_object_hash($response);
if (isset($seen[$hash])) {
throw new Exception(
pht(
'Failure while producing response for object of class "%s": '.
'encountered production cycle (identical object, of class "%s", '.
'was produced twice).',
get_class($original),
get_class($response)));
}
$seen[$hash] = true;
$new_response = $response->produceAphrontResponse();
$this->validateProducerResponse($response, $new_response);
$response = $new_response;
}
return $response;
}
/* -( Error Handling )----------------------------------------------------- */
/**
* Convert an exception which has escaped the controller into a response.
*
* This method delegates exception handling to available subclasses of
* @{class:AphrontRequestExceptionHandler}.
*
* @param Throwable Exception which needs to be handled.
* @return wild Response or response producer, or null if no available
* handler can produce a response.
* @task exception
*/
private function handleThrowable($throwable) {
$handlers = AphrontRequestExceptionHandler::getAllHandlers();
$request = $this->getRequest();
foreach ($handlers as $handler) {
if ($handler->canHandleRequestThrowable($request, $throwable)) {
$response = $handler->handleRequestThrowable($request, $throwable);
$this->validateErrorHandlerResponse($handler, $response);
return $response;
}
}
throw $throwable;
}
private static function newSelfCheckResponse() {
$path = idx($_REQUEST, '__path__', '');
$query = idx($_SERVER, 'QUERY_STRING', '');
$pairs = id(new PhutilQueryStringParser())
->parseQueryStringToPairList($query);
$params = array();
foreach ($pairs as $v) {
$params[] = array(
'name' => $v[0],
'value' => $v[1],
);
}
+ $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),
);
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($result);
}
private static function readHTTPPOSTData() {
$request_method = idx($_SERVER, 'REQUEST_METHOD');
if ($request_method === 'PUT') {
// For PUT requests, do nothing: in particular, do NOT read input. This
// allows us to stream input later and process very large PUT requests,
// like those coming from Git LFS.
return;
}
// For POST requests, we're going to read the raw input ourselves here
// if we can. Among other things, this corrects variable names with
// the "." character in them, which PHP normally converts into "_".
// There are two major considerations here: whether the
// `enable_post_data_reading` option is set, and whether the content
// type is "multipart/form-data" or not.
// If `enable_post_data_reading` is off, we're free to read the entire
// raw request body and parse it -- and we must, because $_POST and
// $_FILES are not built for us. If `enable_post_data_reading` is on,
// which is the default, we may not be able to read the body (the
// documentation says we can't, but empirically we can at least some
// of the time).
// If the content type is "multipart/form-data", we need to build both
// $_POST and $_FILES, which is involved. The body itself is also more
// difficult to parse than other requests.
$raw_input = PhabricatorStartup::getRawInput();
$parser = new PhutilQueryStringParser();
if (strlen($raw_input)) {
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_multipart = preg_match('@^multipart/form-data@i', $content_type);
if ($is_multipart && !ini_get('enable_post_data_reading')) {
$multipart_parser = id(new AphrontMultipartParser())
->setContentType($content_type);
$multipart_parser->beginParse();
$multipart_parser->continueParse($raw_input);
$parts = $multipart_parser->endParse();
// We're building and then parsing a query string so that requests
// with arrays (like "x[]=apple&x[]=banana") work correctly. This also
// means we can't use "phutil_build_http_querystring()", since it
// can't build a query string with duplicate names.
$query_string = array();
foreach ($parts as $part) {
if (!$part->isVariable()) {
continue;
}
$name = $part->getName();
$value = $part->getVariableValue();
$query_string[] = rawurlencode($name).'='.rawurlencode($value);
}
$query_string = implode('&', $query_string);
$post = $parser->parseQueryString($query_string);
$files = array();
foreach ($parts as $part) {
if ($part->isVariable()) {
continue;
}
$files[$part->getName()] = $part->getPHPFileDictionary();
}
$_FILES = $files;
} else {
$post = $parser->parseQueryString($raw_input);
}
$_POST = $post;
PhabricatorStartup::rebuildRequest();
} else if ($_POST) {
$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
if (is_array($post)) {
$_POST = $post;
PhabricatorStartup::rebuildRequest();
}
}
}
}
diff --git a/src/aphront/requeststream/AphrontRequestStream.php b/src/aphront/requeststream/AphrontRequestStream.php
index a48e01ca3e..6bd27712ed 100644
--- a/src/aphront/requeststream/AphrontRequestStream.php
+++ b/src/aphront/requeststream/AphrontRequestStream.php
@@ -1,92 +1,112 @@
<?php
final class AphrontRequestStream extends Phobject {
private $encoding;
private $stream;
private $closed;
private $iterator;
public function setEncoding($encoding) {
$this->encoding = $encoding;
return $this;
}
public function getEncoding() {
return $this->encoding;
}
public function getIterator() {
if (!$this->iterator) {
$this->iterator = new PhutilStreamIterator($this->getStream());
}
return $this->iterator;
}
public function readData() {
if (!$this->iterator) {
$iterator = $this->getIterator();
$iterator->rewind();
} else {
$iterator = $this->getIterator();
}
if (!$iterator->valid()) {
return null;
}
$data = $iterator->current();
$iterator->next();
return $data;
}
private function getStream() {
if (!$this->stream) {
$this->stream = $this->newStream();
}
return $this->stream;
}
private function newStream() {
$stream = fopen('php://input', 'rb');
if (!$stream) {
throw new Exception(
pht(
'Failed to open stream "%s" for reading.',
'php://input'));
}
$encoding = $this->getEncoding();
if ($encoding === 'gzip') {
// This parameter is magic. Values 0-15 express a time/memory tradeoff,
// but the largest value (15) corresponds to only 32KB of memory and
// data encoded with a smaller window size than the one we pass can not
// be decompressed. Always pass the maximum window size.
// Additionally, you can add 16 (to enable gzip) or 32 (to enable both
// gzip and zlib). Add 32 to support both.
$zlib_window = 15 + 32;
$ok = stream_filter_append(
$stream,
'zlib.inflate',
STREAM_FILTER_READ,
array(
'window' => $zlib_window,
));
if (!$ok) {
throw new Exception(
pht(
'Failed to append filter "%s" to input stream while processing '.
'a request with "%s" encoding.',
'zlib.inflate',
$encoding));
}
}
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
index 284b5e2a5f..c913c680f5 100644
--- a/src/applications/config/check/PhabricatorWebServerSetupCheck.php
+++ b/src/applications/config/check/PhabricatorWebServerSetupCheck.php
@@ -1,264 +1,383 @@
<?php
final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
// The documentation says these headers exist, but it's not clear if they
// are entirely reliable in practice.
if (isset($_SERVER['HTTP_X_MOD_PAGESPEED']) ||
isset($_SERVER['HTTP_X_PAGE_SPEED'])) {
$this->newIssue('webserver.pagespeed')
->setName(pht('Disable Pagespeed'))
->setSummary(pht('Pagespeed is enabled, but should be disabled.'))
->setMessage(
pht(
'Phabricator received an "X-Mod-Pagespeed" or "X-Page-Speed" '.
'HTTP header on this request, which indicates that you have '.
'enabled "mod_pagespeed" on this server. This module is not '.
'compatible with Phabricator. You should disable it.'));
}
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
if (!strlen($base_uri)) {
// If `phabricator.base-uri` is not set then we can't really do
// anything.
return;
}
$expect_user = 'alincoln';
$expect_pass = 'hunter2';
$send_path = '/test-%252A/';
$expect_path = '/test-%2A/';
$expect_key = 'duck-sound';
$expect_value = 'quack';
$base_uri = id(new PhutilURI($base_uri))
->setPath($send_path)
->replaceQueryParam($expect_key, $expect_value);
$self_future = id(new HTTPSFuture($base_uri))
->addHeader('X-Phabricator-SelfCheck', 1)
->addHeader('Accept-Encoding', 'gzip')
->setHTTPBasicAuthCredentials(
$expect_user,
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.
$ec2_uri = 'http://169.254.169.254/latest/meta-data/instance-type';
$ec2_future = id(new HTTPSFuture($ec2_uri))
->setTimeout(1);
$futures = array(
$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);
if (preg_match('/^t2/', $body)) {
$message = pht(
'Phabricator appears to be installed on a very small EC2 instance '.
'(of class "%s") with burstable CPU. This is strongly discouraged. '.
'Phabricator regularly needs CPU, and these instances are often '.
'choked to death by CPU throttling. Use an instance with a normal '.
'CPU instead.',
$body);
$this->newIssue('ec2.burstable')
->setName(pht('Installed on Burstable CPU Instance'))
->setSummary(
pht(
'Do not install Phabricator on an instance class with '.
'burstable CPU.'))
->setMessage($message);
}
} catch (Exception $ex) {
// If this fails, just continue. We're probably not running in EC2.
}
try {
list($body, $headers) = $self_future->resolvex();
} catch (Exception $ex) {
// If this fails for whatever reason, just ignore it. Hopefully, the
// error is obvious and the user can correct it on their own, but we
// can't do much to offer diagnostic advice.
return;
}
if (BaseHTTPFuture::getHeader($headers, 'Content-Encoding') != 'gzip') {
$message = pht(
'Phabricator sent itself a request with "Accept-Encoding: gzip", '.
'but received an uncompressed response.'.
"\n\n".
'This may indicate that your webserver is not configured to '.
'compress responses. If so, you should enable compression. '.
'Compression can dramatically improve performance, especially '.
'for clients with less bandwidth.');
$this->newIssue('webserver.gzip')
->setName(pht('GZip Compression May Not Be Enabled'))
->setSummary(pht('Your webserver may have compression disabled.'))
->setMessage($message);
} else {
if (function_exists('gzdecode')) {
$body = gzdecode($body);
} else {
$body = null;
}
if (!$body) {
// For now, just bail if we can't decode the response.
// This might need to use the stronger magic in "AphrontRequestStream"
// to decode more reliably.
return;
}
}
$structure = null;
$extra_whitespace = ($body !== trim($body));
try {
$structure = phutil_json_decode(trim($body));
} catch (Exception $ex) {
// Ignore the exception, we only care if the decode worked or not.
}
if (!$structure || $extra_whitespace) {
if (!$structure) {
$short = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(1024)
->truncateString($body);
$message = pht(
'Phabricator sent itself a test request with the '.
'"X-Phabricator-SelfCheck" header and expected to get a valid JSON '.
'response back. Instead, the response begins:'.
"\n\n".
'%s'.
"\n\n".
'Something is misconfigured or otherwise mangling responses.',
phutil_tag('pre', array(), $short));
} else {
$message = pht(
'Phabricator sent itself a test request and expected to get a bare '.
'JSON response back. It received a JSON response, but the response '.
'had extra whitespace at the beginning or end.'.
"\n\n".
'This usually means you have edited a file and left whitespace '.
'characters before the opening %s tag, or after a closing %s tag. '.
'Remove any leading whitespace, and prefer to omit closing tags.',
phutil_tag('tt', array(), '<?php'),
phutil_tag('tt', array(), '?>'));
}
$this->newIssue('webserver.mangle')
->setName(pht('Mangled Webserver Response'))
->setSummary(pht('Your webserver produced an unexpected response.'))
->setMessage($message);
// We can't run the other checks if we could not decode the response.
if (!$structure) {
return;
}
}
$actual_user = idx($structure, 'user');
$actual_pass = idx($structure, 'pass');
if (($expect_user != $actual_user) || ($actual_pass != $expect_pass)) {
$message = pht(
'Phabricator sent itself a test request with an "Authorization" HTTP '.
'header, and expected those credentials to be transmitted. However, '.
'they were absent or incorrect when received. Phabricator sent '.
'username "%s" with password "%s"; received username "%s" and '.
'password "%s".'.
"\n\n".
'Your webserver may not be configured to forward HTTP basic '.
'authentication. If you plan to use basic authentication (for '.
'example, to access repositories) you should reconfigure it.',
$expect_user,
$expect_pass,
$actual_user,
$actual_pass);
$this->newIssue('webserver.basic-auth')
->setName(pht('HTTP Basic Auth Not Configured'))
->setSummary(pht('Your webserver is not forwarding credentials.'))
->setMessage($message);
}
$actual_path = idx($structure, 'path');
if ($expect_path != $actual_path) {
$message = pht(
'Phabricator sent itself a test request with an unusual path, to '.
'test if your webserver is rewriting paths correctly. The path was '.
'not transmitted correctly.'.
"\n\n".
'Phabricator sent a request to path "%s", and expected the webserver '.
'to decode and rewrite that path so that it received a request for '.
'"%s". However, it received a request for "%s" instead.'.
"\n\n".
'Verify that your rewrite rules are configured correctly, following '.
'the instructions in the documentation. If path encoding is not '.
'working properly you will be unable to access files with unusual '.
'names in repositories, among other issues.'.
"\n\n".
'(This problem can be caused by a missing "B" in your RewriteRule.)',
$send_path,
$expect_path,
$actual_path);
$this->newIssue('webserver.rewrites')
->setName(pht('HTTP Path Rewriting Incorrect'))
->setSummary(pht('Your webserver is rewriting paths improperly.'))
->setMessage($message);
}
$actual_key = pht('<none>');
$actual_value = pht('<none>');
foreach (idx($structure, 'params', array()) as $pair) {
if (idx($pair, 'name') == $expect_key) {
$actual_key = idx($pair, 'name');
$actual_value = idx($pair, 'value');
break;
}
}
if (($expect_key !== $actual_key) || ($expect_value !== $actual_value)) {
$message = pht(
'Phabricator sent itself a test request with an HTTP GET parameter, '.
'but the parameter was not transmitted. Sent "%s" with value "%s", '.
'got "%s" with value "%s".'.
"\n\n".
'Your webserver is configured incorrectly and large parts of '.
'Phabricator will not work until this issue is corrected.'.
"\n\n".
'(This problem can be caused by a missing "QSA" in your RewriteRule.)',
$expect_key,
$expect_value,
$actual_key,
$actual_value);
$this->newIssue('webserver.parameters')
->setName(pht('HTTP Parameters Not Transmitting'))
->setSummary(
pht('Your webserver is not handling GET parameters properly.'))
->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('<empty>');
+ } else {
+ $snip = substr($raw, 0, 24);
+ $display = phutil_loggable_string($snip);
+
+ if (strlen($snip) < strlen($raw)) {
+ $display .= '...';
+ }
+ }
+
+ return phutil_tag('tt', array(), $display);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 28, 10:50 AM (1 d, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
107937
Default Alt Text
(44 KB)

Event Timeline