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