From 08b4b73c6727f52475525d71d618f625c41567d5 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 9 Jul 2017 22:17:52 +0200 Subject: [PATCH] Updating HTTP_Request2 to 2.3.0 Source: https://pear.php.net/package/HTTP_Request2 Release date: 2016-02-13 15:24 UTC --- extlib/HTTP/Request2.php | 17 +- extlib/HTTP/Request2/Adapter.php | 4 +- extlib/HTTP/Request2/Adapter/Curl.php | 64 +++-- extlib/HTTP/Request2/Adapter/Mock.php | 4 +- extlib/HTTP/Request2/Adapter/Socket.php | 49 ++-- extlib/HTTP/Request2/CookieJar.php | 71 ++++- extlib/HTTP/Request2/Exception.php | 12 +- extlib/HTTP/Request2/MultipartBody.php | 4 +- extlib/HTTP/Request2/Observer/Log.php | 4 +- .../Observer/UncompressingDownload.php | 265 ++++++++++++++++++ extlib/HTTP/Request2/Response.php | 111 ++++++-- extlib/HTTP/Request2/SOCKS5.php | 4 +- extlib/HTTP/Request2/SocketWrapper.php | 55 ++-- 13 files changed, 544 insertions(+), 120 deletions(-) create mode 100644 extlib/HTTP/Request2/Observer/UncompressingDownload.php diff --git a/extlib/HTTP/Request2.php b/extlib/HTTP/Request2.php index d2f36e17b9..b835822e04 100644 --- a/extlib/HTTP/Request2.php +++ b/extlib/HTTP/Request2.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -21,7 +21,9 @@ /** * A class representing an URL as per RFC 3986. */ -require_once 'Net/URL2.php'; +if (!class_exists('Net_URL2', true)) { + require_once 'Net/URL2.php'; +} /** * Exception class for HTTP_Request2 package @@ -35,7 +37,7 @@ require_once 'HTTP/Request2/Exception.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 * @link http://tools.ietf.org/html/rfc2616#section-5 */ @@ -213,7 +215,7 @@ class HTTP_Request2 implements SplSubject $this->setMethod($method); } $this->setHeader( - 'user-agent', 'HTTP_Request2/2.2.1 ' . + 'user-agent', 'HTTP_Request2/2.3.0 ' . '(http://pear.php.net/package/http_request2) PHP/' . phpversion() ); } @@ -794,6 +796,11 @@ class HTTP_Request2 implements SplSubject * encoded by Content-Encoding *
  • 'receivedBody' - after receiving the complete response * body, data is HTTP_Request2_Response object
  • + *
  • 'warning' - a problem arose during the request + * that is not severe enough to throw + * an Exception, data is the warning + * message (string). Currently dispatched if + * response body was received incompletely.
  • * * Different adapters may not send all the event types. Mock adapter does * not send any events to the observers. @@ -1022,7 +1029,7 @@ class HTTP_Request2 implements SplSubject } // (deprecated) mime_content_type function available if (empty($info) && function_exists('mime_content_type')) { - return mime_content_type($filename); + $info = mime_content_type($filename); } return empty($info)? 'application/octet-stream': $info; } diff --git a/extlib/HTTP/Request2/Adapter.php b/extlib/HTTP/Request2/Adapter.php index 4e4b0b10a3..035632792c 100644 --- a/extlib/HTTP/Request2/Adapter.php +++ b/extlib/HTTP/Request2/Adapter.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -34,7 +34,7 @@ require_once 'HTTP/Request2/Response.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ abstract class HTTP_Request2_Adapter diff --git a/extlib/HTTP/Request2/Adapter/Curl.php b/extlib/HTTP/Request2/Adapter/Curl.php index ef75b8c957..13d4a2994e 100644 --- a/extlib/HTTP/Request2/Adapter/Curl.php +++ b/extlib/HTTP/Request2/Adapter/Curl.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -30,7 +30,7 @@ require_once 'HTTP/Request2/Adapter.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter @@ -116,6 +116,12 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter */ protected $eventReceivedHeaders = false; + /** + * Whether 'sentBoody' event was sent to observers + * @var boolean + */ + protected $eventSentBody = false; + /** * Position within request body * @var integer @@ -171,6 +177,7 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter $this->position = 0; $this->eventSentHeaders = false; $this->eventReceivedHeaders = false; + $this->eventSentBody = false; try { if (false === curl_exec($ch = $this->createCurlHandle())) { @@ -180,6 +187,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter } if (isset($ch)) { $this->lastInfo = curl_getinfo($ch); + if (CURLE_OK !== curl_errno($ch)) { + $this->request->setLastEvent('warning', curl_error($ch)); + } curl_close($ch); } @@ -191,7 +201,7 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter } if ($jar = $request->getCookieJar()) { - $jar->addCookiesFromResponse($response, $request->getUrl()); + $jar->addCookiesFromResponse($response); } if (0 < $this->lastInfo['size_download']) { @@ -400,9 +410,12 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter protected function workaroundPhpBug47204($ch, &$headers) { // no redirects, no digest auth -> probably no rewind needed + // also apply workaround only for POSTs, othrerwise we get + // https://pear.php.net/bugs/bug.php?id=20440 for PUTs if (!$this->request->getConfig('follow_redirects') && (!($auth = $this->request->getAuth()) || HTTP_Request2::AUTH_DIGEST != $auth['scheme']) + || HTTP_Request2::METHOD_POST !== $this->request->getMethod() ) { curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody')); @@ -469,40 +482,36 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter */ protected function callbackWriteHeader($ch, $string) { - // we may receive a second set of headers if doing e.g. digest auth - if ($this->eventReceivedHeaders || !$this->eventSentHeaders) { - // don't bother with 100-Continue responses (bug #15785) - if (!$this->eventSentHeaders - || $this->response->getStatus() >= 200 - ) { - $this->request->setLastEvent( - 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) - ); - } + if (!$this->eventSentHeaders + // we may receive a second set of headers if doing e.g. digest auth + // but don't bother with 100-Continue responses (bug #15785) + || $this->eventReceivedHeaders && $this->response->getStatus() >= 200 + ) { + $this->request->setLastEvent( + 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) + ); + } + if (!$this->eventSentBody) { $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD); - // if body wasn't read by a callback, send event with total body size + // if body wasn't read by the callback, send event with total body size if ($upload > $this->position) { $this->request->setLastEvent( 'sentBodyPart', $upload - $this->position ); - $this->position = $upload; } - if ($upload && (!$this->eventSentHeaders - || $this->response->getStatus() >= 200) - ) { + if ($upload > 0) { $this->request->setLastEvent('sentBody', $upload); } - $this->eventSentHeaders = true; - // we'll need a new response object - if ($this->eventReceivedHeaders) { - $this->eventReceivedHeaders = false; - $this->response = null; - } } - if (empty($this->response)) { - $this->response = new HTTP_Request2_Response( + $this->eventSentHeaders = true; + $this->eventSentBody = true; + + if ($this->eventReceivedHeaders || empty($this->response)) { + $this->eventReceivedHeaders = false; + $this->response = new HTTP_Request2_Response( $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) ); + } else { $this->response->parseHeaderLine($string); if ('' == trim($string)) { @@ -522,7 +531,7 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter } if ($jar = $this->request->getCookieJar()) { - $jar->addCookiesFromResponse($this->response, $this->request->getUrl()); + $jar->addCookiesFromResponse($this->response); if (!$redirectUrl->isAbsolute()) { $redirectUrl = $this->request->getUrl()->resolve($redirectUrl); } @@ -532,6 +541,7 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter } } $this->eventReceivedHeaders = true; + $this->eventSentBody = false; } } return strlen($string); diff --git a/extlib/HTTP/Request2/Adapter/Mock.php b/extlib/HTTP/Request2/Adapter/Mock.php index d6e274ab9a..22b4282444 100644 --- a/extlib/HTTP/Request2/Adapter/Mock.php +++ b/extlib/HTTP/Request2/Adapter/Mock.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -44,7 +44,7 @@ require_once 'HTTP/Request2/Adapter.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter diff --git a/extlib/HTTP/Request2/Adapter/Socket.php b/extlib/HTTP/Request2/Adapter/Socket.php index 7946b0a374..3a1d18606b 100644 --- a/extlib/HTTP/Request2/Adapter/Socket.php +++ b/extlib/HTTP/Request2/Adapter/Socket.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -34,7 +34,7 @@ require_once 'HTTP/Request2/SocketWrapper.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter @@ -147,7 +147,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter if ($jar = $request->getCookieJar()) { - $jar->addCookiesFromResponse($response, $request->getUrl()); + $jar->addCookiesFromResponse($response); } if (!$this->canKeepAlive($keepAlive, $response)) { @@ -261,9 +261,16 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter foreach ($this->request->getConfig() as $name => $value) { if ('ssl_' == substr($name, 0, 4) && null !== $value) { if ('ssl_verify_host' == $name) { - if ($value) { - $options['ssl']['CN_match'] = $reqHost; + if (version_compare(phpversion(), '5.6', '<')) { + if ($value) { + $options['ssl']['CN_match'] = $reqHost; + } + + } else { + $options['ssl']['verify_peer_name'] = $value; + $options['ssl']['peer_name'] = $reqHost; } + } else { $options['ssl'][substr($name, 4)] = $value; } @@ -281,7 +288,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter // Changing SSL context options after connection is established does *not* // work, we need a new connection if options change - $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'ssl://') + $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'tls://') . $host . ':' . $port; $socketKey = $remote . ( ($secure && $httpProxy || $socksProxy) @@ -312,12 +319,12 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter $conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}"; } else { $this->socket->enableCrypto(); - $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}"; + $conninfo = "tls://{$reqHost}:{$reqPort} via {$remote}"; } } elseif ($secure && $httpProxy && !$tunnel) { $this->establishTunnel(); - $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}"; + $conninfo = "tls://{$reqHost}:{$reqPort} via {$remote}"; } else { $this->socket = new HTTP_Request2_SocketWrapper( @@ -1043,14 +1050,14 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter $chunked = 'chunked' == $response->getHeader('transfer-encoding'); $length = $response->getHeader('content-length'); $hasBody = false; - if ($chunked || null === $length || 0 < intval($length)) { - // RFC 2616, section 4.4: - // 3. ... If a message is received with both a - // Transfer-Encoding header field and a Content-Length header field, - // the latter MUST be ignored. - $toRead = ($chunked || null === $length)? null: $length; - $this->chunkLength = 0; + // RFC 2616, section 4.4: + // 3. ... If a message is received with both a + // Transfer-Encoding header field and a Content-Length header field, + // the latter MUST be ignored. + $toRead = ($chunked || null === $length)? null: $length; + $this->chunkLength = 0; + if ($chunked || null === $length || 0 < intval($length)) { while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) { if ($chunked) { $data = $this->readChunked($bufferSize); @@ -1075,6 +1082,11 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter } } } + if (0 !== $this->chunkLength || null !== $toRead && $toRead > 0) { + $this->request->setLastEvent( + 'warning', 'transfer closed with outstanding read data remaining' + ); + } if ($hasBody) { $this->request->setLastEvent('receivedBody', $response); @@ -1095,11 +1107,16 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter // at start of the next chunk? if (0 == $this->chunkLength) { $line = $this->socket->readLine($bufferSize); - if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) { + if ('' === $line && $this->socket->eof()) { + $this->chunkLength = -1; // indicate missing chunk + return ''; + + } elseif (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) { throw new HTTP_Request2_MessageException( "Cannot decode chunked response, invalid chunk length '{$line}'", HTTP_Request2_Exception::DECODE_ERROR ); + } else { $this->chunkLength = hexdec($matches[1]); // Chunk with zero length indicates the end diff --git a/extlib/HTTP/Request2/CookieJar.php b/extlib/HTTP/Request2/CookieJar.php index 79ac08bb75..f191e2551f 100644 --- a/extlib/HTTP/Request2/CookieJar.php +++ b/extlib/HTTP/Request2/CookieJar.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -61,6 +61,12 @@ class HTTP_Request2_CookieJar implements Serializable */ protected $useList = true; + /** + * Whether an attempt to store an invalid cookie should be ignored, rather than cause an Exception + * @var bool + */ + protected $ignoreInvalid = false; + /** * Array with Public Suffix List data * @var array @@ -75,12 +81,16 @@ class HTTP_Request2_CookieJar implements Serializable * see {@link serializeSessionCookies()} * @param bool $usePublicSuffixList Controls using Public Suffix List, * see {@link usePublicSuffixList()} + * @param bool $ignoreInvalidCookies Whether invalid cookies should be ignored, + * see {@link ignoreInvalidCookies()} */ public function __construct( - $serializeSessionCookies = false, $usePublicSuffixList = true + $serializeSessionCookies = false, $usePublicSuffixList = true, + $ignoreInvalidCookies = false ) { $this->serializeSessionCookies($serializeSessionCookies); $this->usePublicSuffixList($usePublicSuffixList); + $this->ignoreInvalidCookies($ignoreInvalidCookies); } /** @@ -194,11 +204,20 @@ class HTTP_Request2_CookieJar implements Serializable * {@link HTTP_Request2_Response::getCookies()} * @param Net_URL2 $setter URL of the document that sent Set-Cookie header * - * @throws HTTP_Request2_Exception + * @return bool whether the cookie was successfully stored + * @throws HTTP_Request2_Exception */ public function store(array $cookie, Net_URL2 $setter = null) { - $cookie = $this->checkAndUpdateFields($cookie, $setter); + try { + $cookie = $this->checkAndUpdateFields($cookie, $setter); + } catch (HTTP_Request2_Exception $e) { + if ($this->ignoreInvalid) { + return false; + } else { + throw $e; + } + } if (strlen($cookie['value']) && (is_null($cookie['expires']) || $cookie['expires'] > $this->now()) @@ -214,6 +233,8 @@ class HTTP_Request2_CookieJar implements Serializable } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) { unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]); } + + return true; } /** @@ -221,13 +242,29 @@ class HTTP_Request2_CookieJar implements Serializable * * @param HTTP_Request2_Response $response HTTP response message * @param Net_URL2 $setter original request URL, needed for - * setting default domain/path + * setting default domain/path. If not given, + * effective URL from response will be used. + * + * @return bool whether all cookies were successfully stored + * @throws HTTP_Request2_LogicException */ - public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter) + public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter = null) { - foreach ($response->getCookies() as $cookie) { - $this->store($cookie, $setter); + if (null === $setter) { + if (!($effectiveUrl = $response->getEffectiveUrl())) { + throw new HTTP_Request2_LogicException( + 'Response URL required for adding cookies from response', + HTTP_Request2_Exception::MISSING_VALUE + ); + } + $setter = new Net_URL2($effectiveUrl); } + + $success = true; + foreach ($response->getCookies() as $cookie) { + $success = $this->store($cookie, $setter) && $success; + } + return $success; } /** @@ -306,6 +343,18 @@ class HTTP_Request2_CookieJar implements Serializable $this->serializeSession = (bool)$serialize; } + /** + * Sets whether invalid cookies should be silently ignored or cause an Exception + * + * @param boolean $ignore ignore? + * @link http://pear.php.net/bugs/bug.php?id=19937 + * @link http://pear.php.net/bugs/bug.php?id=20401 + */ + public function ignoreInvalidCookies($ignore) + { + $this->ignoreInvalid = (bool)$ignore; + } + /** * Sets whether Public Suffix List should be used for restricting cookie-setting * @@ -352,7 +401,8 @@ class HTTP_Request2_CookieJar implements Serializable return serialize(array( 'cookies' => $cookies, 'serializeSession' => $this->serializeSession, - 'useList' => $this->useList + 'useList' => $this->useList, + 'ignoreInvalid' => $this->ignoreInvalid )); } @@ -369,6 +419,9 @@ class HTTP_Request2_CookieJar implements Serializable $now = $this->now(); $this->serializeSessionCookies($data['serializeSession']); $this->usePublicSuffixList($data['useList']); + if (array_key_exists('ignoreInvalid', $data)) { + $this->ignoreInvalidCookies($data['ignoreInvalid']); + } foreach ($data['cookies'] as $cookie) { if (!empty($cookie['expires']) && $cookie['expires'] <= $now) { continue; diff --git a/extlib/HTTP/Request2/Exception.php b/extlib/HTTP/Request2/Exception.php index d0b5d4ee09..de9432df8c 100644 --- a/extlib/HTTP/Request2/Exception.php +++ b/extlib/HTTP/Request2/Exception.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -30,7 +30,7 @@ require_once 'PEAR/Exception.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=132 */ @@ -97,7 +97,7 @@ class HTTP_Request2_Exception extends PEAR_Exception * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception @@ -118,7 +118,7 @@ class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_LogicException extends HTTP_Request2_Exception @@ -135,7 +135,7 @@ class HTTP_Request2_LogicException extends HTTP_Request2_Exception * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception @@ -151,7 +151,7 @@ class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_MessageException extends HTTP_Request2_Exception diff --git a/extlib/HTTP/Request2/MultipartBody.php b/extlib/HTTP/Request2/MultipartBody.php index c68b6602b8..e5a3845952 100644 --- a/extlib/HTTP/Request2/MultipartBody.php +++ b/extlib/HTTP/Request2/MultipartBody.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -31,7 +31,7 @@ require_once 'HTTP/Request2/Exception.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 * @link http://tools.ietf.org/html/rfc1867 */ diff --git a/extlib/HTTP/Request2/Observer/Log.php b/extlib/HTTP/Request2/Observer/Log.php index 341e29907e..069baf8e95 100644 --- a/extlib/HTTP/Request2/Observer/Log.php +++ b/extlib/HTTP/Request2/Observer/Log.php @@ -14,7 +14,7 @@ * @package HTTP_Request2 * @author David Jean Louis * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -64,7 +64,7 @@ require_once 'HTTP/Request2/Exception.php'; * @author David Jean Louis * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_Observer_Log implements SplObserver diff --git a/extlib/HTTP/Request2/Observer/UncompressingDownload.php b/extlib/HTTP/Request2/Observer/UncompressingDownload.php new file mode 100644 index 0000000000..8a3430a01e --- /dev/null +++ b/extlib/HTTP/Request2/Observer/UncompressingDownload.php @@ -0,0 +1,265 @@ + + * @author Alexey Borzov + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +require_once 'HTTP/Request2/Response.php'; + +/** + * An observer that saves response body to stream, possibly uncompressing it + * + * This Observer is written in compliment to pear's HTTP_Request2 in order to + * avoid reading the whole response body in memory. Instead it writes the body + * to a stream. If the body is transferred with content-encoding set to + * "deflate" or "gzip" it is decoded on the fly. + * + * The constructor accepts an already opened (for write) stream (file_descriptor). + * If the response is deflate/gzip encoded a "zlib.inflate" filter is applied + * to the stream. When the body has been read from the request and written to + * the stream ("receivedBody" event) the filter is removed from the stream. + * + * The "zlib.inflate" filter works fine with pure "deflate" encoding. It does + * not understand the "deflate+zlib" and "gzip" headers though, so they have to + * be removed prior to being passed to the stream. This is done in the "update" + * method. + * + * It is also possible to limit the size of written extracted bytes by passing + * "max_bytes" to the constructor. This is important because e.g. 1GB of + * zeroes take about a MB when compressed. + * + * Exceptions are being thrown if data could not be written to the stream or + * the written bytes have already exceeded the requested maximum. If the "gzip" + * header is malformed or could not be parsed an exception will be thrown too. + * + * Example usage follows: + * + * + * require_once 'HTTP/Request2.php'; + * require_once 'HTTP/Request2/Observer/UncompressingDownload.php'; + * + * #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html'; + * #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on'; + * $inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on&zlib=on'; + * #$outPath = "/dev/null"; + * $outPath = "delme"; + * + * $stream = fopen($outPath, 'wb'); + * if (!$stream) { + * throw new Exception('fopen failed'); + * } + * + * $request = new HTTP_Request2( + * $inPath, + * HTTP_Request2::METHOD_GET, + * array( + * 'store_body' => false, + * 'connect_timeout' => 5, + * 'timeout' => 10, + * 'ssl_verify_peer' => true, + * 'ssl_verify_host' => true, + * 'ssl_cafile' => null, + * 'ssl_capath' => '/etc/ssl/certs', + * 'max_redirects' => 10, + * 'follow_redirects' => true, + * 'strict_redirects' => false + * ) + * ); + * + * $observer = new HTTP_Request2_Observer_UncompressingDownload($stream, 9999999); + * $request->attach($observer); + * + * $response = $request->send(); + * + * fclose($stream); + * echo "OK\n"; + * + * + * @category HTTP + * @package HTTP_Request2 + * @author Delian Krustev + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Observer_UncompressingDownload implements SplObserver +{ + /** + * The stream to write response body to + * @var resource + */ + private $_stream; + + /** + * zlib.inflate filter possibly added to stream + * @var resource + */ + private $_streamFilter; + + /** + * The value of response's Content-Encoding header + * @var string + */ + private $_encoding; + + /** + * Whether the observer is still waiting for gzip/deflate header + * @var bool + */ + private $_processingHeader = true; + + /** + * Starting position in the stream observer writes to + * @var int + */ + private $_startPosition = 0; + + /** + * Maximum bytes to write + * @var int|null + */ + private $_maxDownloadSize; + + /** + * Whether response being received is a redirect + * @var bool + */ + private $_redirect = false; + + /** + * Accumulated body chunks that may contain (gzip) header + * @var string + */ + private $_possibleHeader = ''; + + /** + * Class constructor + * + * Note that there might be problems with max_bytes and files bigger + * than 2 GB on 32bit platforms + * + * @param resource $stream a stream (or file descriptor) opened for writing. + * @param int $maxDownloadSize maximum bytes to write + */ + public function __construct($stream, $maxDownloadSize = null) + { + $this->_stream = $stream; + if ($maxDownloadSize) { + $this->_maxDownloadSize = $maxDownloadSize; + $this->_startPosition = ftell($this->_stream); + } + } + + /** + * Called when the request notifies us of an event. + * + * @param SplSubject $request The HTTP_Request2 instance + * + * @return void + * @throws HTTP_Request2_MessageException + */ + public function update(SplSubject $request) + { + /* @var $request HTTP_Request2 */ + $event = $request->getLastEvent(); + $encoded = false; + + /* @var $event['data'] HTTP_Request2_Response */ + switch ($event['name']) { + case 'receivedHeaders': + $this->_processingHeader = true; + $this->_redirect = $event['data']->isRedirect(); + $this->_encoding = strtolower($event['data']->getHeader('content-encoding')); + $this->_possibleHeader = ''; + break; + + case 'receivedEncodedBodyPart': + if (!$this->_streamFilter + && ($this->_encoding === 'deflate' || $this->_encoding === 'gzip') + ) { + $this->_streamFilter = stream_filter_append( + $this->_stream, 'zlib.inflate', STREAM_FILTER_WRITE + ); + } + $encoded = true; + // fall-through is intentional + + case 'receivedBodyPart': + if ($this->_redirect) { + break; + } + + if (!$encoded || !$this->_processingHeader) { + $bytes = fwrite($this->_stream, $event['data']); + + } else { + $offset = 0; + $this->_possibleHeader .= $event['data']; + if ('deflate' === $this->_encoding) { + if (2 > strlen($this->_possibleHeader)) { + break; + } + $header = unpack('n', substr($this->_possibleHeader, 0, 2)); + if (0 == $header[1] % 31) { + $offset = 2; + } + + } elseif ('gzip' === $this->_encoding) { + if (10 > strlen($this->_possibleHeader)) { + break; + } + try { + $offset = HTTP_Request2_Response::parseGzipHeader($this->_possibleHeader, false); + + } catch (HTTP_Request2_MessageException $e) { + // need more data? + if (false !== strpos($e->getMessage(), 'data too short')) { + break; + } + throw $e; + } + } + + $this->_processingHeader = false; + $bytes = fwrite($this->_stream, substr($this->_possibleHeader, $offset)); + } + + if (false === $bytes) { + throw new HTTP_Request2_MessageException('fwrite failed.'); + } + + if ($this->_maxDownloadSize + && ftell($this->_stream) - $this->_startPosition > $this->_maxDownloadSize + ) { + throw new HTTP_Request2_MessageException(sprintf( + 'Body length limit (%d bytes) reached', + $this->_maxDownloadSize + )); + } + break; + + case 'receivedBody': + if ($this->_streamFilter) { + stream_filter_remove($this->_streamFilter); + $this->_streamFilter = null; + } + break; + } + } +} diff --git a/extlib/HTTP/Request2/Response.php b/extlib/HTTP/Request2/Response.php index d2f414cf6c..b144fdae09 100644 --- a/extlib/HTTP/Request2/Response.php +++ b/extlib/HTTP/Request2/Response.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -47,7 +47,7 @@ require_once 'HTTP/Request2/Exception.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 * @link http://tools.ietf.org/html/rfc2616#section-6 */ @@ -465,32 +465,46 @@ class HTTP_Request2_Response } /** - * Decodes the message-body encoded by gzip + * Checks whether data starts with GZIP format identification bytes from RFC 1952 * - * The real decoding work is done by gzinflate() built-in function, this - * method only parses the header and checks data for compliance with - * RFC 1952 + * @param string $data gzip-encoded (presumably) data * - * @param string $data gzip-encoded data - * - * @return string decoded data - * @throws HTTP_Request2_LogicException - * @throws HTTP_Request2_MessageException - * @link http://tools.ietf.org/html/rfc1952 + * @return bool */ - public static function decodeGzip($data) + public static function hasGzipIdentification($data) { - $length = strlen($data); - // If it doesn't look like gzip-encoded data, don't bother - if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) { - return $data; - } - if (!function_exists('gzinflate')) { - throw new HTTP_Request2_LogicException( - 'Unable to decode body: gzip extension not available', - HTTP_Request2_Exception::MISCONFIGURATION + return 0 === strcmp(substr($data, 0, 2), "\x1f\x8b"); + } + + /** + * Tries to parse GZIP format header in the given string + * + * If the header conforms to RFC 1952, its length is returned. If any + * sanity check fails, HTTP_Request2_MessageException is thrown. + * + * Note: This function might be usable outside of HTTP_Request2 so it might + * be good idea to be moved to some common package. (Delian Krustev) + * + * @param string $data Either the complete response body or + * the leading part of it + * @param boolean $dataComplete Whether $data contains complete response body + * + * @return int gzip header length in bytes + * @throws HTTP_Request2_MessageException + * @link http://tools.ietf.org/html/rfc1952 + */ + public static function parseGzipHeader($data, $dataComplete = false) + { + // if data is complete, trailing 8 bytes should be present for size and crc32 + $length = strlen($data) - ($dataComplete ? 8 : 0); + + if ($length < 10 || !self::hasGzipIdentification($data)) { + throw new HTTP_Request2_MessageException( + 'The data does not seem to contain a valid gzip header', + HTTP_Request2_Exception::DECODE_ERROR ); } + $method = ord(substr($data, 2, 1)); if (8 != $method) { throw new HTTP_Request2_MessageException( @@ -510,14 +524,14 @@ class HTTP_Request2_Response $headerLength = 10; // extra fields, need to skip 'em if ($flags & 4) { - if ($length - $headerLength - 2 < 8) { + if ($length - $headerLength - 2 < 0) { throw new HTTP_Request2_MessageException( 'Error parsing gzip header: data too short', HTTP_Request2_Exception::DECODE_ERROR ); } $extraLength = unpack('v', substr($data, 10, 2)); - if ($length - $headerLength - 2 - $extraLength[1] < 8) { + if ($length - $headerLength - 2 - $extraLength[1] < 0) { throw new HTTP_Request2_MessageException( 'Error parsing gzip header: data too short', HTTP_Request2_Exception::DECODE_ERROR @@ -527,14 +541,16 @@ class HTTP_Request2_Response } // file name, need to skip that if ($flags & 8) { - if ($length - $headerLength - 1 < 8) { + if ($length - $headerLength - 1 < 0) { throw new HTTP_Request2_MessageException( 'Error parsing gzip header: data too short', HTTP_Request2_Exception::DECODE_ERROR ); } $filenameLength = strpos(substr($data, $headerLength), chr(0)); - if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { + if (false === $filenameLength + || $length - $headerLength - $filenameLength - 1 < 0 + ) { throw new HTTP_Request2_MessageException( 'Error parsing gzip header: data too short', HTTP_Request2_Exception::DECODE_ERROR @@ -544,14 +560,16 @@ class HTTP_Request2_Response } // comment, need to skip that also if ($flags & 16) { - if ($length - $headerLength - 1 < 8) { + if ($length - $headerLength - 1 < 0) { throw new HTTP_Request2_MessageException( 'Error parsing gzip header: data too short', HTTP_Request2_Exception::DECODE_ERROR ); } $commentLength = strpos(substr($data, $headerLength), chr(0)); - if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { + if (false === $commentLength + || $length - $headerLength - $commentLength - 1 < 0 + ) { throw new HTTP_Request2_MessageException( 'Error parsing gzip header: data too short', HTTP_Request2_Exception::DECODE_ERROR @@ -561,7 +579,7 @@ class HTTP_Request2_Response } // have a CRC for header. let's check if ($flags & 2) { - if ($length - $headerLength - 2 < 8) { + if ($length - $headerLength - 2 < 0) { throw new HTTP_Request2_MessageException( 'Error parsing gzip header: data too short', HTTP_Request2_Exception::DECODE_ERROR @@ -577,12 +595,43 @@ class HTTP_Request2_Response } $headerLength += 2; } + return $headerLength; + } + + /** + * Decodes the message-body encoded by gzip + * + * The real decoding work is done by gzinflate() built-in function, this + * method only parses the header and checks data for compliance with + * RFC 1952 + * + * @param string $data gzip-encoded data + * + * @return string decoded data + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_MessageException + * @link http://tools.ietf.org/html/rfc1952 + */ + public static function decodeGzip($data) + { + // If it doesn't look like gzip-encoded data, don't bother + if (!self::hasGzipIdentification($data)) { + return $data; + } + if (!function_exists('gzinflate')) { + throw new HTTP_Request2_LogicException( + 'Unable to decode body: gzip extension not available', + HTTP_Request2_Exception::MISCONFIGURATION + ); + } + // unpacked data CRC and size at the end of encoded data $tmp = unpack('V2', substr($data, -8)); $dataCrc = $tmp[1]; $dataSize = $tmp[2]; - // finally, call the gzinflate() function + $headerLength = self::parseGzipHeader($data, true); + // don't pass $dataSize to gzinflate, see bugs #13135, #14370 $unpacked = gzinflate(substr($data, $headerLength, -8)); if (false === $unpacked) { @@ -596,7 +645,7 @@ class HTTP_Request2_Response HTTP_Request2_Exception::DECODE_ERROR ); } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { - throw new HTTP_Request2_Exception( + throw new HTTP_Request2_MessageException( 'Data CRC check failed', HTTP_Request2_Exception::DECODE_ERROR ); diff --git a/extlib/HTTP/Request2/SOCKS5.php b/extlib/HTTP/Request2/SOCKS5.php index d540495859..2a782ceda1 100644 --- a/extlib/HTTP/Request2/SOCKS5.php +++ b/extlib/HTTP/Request2/SOCKS5.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -28,7 +28,7 @@ require_once 'HTTP/Request2/SocketWrapper.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 * @link http://pear.php.net/bugs/bug.php?id=19332 * @link http://tools.ietf.org/html/rfc1928 diff --git a/extlib/HTTP/Request2/SocketWrapper.php b/extlib/HTTP/Request2/SocketWrapper.php index 43081a1966..2cf4257db2 100644 --- a/extlib/HTTP/Request2/SocketWrapper.php +++ b/extlib/HTTP/Request2/SocketWrapper.php @@ -13,7 +13,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov + * @copyright 2008-2016 Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License * @link http://pear.php.net/package/HTTP_Request2 */ @@ -31,7 +31,7 @@ require_once 'HTTP/Request2/Exception.php'; * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 + * @version Release: 2.3.0 * @link http://pear.php.net/package/HTTP_Request2 * @link http://pear.php.net/bugs/bug.php?id=19332 * @link http://tools.ietf.org/html/rfc1928 @@ -81,6 +81,32 @@ class HTTP_Request2_SocketWrapper // Backwards compatibility with 2.1.0 and 2.1.1 releases $contextOptions = array('ssl' => $contextOptions); } + if (isset($contextOptions['ssl'])) { + $contextOptions['ssl'] += array( + // Using "Intermediate compatibility" cipher bundle from + // https://wiki.mozilla.org/Security/Server_Side_TLS + 'ciphers' => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:' + . 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:' + . 'DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:' + . 'ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:' + . 'ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:' + . 'ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:' + . 'ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:' + . 'DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:' + . 'DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:' + . 'ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:' + . 'AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:' + . 'AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:' + . '!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + ); + if (version_compare(phpversion(), '5.4.13', '>=')) { + $contextOptions['ssl']['disable_compression'] = true; + if (version_compare(phpversion(), '5.6', '>=')) { + $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + } + } + } $context = stream_context_create(); foreach ($contextOptions as $wrapper => $options) { foreach ($options as $name => $value) { @@ -239,21 +265,18 @@ class HTTP_Request2_SocketWrapper */ public function enableCrypto() { - $modes = array( - STREAM_CRYPTO_METHOD_TLS_CLIENT, - STREAM_CRYPTO_METHOD_SSLv3_CLIENT, - STREAM_CRYPTO_METHOD_SSLv23_CLIENT, - STREAM_CRYPTO_METHOD_SSLv2_CLIENT - ); - - foreach ($modes as $mode) { - if (stream_socket_enable_crypto($this->socket, true, $mode)) { - return; - } + if (version_compare(phpversion(), '5.6', '<')) { + $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT; + } else { + $cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + } + + if (!stream_socket_enable_crypto($this->socket, true, $cryptoMethod)) { + throw new HTTP_Request2_ConnectionException( + 'Failed to enable secure connection when connecting through proxy' + ); } - throw new HTTP_Request2_ConnectionException( - 'Failed to enable secure connection when connecting through proxy' - ); } /**