From 7ddd5b57c320ec3329e99486fcf750ea2d05cbcb Mon Sep 17 00:00:00 2001 From: Mateusz Uzdowski Date: Fri, 20 Sep 2013 14:20:35 +1200 Subject: [PATCH] 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. --- api/RestfulService.php | 27 +++++--- tests/api/RestfulServiceTest.php | 114 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/api/RestfulService.php b/api/RestfulService.php index 5c95d451d..2d1c5eeee 100644 --- a/api/RestfulService.php +++ b/api/RestfulService.php @@ -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); } /** diff --git a/tests/api/RestfulServiceTest.php b/tests/api/RestfulServiceTest.php index 772ccd9ef..cf5d77e04 100644 --- a/tests/api/RestfulServiceTest.php +++ b/tests/api/RestfulServiceTest.php @@ -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" . + ""; + + $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", + '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" . + "\r\n" . + "\r\n" . + ""; + + $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(), "\r\n\r\n", '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 {