BUG Do not rely on broken curl header size calculation.

Header parsing now takes into account situations like a proxy or
redirections. Works around the curl issue.

Also fixes the issue when a redirected request would cause a double
amount of headers coming out of the parser - it would merrily process
anything that's in key:value format even if it was two distinct headers.
This commit is contained in:
Mateusz Uzdowski 2013-09-20 14:20:35 +12:00
parent eb3cd197ac
commit 7ddd5b57c3
2 changed files with 131 additions and 10 deletions

View File

@ -308,7 +308,8 @@ class RestfulService extends ViewableData {
}
/**
* Extracts the response body and headers from a full curl response
* Build the response from raw data. The response could have multiple redirection
* and proxy connect headers, so we are only interested in the last header before the body.
*
* @param curl_handle $ch The curl handle for the request
* @param string $rawResponse The raw response text
@ -322,15 +323,21 @@ class RestfulService extends ViewableData {
$curlError = curl_error($ch);
//normalise the status code
if(curl_error($ch) !== '' || $statusCode == 0) $statusCode = 500;
//calculate the length of the header and extract it
$headerLength = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($rawResponse, 0, $headerLength);
//extract the body
$body = substr($rawResponse, $headerLength);
//parse the headers
$headers = $this->parseRawHeaders($rawHeaders);
//return the response object
return new RestfulService_Response($body, $statusCode, $headers);
// Parse the headers and body from the response.
// We cannot rely on CURLINFO_HEADER_SIZE here, it's miscalculated when connecting via
// a proxy (see http://sourceforge.net/p/curl/bugs/1204/). This is fixed in curl 7.30.0.
$headerParts = array();
$parts = explode("\r\n\r\n", $rawResponse);
while (isset($parts[0])) {
if (strpos($parts[0], 'HTTP/')===0) $headerParts[] = array_shift($parts);
else break; // We have reached the body.
}
$lastHeader = array_pop($headerParts);
$body = implode("\r\n\r\n", $parts);
$parsedHeader = $this->parseRawHeaders($lastHeader);
return new RestfulService_Response($body, $statusCode, $parsedHeader);
}
/**

View File

@ -196,6 +196,120 @@ class RestfulServiceTest extends SapphireTest {
);
}
public function testExtractResponseRedirectionAndProxy() {
// This is an example of real raw response for a request via a proxy that gets redirected.
$rawResponse =
"HTTP/1.0 200 Connection established\r\n" .
"\r\n" .
"HTTP/1.1 301 Moved Permanently\r\n" .
"Server: nginx\r\n" .
"Date: Fri, 20 Sep 2013 01:53:07 GMT\r\n" .
"Content-Type: text/html\r\n" .
"Content-Length: 178\r\n" .
"Connection: keep-alive\r\n" .
"Location: https://www.foobar.org.nz/\r\n" .
"\r\n" .
"HTTP/1.0 200 Connection established\r\n" .
"\r\n" .
"HTTP/1.1 200 OK\r\n" .
"Server: nginx\r\n" .
"Date: Fri, 20 Sep 2013 01:53:08 GMT\r\n" .
"Content-Type: text/html; charset=utf-8\r\n" .
"Transfer-Encoding: chunked\r\n" .
"Connection: keep-alive\r\n" .
"X-Frame-Options: SAMEORIGIN\r\n" .
"Cache-Control: no-cache, max-age=0, must-revalidate, no-transform\r\n" .
"Vary: Accept-Encoding\r\n" .
"\r\n" .
"<!doctype html></html>";
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
$headerFunction->setAccessible(true);
$ch = curl_init();
$response = $headerFunction->invoke(
new RestfulService(Director::absoluteBaseURL(),0),
$ch,
$rawResponse
);
$this->assertEquals($response->getBody(), '<!doctype html></html>', 'Body is correctly extracted.');
$this->assertEquals(
$response->getHeaders(),
array(
'Server' => "nginx",
'Date' => "Fri, 20 Sep 2013 01:53:08 GMT",
'Content-Type' => "text/html; charset=utf-8",
'Transfer-Encoding' => "chunked",
'Connection' => "keep-alive",
'X-Frame-Options' => "SAMEORIGIN",
'Cache-Control' => "no-cache, max-age=0, must-revalidate, no-transform",
'Vary' => "Accept-Encoding"
),
'Only last header is extracted and parsed.'
);
}
public function testExtractResponseNewlinesInBody() {
$rawResponse =
"HTTP/1.1 200 OK\r\n" .
"Server: nginx\r\n" .
"\r\n" .
"<!doctype html>\r\n" .
"\r\n" .
"</html>";
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
$headerFunction->setAccessible(true);
$ch = curl_init();
$response = $headerFunction->invoke(
new RestfulService(Director::absoluteBaseURL(),0),
$ch,
$rawResponse
);
$this->assertEquals($response->getBody(), "<!doctype html>\r\n\r\n</html>", 'Body is correctly extracted.');
$this->assertEquals($response->getHeaders(), array('Server' => "nginx"), 'Headers are correctly extracted.');
}
public function testExtractResponseNoBody() {
// For example a response to HEAD request.
$rawResponse =
"HTTP/1.1 200 OK\r\n" .
"Server: nginx";
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
$headerFunction->setAccessible(true);
$ch = curl_init();
$response = $headerFunction->invoke(
new RestfulService(Director::absoluteBaseURL(),0),
$ch,
$rawResponse
);
$this->assertEquals($response->getBody(), "", 'Body is correctly extracted.');
$this->assertEquals($response->getHeaders(), array('Server' => "nginx"), 'Headers are correctly extracted.');
}
public function testExtractResponseNoHead() {
// Malformed response.
$rawResponse = "I am a malformed response";
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
$headerFunction->setAccessible(true);
$ch = curl_init();
$response = $headerFunction->invoke(
new RestfulService(Director::absoluteBaseURL(),0),
$ch,
$rawResponse
);
$this->assertEquals($response->getBody(), "I am a malformed response", 'Body is correctly extracted.');
$this->assertEquals($response->getHeaders(), array(), 'Headers are correctly extracted.');
}
}
class RestfulServiceTest_Controller extends Controller implements TestOnly {