Author: chabotc Date: Fri Oct 16 13:47:21 2009 New Revision: 825896 URL: http://svn.apache.org/viewvc?rev=825896&view=rev Log: SHINDIG-1191 by Arne Roomann-Kurrik - Strip headers from makeRequest requests/responses
Modified: incubator/shindig/trunk/php/src/gadgets/MakeRequest.php incubator/shindig/trunk/php/src/gadgets/MakeRequestHandler.php incubator/shindig/trunk/php/src/gadgets/MakeRequestOptions.php incubator/shindig/trunk/php/src/gadgets/ProxyBase.php incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php incubator/shindig/trunk/php/test/gadgets/MakeRequestTest.php Modified: incubator/shindig/trunk/php/src/gadgets/MakeRequest.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/MakeRequest.php?rev=825896&r1=825895&r2=825896&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/MakeRequest.php (original) +++ incubator/shindig/trunk/php/src/gadgets/MakeRequest.php Fri Oct 16 13:47:21 2009 @@ -39,7 +39,21 @@ */ class MakeRequest { - + /* + * List of disallowed request headers taken from + * /java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpRequestHandler.java + */ + static $BAD_REQUEST_HEADERS = array("HOST", "ACCEPT", "ACCEPT-ENCODING"); + + /* + * List of disallowed response headers taken from + * /java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java + */ + static $BAD_RESPONSE_HEADERS = array( + "SET-COOKIE", "CONTENT-LENGTH", "CONTENT-ENCODING", "ETAG", "LAST-MODIFIED", + "ACCEPT-RANGES", "VARY", "EXPIRES", "DATE", "PRAGMA", "CACHE-CONTROL", + "TRANSFER-ENCODING", "WWW-AUTHENTICATE"); + private $remoteFetcher; /** @@ -100,10 +114,27 @@ if (strpos($result->getResponseContent(), '\u')) { $result->setResponseContent($this->decodeUtf8($result->getResponseContent())); } + return $result; } /** + * Returns a response header array with invalid headers removed. + * @param array $headers An associative array of header/value pairs. + * The reason this cleaning is not automatic is because some consumers of + * MakeRequest may have need to access the stripped headers before + * delivering the response to a client. The reason for removing these headers + * is also mostly for performance, rather than security. + * + * @return array An array with the headers defined in + * MakeRequest::$BAD_RESPONSE_HEADERS removed. The removal is + * case-insensitive. + */ + public function cleanResponseHeaders($headers) { + return $this->stripInvalidArrayKeys($headers, MakeRequest::$BAD_RESPONSE_HEADERS); + } + + /** * Builds a request to retrieve the actual content. * * @param GadgetContext $context The rendering context. @@ -148,16 +179,33 @@ $token = $context->validateToken($st, $signer); $request->setToken($token); } - $headers = $params->getFormattedRequestHeaders(); + + // Strip invalid request headers. This limits the utility of the + // MakeRequest class a little bit, but ensures that none of the invalid + // headers are present in any request going through this class. + $headers = $params->getRequestHeadersArray(); if ($headers !== false) { - // The request expects headers to be stored as a normal header text blob. - // ex: Content-Type: application/atom+xml - // Accept-Language: en-us - $request->setHeaders($headers); + $headers = $this->stripInvalidArrayKeys($headers, MakeRequest::$BAD_REQUEST_HEADERS); + $params->setRequestHeaders($headers); + } + + // The request expects headers to be stored as a normal header text blob. + // ex: Content-Type: application/atom+xml + // Accept-Language: en-us + $formattedHeaders = $params->getFormattedRequestHeaders(); + if ($formattedHeaders !== false) { + $request->setHeaders($formattedHeaders); } + return $request; } + /** + * Decodes UTF-8 numeric codes (&#xXXXX, or \uXXXX) from a content string. + * @param string $content The content string to decode. + * @return string A UTF-8 string where numeric codes have been converted into + * their UTF character representations. + */ public function decodeUtf8($content) { if (preg_match("/&#[xX][0-9a-zA-Z]{2,8};/", $content)) { $content = preg_replace("/&#[xX]([0-9a-zA-Z]{2,8});/e", "'&#'.hexdec('$1').';'", $content); @@ -169,6 +217,29 @@ } /** + * Removes any invalid keys from an array in a case insensitive manner. + * Used for stripping invalid http headers from a request or response. + * + * @param array $target The associative array to check for invalid keys. + * @param array $invalidKeys An array of keys which are invalid. + * @return array A copy of $target with any keys matching a value in + * $invalidKeys removed. + */ + private function stripInvalidArrayKeys($target, $invalidKeys) { + $cleaned = array(); + $upperInvalidKeys = array_map('strtoupper', $invalidKeys); + + foreach ($target as $key => $value) { + $upperKey = strtoupper($key); + if (in_array($upperKey, $upperInvalidKeys) === FALSE) { + $cleaned[$key] = $value; + } + } + + return $cleaned; + } + + /** * Handles (RSS & Atom) Type.FEED parsing using Zend's feed parser * * @return response string, either a json encoded feed structure or an error message Modified: incubator/shindig/trunk/php/src/gadgets/MakeRequestHandler.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/MakeRequestHandler.php?rev=825896&r1=825895&r2=825896&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/MakeRequestHandler.php (original) +++ incubator/shindig/trunk/php/src/gadgets/MakeRequestHandler.php Fri Oct 16 13:47:21 2009 @@ -47,9 +47,8 @@ $result = $this->makeRequest->fetch($this->context, $params); $responseArray = array( 'rc' => (int)$result->getHttpCode(), - 'body' => $result->getResponseContent() - //FIXME: the spec seems to state the response should contain the headers, however java shindig doesn't (and it's a waste of bandwidth 99% of the time), check to see what is correct - //'headers' => $result->getResponseHeaders() + 'body' => $result->getResponseContent(), + 'headers' => $this->makeRequest->cleanResponseHeaders($result->getResponseHeaders()) ); $responseArray = array_merge($responseArray, $result->getMetadatas()); $json = array($params->getHref() => $responseArray); Modified: incubator/shindig/trunk/php/src/gadgets/MakeRequestOptions.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/MakeRequestOptions.php?rev=825896&r1=825895&r2=825896&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/MakeRequestOptions.php (original) +++ incubator/shindig/trunk/php/src/gadgets/MakeRequestOptions.php Fri Oct 16 13:47:21 2009 @@ -206,6 +206,50 @@ } /** + * Builds a MakeRequestOptions object from a RequestItem instance. This is + * a helper for dealing with Handler services which need to call MakeRequest. + * The parameter names were taken from the osapi.http spec documents, although + * several parameters not in the spec are also supported to allow full + * functionality. + * + * @param RpcRequestItem $request The RpcRequestItem to parse. The reason + * RpcRequestItem is needed is because of the way getService() and + * getMethod() are overloaded in the RequestItem subclasses. This + * function needs a reliable way to get the http method. + * @return MakeRequestOptions An object initialized from the current request. + * @throws MakeRequestParameterException If any of the parameters were + * invalid. + */ + public static function fromRpcRequestItem(RpcRequestItem $request) { + $href = $request->getParameter('href'); + if (!isset($href)) { + $href = $request->getParameter('url'); + } + + $options = new MakeRequestOptions($href); + $options->setHttpMethod($request->getMethod()) + ->setRequestBody($request->getParameter('body')) + ->setRequestHeaders($request->getParameter('headers')) + ->setResponseFormat($request->getParameter('format')) + ->setAuthz($request->getParameter('authz')) + ->setSignViewer($request->getParameter('sign_viewer')) + ->setSignOwner($request->getParameter('sign_owner')) + ->setNumEntries($request->getParameter('numEntries')) // Not in osapi.http spec, but nice to support + ->setGetSummaries($request->getParameter('getSummaries')) // Not in osapi.http spec, but nice to support + ->setRefreshInterval($request->getParameter('refreshInterval')) + ->setNoCache($request->getParameter('nocache')) // Not in osapi.http spec, but nice to support + ->setOAuthServiceName($request->getParameter('oauth_service_name')) + ->setOAuthTokenName($request->getParameter('oauth_token_name')) + ->setOAuthRequestToken($request->getParameter('oauth_request_token')) + ->setOAuthRequestTokenSecret($request->getParameter('oauth_request_token_secret')) + ->setOAuthUseToken($request->getParameter('oauth_use_token')) + ->setOAuthClientState($request->getParameter('oauth_state')) // Not in osapi.http spec, but nice to support + ->setSecurityTokenString(urlencode(base64_encode($request->getToken()->toSerialForm()))); + + return $options; + } + + /** * Gets the configured URL. * * @return string The value of this parameter. @@ -323,6 +367,19 @@ } /** + * Returns the request headers as an array. + * + * @return array The request header array. + */ + public function getRequestHeadersArray() { + if (isset($this->headers)) { + return $this->headers; + } else { + return false; + } + } + + /** * Sets the expected response format for this type of request. Valid values * are one of {...@link MakeRequestOptions::$VALID_OUTPUT_FORMATS}. * Modified: incubator/shindig/trunk/php/src/gadgets/ProxyBase.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/ProxyBase.php?rev=825896&r1=825895&r2=825896&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/ProxyBase.php (original) +++ incubator/shindig/trunk/php/src/gadgets/ProxyBase.php Fri Oct 16 13:47:21 2009 @@ -39,29 +39,6 @@ } /** - * Retrieves the actual content - * - * @param string $url the url to fetch - * @param string $method http method - * @param SecurityTokenDecoder $signer - * @return RemoteContentRequest the filled in request (RemoteContentRequest) - */ - protected function buildRequest($url, $method = 'GET', $signer = null) { - // TODO: Check to see if we can just use MakeRequestOptions::fromCurrentRequest - $st = isset($_GET['st']) ? $_GET['st'] : (isset($_POST['st']) ? $_POST['st'] : false); - $body = isset($_GET['postData']) ? $_GET['postData'] : (isset($_POST['postData']) ? $_POST['postData'] : false); - $authz = isset($_GET['authz']) ? $_GET['authz'] : (isset($_POST['authz']) ? $_POST['authz'] : null); - $headers = isset($_GET['headers']) ? $_GET['headers'] : (isset($_POST['headers']) ? $_POST['headers'] : null); - $params = new MakeRequestOptions($url); - $params->setSecurityTokenString($st) - ->setAuthz($authz) - ->setRequestBody($body) - ->setHttpMethod($method) - ->setFormEncodedRequestHeaders($headers); - return $this->makeRequest->buildRequest($this->context, $params, $signer); - } - - /** * Sets the caching (Cache-Control & Expires) with a cache age of $lastModified * or if $lastModified === false, sets Pragma: no-cache & Cache-Control: no-cache */ @@ -85,16 +62,6 @@ } /** - * Does a quick-and-dirty url validation - * - * @param string $url - * @return string the 'validated' url - */ - protected function validateUrl($url) { - return MakeRequestOptions::validateUrl($url); - } - - /** * Returns the request headers, using the apache_request_headers function if it's * available, and otherwise tries to guess them from the $_SERVER superglobal * Modified: incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php?rev=825896&r1=825895&r2=825896&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php (original) +++ incubator/shindig/trunk/php/src/gadgets/ProxyHandler.php Fri Oct 16 13:47:21 2009 @@ -25,10 +25,6 @@ * */ class ProxyHandler extends ProxyBase { - protected $disallowedHeaders = array('User-Agent', 'Keep-Alive', 'Host', 'Accept-Encoding', 'Set-Cookie', - 'Content-Length', 'Content-Encoding', 'ETag', 'Last-Modified', 'Accept-Ranges', 'Vary', - 'Expires', 'Date', 'Pragma', 'Cache-Control', 'Transfer-Encoding', 'If-Modified-Since'); - /** * Fetches the content and returns it as-is using the headers as returned * by the remote host. @@ -36,17 +32,27 @@ * @param string $url the url to retrieve */ public function fetch($url) { - $url = $this->validateUrl($url); - $request = $this->buildRequest($url, 'GET'); - $request->getOptions()->ignoreCache = $this->context->getIgnoreCache(); - $result = $this->context->getHttpFetcher()->fetch($request); + // TODO: Check to see if we can just use MakeRequestOptions::fromCurrentRequest + $st = isset($_GET['st']) ? $_GET['st'] : (isset($_POST['st']) ? $_POST['st'] : false); + $body = isset($_GET['postData']) ? $_GET['postData'] : (isset($_POST['postData']) ? $_POST['postData'] : false); + $authz = isset($_GET['authz']) ? $_GET['authz'] : (isset($_POST['authz']) ? $_POST['authz'] : null); + $headers = isset($_GET['headers']) ? $_GET['headers'] : (isset($_POST['headers']) ? $_POST['headers'] : null); + $params = new MakeRequestOptions($url); + $params->setSecurityTokenString($st) + ->setAuthz($authz) + ->setRequestBody($body) + ->setHttpMethod('GET') + ->setFormEncodedRequestHeaders($headers) + ->setNoCache($this->context->getIgnoreCache()); + + $result = $this->makeRequest->fetch($this->context, $params); $httpCode = (int)$result->getHttpCode(); + $cleanedResponseHeaders = $this->makeRequest->cleanResponseHeaders($result->getResponseHeaders()); $isShockwaveFlash = false; - foreach ($result->getResponseHeaders() as $key => $val) { - if (! in_array($key, $this->disallowedHeaders)) { - header("$key: $val", true); - } - if ($key == 'Content-Type' && strtolower($val) == 'application/x-shockwave-flash') { + + foreach ($cleanedResponseHeaders as $key => $val) { + header("$key: $val", true); + if (strtoupper($key) == 'CONTENT-TYPE' && strtolower($val) == 'application/x-shockwave-flash') { // We're skipping the content disposition header for flash due to an issue with Flash player 10 // This does make some sites a higher value phishing target, but this can be mitigated by // additional referer checks. Modified: incubator/shindig/trunk/php/test/gadgets/MakeRequestTest.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/test/gadgets/MakeRequestTest.php?rev=825896&r1=825895&r2=825896&view=diff ============================================================================== --- incubator/shindig/trunk/php/test/gadgets/MakeRequestTest.php (original) +++ incubator/shindig/trunk/php/test/gadgets/MakeRequestTest.php Fri Oct 16 13:47:21 2009 @@ -233,6 +233,33 @@ } /** + * Tests that setting invalid request headers are not passed in the outgoing + * request. + */ + public function testInvalidRequestHeaders(){ + $params = new MakeRequestOptions('http://www.example.com'); + $params->setRequestHeaders(array( + "Content-Type" => "application/json", + "Accept-Language" => "en-us", + "Host" => "http://www.evil.com", + "host" => "http://www.evil.com", + "HOST" => "http://www.evil.com", + "Accept" => "blah", + "Accept-Encoding" => "blah" + )); + $params->setNoCache(true); + + $request = $this->catchRequest($params, $this->response); + $this->assertTrue($request->hasHeaders()); + $this->assertEquals('application/json', $request->getHeader('Content-Type')); + $this->assertEquals('en-us', $request->getHeader('Accept-Language')); + + $this->assertNull($request->getHeader('Host')); + $this->assertNull($request->getHeader('Accept')); + $this->assertNull($request->getHeader('Accept-Encoding')); + } + + /** * Tests that setting request headers in a form urlencoded way are passed in the outgoing request. */ public function testFormEncodedRequestHeaders(){ @@ -244,5 +271,57 @@ $this->assertTrue($request->hasHeaders()); $this->assertEquals('application/x-www-form-urlencoded', $request->getHeader('Content-Type')); } + + public function testResponseHeaders() { + $params = new MakeRequestOptions('http://www.example.com'); + $params->setNoCache(true); + + $headers = array( + 'Content-Type' => 'text/plain' + ); + $this->response->setResponseHeaders($headers); + $this->fetcher->enqueueResponse($this->response); + + $result = $this->makeRequest->fetch($this->context, $params); + $response_headers = $result->getResponseHeaders(); + + $this->assertArrayHasKey('Content-Type', $response_headers); + $this->assertEquals('text/plain', $response_headers['Content-Type']); + } + + public function testCleanResponseHeaders() { + $response_headers = array( + 'Content-Type' => 'text/plain', + 'Set-Cookie' => 'blah', + 'set-cookie' => 'blah', + 'SET-COOKIE' => 'blah', + 'sEt-cOoKiE' => 'blah', + 'Accept-Ranges' => 'blah', + 'Vary' => 'blah', + 'Expires' => 'blah', + 'Date' => 'blah', + 'Pragma' => 'blah', + 'Cache-Control' => 'blah', + 'Transfer-Encoding' => 'blah', + 'WWW-Authenticate' => 'blah' + ); + + $cleaned_headers = $this->makeRequest->cleanResponseHeaders($response_headers); + + $this->assertArrayHasKey('Content-Type', $cleaned_headers); + $this->assertEquals('text/plain', $cleaned_headers['Content-Type']); + $this->assertArrayNotHasKey('Set-Cookie', $cleaned_headers); + $this->assertArrayNotHasKey('set-cookie', $cleaned_headers); + $this->assertArrayNotHasKey('SET-COOKIE', $cleaned_headers); + $this->assertArrayNotHasKey('sEt-cOoKiE', $cleaned_headers); + $this->assertArrayNotHasKey('Accept-Ranges', $cleaned_headers); + $this->assertArrayNotHasKey('Vary', $cleaned_headers); + $this->assertArrayNotHasKey('Expires', $cleaned_headers); + $this->assertArrayNotHasKey('Date', $cleaned_headers); + $this->assertArrayNotHasKey('Pragma', $cleaned_headers); + $this->assertArrayNotHasKey('Cache-Control', $cleaned_headers); + $this->assertArrayNotHasKey('Transfer-Encoding', $cleaned_headers); + $this->assertArrayNotHasKey('WWW-Authenticate', $cleaned_headers); + } }