diff --git a/src/aphront/requeststream/AphrontRequestStream.php b/src/aphront/requeststream/AphrontRequestStream.php index 6bd27712ed..009451c3ad 100644 --- a/src/aphront/requeststream/AphrontRequestStream.php +++ b/src/aphront/requeststream/AphrontRequestStream.php @@ -1,112 +1,113 @@ 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)) { + if (!strncasecmp($filter, 'zlib.', strlen('zlib.'))) { $has_zlib = true; + break; } } return $has_zlib; } } diff --git a/src/aphront/response/AphrontJSONResponse.php b/src/aphront/response/AphrontJSONResponse.php index 3d1c429d41..228a1a1721 100644 --- a/src/aphront/response/AphrontJSONResponse.php +++ b/src/aphront/response/AphrontJSONResponse.php @@ -1,41 +1,41 @@ content = $content; return $this; } public function setAddJSONShield($should_add) { $this->addJSONShield = $should_add; return $this; } public function shouldAddJSONShield() { if ($this->addJSONShield === null) { return true; } return (bool)$this->addJSONShield; } public function buildResponseString() { $response = $this->encodeJSONForHTTPResponse($this->content); if ($this->shouldAddJSONShield()) { $response = $this->addJSONShield($response); } return $response; } public function getHeaders() { - $headers = array( - array('Content-Type', 'application/json'), - ); - $headers = array_merge(parent::getHeaders(), $headers); + $headers = parent::getHeaders(); + + $headers[] = array('Content-Type', 'application/json'); + return $headers; } } diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 8a94adf38d..5dae168c73 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -1,440 +1,449 @@ request = $request; return $this; } public function getRequest() { return $this->request; } final public function addContentSecurityPolicyURI($kind, $uri) { if ($this->contentSecurityPolicyURIs === null) { $this->contentSecurityPolicyURIs = array( 'script-src' => array(), 'connect-src' => array(), 'frame-src' => array(), 'form-action' => array(), 'object-src' => array(), ); } if (!isset($this->contentSecurityPolicyURIs[$kind])) { throw new Exception( pht( 'Unknown Content-Security-Policy URI kind "%s".', $kind)); } $this->contentSecurityPolicyURIs[$kind][] = (string)$uri; return $this; } final public function setDisableContentSecurityPolicy($disable) { $this->disableContentSecurityPolicy = $disable; return $this; } + final public function addHeader($key, $value) { + $this->headers[] = array($key, $value); + return $this; + } + /* -( Content )------------------------------------------------------------ */ public function getContentIterator() { // By default, make sure responses are truly returning a string, not some // kind of object that behaves like a string. // We're going to remove the execution time limit before dumping the // response into the sink, and want any rendering that's going to occur // to happen BEFORE we release the limit. return array( (string)$this->buildResponseString(), ); } public function buildResponseString() { throw new PhutilMethodNotImplementedException(); } /* -( Metadata )----------------------------------------------------------- */ public function getHeaders() { $headers = array(); if (!$this->frameable) { $headers[] = array('X-Frame-Options', 'Deny'); } if ($this->getRequest() && $this->getRequest()->isHTTPS()) { $hsts_key = 'security.strict-transport-security'; $use_hsts = PhabricatorEnv::getEnvConfig($hsts_key); if ($use_hsts) { $duration = phutil_units('365 days in seconds'); } else { // If HSTS has been disabled, tell browsers to turn it off. This may // not be effective because we can only disable it over a valid HTTPS // connection, but it best represents the configured intent. $duration = 0; } $headers[] = array( 'Strict-Transport-Security', "max-age={$duration}; includeSubdomains; preload", ); } $csp = $this->newContentSecurityPolicyHeader(); if ($csp !== null) { $headers[] = array('Content-Security-Policy', $csp); } $headers[] = array('Referrer-Policy', 'no-referrer'); + foreach ($this->headers as $header) { + $headers[] = $header; + } + return $headers; } private function newContentSecurityPolicyHeader() { if ($this->disableContentSecurityPolicy) { return null; } // NOTE: We may return a response during preflight checks (for example, // if a user has a bad version of PHP). // In this case, setup isn't complete yet and we can't access environmental // configuration. If we aren't able to read the environment, just decline // to emit a Content-Security-Policy header. try { $cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); $base_uri = PhabricatorEnv::getURI('/'); } catch (Exception $ex) { return null; } $csp = array(); if ($cdn) { $default = $this->newContentSecurityPolicySource($cdn); } else { // If an alternate file domain is not configured and the user is viewing // a Phame blog on a custom domain or some other custom site, we'll still // serve resources from the main site. Include the main site explicitly. $base_uri = $this->newContentSecurityPolicySource($base_uri); $default = "'self' {$base_uri}"; } $csp[] = "default-src {$default}"; // We use "data:" URIs to inline small images into CSS. This policy allows // "data:" URIs to be used anywhere, but there doesn't appear to be a way // to say that "data:" URIs are okay in CSS files but not in the document. $csp[] = "img-src {$default} data:"; // We use inline style="..." attributes in various places, many of which // are legitimate. We also currently use a