Path: blob/master/src/aphront/response/AphrontResponse.php
12241 views
<?php12abstract class AphrontResponse extends Phobject {34private $request;5private $cacheable = false;6private $canCDN;7private $responseCode = 200;8private $lastModified = null;9private $contentSecurityPolicyURIs;10private $disableContentSecurityPolicy;11protected $frameable;12private $headers = array();1314public function setRequest($request) {15$this->request = $request;16return $this;17}1819public function getRequest() {20return $this->request;21}2223final public function addContentSecurityPolicyURI($kind, $uri) {24if ($this->contentSecurityPolicyURIs === null) {25$this->contentSecurityPolicyURIs = array(26'script-src' => array(),27'connect-src' => array(),28'frame-src' => array(),29'form-action' => array(),30'object-src' => array(),31);32}3334if (!isset($this->contentSecurityPolicyURIs[$kind])) {35throw new Exception(36pht(37'Unknown Content-Security-Policy URI kind "%s".',38$kind));39}4041$this->contentSecurityPolicyURIs[$kind][] = (string)$uri;4243return $this;44}4546final public function setDisableContentSecurityPolicy($disable) {47$this->disableContentSecurityPolicy = $disable;48return $this;49}5051final public function addHeader($key, $value) {52$this->headers[] = array($key, $value);53return $this;54}555657/* -( Content )------------------------------------------------------------ */585960public function getContentIterator() {61// By default, make sure responses are truly returning a string, not some62// kind of object that behaves like a string.6364// We're going to remove the execution time limit before dumping the65// response into the sink, and want any rendering that's going to occur66// to happen BEFORE we release the limit.6768return array(69(string)$this->buildResponseString(),70);71}7273public function buildResponseString() {74throw new PhutilMethodNotImplementedException();75}767778/* -( Metadata )----------------------------------------------------------- */798081public function getHeaders() {82$headers = array();83if (!$this->frameable) {84$headers[] = array('X-Frame-Options', 'Deny');85}8687if ($this->getRequest() && $this->getRequest()->isHTTPS()) {88$hsts_key = 'security.strict-transport-security';89$use_hsts = PhabricatorEnv::getEnvConfig($hsts_key);90if ($use_hsts) {91$duration = phutil_units('365 days in seconds');92} else {93// If HSTS has been disabled, tell browsers to turn it off. This may94// not be effective because we can only disable it over a valid HTTPS95// connection, but it best represents the configured intent.96$duration = 0;97}9899$headers[] = array(100'Strict-Transport-Security',101"max-age={$duration}; includeSubdomains; preload",102);103}104105$csp = $this->newContentSecurityPolicyHeader();106if ($csp !== null) {107$headers[] = array('Content-Security-Policy', $csp);108}109110$headers[] = array('Referrer-Policy', 'no-referrer');111112foreach ($this->headers as $header) {113$headers[] = $header;114}115116return $headers;117}118119private function newContentSecurityPolicyHeader() {120if ($this->disableContentSecurityPolicy) {121return null;122}123124// NOTE: We may return a response during preflight checks (for example,125// if a user has a bad version of PHP).126127// In this case, setup isn't complete yet and we can't access environmental128// configuration. If we aren't able to read the environment, just decline129// to emit a Content-Security-Policy header.130131try {132$cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');133$base_uri = PhabricatorEnv::getURI('/');134} catch (Exception $ex) {135return null;136}137138$csp = array();139if ($cdn) {140$default = $this->newContentSecurityPolicySource($cdn);141} else {142// If an alternate file domain is not configured and the user is viewing143// a Phame blog on a custom domain or some other custom site, we'll still144// serve resources from the main site. Include the main site explicitly.145$base_uri = $this->newContentSecurityPolicySource($base_uri);146147$default = "'self' {$base_uri}";148}149150$csp[] = "default-src {$default}";151152// We use "data:" URIs to inline small images into CSS. This policy allows153// "data:" URIs to be used anywhere, but there doesn't appear to be a way154// to say that "data:" URIs are okay in CSS files but not in the document.155$csp[] = "img-src {$default} data:";156157// We use inline style="..." attributes in various places, many of which158// are legitimate. We also currently use a <style> tag to implement the159// "Monospaced Font Preference" setting.160$csp[] = "style-src {$default} 'unsafe-inline'";161162// On a small number of pages, including the Stripe workflow and the163// ReCAPTCHA challenge, we embed external Javascript directly.164$csp[] = $this->newContentSecurityPolicy('script-src', $default);165166// We need to specify that we can connect to ourself in order for AJAX167// requests to work.168$csp[] = $this->newContentSecurityPolicy('connect-src', "'self'");169170// DarkConsole and PHPAST both use frames to render some content.171$csp[] = $this->newContentSecurityPolicy('frame-src', "'self'");172173// This is a more modern flavor of of "X-Frame-Options" and prevents174// clickjacking attacks where the page is included in a tiny iframe and175// the user is convinced to click a element on the page, which really176// clicks a dangerous button hidden under a picture of a cat.177if ($this->frameable) {178$csp[] = "frame-ancestors 'self'";179} else {180$csp[] = "frame-ancestors 'none'";181}182183// Block relics of the old world: Flash, Java applets, and so on. Note184// that Chrome prevents the user from viewing PDF documents if they are185// served with a policy which excludes the domain they are served from.186$csp[] = $this->newContentSecurityPolicy('object-src', "'none'");187188// Don't allow forms to submit offsite.189190// This can result in some trickiness with file downloads if applications191// try to start downloads by submitting a dialog. Redirect to the file's192// download URI instead of submitting a form to it.193$csp[] = $this->newContentSecurityPolicy('form-action', "'self'");194195// Block use of "<base>" to change the origin of relative URIs on the page.196$csp[] = "base-uri 'none'";197198$csp = implode('; ', $csp);199200return $csp;201}202203private function newContentSecurityPolicy($type, $defaults) {204if ($defaults === null) {205$sources = array();206} else {207$sources = (array)$defaults;208}209210$uris = $this->contentSecurityPolicyURIs;211if (isset($uris[$type])) {212foreach ($uris[$type] as $uri) {213$sources[] = $this->newContentSecurityPolicySource($uri);214}215}216$sources = array_unique($sources);217218return $type.' '.implode(' ', $sources);219}220221private function newContentSecurityPolicySource($uri) {222// Some CSP URIs are ultimately user controlled (like notification server223// URIs and CDN URIs) so attempt to stop an attacker from injecting an224// unsafe source (like 'unsafe-eval') into the CSP header.225226$uri = id(new PhutilURI($uri))227->setPath(null)228->setFragment(null)229->removeAllQueryParams();230231$uri = (string)$uri;232if (preg_match('/[ ;\']/', $uri)) {233throw new Exception(234pht(235'Attempting to emit a response with an unsafe source ("%s") in the '.236'Content-Security-Policy header.',237$uri));238}239240return $uri;241}242243public function setCacheDurationInSeconds($duration) {244$this->cacheable = $duration;245return $this;246}247248public function setCanCDN($can_cdn) {249$this->canCDN = $can_cdn;250return $this;251}252253public function setLastModified($epoch_timestamp) {254$this->lastModified = $epoch_timestamp;255return $this;256}257258public function setHTTPResponseCode($code) {259$this->responseCode = $code;260return $this;261}262263public function getHTTPResponseCode() {264return $this->responseCode;265}266267public function getHTTPResponseMessage() {268switch ($this->getHTTPResponseCode()) {269case 100: return 'Continue';270case 101: return 'Switching Protocols';271case 200: return 'OK';272case 201: return 'Created';273case 202: return 'Accepted';274case 203: return 'Non-Authoritative Information';275case 204: return 'No Content';276case 205: return 'Reset Content';277case 206: return 'Partial Content';278case 300: return 'Multiple Choices';279case 301: return 'Moved Permanently';280case 302: return 'Found';281case 303: return 'See Other';282case 304: return 'Not Modified';283case 305: return 'Use Proxy';284case 306: return 'Switch Proxy';285case 307: return 'Temporary Redirect';286case 400: return 'Bad Request';287case 401: return 'Unauthorized';288case 402: return 'Payment Required';289case 403: return 'Forbidden';290case 404: return 'Not Found';291case 405: return 'Method Not Allowed';292case 406: return 'Not Acceptable';293case 407: return 'Proxy Authentication Required';294case 408: return 'Request Timeout';295case 409: return 'Conflict';296case 410: return 'Gone';297case 411: return 'Length Required';298case 412: return 'Precondition Failed';299case 413: return 'Request Entity Too Large';300case 414: return 'Request-URI Too Long';301case 415: return 'Unsupported Media Type';302case 416: return 'Requested Range Not Satisfiable';303case 417: return 'Expectation Failed';304case 418: return "I'm a teapot";305case 426: return 'Upgrade Required';306case 500: return 'Internal Server Error';307case 501: return 'Not Implemented';308case 502: return 'Bad Gateway';309case 503: return 'Service Unavailable';310case 504: return 'Gateway Timeout';311case 505: return 'HTTP Version Not Supported';312default: return '';313}314}315316public function setFrameable($frameable) {317$this->frameable = $frameable;318return $this;319}320321public static function processValueForJSONEncoding(&$value, $key) {322if ($value instanceof PhutilSafeHTMLProducerInterface) {323// This renders the producer down to PhutilSafeHTML, which will then324// be simplified into a string below.325$value = hsprintf('%s', $value);326}327328if ($value instanceof PhutilSafeHTML) {329// TODO: Javelin supports implicity conversion of '__html' objects to330// JX.HTML, but only for Ajax responses, not behaviors. Just leave things331// as they are for now (where behaviors treat responses as HTML or plain332// text at their discretion).333$value = $value->getHTMLContent();334}335}336337public static function encodeJSONForHTTPResponse(array $object) {338339array_walk_recursive(340$object,341array(__CLASS__, 'processValueForJSONEncoding'));342343$response = phutil_json_encode($object);344345// Prevent content sniffing attacks by encoding "<" and ">", so browsers346// won't try to execute the document as HTML even if they ignore347// Content-Type and X-Content-Type-Options. See T865.348$response = str_replace(349array('<', '>'),350array('\u003c', '\u003e'),351$response);352353return $response;354}355356protected function addJSONShield($json_response) {357// Add a shield to prevent "JSON Hijacking" attacks where an attacker358// requests a JSON response using a normal <script /> tag and then uses359// Object.prototype.__defineSetter__() or similar to read response data.360// This header causes the browser to loop infinitely instead of handing over361// sensitive data.362363$shield = 'for (;;);';364365$response = $shield.$json_response;366367return $response;368}369370public function getCacheHeaders() {371$headers = array();372if ($this->cacheable) {373$cache_control = array();374$cache_control[] = sprintf('max-age=%d', $this->cacheable);375376if ($this->canCDN) {377$cache_control[] = 'public';378} else {379$cache_control[] = 'private';380}381382$headers[] = array(383'Cache-Control',384implode(', ', $cache_control),385);386387$headers[] = array(388'Expires',389$this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable),390);391} else {392$headers[] = array(393'Cache-Control',394'no-store',395);396$headers[] = array(397'Expires',398'Sat, 01 Jan 2000 00:00:00 GMT',399);400}401402if ($this->lastModified) {403$headers[] = array(404'Last-Modified',405$this->formatEpochTimestampForHTTPHeader($this->lastModified),406);407}408409// IE has a feature where it may override an explicit Content-Type410// declaration by inferring a content type. This can be a security risk411// and we always explicitly transmit the correct Content-Type header, so412// prevent IE from using inferred content types. This only offers protection413// on recent versions of IE; IE6/7 and Opera currently ignore this header.414$headers[] = array('X-Content-Type-Options', 'nosniff');415416return $headers;417}418419private function formatEpochTimestampForHTTPHeader($epoch_timestamp) {420return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';421}422423protected function shouldCompressResponse() {424return true;425}426427public function willBeginWrite() {428// If we've already sent headers, these "ini_set()" calls will warn that429// they have no effect. Today, this always happens because we're inside430// a unit test, so just skip adjusting the setting.431432if (!headers_sent()) {433if ($this->shouldCompressResponse()) {434// Enable automatic compression here. Webservers sometimes do this for435// us, but we now detect the absence of compression and warn users about436// it so try to cover our bases more thoroughly.437ini_set('zlib.output_compression', 1);438} else {439ini_set('zlib.output_compression', 0);440}441}442}443444public function didCompleteWrite($aborted) {445return;446}447448}449450451