diff --git a/src/Client.php b/src/Client.php index 061e7547d8..6b0365b302 100644 --- a/src/Client.php +++ b/src/Client.php @@ -298,8 +298,18 @@ public function getRedirectionsCount() public function setUri($uri) { if (!empty($uri)) { + // remember host of last request + $lastHost = $this->getRequest()->getUri()->getHost(); $this->getRequest()->setUri($uri); + // if host changed, the HTTP authentication should be cleared for security + // reasons, see #4215 for a discussion - currently authentication is also + // cleared for peer subdomains due to technical limits + $nextHost = $this->getRequest()->getUri()->getHost(); + if (!preg_match('/' . preg_quote($lastHost, '/') . '$/i', $nextHost)) { + $this->clearAuth(); + } + // Set auth if username and password has been specified in the uri if ($this->getUri()->getUser() && $this->getUri()->getPassword()) { $this->setAuth($this->getUri()->getUser(), $this->getUri()->getPassword()); @@ -444,6 +454,37 @@ public function setParameterGet(array $query) return $this; } + /** + * Reset all the HTTP parameters (request, response, etc) + * + * @param bool $clearCookies Also clear all valid cookies? (defaults to false) + * @param bool $clearAuth Also clear http authentication? (defaults to true) + * @return Client + */ + public function resetParameters($clearCookies = false, $clearAuth = true) + { + $uri = $this->getUri(); + + $this->streamName = null; + $this->encType = null; + $this->request = null; + $this->response = null; + $this->lastRawRequest = null; + $this->lastRawResponse = null; + + $this->setUri($uri); + + if ($clearCookies) { + $this->clearCookies(); + } + + if ($clearAuth) { + $this->clearAuth(); + } + + return $this; + } + /** * Return the current cookies * @@ -673,6 +714,14 @@ public function setAuth($user, $password, $type = self::AUTH_BASIC) return $this; } + /** + * Clear http authentication + */ + public function clearAuth() + { + $this->auth = array(); + } + /** * Calculate the response value according to the HTTP authentication type * @@ -728,31 +777,6 @@ protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $d return $response; } - /** - * Reset all the HTTP parameters (auth,cookies,request, response, etc) - * - * @param bool $clearCookies Also clear all valid cookies? (defaults to false) - * @return Client - */ - public function resetParameters($clearCookies = false) - { - $uri = $this->getUri(); - - $this->auth = null; - $this->streamName = null; - $this->encType = null; - $this->request = null; - $this->response = null; - - $this->setUri($uri); - - if ($clearCookies) { - $this->clearCookies(); - } - - return $this; - } - /** * Dispatch * @@ -897,13 +921,15 @@ public function send(Request $request = null) ((! $this->config['strictredirects']) && ($response->getStatusCode() == 302 || $response->getStatusCode() == 301))) { - $this->resetParameters(); + $this->resetParameters(false, false); $this->setMethod(Request::METHOD_GET); } + // If we got a well formed absolute URI if (($scheme = substr($location, 0, 6)) && ($scheme == 'http:/' || $scheme == 'https:')) { + // setURI() clears parameters if host changed, see #4215 $this->setUri($location); } else { @@ -933,12 +959,26 @@ public function send(Request $request = null) break; } - } while ($this->redirectCounter < $this->config['maxredirects']); + } while ($this->redirectCounter <= $this->config['maxredirects']); $this->response = $response; return $response; } + /** + * Fully reset the HTTP client (auth, cookies, request, response, etc.) + * + * @return Client + */ + public function reset() + { + $this->resetParameters(); + $this->clearAuth(); + $this->clearCookies(); + + return $this; + } + /** * Set a file to upload (using a POST request) * diff --git a/test/ClientTest.php b/test/ClientTest.php index cd983343dd..505e13388f 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -18,6 +18,7 @@ use Zend\Http\Header\SetCookie; use Zend\Http\Request; use Zend\Http\Response; +use Zend\Http\Client\Adapter\Test; class ClientTest extends \PHPUnit_Framework_TestCase @@ -198,4 +199,144 @@ public function testEncodeAuthHeaderThrowsExceptionWhenInvalidAuthTypeIsUsed() { $encoded = Client::encodeAuthHeader('test', 'test', 'test'); } + + public function testIfMaxredirectWorksCorrectly() + { + $testAdapter = new Test(); + // first response, contains a redirect + $testAdapter->setResponse( + "HTTP/1.1 303 See Other\r\n" + . "Location: http://www.example.org/part2\r\n\r\n" + . "Page #1" + ); + // seconds response, contains a redirect + $testAdapter->addResponse( + "HTTP/1.1 303 See Other\r\n" + . "Location: http://www.example.org/part3\r\n\r\n" + . "Page #2" + ); + // third response + $testAdapter->addResponse( + "HTTP/1.1 303 See Other\r\n\r\n" + . "Page #3" + ); + + // create a client which allows one redirect at most! + $client = new Client('http://www.example.org/part1', array( + 'adapter' => $testAdapter, + 'maxredirects' => 1, + 'storeresponse' => true + )); + + // do the request + $response = $client->setMethod('GET')->send(); + + // response should be the second response, since third response should not + // be requested, due to the maxredirects = 1 limit + $this->assertEquals($response->getContent(), "Page #2"); + } + + public function testIfClientDoesNotLooseAuthenticationOnRedirect() + { + // set up user credentials + $user = 'username123'; + $password = 'password456'; + $encoded = Client::encodeAuthHeader($user, $password, Client::AUTH_BASIC); + + // set up two responses that simulate a redirection + $testAdapter = new Test(); + $testAdapter->setResponse( + "HTTP/1.1 303 See Other\r\n" + . "Location: http://www.example.org/part2\r\n\r\n" + . "The URL of this page has changed." + ); + $testAdapter->addResponse( + "HTTP/1.1 200 OK\r\n\r\n" + . "Welcome to this Website." + ); + + // create client with HTTP basic authentication + $client = new Client('http://www.example.org/part1', array( + 'adapter' => $testAdapter, + 'maxredirects' => 1 + )); + $client->setAuth($user, $password, Client::AUTH_BASIC); + + // do request + $response = $client->setMethod('GET')->send(); + + // the last request should contain the Authorization header + $this->assertTrue(strpos($client->getLastRawRequest(), $encoded) !== false); + } + + public function testIfClientDoesNotForwardAuthenticationToForeignHost() + { + // set up user credentials + $user = 'username123'; + $password = 'password456'; + $encoded = Client::encodeAuthHeader($user, $password, Client::AUTH_BASIC); + + $testAdapter = new Test(); + $client = new Client(null, array('adapter' => $testAdapter)); + + // set up two responses that simulate a redirection from example.org to example.com + $testAdapter->setResponse( + "HTTP/1.1 303 See Other\r\n" + . "Location: http://example.com/part2\r\n\r\n" + . "The URL of this page has changed." + ); + $testAdapter->addResponse( + "HTTP/1.1 200 OK\r\n\r\n" + . "Welcome to this Website." + ); + + // set auth and do request + $client->setUri('http://example.org/part1') + ->setAuth($user, $password, Client::AUTH_BASIC); + $response = $client->setMethod('GET')->send(); + + // the last request should NOT contain the Authorization header, + // because example.com is different from example.org + $this->assertTrue(strpos($client->getLastRawRequest(), $encoded) === false); + + // set up two responses that simulate a rediration from example.org to sub.example.org + $testAdapter->setResponse( + "HTTP/1.1 303 See Other\r\n" + . "Location: http://sub.example.org/part2\r\n\r\n" + . "The URL of this page has changed." + ); + $testAdapter->addResponse( + "HTTP/1.1 200 OK\r\n\r\n" + . "Welcome to this Website." + ); + + // set auth and do request + $client->setUri('http://example.org/part1') + ->setAuth($user, $password, Client::AUTH_BASIC); + $response = $client->setMethod('GET')->send(); + + // the last request should contain the Authorization header, + // because sub.example.org is a subdomain unter example.org + $this->assertFalse(strpos($client->getLastRawRequest(), $encoded) === false); + + // set up two responses that simulate a rediration from sub.example.org to example.org + $testAdapter->setResponse( + "HTTP/1.1 303 See Other\r\n" + . "Location: http://example.org/part2\r\n\r\n" + . "The URL of this page has changed." + ); + $testAdapter->addResponse( + "HTTP/1.1 200 OK\r\n\r\n" + . "Welcome to this Website." + ); + + // set auth and do request + $client->setUri('http://sub.example.org/part1') + ->setAuth($user, $password, Client::AUTH_BASIC); + $response = $client->setMethod('GET')->send(); + + // the last request should NOT contain the Authorization header, + // because example.org is not a subdomain unter sub.example.org + $this->assertTrue(strpos($client->getLastRawRequest(), $encoded) === false); + } }