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);
+  }
 }
 


Reply via email to