Path: blob/master/src/aphront/configuration/AphrontApplicationConfiguration.php
12241 views
<?php12/**3* @task routing URI Routing4* @task response Response Handling5* @task exception Exception Handling6*/7final class AphrontApplicationConfiguration8extends Phobject {910private $request;11private $host;12private $path;13private $console;1415public function buildRequest() {16$parser = new PhutilQueryStringParser();1718$data = array();19$data += $_POST;20$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));2122$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');2324$request = new AphrontRequest($this->getHost(), $this->getPath());25$request->setRequestData($data);26$request->setApplicationConfiguration($this);27$request->setCookiePrefix($cookie_prefix);2829$request->updateEphemeralCookies();3031return $request;32}3334public function buildRedirectController($uri, $external) {35return array(36new PhabricatorRedirectController(),37array(38'uri' => $uri,39'external' => $external,40),41);42}4344public function setRequest(AphrontRequest $request) {45$this->request = $request;46return $this;47}4849public function getRequest() {50return $this->request;51}5253public function getConsole() {54return $this->console;55}5657public function setConsole($console) {58$this->console = $console;59return $this;60}6162public function setHost($host) {63$this->host = $host;64return $this;65}6667public function getHost() {68return $this->host;69}7071public function setPath($path) {72$this->path = $path;73return $this;74}7576public function getPath() {77return $this->path;78}798081/**82* @phutil-external-symbol class PhabricatorStartup83*/84public static function runHTTPRequest(AphrontHTTPSink $sink) {85if (isset($_SERVER['HTTP_X_SETUP_SELFCHECK'])) {86$response = self::newSelfCheckResponse();87return self::writeResponse($sink, $response);88}8990PhabricatorStartup::beginStartupPhase('multimeter');91$multimeter = MultimeterControl::newInstance();92$multimeter->setEventContext('<http-init>');93$multimeter->setEventViewer('<none>');9495// Build a no-op write guard for the setup phase. We'll replace this with a96// real write guard later on, but we need to survive setup and build a97// request object first.98$write_guard = new AphrontWriteGuard('id');99100PhabricatorStartup::beginStartupPhase('preflight');101102$response = PhabricatorSetupCheck::willPreflightRequest();103if ($response) {104return self::writeResponse($sink, $response);105}106107PhabricatorStartup::beginStartupPhase('env.init');108109self::readHTTPPOSTData();110111try {112PhabricatorEnv::initializeWebEnvironment();113$database_exception = null;114} catch (PhabricatorClusterStrandedException $ex) {115$database_exception = $ex;116}117118// If we're in developer mode, set a flag so that top-level exception119// handlers can add more information.120if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {121$sink->setShowStackTraces(true);122}123124if ($database_exception) {125$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(126$database_exception,127true);128$response = PhabricatorSetupCheck::newIssueResponse($issue);129return self::writeResponse($sink, $response);130}131132$multimeter->setSampleRate(133PhabricatorEnv::getEnvConfig('debug.sample-rate'));134135$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');136if ($debug_time_limit) {137PhabricatorStartup::setDebugTimeLimit($debug_time_limit);138}139140// This is the earliest we can get away with this, we need env config first.141PhabricatorStartup::beginStartupPhase('log.access');142PhabricatorAccessLog::init();143$access_log = PhabricatorAccessLog::getLog();144PhabricatorStartup::setAccessLog($access_log);145146$address = PhabricatorEnv::getRemoteAddress();147if ($address) {148$address_string = $address->getAddress();149} else {150$address_string = '-';151}152153$access_log->setData(154array(155'R' => AphrontRequest::getHTTPHeader('Referer', '-'),156'r' => $address_string,157'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),158));159160DarkConsoleXHProfPluginAPI::hookProfiler();161162// We just activated the profiler, so we don't need to keep track of163// startup phases anymore: it can take over from here.164PhabricatorStartup::beginStartupPhase('startup.done');165166DarkConsoleErrorLogPluginAPI::registerErrorHandler();167168$response = PhabricatorSetupCheck::willProcessRequest();169if ($response) {170return self::writeResponse($sink, $response);171}172173$host = AphrontRequest::getHTTPHeader('Host');174$path = PhabricatorStartup::getRequestPath();175176$application = new self();177178$application->setHost($host);179$application->setPath($path);180$request = $application->buildRequest();181182// Now that we have a request, convert the write guard into one which183// actually checks CSRF tokens.184$write_guard->dispose();185$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));186187// Build the server URI implied by the request headers. If an administrator188// has not configured "phabricator.base-uri" yet, we'll use this to generate189// links.190191$request_protocol = ($request->isHTTPS() ? 'https' : 'http');192$request_base_uri = "{$request_protocol}://{$host}/";193PhabricatorEnv::setRequestBaseURI($request_base_uri);194195$access_log->setData(196array(197'U' => (string)$request->getRequestURI()->getPath(),198));199200$processing_exception = null;201try {202$response = $application->processRequest(203$request,204$access_log,205$sink,206$multimeter);207$response_code = $response->getHTTPResponseCode();208} catch (Exception $ex) {209$processing_exception = $ex;210$response_code = 500;211}212213$write_guard->dispose();214215$access_log->setData(216array(217'c' => $response_code,218'T' => PhabricatorStartup::getMicrosecondsSinceStart(),219));220221$multimeter->newEvent(222MultimeterEvent::TYPE_REQUEST_TIME,223$multimeter->getEventContext(),224PhabricatorStartup::getMicrosecondsSinceStart());225226$access_log->write();227228$multimeter->saveEvents();229230DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);231232PhabricatorStartup::disconnectRateLimits(233array(234'viewer' => $request->getUser(),235));236237if ($processing_exception) {238throw $processing_exception;239}240}241242243public function processRequest(244AphrontRequest $request,245PhutilDeferredLog $access_log,246AphrontHTTPSink $sink,247MultimeterControl $multimeter) {248249$this->setRequest($request);250251list($controller, $uri_data) = $this->buildController();252253$controller_class = get_class($controller);254$access_log->setData(255array(256'C' => $controller_class,257));258$multimeter->setEventContext('web.'.$controller_class);259260$request->setController($controller);261$request->setURIMap($uri_data);262263$controller->setRequest($request);264265// If execution throws an exception and then trying to render that266// exception throws another exception, we want to show the original267// exception, as it is likely the root cause of the rendering exception.268$original_exception = null;269try {270$response = $controller->willBeginExecution();271272if ($request->getUser() && $request->getUser()->getPHID()) {273$access_log->setData(274array(275'u' => $request->getUser()->getUserName(),276'P' => $request->getUser()->getPHID(),277));278$multimeter->setEventViewer('user.'.$request->getUser()->getPHID());279}280281if (!$response) {282$controller->willProcessRequest($uri_data);283$response = $controller->handleRequest($request);284$this->validateControllerResponse($controller, $response);285}286} catch (Exception $ex) {287$original_exception = $ex;288} catch (Throwable $ex) {289$original_exception = $ex;290}291292$response_exception = null;293try {294if ($original_exception) {295$response = $this->handleThrowable($original_exception);296}297298$response = $this->produceResponse($request, $response);299$response = $controller->willSendResponse($response);300$response->setRequest($request);301302self::writeResponse($sink, $response);303} catch (Exception $ex) {304$response_exception = $ex;305} catch (Throwable $ex) {306$response_exception = $ex;307}308309if ($response_exception) {310// If we encountered an exception while building a normal response, then311// encountered another exception while building a response for the first312// exception, throw an aggregate exception that will be unpacked by the313// higher-level handler. This is above our pay grade.314if ($original_exception) {315throw new PhutilAggregateException(316pht(317'Encountered a processing exception, then another exception when '.318'trying to build a response for the first exception.'),319array(320$response_exception,321$original_exception,322));323}324325// If we built a response successfully and then ran into an exception326// trying to render it, try to handle and present that exception to the327// user using the standard handler.328329// The problem here might be in rendering (more common) or in the actual330// response mechanism (less common). If it's in rendering, we can likely331// still render a nice exception page: the majority of rendering issues332// are in main page content, not content shared with the exception page.333334$handling_exception = null;335try {336$response = $this->handleThrowable($response_exception);337338$response = $this->produceResponse($request, $response);339$response = $controller->willSendResponse($response);340$response->setRequest($request);341342self::writeResponse($sink, $response);343} catch (Exception $ex) {344$handling_exception = $ex;345} catch (Throwable $ex) {346$handling_exception = $ex;347}348349// If we didn't have any luck with that, raise the original response350// exception. As above, this is the root cause exception and more likely351// to be useful. This will go to the fallback error handler at top352// level.353354if ($handling_exception) {355throw $response_exception;356}357}358359return $response;360}361362private static function writeResponse(363AphrontHTTPSink $sink,364AphrontResponse $response) {365366$unexpected_output = PhabricatorStartup::endOutputCapture();367if ($unexpected_output) {368$unexpected_output = pht(369"Unexpected output:\n\n%s",370$unexpected_output);371372phlog($unexpected_output);373374if ($response instanceof AphrontWebpageResponse) {375$response->setUnexpectedOutput($unexpected_output);376}377}378379$sink->writeResponse($response);380}381382383/* -( URI Routing )-------------------------------------------------------- */384385386/**387* Build a controller to respond to the request.388*389* @return pair<AphrontController,dict> Controller and dictionary of request390* parameters.391* @task routing392*/393private function buildController() {394$request = $this->getRequest();395396// If we're configured to operate in cluster mode, reject requests which397// were not received on a cluster interface.398//399// For example, a host may have an internal address like "170.0.0.1", and400// also have a public address like "51.23.95.16". Assuming the cluster401// is configured on a range like "170.0.0.0/16", we want to reject the402// requests received on the public interface.403//404// Ideally, nodes in a cluster should only be listening on internal405// interfaces, but they may be configured in such a way that they also406// listen on external interfaces, since this is easy to forget about or407// get wrong. As a broad security measure, reject requests received on any408// interfaces which aren't on the whitelist.409410$cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');411if ($cluster_addresses) {412$server_addr = idx($_SERVER, 'SERVER_ADDR');413if (!$server_addr) {414if (php_sapi_name() == 'cli') {415// This is a command line script (probably something like a unit416// test) so it's fine that we don't have SERVER_ADDR defined.417} else {418throw new AphrontMalformedRequestException(419pht('No %s', 'SERVER_ADDR'),420pht(421'This service is configured to operate in cluster mode, but '.422'%s is not defined in the request context. Your webserver '.423'configuration needs to forward %s to PHP so the software can '.424'reject requests received on external interfaces.',425'SERVER_ADDR',426'SERVER_ADDR'));427}428} else {429if (!PhabricatorEnv::isClusterAddress($server_addr)) {430throw new AphrontMalformedRequestException(431pht('External Interface'),432pht(433'This service is configured in cluster mode and the address '.434'this request was received on ("%s") is not whitelisted as '.435'a cluster address.',436$server_addr));437}438}439}440441$site = $this->buildSiteForRequest($request);442443if ($site->shouldRequireHTTPS()) {444if (!$request->isHTTPS()) {445446// Don't redirect intracluster requests: doing so drops headers and447// parameters, imposes a performance penalty, and indicates a448// misconfiguration.449if ($request->isProxiedClusterRequest()) {450throw new AphrontMalformedRequestException(451pht('HTTPS Required'),452pht(453'This request reached a site which requires HTTPS, but the '.454'request is not marked as HTTPS.'));455}456457$https_uri = $request->getRequestURI();458$https_uri->setDomain($request->getHost());459$https_uri->setProtocol('https');460461// In this scenario, we'll be redirecting to HTTPS using an absolute462// URI, so we need to permit an external redirect.463return $this->buildRedirectController($https_uri, true);464}465}466467$maps = $site->getRoutingMaps();468$path = $request->getPath();469470$result = $this->routePath($maps, $path);471if ($result) {472return $result;473}474475// If we failed to match anything but don't have a trailing slash, try476// to add a trailing slash and issue a redirect if that resolves.477478// NOTE: We only do this for GET, since redirects switch to GET and drop479// data like POST parameters.480if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {481$result = $this->routePath($maps, $path.'/');482if ($result) {483$target_uri = $request->getAbsoluteRequestURI();484485// We need to restore URI encoding because the webserver has486// interpreted it. For example, this allows us to redirect a path487// like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be488// resolved meaningfully by an application.489$target_path = phutil_escape_uri($path.'/');490$target_uri->setPath($target_path);491$target_uri = (string)$target_uri;492493return $this->buildRedirectController($target_uri, true);494}495}496497$result = $site->new404Controller($request);498if ($result) {499return array($result, array());500}501502throw new Exception(503pht(504'Aphront site ("%s") failed to build a 404 controller.',505get_class($site)));506}507508/**509* Map a specific path to the corresponding controller. For a description510* of routing, see @{method:buildController}.511*512* @param list<AphrontRoutingMap> List of routing maps.513* @param string Path to route.514* @return pair<AphrontController,dict> Controller and dictionary of request515* parameters.516* @task routing517*/518private function routePath(array $maps, $path) {519foreach ($maps as $map) {520$result = $map->routePath($path);521if ($result) {522return array($result->getController(), $result->getURIData());523}524}525}526527private function buildSiteForRequest(AphrontRequest $request) {528$sites = PhabricatorSite::getAllSites();529530$site = null;531foreach ($sites as $candidate) {532$site = $candidate->newSiteForRequest($request);533if ($site) {534break;535}536}537538if (!$site) {539$path = $request->getPath();540$host = $request->getHost();541throw new AphrontMalformedRequestException(542pht('Site Not Found'),543pht(544'This request asked for "%s" on host "%s", but no site is '.545'configured which can serve this request.',546$path,547$host),548true);549}550551$request->setSite($site);552553return $site;554}555556557/* -( Response Handling )-------------------------------------------------- */558559560/**561* Tests if a response is of a valid type.562*563* @param wild Supposedly valid response.564* @return bool True if the object is of a valid type.565* @task response566*/567private function isValidResponseObject($response) {568if ($response instanceof AphrontResponse) {569return true;570}571572if ($response instanceof AphrontResponseProducerInterface) {573return true;574}575576return false;577}578579580/**581* Verifies that the return value from an @{class:AphrontController} is582* of an allowed type.583*584* @param AphrontController Controller which returned the response.585* @param wild Supposedly valid response.586* @return void587* @task response588*/589private function validateControllerResponse(590AphrontController $controller,591$response) {592593if ($this->isValidResponseObject($response)) {594return;595}596597throw new Exception(598pht(599'Controller "%s" returned an invalid response from call to "%s". '.600'This method must return an object of class "%s", or an object '.601'which implements the "%s" interface.',602get_class($controller),603'handleRequest()',604'AphrontResponse',605'AphrontResponseProducerInterface'));606}607608609/**610* Verifies that the return value from an611* @{class:AphrontResponseProducerInterface} is of an allowed type.612*613* @param AphrontResponseProducerInterface Object which produced614* this response.615* @param wild Supposedly valid response.616* @return void617* @task response618*/619private function validateProducerResponse(620AphrontResponseProducerInterface $producer,621$response) {622623if ($this->isValidResponseObject($response)) {624return;625}626627throw new Exception(628pht(629'Producer "%s" returned an invalid response from call to "%s". '.630'This method must return an object of class "%s", or an object '.631'which implements the "%s" interface.',632get_class($producer),633'produceAphrontResponse()',634'AphrontResponse',635'AphrontResponseProducerInterface'));636}637638639/**640* Verifies that the return value from an641* @{class:AphrontRequestExceptionHandler} is of an allowed type.642*643* @param AphrontRequestExceptionHandler Object which produced this644* response.645* @param wild Supposedly valid response.646* @return void647* @task response648*/649private function validateErrorHandlerResponse(650AphrontRequestExceptionHandler $handler,651$response) {652653if ($this->isValidResponseObject($response)) {654return;655}656657throw new Exception(658pht(659'Exception handler "%s" returned an invalid response from call to '.660'"%s". This method must return an object of class "%s", or an object '.661'which implements the "%s" interface.',662get_class($handler),663'handleRequestException()',664'AphrontResponse',665'AphrontResponseProducerInterface'));666}667668669/**670* Resolves a response object into an @{class:AphrontResponse}.671*672* Controllers are permitted to return actual responses of class673* @{class:AphrontResponse}, or other objects which implement674* @{interface:AphrontResponseProducerInterface} and can produce a response.675*676* If a controller returns a response producer, invoke it now and produce677* the real response.678*679* @param AphrontRequest Request being handled.680* @param AphrontResponse|AphrontResponseProducerInterface Response, or681* response producer.682* @return AphrontResponse Response after any required production.683* @task response684*/685private function produceResponse(AphrontRequest $request, $response) {686$original = $response;687688// Detect cycles on the exact same objects. It's still possible to produce689// infinite responses as long as they're all unique, but we can only690// reasonably detect cycles, not guarantee that response production halts.691692$seen = array();693while (true) {694// NOTE: It is permissible for an object to be both a response and a695// response producer. If so, being a producer is "stronger". This is696// used by AphrontProxyResponse.697698// If this response is a valid response, hand over the request first.699if ($response instanceof AphrontResponse) {700$response->setRequest($request);701}702703// If this isn't a producer, we're all done.704if (!($response instanceof AphrontResponseProducerInterface)) {705break;706}707708$hash = spl_object_hash($response);709if (isset($seen[$hash])) {710throw new Exception(711pht(712'Failure while producing response for object of class "%s": '.713'encountered production cycle (identical object, of class "%s", '.714'was produced twice).',715get_class($original),716get_class($response)));717}718719$seen[$hash] = true;720721$new_response = $response->produceAphrontResponse();722$this->validateProducerResponse($response, $new_response);723$response = $new_response;724}725726return $response;727}728729730/* -( Error Handling )----------------------------------------------------- */731732733/**734* Convert an exception which has escaped the controller into a response.735*736* This method delegates exception handling to available subclasses of737* @{class:AphrontRequestExceptionHandler}.738*739* @param Throwable Exception which needs to be handled.740* @return wild Response or response producer, or null if no available741* handler can produce a response.742* @task exception743*/744private function handleThrowable($throwable) {745$handlers = AphrontRequestExceptionHandler::getAllHandlers();746747$request = $this->getRequest();748foreach ($handlers as $handler) {749if ($handler->canHandleRequestThrowable($request, $throwable)) {750$response = $handler->handleRequestThrowable($request, $throwable);751$this->validateErrorHandlerResponse($handler, $response);752return $response;753}754}755756throw $throwable;757}758759private static function newSelfCheckResponse() {760$path = PhabricatorStartup::getRequestPath();761$query = idx($_SERVER, 'QUERY_STRING', '');762763$pairs = id(new PhutilQueryStringParser())764->parseQueryStringToPairList($query);765766$params = array();767foreach ($pairs as $v) {768$params[] = array(769'name' => $v[0],770'value' => $v[1],771);772}773774$raw_input = @file_get_contents('php://input');775if ($raw_input !== false) {776$base64_input = base64_encode($raw_input);777} else {778$base64_input = null;779}780781$result = array(782'path' => $path,783'params' => $params,784'user' => idx($_SERVER, 'PHP_AUTH_USER'),785'pass' => idx($_SERVER, 'PHP_AUTH_PW'),786787'raw.base64' => $base64_input,788789// This just makes sure that the response compresses well, so reasonable790// algorithms should want to gzip or deflate it.791'filler' => str_repeat('Q', 1024 * 16),792);793794return id(new AphrontJSONResponse())795->setAddJSONShield(false)796->setContent($result);797}798799private static function readHTTPPOSTData() {800$request_method = idx($_SERVER, 'REQUEST_METHOD');801if ($request_method === 'PUT') {802// For PUT requests, do nothing: in particular, do NOT read input. This803// allows us to stream input later and process very large PUT requests,804// like those coming from Git LFS.805return;806}807808809// For POST requests, we're going to read the raw input ourselves here810// if we can. Among other things, this corrects variable names with811// the "." character in them, which PHP normally converts into "_".812813// If "enable_post_data_reading" is on, the documentation suggests we814// can not read the body. In practice, we seem to be able to. This may815// need to be resolved at some point, likely by instructing installs816// to disable this option.817818// If the content type is "multipart/form-data", we need to build both819// $_POST and $_FILES, which is involved. The body itself is also more820// difficult to parse than other requests.821822$raw_input = PhabricatorStartup::getRawInput();823$parser = new PhutilQueryStringParser();824825if (strlen($raw_input)) {826$content_type = idx($_SERVER, 'CONTENT_TYPE');827$is_multipart = preg_match('@^multipart/form-data@i', $content_type);828if ($is_multipart) {829$multipart_parser = id(new AphrontMultipartParser())830->setContentType($content_type);831832$multipart_parser->beginParse();833$multipart_parser->continueParse($raw_input);834$parts = $multipart_parser->endParse();835836// We're building and then parsing a query string so that requests837// with arrays (like "x[]=apple&x[]=banana") work correctly. This also838// means we can't use "phutil_build_http_querystring()", since it839// can't build a query string with duplicate names.840841$query_string = array();842foreach ($parts as $part) {843if (!$part->isVariable()) {844continue;845}846847$name = $part->getName();848$value = $part->getVariableValue();849$query_string[] = rawurlencode($name).'='.rawurlencode($value);850}851$query_string = implode('&', $query_string);852$post = $parser->parseQueryString($query_string);853854$files = array();855foreach ($parts as $part) {856if ($part->isVariable()) {857continue;858}859860$files[$part->getName()] = $part->getPHPFileDictionary();861}862$_FILES = $files;863} else {864$post = $parser->parseQueryString($raw_input);865}866867$_POST = $post;868PhabricatorStartup::rebuildRequest();869} else if ($_POST) {870$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);871if (is_array($post)) {872$_POST = $post;873PhabricatorStartup::rebuildRequest();874}875}876}877878}879880881