diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php index 88773cdff..875aafa3f 100644 --- a/src/aphront/sink/AphrontHTTPSink.php +++ b/src/aphront/sink/AphrontHTTPSink.php @@ -1,115 +1,118 @@ <?php /** * Abstract class which wraps some sort of output mechanism for HTTP responses. * Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and * "header()" to emit responses. * * Mostly, this class allows us to do install security or metrics hooks in the * output pipeline. * * @task write Writing Response Components * @task emit Emitting the Response * * @group aphront */ abstract class AphrontHTTPSink { /* -( Writing Response Components )---------------------------------------- */ /** * Write an HTTP status code to the output. * * @param int Numeric HTTP status code. * @return void */ final public function writeHTTPStatus($code) { if (!preg_match('/^\d{3}$/', $code)) { throw new Exception("Malformed HTTP status code '{$code}'!"); } $code = (int)$code; $this->emitHTTPStatus($code); } /** * Write HTTP headers to the output. * * @param list<pair> List of <name, value> pairs. * @return void */ final public function writeHeaders(array $headers) { foreach ($headers as $header) { if (!is_array($header) || count($header) !== 2) { throw new Exception('Malformed header.'); } list($name, $value) = $header; if (strpos($name, ':') !== false) { throw new Exception( "Declining to emit response with malformed HTTP header name: ". $name); } // Attackers may perform an "HTTP response splitting" attack by making // the application emit certain types of headers containing newlines: // // http://en.wikipedia.org/wiki/HTTP_response_splitting // // PHP has built-in protections against HTTP response-splitting, but they // are of dubious trustworthiness: // // http://news.php.net/php.internals/57655 if (preg_match('/[\r\n\0]/', $name.$value)) { throw new Exception( "Declining to emit response with unsafe HTTP header: ". "<'".$name."', '".$value."'>."); } } foreach ($headers as $header) { list($name, $value) = $header; $this->emitHeader($name, $value); } } /** * Write HTTP body data to the output. * * @param string Body data. * @return void */ final public function writeData($data) { $this->emitData($data); } /** * Write an entire @{class:AphrontResponse} to the output. * * @param AphrontResponse The response object to write. * @return void */ final public function writeResponse(AphrontResponse $response) { + // Do this first, in case it throws. + $response_string = $response->buildResponseString(); + $all_headers = array_merge( $response->getHeaders(), $response->getCacheHeaders()); $this->writeHTTPStatus($response->getHTTPResponseCode()); $this->writeHeaders($all_headers); - $this->writeData($response->buildResponseString()); + $this->writeData($response_string); } /* -( Emitting the Response )---------------------------------------------- */ abstract protected function emitHTTPStatus($code); abstract protected function emitHeader($name, $value); abstract protected function emitData($data); } diff --git a/webroot/index.php b/webroot/index.php index 1f56a1d0b..53069daea 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -1,193 +1,184 @@ <?php require_once dirname(dirname(__FILE__)).'/support/PhabricatorStartup.php'; PhabricatorStartup::didStartup(); try { PhabricatorStartup::loadCoreLibraries(); PhabricatorEnv::initializeWebEnvironment(); // This is the earliest we can get away with this, we need env config first. PhabricatorAccessLog::init(); $access_log = PhabricatorAccessLog::getLog(); if ($access_log) { PhabricatorStartup::setGlobal('log.access', $access_log); $access_log->setData( array( 'R' => idx($_SERVER, 'HTTP_REFERER', '-'), 'r' => idx($_SERVER, 'REMOTE_ADDR', '-'), 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), )); } DarkConsoleXHProfPluginAPI::hookProfiler(); PhutilErrorHandler::setErrorListener( array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); + $sink = new AphrontPHPHTTPSink(); + if (PhabricatorEnv::getEnvConfig('phabricator.setup')) { try { PhabricatorSetup::runSetup(); } catch (Exception $ex) { echo "EXCEPTION!\n"; echo $ex; } return; } phabricator_detect_bad_base_uri(); $host = $_SERVER['HTTP_HOST']; $path = $_REQUEST['__path__']; switch ($host) { default: $config_key = 'aphront.default-application-configuration-class'; $application = PhabricatorEnv::newObjectFromConfig($config_key); break; } - $application->setHost($host); $application->setPath($path); $application->willBuildRequest(); $request = $application->buildRequest(); $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); PhabricatorEventEngine::initialize(); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); if ($access_log) { $access_log->setData( array( 'U' => (string)$request->getRequestURI()->getPath(), 'C' => get_class($controller), )); } // 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 ($access_log) { if ($request->getUser() && $request->getUser()->getPHID()) { $access_log->setData( array( 'u' => $request->getUser()->getUserName(), )); } } if (!$response) { $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } } catch (AphrontRedirectException $ex) { $response = id(new AphrontRedirectResponse()) ->setURI($ex->getURI()); } catch (Exception $ex) { $original_exception = $ex; $response = $application->handleException($ex); } try { $response = $controller->didProcessRequest($response); $response = $application->willSendResponse($response, $controller); $response->setRequest($request); - $response_string = $response->buildResponseString(); + + $sink->writeResponse($response); + } catch (Exception $ex) { $write_guard->dispose(); if ($access_log) { $access_log->write(); } if ($original_exception) { $ex = new PhutilAggregateException( "Multiple exceptions during processing and rendering.", array( $original_exception, $ex, )); } PhabricatorStartup::didFatal('[Rendering Exception] '.$ex->getMessage()); } $write_guard->dispose(); - // TODO: Share the $sink->writeResponse() pathway here? - - $sink = new AphrontPHPHTTPSink(); - $sink->writeHTTPStatus($response->getHTTPResponseCode()); - - $headers = $response->getCacheHeaders(); - $headers = array_merge($headers, $response->getHeaders()); - - $sink->writeHeaders($headers); - - $sink->writeData($response_string); - if ($access_log) { $request_start = PhabricatorStartup::getStartTime(); $access_log->setData( array( 'c' => $response->getHTTPResponseCode(), 'T' => (int)(1000000 * (microtime(true) - $request_start)), )); $access_log->write(); } if (DarkConsoleXHProfPluginAPI::isProfilerRequested()) { $profile = DarkConsoleXHProfPluginAPI::stopProfiler(); $profile_sample = id(new PhabricatorXHProfSample()) ->setFilePHID($profile); if (empty($_REQUEST['__profile__'])) { $sample_rate = PhabricatorEnv::getEnvConfig('debug.profile-rate'); } else { $sample_rate = 0; } $profile_sample->setSampleRate($sample_rate); if ($access_log) { $profile_sample->setUsTotal($access_log->getData('T')) ->setHostname($access_log->getData('h')) ->setRequestPath($access_log->getData('U')) ->setController($access_log->getData('C')) ->setUserPHID($request->getUser()->getPHID()); } $profile_sample->save(); } } catch (Exception $ex) { PhabricatorStartup::didFatal("[Exception] ".$ex->getMessage()); } function phabricator_detect_bad_base_uri() { $conf = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $uri = new PhutilURI($conf); switch ($uri->getProtocol()) { case 'http': case 'https': break; default: PhabricatorStartup::didFatal( "'phabricator.base-uri' is set to '{$conf}', which is invalid. ". "The URI must start with 'http://' or 'https://'."); return; } if (strpos($uri->getDomain(), '.') === false) { PhabricatorStartup::didFatal( "'phabricator.base-uri' is set to '{$conf}', which is invalid. The URI ". "must contain a dot ('.'), like 'http://example.com/', not just ". "'http://example/'. Some web browsers will not set cookies on domains ". "with no TLD, and Phabricator requires cookies for login. ". "If you are using localhost, create an entry in the hosts file like ". "'127.0.0.1 example.com', and access the localhost with ". "'http://example.com/'."); } }