4 * @task routing URI Routing
5 * @task response Response Handling
6 * @task exception Exception Handling
8 final class AphrontApplicationConfiguration
16 public function buildRequest() {
17 $parser = new PhutilQueryStringParser();
21 $data +
= $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
23 $cookie_prefix = PhabricatorEnv
::getEnvConfig('phabricator.cookie-prefix');
25 $request = new AphrontRequest($this->getHost(), $this->getPath());
26 $request->setRequestData($data);
27 $request->setApplicationConfiguration($this);
28 $request->setCookiePrefix($cookie_prefix);
30 $request->updateEphemeralCookies();
35 public function buildRedirectController($uri, $external) {
37 new PhabricatorRedirectController(),
40 'external' => $external,
45 public function setRequest(AphrontRequest
$request) {
46 $this->request
= $request;
50 public function getRequest() {
51 return $this->request
;
54 public function getConsole() {
55 return $this->console
;
58 public function setConsole($console) {
59 $this->console
= $console;
63 public function setHost($host) {
68 public function getHost() {
72 public function setPath($path) {
77 public function getPath() {
83 * @phutil-external-symbol class PhabricatorStartup
85 public static function runHTTPRequest(AphrontHTTPSink
$sink) {
86 if (isset($_SERVER['HTTP_X_SETUP_SELFCHECK'])) {
87 $response = self
::newSelfCheckResponse();
88 return self
::writeResponse($sink, $response);
91 PhabricatorStartup
::beginStartupPhase('multimeter');
92 $multimeter = MultimeterControl
::newInstance();
93 $multimeter->setEventContext('<http-init>');
94 $multimeter->setEventViewer('<none>');
96 // Build a no-op write guard for the setup phase. We'll replace this with a
97 // real write guard later on, but we need to survive setup and build a
98 // request object first.
99 $write_guard = new AphrontWriteGuard('id');
101 PhabricatorStartup
::beginStartupPhase('preflight');
103 $response = PhabricatorSetupCheck
::willPreflightRequest();
105 return self
::writeResponse($sink, $response);
108 PhabricatorStartup
::beginStartupPhase('env.init');
110 self
::readHTTPPOSTData();
113 PhabricatorEnv
::initializeWebEnvironment();
114 $database_exception = null;
115 } catch (PhabricatorClusterStrandedException
$ex) {
116 $database_exception = $ex;
119 // If we're in developer mode, set a flag so that top-level exception
120 // handlers can add more information.
121 if (PhabricatorEnv
::getEnvConfig('phabricator.developer-mode')) {
122 $sink->setShowStackTraces(true);
125 if ($database_exception) {
126 $issue = PhabricatorSetupIssue
::newDatabaseConnectionIssue(
129 $response = PhabricatorSetupCheck
::newIssueResponse($issue);
130 return self
::writeResponse($sink, $response);
133 $multimeter->setSampleRate(
134 PhabricatorEnv
::getEnvConfig('debug.sample-rate'));
136 $debug_time_limit = PhabricatorEnv
::getEnvConfig('debug.time-limit');
137 if ($debug_time_limit) {
138 PhabricatorStartup
::setDebugTimeLimit($debug_time_limit);
141 // This is the earliest we can get away with this, we need env config first.
142 PhabricatorStartup
::beginStartupPhase('log.access');
143 PhabricatorAccessLog
::init();
144 $access_log = PhabricatorAccessLog
::getLog();
145 PhabricatorStartup
::setAccessLog($access_log);
147 $address = PhabricatorEnv
::getRemoteAddress();
149 $address_string = $address->getAddress();
151 $address_string = '-';
154 $access_log->setData(
156 'R' => AphrontRequest
::getHTTPHeader('Referer', '-'),
157 'r' => $address_string,
158 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
161 DarkConsoleXHProfPluginAPI
::hookProfiler();
163 // We just activated the profiler, so we don't need to keep track of
164 // startup phases anymore: it can take over from here.
165 PhabricatorStartup
::beginStartupPhase('startup.done');
167 DarkConsoleErrorLogPluginAPI
::registerErrorHandler();
169 $response = PhabricatorSetupCheck
::willProcessRequest();
171 return self
::writeResponse($sink, $response);
174 $host = AphrontRequest
::getHTTPHeader('Host');
175 $path = PhabricatorStartup
::getRequestPath();
177 $application = new self();
179 $application->setHost($host);
180 $application->setPath($path);
181 $request = $application->buildRequest();
183 // Now that we have a request, convert the write guard into one which
184 // actually checks CSRF tokens.
185 $write_guard->dispose();
186 $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
188 // Build the server URI implied by the request headers. If an administrator
189 // has not configured "phabricator.base-uri" yet, we'll use this to generate
192 $request_protocol = ($request->isHTTPS() ?
'https' : 'http');
193 $request_base_uri = "{$request_protocol}://{$host}/";
194 PhabricatorEnv
::setRequestBaseURI($request_base_uri);
196 $access_log->setData(
198 'U' => (string)$request->getRequestURI()->getPath(),
201 $processing_exception = null;
203 $response = $application->processRequest(
208 $response_code = $response->getHTTPResponseCode();
209 } catch (Exception
$ex) {
210 $processing_exception = $ex;
211 $response_code = 500;
214 $write_guard->dispose();
216 $access_log->setData(
218 'c' => $response_code,
219 'T' => PhabricatorStartup
::getMicrosecondsSinceStart(),
222 $multimeter->newEvent(
223 MultimeterEvent
::TYPE_REQUEST_TIME
,
224 $multimeter->getEventContext(),
225 PhabricatorStartup
::getMicrosecondsSinceStart());
227 $access_log->write();
229 $multimeter->saveEvents();
231 DarkConsoleXHProfPluginAPI
::saveProfilerSample($access_log);
233 PhabricatorStartup
::disconnectRateLimits(
235 'viewer' => $request->getUser(),
238 if ($processing_exception) {
239 throw $processing_exception;
244 public function processRequest(
245 AphrontRequest
$request,
246 PhutilDeferredLog
$access_log,
247 AphrontHTTPSink
$sink,
248 MultimeterControl
$multimeter) {
250 $this->setRequest($request);
252 list($controller, $uri_data) = $this->buildController();
254 $controller_class = get_class($controller);
255 $access_log->setData(
257 'C' => $controller_class,
259 $multimeter->setEventContext('web.'.$controller_class);
261 $request->setController($controller);
262 $request->setURIMap($uri_data);
264 $controller->setRequest($request);
266 // If execution throws an exception and then trying to render that
267 // exception throws another exception, we want to show the original
268 // exception, as it is likely the root cause of the rendering exception.
269 $original_exception = null;
271 $response = $controller->willBeginExecution();
273 if ($request->getUser() && $request->getUser()->getPHID()) {
274 $access_log->setData(
276 'u' => $request->getUser()->getUserName(),
277 'P' => $request->getUser()->getPHID(),
279 $multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
283 $controller->willProcessRequest($uri_data);
284 $response = $controller->handleRequest($request);
285 $this->validateControllerResponse($controller, $response);
287 } catch (Exception
$ex) {
288 $original_exception = $ex;
289 } catch (Throwable
$ex) {
290 $original_exception = $ex;
293 $response_exception = null;
295 if ($original_exception) {
296 $response = $this->handleThrowable($original_exception);
299 $response = $this->produceResponse($request, $response);
300 $response = $controller->willSendResponse($response);
301 $response->setRequest($request);
303 self
::writeResponse($sink, $response);
304 } catch (Exception
$ex) {
305 $response_exception = $ex;
306 } catch (Throwable
$ex) {
307 $response_exception = $ex;
310 if ($response_exception) {
311 // If we encountered an exception while building a normal response, then
312 // encountered another exception while building a response for the first
313 // exception, throw an aggregate exception that will be unpacked by the
314 // higher-level handler. This is above our pay grade.
315 if ($original_exception) {
316 throw new PhutilAggregateException(
318 'Encountered a processing exception, then another exception when '.
319 'trying to build a response for the first exception.'),
326 // If we built a response successfully and then ran into an exception
327 // trying to render it, try to handle and present that exception to the
328 // user using the standard handler.
330 // The problem here might be in rendering (more common) or in the actual
331 // response mechanism (less common). If it's in rendering, we can likely
332 // still render a nice exception page: the majority of rendering issues
333 // are in main page content, not content shared with the exception page.
335 $handling_exception = null;
337 $response = $this->handleThrowable($response_exception);
339 $response = $this->produceResponse($request, $response);
340 $response = $controller->willSendResponse($response);
341 $response->setRequest($request);
343 self
::writeResponse($sink, $response);
344 } catch (Exception
$ex) {
345 $handling_exception = $ex;
346 } catch (Throwable
$ex) {
347 $handling_exception = $ex;
350 // If we didn't have any luck with that, raise the original response
351 // exception. As above, this is the root cause exception and more likely
352 // to be useful. This will go to the fallback error handler at top
355 if ($handling_exception) {
356 throw $response_exception;
363 private static function writeResponse(
364 AphrontHTTPSink
$sink,
365 AphrontResponse
$response) {
367 $unexpected_output = PhabricatorStartup
::endOutputCapture();
368 if ($unexpected_output) {
369 $unexpected_output = pht(
370 "Unexpected output:\n\n%s",
373 phlog($unexpected_output);
375 if ($response instanceof AphrontWebpageResponse
) {
376 $response->setUnexpectedOutput($unexpected_output);
380 $sink->writeResponse($response);
384 /* -( URI Routing )-------------------------------------------------------- */
388 * Build a controller to respond to the request.
390 * @return pair<AphrontController,dict> Controller and dictionary of request
394 private function buildController() {
395 $request = $this->getRequest();
397 // If we're configured to operate in cluster mode, reject requests which
398 // were not received on a cluster interface.
400 // For example, a host may have an internal address like "170.0.0.1", and
401 // also have a public address like "51.23.95.16". Assuming the cluster
402 // is configured on a range like "170.0.0.0/16", we want to reject the
403 // requests received on the public interface.
405 // Ideally, nodes in a cluster should only be listening on internal
406 // interfaces, but they may be configured in such a way that they also
407 // listen on external interfaces, since this is easy to forget about or
408 // get wrong. As a broad security measure, reject requests received on any
409 // interfaces which aren't on the whitelist.
411 $cluster_addresses = PhabricatorEnv
::getEnvConfig('cluster.addresses');
412 if ($cluster_addresses) {
413 $server_addr = idx($_SERVER, 'SERVER_ADDR');
415 if (php_sapi_name() == 'cli') {
416 // This is a command line script (probably something like a unit
417 // test) so it's fine that we don't have SERVER_ADDR defined.
419 throw new AphrontMalformedRequestException(
420 pht('No %s', 'SERVER_ADDR'),
422 'This service is configured to operate in cluster mode, but '.
423 '%s is not defined in the request context. Your webserver '.
424 'configuration needs to forward %s to PHP so the software can '.
425 'reject requests received on external interfaces.',
430 if (!PhabricatorEnv
::isClusterAddress($server_addr)) {
431 throw new AphrontMalformedRequestException(
432 pht('External Interface'),
434 'This service is configured in cluster mode and the address '.
435 'this request was received on ("%s") is not whitelisted as '.
436 'a cluster address.',
442 $site = $this->buildSiteForRequest($request);
444 if ($site->shouldRequireHTTPS()) {
445 if (!$request->isHTTPS()) {
447 // Don't redirect intracluster requests: doing so drops headers and
448 // parameters, imposes a performance penalty, and indicates a
450 if ($request->isProxiedClusterRequest()) {
451 throw new AphrontMalformedRequestException(
452 pht('HTTPS Required'),
454 'This request reached a site which requires HTTPS, but the '.
455 'request is not marked as HTTPS.'));
458 $https_uri = $request->getRequestURI();
459 $https_uri->setDomain($request->getHost());
460 $https_uri->setProtocol('https');
462 // In this scenario, we'll be redirecting to HTTPS using an absolute
463 // URI, so we need to permit an external redirect.
464 return $this->buildRedirectController($https_uri, true);
468 $maps = $site->getRoutingMaps();
469 $path = $request->getPath();
471 $result = $this->routePath($maps, $path);
476 // If we failed to match anything but don't have a trailing slash, try
477 // to add a trailing slash and issue a redirect if that resolves.
479 // NOTE: We only do this for GET, since redirects switch to GET and drop
480 // data like POST parameters.
481 if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {
482 $result = $this->routePath($maps, $path.'/');
484 $target_uri = $request->getAbsoluteRequestURI();
486 // We need to restore URI encoding because the webserver has
487 // interpreted it. For example, this allows us to redirect a path
488 // like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
489 // resolved meaningfully by an application.
490 $target_path = phutil_escape_uri($path.'/');
491 $target_uri->setPath($target_path);
492 $target_uri = (string)$target_uri;
494 return $this->buildRedirectController($target_uri, true);
498 $result = $site->new404Controller($request);
500 return array($result, array());
505 'Aphront site ("%s") failed to build a 404 controller.',
510 * Map a specific path to the corresponding controller. For a description
511 * of routing, see @{method:buildController}.
513 * @param list<AphrontRoutingMap> List of routing maps.
514 * @param string Path to route.
515 * @return pair<AphrontController,dict> Controller and dictionary of request
519 private function routePath(array $maps, $path) {
520 foreach ($maps as $map) {
521 $result = $map->routePath($path);
523 return array($result->getController(), $result->getURIData());
528 private function buildSiteForRequest(AphrontRequest
$request) {
529 $sites = PhabricatorSite
::getAllSites();
532 foreach ($sites as $candidate) {
533 $site = $candidate->newSiteForRequest($request);
540 $path = $request->getPath();
541 $host = $request->getHost();
542 throw new AphrontMalformedRequestException(
543 pht('Site Not Found'),
545 'This request asked for "%s" on host "%s", but no site is '.
546 'configured which can serve this request.',
552 $request->setSite($site);
558 /* -( Response Handling )-------------------------------------------------- */
562 * Tests if a response is of a valid type.
564 * @param wild Supposedly valid response.
565 * @return bool True if the object is of a valid type.
568 private function isValidResponseObject($response) {
569 if ($response instanceof AphrontResponse
) {
573 if ($response instanceof AphrontResponseProducerInterface
) {
582 * Verifies that the return value from an @{class:AphrontController} is
583 * of an allowed type.
585 * @param AphrontController Controller which returned the response.
586 * @param wild Supposedly valid response.
590 private function validateControllerResponse(
591 AphrontController
$controller,
594 if ($this->isValidResponseObject($response)) {
600 'Controller "%s" returned an invalid response from call to "%s". '.
601 'This method must return an object of class "%s", or an object '.
602 'which implements the "%s" interface.',
603 get_class($controller),
606 'AphrontResponseProducerInterface'));
611 * Verifies that the return value from an
612 * @{class:AphrontResponseProducerInterface} is of an allowed type.
614 * @param AphrontResponseProducerInterface Object which produced
616 * @param wild Supposedly valid response.
620 private function validateProducerResponse(
621 AphrontResponseProducerInterface
$producer,
624 if ($this->isValidResponseObject($response)) {
630 'Producer "%s" returned an invalid response from call to "%s". '.
631 'This method must return an object of class "%s", or an object '.
632 'which implements the "%s" interface.',
633 get_class($producer),
634 'produceAphrontResponse()',
636 'AphrontResponseProducerInterface'));
641 * Verifies that the return value from an
642 * @{class:AphrontRequestExceptionHandler} is of an allowed type.
644 * @param AphrontRequestExceptionHandler Object which produced this
646 * @param wild Supposedly valid response.
650 private function validateErrorHandlerResponse(
651 AphrontRequestExceptionHandler
$handler,
654 if ($this->isValidResponseObject($response)) {
660 'Exception handler "%s" returned an invalid response from call to '.
661 '"%s". This method must return an object of class "%s", or an object '.
662 'which implements the "%s" interface.',
664 'handleRequestException()',
666 'AphrontResponseProducerInterface'));
671 * Resolves a response object into an @{class:AphrontResponse}.
673 * Controllers are permitted to return actual responses of class
674 * @{class:AphrontResponse}, or other objects which implement
675 * @{interface:AphrontResponseProducerInterface} and can produce a response.
677 * If a controller returns a response producer, invoke it now and produce
680 * @param AphrontRequest Request being handled.
681 * @param AphrontResponse|AphrontResponseProducerInterface Response, or
683 * @return AphrontResponse Response after any required production.
686 private function produceResponse(AphrontRequest
$request, $response) {
687 $original = $response;
689 // Detect cycles on the exact same objects. It's still possible to produce
690 // infinite responses as long as they're all unique, but we can only
691 // reasonably detect cycles, not guarantee that response production halts.
695 // NOTE: It is permissible for an object to be both a response and a
696 // response producer. If so, being a producer is "stronger". This is
697 // used by AphrontProxyResponse.
699 // If this response is a valid response, hand over the request first.
700 if ($response instanceof AphrontResponse
) {
701 $response->setRequest($request);
704 // If this isn't a producer, we're all done.
705 if (!($response instanceof AphrontResponseProducerInterface
)) {
709 $hash = spl_object_hash($response);
710 if (isset($seen[$hash])) {
713 'Failure while producing response for object of class "%s": '.
714 'encountered production cycle (identical object, of class "%s", '.
715 'was produced twice).',
716 get_class($original),
717 get_class($response)));
722 $new_response = $response->produceAphrontResponse();
723 $this->validateProducerResponse($response, $new_response);
724 $response = $new_response;
731 /* -( Error Handling )----------------------------------------------------- */
735 * Convert an exception which has escaped the controller into a response.
737 * This method delegates exception handling to available subclasses of
738 * @{class:AphrontRequestExceptionHandler}.
740 * @param Throwable Exception which needs to be handled.
741 * @return wild Response or response producer, or null if no available
742 * handler can produce a response.
745 private function handleThrowable($throwable) {
746 $handlers = AphrontRequestExceptionHandler
::getAllHandlers();
748 $request = $this->getRequest();
749 foreach ($handlers as $handler) {
750 if ($handler->canHandleRequestThrowable($request, $throwable)) {
751 $response = $handler->handleRequestThrowable($request, $throwable);
752 $this->validateErrorHandlerResponse($handler, $response);
760 private static function newSelfCheckResponse() {
761 $path = PhabricatorStartup
::getRequestPath();
762 $query = idx($_SERVER, 'QUERY_STRING', '');
764 $pairs = id(new PhutilQueryStringParser())
765 ->parseQueryStringToPairList($query);
768 foreach ($pairs as $v) {
775 $raw_input = @file_get_contents
('php://input');
776 if ($raw_input !== false) {
777 $base64_input = base64_encode($raw_input);
779 $base64_input = null;
785 'user' => idx($_SERVER, 'PHP_AUTH_USER'),
786 'pass' => idx($_SERVER, 'PHP_AUTH_PW'),
788 'raw.base64' => $base64_input,
790 // This just makes sure that the response compresses well, so reasonable
791 // algorithms should want to gzip or deflate it.
792 'filler' => str_repeat('Q', 1024 * 16),
795 return id(new AphrontJSONResponse())
796 ->setAddJSONShield(false)
797 ->setContent($result);
800 private static function readHTTPPOSTData() {
801 $request_method = idx($_SERVER, 'REQUEST_METHOD');
802 if ($request_method === 'PUT') {
803 // For PUT requests, do nothing: in particular, do NOT read input. This
804 // allows us to stream input later and process very large PUT requests,
805 // like those coming from Git LFS.
810 // For POST requests, we're going to read the raw input ourselves here
811 // if we can. Among other things, this corrects variable names with
812 // the "." character in them, which PHP normally converts into "_".
814 // If "enable_post_data_reading" is on, the documentation suggests we
815 // can not read the body. In practice, we seem to be able to. This may
816 // need to be resolved at some point, likely by instructing installs
817 // to disable this option.
819 // If the content type is "multipart/form-data", we need to build both
820 // $_POST and $_FILES, which is involved. The body itself is also more
821 // difficult to parse than other requests.
823 $raw_input = PhabricatorStartup
::getRawInput();
824 $parser = new PhutilQueryStringParser();
826 if (strlen($raw_input)) {
827 $content_type = idx($_SERVER, 'CONTENT_TYPE');
828 $is_multipart = preg_match('@^multipart/form-data@i', $content_type);
830 $multipart_parser = id(new AphrontMultipartParser())
831 ->setContentType($content_type);
833 $multipart_parser->beginParse();
834 $multipart_parser->continueParse($raw_input);
835 $parts = $multipart_parser->endParse();
837 // We're building and then parsing a query string so that requests
838 // with arrays (like "x[]=apple&x[]=banana") work correctly. This also
839 // means we can't use "phutil_build_http_querystring()", since it
840 // can't build a query string with duplicate names.
842 $query_string = array();
843 foreach ($parts as $part) {
844 if (!$part->isVariable()) {
848 $name = $part->getName();
849 $value = $part->getVariableValue();
850 $query_string[] = rawurlencode($name).'='.rawurlencode($value);
852 $query_string = implode('&', $query_string);
853 $post = $parser->parseQueryString($query_string);
856 foreach ($parts as $part) {
857 if ($part->isVariable()) {
861 $files[$part->getName()] = $part->getPHPFileDictionary();
865 $post = $parser->parseQueryString($raw_input);
869 PhabricatorStartup
::rebuildRequest();
871 $post = filter_input_array(INPUT_POST
, FILTER_UNSAFE_RAW
);
872 if (is_array($post)) {
874 PhabricatorStartup
::rebuildRequest();