| 1 | <?php
|
|---|
| 2 | /**
|
|---|
| 3 | * Socket-based adapter for HTTP_Request2
|
|---|
| 4 | *
|
|---|
| 5 | * PHP version 5
|
|---|
| 6 | *
|
|---|
| 7 | * LICENSE:
|
|---|
| 8 | *
|
|---|
| 9 | * Copyright (c) 2008-2012, Alexey Borzov <[email protected]>
|
|---|
| 10 | * All rights reserved.
|
|---|
| 11 | *
|
|---|
| 12 | * Redistribution and use in source and binary forms, with or without
|
|---|
| 13 | * modification, are permitted provided that the following conditions
|
|---|
| 14 | * are met:
|
|---|
| 15 | *
|
|---|
| 16 | * * Redistributions of source code must retain the above copyright
|
|---|
| 17 | * notice, this list of conditions and the following disclaimer.
|
|---|
| 18 | * * Redistributions in binary form must reproduce the above copyright
|
|---|
| 19 | * notice, this list of conditions and the following disclaimer in the
|
|---|
| 20 | * documentation and/or other materials provided with the distribution.
|
|---|
| 21 | * * The names of the authors may not be used to endorse or promote products
|
|---|
| 22 | * derived from this software without specific prior written permission.
|
|---|
| 23 | *
|
|---|
| 24 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|---|
| 25 | * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|---|
| 26 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|---|
| 27 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|---|
| 28 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|---|
| 29 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|---|
| 30 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|---|
| 31 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|---|
| 32 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|---|
| 33 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|---|
| 34 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|---|
| 35 | *
|
|---|
| 36 | * @category HTTP
|
|---|
| 37 | * @package HTTP_Request2
|
|---|
| 38 | * @author Alexey Borzov <[email protected]>
|
|---|
| 39 | * @license http://opensource.org/licenses/bsd-license.php New BSD License
|
|---|
| 40 | * @version SVN: $Id: Socket.php 324953 2012-04-08 07:24:12Z avb $
|
|---|
| 41 | * @link http://pear.php.net/package/HTTP_Request2
|
|---|
| 42 | */
|
|---|
| 43 |
|
|---|
| 44 | /** Base class for HTTP_Request2 adapters */
|
|---|
| 45 | require_once 'HTTP/Request2/Adapter.php';
|
|---|
| 46 |
|
|---|
| 47 | /** Socket wrapper class */
|
|---|
| 48 | require_once 'HTTP/Request2/SocketWrapper.php';
|
|---|
| 49 |
|
|---|
| 50 | /**
|
|---|
| 51 | * Socket-based adapter for HTTP_Request2
|
|---|
| 52 | *
|
|---|
| 53 | * This adapter uses only PHP sockets and will work on almost any PHP
|
|---|
| 54 | * environment. Code is based on original HTTP_Request PEAR package.
|
|---|
| 55 | *
|
|---|
| 56 | * @category HTTP
|
|---|
| 57 | * @package HTTP_Request2
|
|---|
| 58 | * @author Alexey Borzov <[email protected]>
|
|---|
| 59 | * @license http://opensource.org/licenses/bsd-license.php New BSD License
|
|---|
| 60 | * @version Release: 2.1.1
|
|---|
| 61 | * @link http://pear.php.net/package/HTTP_Request2
|
|---|
| 62 | */
|
|---|
| 63 | class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
|
|---|
| 64 | {
|
|---|
| 65 | /**
|
|---|
| 66 | * Regular expression for 'token' rule from RFC 2616
|
|---|
| 67 | */
|
|---|
| 68 | const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';
|
|---|
| 69 |
|
|---|
| 70 | /**
|
|---|
| 71 | * Regular expression for 'quoted-string' rule from RFC 2616
|
|---|
| 72 | */
|
|---|
| 73 | const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"';
|
|---|
| 74 |
|
|---|
| 75 | /**
|
|---|
| 76 | * Connected sockets, needed for Keep-Alive support
|
|---|
| 77 | * @var array
|
|---|
| 78 | * @see connect()
|
|---|
| 79 | */
|
|---|
| 80 | protected static $sockets = array();
|
|---|
| 81 |
|
|---|
| 82 | /**
|
|---|
| 83 | * Data for digest authentication scheme
|
|---|
| 84 | *
|
|---|
| 85 | * The keys for the array are URL prefixes.
|
|---|
| 86 | *
|
|---|
| 87 | * The values are associative arrays with data (realm, nonce, nonce-count,
|
|---|
| 88 | * opaque...) needed for digest authentication. Stored here to prevent making
|
|---|
| 89 | * duplicate requests to digest-protected resources after we have already
|
|---|
| 90 | * received the challenge.
|
|---|
| 91 | *
|
|---|
| 92 | * @var array
|
|---|
| 93 | */
|
|---|
| 94 | protected static $challenges = array();
|
|---|
| 95 |
|
|---|
| 96 | /**
|
|---|
| 97 | * Connected socket
|
|---|
| 98 | * @var HTTP_Request2_SocketWrapper
|
|---|
| 99 | * @see connect()
|
|---|
| 100 | */
|
|---|
| 101 | protected $socket;
|
|---|
| 102 |
|
|---|
| 103 | /**
|
|---|
| 104 | * Challenge used for server digest authentication
|
|---|
| 105 | * @var array
|
|---|
| 106 | */
|
|---|
| 107 | protected $serverChallenge;
|
|---|
| 108 |
|
|---|
| 109 | /**
|
|---|
| 110 | * Challenge used for proxy digest authentication
|
|---|
| 111 | * @var array
|
|---|
| 112 | */
|
|---|
| 113 | protected $proxyChallenge;
|
|---|
| 114 |
|
|---|
| 115 | /**
|
|---|
| 116 | * Remaining length of the current chunk, when reading chunked response
|
|---|
| 117 | * @var integer
|
|---|
| 118 | * @see readChunked()
|
|---|
| 119 | */
|
|---|
| 120 | protected $chunkLength = 0;
|
|---|
| 121 |
|
|---|
| 122 | /**
|
|---|
| 123 | * Remaining amount of redirections to follow
|
|---|
| 124 | *
|
|---|
| 125 | * Starts at 'max_redirects' configuration parameter and is reduced on each
|
|---|
| 126 | * subsequent redirect. An Exception will be thrown once it reaches zero.
|
|---|
| 127 | *
|
|---|
| 128 | * @var integer
|
|---|
| 129 | */
|
|---|
| 130 | protected $redirectCountdown = null;
|
|---|
| 131 |
|
|---|
| 132 | /**
|
|---|
| 133 | * Sends request to the remote server and returns its response
|
|---|
| 134 | *
|
|---|
| 135 | * @param HTTP_Request2 $request HTTP request message
|
|---|
| 136 | *
|
|---|
| 137 | * @return HTTP_Request2_Response
|
|---|
| 138 | * @throws HTTP_Request2_Exception
|
|---|
| 139 | */
|
|---|
| 140 | public function sendRequest(HTTP_Request2 $request)
|
|---|
| 141 | {
|
|---|
| 142 | $this->request = $request;
|
|---|
| 143 |
|
|---|
| 144 | try {
|
|---|
| 145 | $keepAlive = $this->connect();
|
|---|
| 146 | $headers = $this->prepareHeaders();
|
|---|
| 147 | $this->socket->write($headers);
|
|---|
| 148 | // provide request headers to the observer, see request #7633
|
|---|
| 149 | $this->request->setLastEvent('sentHeaders', $headers);
|
|---|
| 150 | $this->writeBody();
|
|---|
| 151 |
|
|---|
| 152 | $response = $this->readResponse();
|
|---|
| 153 |
|
|---|
| 154 | if ($jar = $request->getCookieJar()) {
|
|---|
| 155 | $jar->addCookiesFromResponse($response, $request->getUrl());
|
|---|
| 156 | }
|
|---|
| 157 |
|
|---|
| 158 | if (!$this->canKeepAlive($keepAlive, $response)) {
|
|---|
| 159 | $this->disconnect();
|
|---|
| 160 | }
|
|---|
| 161 |
|
|---|
| 162 | if ($this->shouldUseProxyDigestAuth($response)) {
|
|---|
| 163 | return $this->sendRequest($request);
|
|---|
| 164 | }
|
|---|
| 165 | if ($this->shouldUseServerDigestAuth($response)) {
|
|---|
| 166 | return $this->sendRequest($request);
|
|---|
| 167 | }
|
|---|
| 168 | if ($authInfo = $response->getHeader('authentication-info')) {
|
|---|
| 169 | $this->updateChallenge($this->serverChallenge, $authInfo);
|
|---|
| 170 | }
|
|---|
| 171 | if ($proxyInfo = $response->getHeader('proxy-authentication-info')) {
|
|---|
| 172 | $this->updateChallenge($this->proxyChallenge, $proxyInfo);
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | } catch (Exception $e) {
|
|---|
| 176 | $this->disconnect();
|
|---|
| 177 | }
|
|---|
| 178 |
|
|---|
| 179 | unset($this->request, $this->requestBody);
|
|---|
| 180 |
|
|---|
| 181 | if (!empty($e)) {
|
|---|
| 182 | $this->redirectCountdown = null;
|
|---|
| 183 | throw $e;
|
|---|
| 184 | }
|
|---|
| 185 |
|
|---|
| 186 | if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) {
|
|---|
| 187 | $this->redirectCountdown = null;
|
|---|
| 188 | return $response;
|
|---|
| 189 | } else {
|
|---|
| 190 | return $this->handleRedirect($request, $response);
|
|---|
| 191 | }
|
|---|
| 192 | }
|
|---|
| 193 |
|
|---|
| 194 | /**
|
|---|
| 195 | * Connects to the remote server
|
|---|
| 196 | *
|
|---|
| 197 | * @return bool whether the connection can be persistent
|
|---|
| 198 | * @throws HTTP_Request2_Exception
|
|---|
| 199 | */
|
|---|
| 200 | protected function connect()
|
|---|
| 201 | {
|
|---|
| 202 | $secure = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https');
|
|---|
| 203 | $tunnel = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
|
|---|
| 204 | $headers = $this->request->getHeaders();
|
|---|
| 205 | $reqHost = $this->request->getUrl()->getHost();
|
|---|
| 206 | if (!($reqPort = $this->request->getUrl()->getPort())) {
|
|---|
| 207 | $reqPort = $secure? 443: 80;
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | $httpProxy = $socksProxy = false;
|
|---|
| 211 | if (!($host = $this->request->getConfig('proxy_host'))) {
|
|---|
| 212 | $host = $reqHost;
|
|---|
| 213 | $port = $reqPort;
|
|---|
| 214 | } else {
|
|---|
| 215 | if (!($port = $this->request->getConfig('proxy_port'))) {
|
|---|
| 216 | throw new HTTP_Request2_LogicException(
|
|---|
| 217 | 'Proxy port not provided',
|
|---|
| 218 | HTTP_Request2_Exception::MISSING_VALUE
|
|---|
| 219 | );
|
|---|
| 220 | }
|
|---|
| 221 | if ('http' == ($type = $this->request->getConfig('proxy_type'))) {
|
|---|
| 222 | $httpProxy = true;
|
|---|
| 223 | } elseif ('socks5' == $type) {
|
|---|
| 224 | $socksProxy = true;
|
|---|
| 225 | } else {
|
|---|
| 226 | throw new HTTP_Request2_NotImplementedException(
|
|---|
| 227 | "Proxy type '{$type}' is not supported"
|
|---|
| 228 | );
|
|---|
| 229 | }
|
|---|
| 230 | }
|
|---|
| 231 |
|
|---|
| 232 | if ($tunnel && !$httpProxy) {
|
|---|
| 233 | throw new HTTP_Request2_LogicException(
|
|---|
| 234 | "Trying to perform CONNECT request without proxy",
|
|---|
| 235 | HTTP_Request2_Exception::MISSING_VALUE
|
|---|
| 236 | );
|
|---|
| 237 | }
|
|---|
| 238 | if ($secure && !in_array('ssl', stream_get_transports())) {
|
|---|
| 239 | throw new HTTP_Request2_LogicException(
|
|---|
| 240 | 'Need OpenSSL support for https:// requests',
|
|---|
| 241 | HTTP_Request2_Exception::MISCONFIGURATION
|
|---|
| 242 | );
|
|---|
| 243 | }
|
|---|
| 244 |
|
|---|
| 245 | // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
|
|---|
| 246 | // connection token to a proxy server...
|
|---|
| 247 | if ($httpProxy && !$secure && !empty($headers['connection'])
|
|---|
| 248 | && 'Keep-Alive' == $headers['connection']
|
|---|
| 249 | ) {
|
|---|
| 250 | $this->request->setHeader('connection');
|
|---|
| 251 | }
|
|---|
| 252 |
|
|---|
| 253 | $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') &&
|
|---|
| 254 | empty($headers['connection'])) ||
|
|---|
| 255 | (!empty($headers['connection']) &&
|
|---|
| 256 | 'Keep-Alive' == $headers['connection']);
|
|---|
| 257 |
|
|---|
| 258 | $options = array();
|
|---|
| 259 | if ($secure || $tunnel) {
|
|---|
| 260 | foreach ($this->request->getConfig() as $name => $value) {
|
|---|
| 261 | if ('ssl_' == substr($name, 0, 4) && null !== $value) {
|
|---|
| 262 | if ('ssl_verify_host' == $name) {
|
|---|
| 263 | if ($value) {
|
|---|
| 264 | $options['CN_match'] = $reqHost;
|
|---|
| 265 | }
|
|---|
| 266 | } else {
|
|---|
| 267 | $options[substr($name, 4)] = $value;
|
|---|
| 268 | }
|
|---|
| 269 | }
|
|---|
| 270 | }
|
|---|
| 271 | ksort($options);
|
|---|
| 272 | }
|
|---|
| 273 |
|
|---|
| 274 | // Use global request timeout if given, see feature requests #5735, #8964
|
|---|
| 275 | if ($timeout = $this->request->getConfig('timeout')) {
|
|---|
| 276 | $deadline = time() + $timeout;
|
|---|
| 277 | } else {
|
|---|
| 278 | $deadline = null;
|
|---|
| 279 | }
|
|---|
| 280 |
|
|---|
| 281 | // Changing SSL context options after connection is established does *not*
|
|---|
| 282 | // work, we need a new connection if options change
|
|---|
| 283 | $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'ssl://')
|
|---|
| 284 | . $host . ':' . $port;
|
|---|
| 285 | $socketKey = $remote . (
|
|---|
| 286 | ($secure && $httpProxy || $socksProxy)
|
|---|
| 287 | ? "->{$reqHost}:{$reqPort}" : ''
|
|---|
| 288 | ) . (empty($options)? '': ':' . serialize($options));
|
|---|
| 289 | unset($this->socket);
|
|---|
| 290 |
|
|---|
| 291 | // We use persistent connections and have a connected socket?
|
|---|
| 292 | // Ensure that the socket is still connected, see bug #16149
|
|---|
| 293 | if ($keepAlive && !empty(self::$sockets[$socketKey])
|
|---|
| 294 | && !self::$sockets[$socketKey]->eof()
|
|---|
| 295 | ) {
|
|---|
| 296 | $this->socket =& self::$sockets[$socketKey];
|
|---|
| 297 |
|
|---|
| 298 | } else {
|
|---|
| 299 | if ($socksProxy) {
|
|---|
| 300 | require_once 'HTTP/Request2/SOCKS5.php';
|
|---|
| 301 |
|
|---|
| 302 | $this->socket = new HTTP_Request2_SOCKS5(
|
|---|
| 303 | $remote, $this->request->getConfig('connect_timeout'),
|
|---|
| 304 | $options, $this->request->getConfig('proxy_user'),
|
|---|
| 305 | $this->request->getConfig('proxy_password')
|
|---|
| 306 | );
|
|---|
| 307 | // handle request timeouts ASAP
|
|---|
| 308 | $this->socket->setDeadline($deadline, $this->request->getConfig('timeout'));
|
|---|
| 309 | $this->socket->connect($reqHost, $reqPort);
|
|---|
| 310 | if (!$secure) {
|
|---|
| 311 | $conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}";
|
|---|
| 312 | } else {
|
|---|
| 313 | $this->socket->enableCrypto();
|
|---|
| 314 | $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";
|
|---|
| 315 | }
|
|---|
| 316 |
|
|---|
| 317 | } elseif ($secure && $httpProxy && !$tunnel) {
|
|---|
| 318 | $this->establishTunnel();
|
|---|
| 319 | $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";
|
|---|
| 320 |
|
|---|
| 321 | } else {
|
|---|
| 322 | $this->socket = new HTTP_Request2_SocketWrapper(
|
|---|
| 323 | $remote, $this->request->getConfig('connect_timeout'), $options
|
|---|
| 324 | );
|
|---|
| 325 | }
|
|---|
| 326 | $this->request->setLastEvent('connect', empty($conninfo)? $remote: $conninfo);
|
|---|
| 327 | self::$sockets[$socketKey] =& $this->socket;
|
|---|
| 328 | }
|
|---|
| 329 | $this->socket->setDeadline($deadline, $this->request->getConfig('timeout'));
|
|---|
| 330 | return $keepAlive;
|
|---|
| 331 | }
|
|---|
| 332 |
|
|---|
| 333 | /**
|
|---|
| 334 | * Establishes a tunnel to a secure remote server via HTTP CONNECT request
|
|---|
| 335 | *
|
|---|
| 336 | * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP
|
|---|
| 337 | * sees that we are connected to a proxy server (duh!) rather than the server
|
|---|
| 338 | * that presents its certificate.
|
|---|
| 339 | *
|
|---|
| 340 | * @link http://tools.ietf.org/html/rfc2817#section-5.2
|
|---|
| 341 | * @throws HTTP_Request2_Exception
|
|---|
| 342 | */
|
|---|
| 343 | protected function establishTunnel()
|
|---|
| 344 | {
|
|---|
| 345 | $donor = new self;
|
|---|
| 346 | $connect = new HTTP_Request2(
|
|---|
| 347 | $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT,
|
|---|
| 348 | array_merge($this->request->getConfig(), array('adapter' => $donor))
|
|---|
| 349 | );
|
|---|
| 350 | $response = $connect->send();
|
|---|
| 351 | // Need any successful (2XX) response
|
|---|
| 352 | if (200 > $response->getStatus() || 300 <= $response->getStatus()) {
|
|---|
| 353 | throw new HTTP_Request2_ConnectionException(
|
|---|
| 354 | 'Failed to connect via HTTPS proxy. Proxy response: ' .
|
|---|
| 355 | $response->getStatus() . ' ' . $response->getReasonPhrase()
|
|---|
| 356 | );
|
|---|
| 357 | }
|
|---|
| 358 | $this->socket = $donor->socket;
|
|---|
| 359 | $this->socket->enableCrypto();
|
|---|
| 360 | }
|
|---|
| 361 |
|
|---|
| 362 | /**
|
|---|
| 363 | * Checks whether current connection may be reused or should be closed
|
|---|
| 364 | *
|
|---|
| 365 | * @param boolean $requestKeepAlive whether connection could
|
|---|
| 366 | * be persistent in the first place
|
|---|
| 367 | * @param HTTP_Request2_Response $response response object to check
|
|---|
| 368 | *
|
|---|
| 369 | * @return boolean
|
|---|
| 370 | */
|
|---|
| 371 | protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response)
|
|---|
| 372 | {
|
|---|
| 373 | // Do not close socket on successful CONNECT request
|
|---|
| 374 | if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod()
|
|---|
| 375 | && 200 <= $response->getStatus() && 300 > $response->getStatus()
|
|---|
| 376 | ) {
|
|---|
| 377 | return true;
|
|---|
| 378 | }
|
|---|
| 379 |
|
|---|
| 380 | $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding'))
|
|---|
| 381 | || null !== $response->getHeader('content-length')
|
|---|
| 382 | // no body possible for such responses, see also request #17031
|
|---|
| 383 | || HTTP_Request2::METHOD_HEAD == $this->request->getMethod()
|
|---|
| 384 | || in_array($response->getStatus(), array(204, 304));
|
|---|
| 385 | $persistent = 'keep-alive' == strtolower($response->getHeader('connection')) ||
|
|---|
| 386 | (null === $response->getHeader('connection') &&
|
|---|
| 387 | '1.1' == $response->getVersion());
|
|---|
| 388 | return $requestKeepAlive && $lengthKnown && $persistent;
|
|---|
| 389 | }
|
|---|
| 390 |
|
|---|
| 391 | /**
|
|---|
| 392 | * Disconnects from the remote server
|
|---|
| 393 | */
|
|---|
| 394 | protected function disconnect()
|
|---|
| 395 | {
|
|---|
| 396 | if (!empty($this->socket)) {
|
|---|
| 397 | $this->socket = null;
|
|---|
| 398 | $this->request->setLastEvent('disconnect');
|
|---|
| 399 | }
|
|---|
| 400 | }
|
|---|
| 401 |
|
|---|
| 402 | /**
|
|---|
| 403 | * Handles HTTP redirection
|
|---|
| 404 | *
|
|---|
| 405 | * This method will throw an Exception if redirect to a non-HTTP(S) location
|
|---|
| 406 | * is attempted, also if number of redirects performed already is equal to
|
|---|
| 407 | * 'max_redirects' configuration parameter.
|
|---|
| 408 | *
|
|---|
| 409 | * @param HTTP_Request2 $request Original request
|
|---|
| 410 | * @param HTTP_Request2_Response $response Response containing redirect
|
|---|
| 411 | *
|
|---|
| 412 | * @return HTTP_Request2_Response Response from a new location
|
|---|
| 413 | * @throws HTTP_Request2_Exception
|
|---|
| 414 | */
|
|---|
| 415 | protected function handleRedirect(
|
|---|
| 416 | HTTP_Request2 $request, HTTP_Request2_Response $response
|
|---|
| 417 | ) {
|
|---|
| 418 | if (is_null($this->redirectCountdown)) {
|
|---|
| 419 | $this->redirectCountdown = $request->getConfig('max_redirects');
|
|---|
| 420 | }
|
|---|
| 421 | if (0 == $this->redirectCountdown) {
|
|---|
| 422 | $this->redirectCountdown = null;
|
|---|
| 423 | // Copying cURL behaviour
|
|---|
| 424 | throw new HTTP_Request2_MessageException(
|
|---|
| 425 | 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed',
|
|---|
| 426 | HTTP_Request2_Exception::TOO_MANY_REDIRECTS
|
|---|
| 427 | );
|
|---|
| 428 | }
|
|---|
| 429 | $redirectUrl = new Net_URL2(
|
|---|
| 430 | $response->getHeader('location'),
|
|---|
| 431 | array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets'))
|
|---|
| 432 | );
|
|---|
| 433 | // refuse non-HTTP redirect
|
|---|
| 434 | if ($redirectUrl->isAbsolute()
|
|---|
| 435 | && !in_array($redirectUrl->getScheme(), array('http', 'https'))
|
|---|
| 436 | ) {
|
|---|
| 437 | $this->redirectCountdown = null;
|
|---|
| 438 | throw new HTTP_Request2_MessageException(
|
|---|
| 439 | 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(),
|
|---|
| 440 | HTTP_Request2_Exception::NON_HTTP_REDIRECT
|
|---|
| 441 | );
|
|---|
| 442 | }
|
|---|
| 443 | // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),
|
|---|
| 444 | // but in practice it is often not
|
|---|
| 445 | if (!$redirectUrl->isAbsolute()) {
|
|---|
| 446 | $redirectUrl = $request->getUrl()->resolve($redirectUrl);
|
|---|
| 447 | }
|
|---|
| 448 | $redirect = clone $request;
|
|---|
| 449 | $redirect->setUrl($redirectUrl);
|
|---|
| 450 | if (303 == $response->getStatus()
|
|---|
| 451 | || (!$request->getConfig('strict_redirects')
|
|---|
| 452 | && in_array($response->getStatus(), array(301, 302)))
|
|---|
| 453 | ) {
|
|---|
| 454 | $redirect->setMethod(HTTP_Request2::METHOD_GET);
|
|---|
| 455 | $redirect->setBody('');
|
|---|
| 456 | }
|
|---|
| 457 |
|
|---|
| 458 | if (0 < $this->redirectCountdown) {
|
|---|
| 459 | $this->redirectCountdown--;
|
|---|
| 460 | }
|
|---|
| 461 | return $this->sendRequest($redirect);
|
|---|
| 462 | }
|
|---|
| 463 |
|
|---|
| 464 | /**
|
|---|
| 465 | * Checks whether another request should be performed with server digest auth
|
|---|
| 466 | *
|
|---|
| 467 | * Several conditions should be satisfied for it to return true:
|
|---|
| 468 | * - response status should be 401
|
|---|
| 469 | * - auth credentials should be set in the request object
|
|---|
| 470 | * - response should contain WWW-Authenticate header with digest challenge
|
|---|
| 471 | * - there is either no challenge stored for this URL or new challenge
|
|---|
| 472 | * contains stale=true parameter (in other case we probably just failed
|
|---|
| 473 | * due to invalid username / password)
|
|---|
| 474 | *
|
|---|
| 475 | * The method stores challenge values in $challenges static property
|
|---|
| 476 | *
|
|---|
| 477 | * @param HTTP_Request2_Response $response response to check
|
|---|
| 478 | *
|
|---|
| 479 | * @return boolean whether another request should be performed
|
|---|
| 480 | * @throws HTTP_Request2_Exception in case of unsupported challenge parameters
|
|---|
| 481 | */
|
|---|
| 482 | protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response)
|
|---|
| 483 | {
|
|---|
| 484 | // no sense repeating a request if we don't have credentials
|
|---|
| 485 | if (401 != $response->getStatus() || !$this->request->getAuth()) {
|
|---|
| 486 | return false;
|
|---|
| 487 | }
|
|---|
| 488 | if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) {
|
|---|
| 489 | return false;
|
|---|
| 490 | }
|
|---|
| 491 |
|
|---|
| 492 | $url = $this->request->getUrl();
|
|---|
| 493 | $scheme = $url->getScheme();
|
|---|
| 494 | $host = $scheme . '://' . $url->getHost();
|
|---|
| 495 | if ($port = $url->getPort()) {
|
|---|
| 496 | if ((0 == strcasecmp($scheme, 'http') && 80 != $port)
|
|---|
| 497 | || (0 == strcasecmp($scheme, 'https') && 443 != $port)
|
|---|
| 498 | ) {
|
|---|
| 499 | $host .= ':' . $port;
|
|---|
| 500 | }
|
|---|
| 501 | }
|
|---|
| 502 |
|
|---|
| 503 | if (!empty($challenge['domain'])) {
|
|---|
| 504 | $prefixes = array();
|
|---|
| 505 | foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) {
|
|---|
| 506 | // don't bother with different servers
|
|---|
| 507 | if ('/' == substr($prefix, 0, 1)) {
|
|---|
| 508 | $prefixes[] = $host . $prefix;
|
|---|
| 509 | }
|
|---|
| 510 | }
|
|---|
| 511 | }
|
|---|
| 512 | if (empty($prefixes)) {
|
|---|
| 513 | $prefixes = array($host . '/');
|
|---|
| 514 | }
|
|---|
| 515 |
|
|---|
| 516 | $ret = true;
|
|---|
| 517 | foreach ($prefixes as $prefix) {
|
|---|
| 518 | if (!empty(self::$challenges[$prefix])
|
|---|
| 519 | && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
|
|---|
| 520 | ) {
|
|---|
| 521 | // probably credentials are invalid
|
|---|
| 522 | $ret = false;
|
|---|
| 523 | }
|
|---|
| 524 | self::$challenges[$prefix] =& $challenge;
|
|---|
| 525 | }
|
|---|
| 526 | return $ret;
|
|---|
| 527 | }
|
|---|
| 528 |
|
|---|
| 529 | /**
|
|---|
| 530 | * Checks whether another request should be performed with proxy digest auth
|
|---|
| 531 | *
|
|---|
| 532 | * Several conditions should be satisfied for it to return true:
|
|---|
| 533 | * - response status should be 407
|
|---|
| 534 | * - proxy auth credentials should be set in the request object
|
|---|
| 535 | * - response should contain Proxy-Authenticate header with digest challenge
|
|---|
| 536 | * - there is either no challenge stored for this proxy or new challenge
|
|---|
| 537 | * contains stale=true parameter (in other case we probably just failed
|
|---|
| 538 | * due to invalid username / password)
|
|---|
| 539 | *
|
|---|
| 540 | * The method stores challenge values in $challenges static property
|
|---|
| 541 | *
|
|---|
| 542 | * @param HTTP_Request2_Response $response response to check
|
|---|
| 543 | *
|
|---|
| 544 | * @return boolean whether another request should be performed
|
|---|
| 545 | * @throws HTTP_Request2_Exception in case of unsupported challenge parameters
|
|---|
| 546 | */
|
|---|
| 547 | protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response)
|
|---|
| 548 | {
|
|---|
| 549 | if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) {
|
|---|
| 550 | return false;
|
|---|
| 551 | }
|
|---|
| 552 | if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) {
|
|---|
| 553 | return false;
|
|---|
| 554 | }
|
|---|
| 555 |
|
|---|
| 556 | $key = 'proxy://' . $this->request->getConfig('proxy_host') .
|
|---|
| 557 | ':' . $this->request->getConfig('proxy_port');
|
|---|
| 558 |
|
|---|
| 559 | if (!empty(self::$challenges[$key])
|
|---|
| 560 | && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
|
|---|
| 561 | ) {
|
|---|
| 562 | $ret = false;
|
|---|
| 563 | } else {
|
|---|
| 564 | $ret = true;
|
|---|
| 565 | }
|
|---|
| 566 | self::$challenges[$key] = $challenge;
|
|---|
| 567 | return $ret;
|
|---|
| 568 | }
|
|---|
| 569 |
|
|---|
| 570 | /**
|
|---|
| 571 | * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value
|
|---|
| 572 | *
|
|---|
| 573 | * There is a problem with implementation of RFC 2617: several of the parameters
|
|---|
| 574 | * are defined as quoted-string there and thus may contain backslash escaped
|
|---|
| 575 | * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as
|
|---|
| 576 | * just value of quoted-string X without surrounding quotes, it doesn't speak
|
|---|
| 577 | * about removing backslash escaping.
|
|---|
| 578 | *
|
|---|
| 579 | * Now realm parameter is user-defined and human-readable, strange things
|
|---|
| 580 | * happen when it contains quotes:
|
|---|
| 581 | * - Apache allows quotes in realm, but apparently uses realm value without
|
|---|
| 582 | * backslashes for digest computation
|
|---|
| 583 | * - Squid allows (manually escaped) quotes there, but it is impossible to
|
|---|
| 584 | * authorize with either escaped or unescaped quotes used in digest,
|
|---|
| 585 | * probably it can't parse the response (?)
|
|---|
| 586 | * - Both IE and Firefox display realm value with backslashes in
|
|---|
| 587 | * the password popup and apparently use the same value for digest
|
|---|
| 588 | *
|
|---|
| 589 | * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in
|
|---|
| 590 | * quoted-string handling, unfortunately that means failure to authorize
|
|---|
| 591 | * sometimes
|
|---|
| 592 | *
|
|---|
| 593 | * @param string $headerValue value of WWW-Authenticate or Proxy-Authenticate header
|
|---|
| 594 | *
|
|---|
| 595 | * @return mixed associative array with challenge parameters, false if
|
|---|
| 596 | * no challenge is present in header value
|
|---|
| 597 | * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters
|
|---|
| 598 | */
|
|---|
| 599 | protected function parseDigestChallenge($headerValue)
|
|---|
| 600 | {
|
|---|
| 601 | $authParam = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
|
|---|
| 602 | self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')';
|
|---|
| 603 | $challenge = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!";
|
|---|
| 604 | if (!preg_match($challenge, $headerValue, $matches)) {
|
|---|
| 605 | return false;
|
|---|
| 606 | }
|
|---|
| 607 |
|
|---|
| 608 | preg_match_all('!' . $authParam . '!', $matches[0], $params);
|
|---|
| 609 | $paramsAry = array();
|
|---|
| 610 | $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale',
|
|---|
| 611 | 'algorithm', 'qop');
|
|---|
| 612 | for ($i = 0; $i < count($params[0]); $i++) {
|
|---|
| 613 | // section 3.2.1: Any unrecognized directive MUST be ignored.
|
|---|
| 614 | if (in_array($params[1][$i], $knownParams)) {
|
|---|
| 615 | if ('"' == substr($params[2][$i], 0, 1)) {
|
|---|
| 616 | $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
|
|---|
| 617 | } else {
|
|---|
| 618 | $paramsAry[$params[1][$i]] = $params[2][$i];
|
|---|
| 619 | }
|
|---|
| 620 | }
|
|---|
| 621 | }
|
|---|
| 622 | // we only support qop=auth
|
|---|
| 623 | if (!empty($paramsAry['qop'])
|
|---|
| 624 | && !in_array('auth', array_map('trim', explode(',', $paramsAry['qop'])))
|
|---|
| 625 | ) {
|
|---|
| 626 | throw new HTTP_Request2_NotImplementedException(
|
|---|
| 627 | "Only 'auth' qop is currently supported in digest authentication, " .
|
|---|
| 628 | "server requested '{$paramsAry['qop']}'"
|
|---|
| 629 | );
|
|---|
| 630 | }
|
|---|
| 631 | // we only support algorithm=MD5
|
|---|
| 632 | if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) {
|
|---|
| 633 | throw new HTTP_Request2_NotImplementedException(
|
|---|
| 634 | "Only 'MD5' algorithm is currently supported in digest authentication, " .
|
|---|
| 635 | "server requested '{$paramsAry['algorithm']}'"
|
|---|
| 636 | );
|
|---|
| 637 | }
|
|---|
| 638 |
|
|---|
| 639 | return $paramsAry;
|
|---|
| 640 | }
|
|---|
| 641 |
|
|---|
| 642 | /**
|
|---|
| 643 | * Parses [Proxy-]Authentication-Info header value and updates challenge
|
|---|
| 644 | *
|
|---|
| 645 | * @param array &$challenge challenge to update
|
|---|
| 646 | * @param string $headerValue value of [Proxy-]Authentication-Info header
|
|---|
| 647 | *
|
|---|
| 648 | * @todo validate server rspauth response
|
|---|
| 649 | */
|
|---|
| 650 | protected function updateChallenge(&$challenge, $headerValue)
|
|---|
| 651 | {
|
|---|
| 652 | $authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
|
|---|
| 653 | self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!';
|
|---|
| 654 | $paramsAry = array();
|
|---|
| 655 |
|
|---|
| 656 | preg_match_all($authParam, $headerValue, $params);
|
|---|
| 657 | for ($i = 0; $i < count($params[0]); $i++) {
|
|---|
| 658 | if ('"' == substr($params[2][$i], 0, 1)) {
|
|---|
| 659 | $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
|
|---|
| 660 | } else {
|
|---|
| 661 | $paramsAry[$params[1][$i]] = $params[2][$i];
|
|---|
| 662 | }
|
|---|
| 663 | }
|
|---|
| 664 | // for now, just update the nonce value
|
|---|
| 665 | if (!empty($paramsAry['nextnonce'])) {
|
|---|
| 666 | $challenge['nonce'] = $paramsAry['nextnonce'];
|
|---|
| 667 | $challenge['nc'] = 1;
|
|---|
| 668 | }
|
|---|
| 669 | }
|
|---|
| 670 |
|
|---|
| 671 | /**
|
|---|
| 672 | * Creates a value for [Proxy-]Authorization header when using digest authentication
|
|---|
| 673 | *
|
|---|
| 674 | * @param string $user user name
|
|---|
| 675 | * @param string $password password
|
|---|
| 676 | * @param string $url request URL
|
|---|
| 677 | * @param array &$challenge digest challenge parameters
|
|---|
| 678 | *
|
|---|
| 679 | * @return string value of [Proxy-]Authorization request header
|
|---|
| 680 | * @link http://tools.ietf.org/html/rfc2617#section-3.2.2
|
|---|
| 681 | */
|
|---|
| 682 | protected function createDigestResponse($user, $password, $url, &$challenge)
|
|---|
| 683 | {
|
|---|
| 684 | if (false !== ($q = strpos($url, '?'))
|
|---|
| 685 | && $this->request->getConfig('digest_compat_ie')
|
|---|
| 686 | ) {
|
|---|
| 687 | $url = substr($url, 0, $q);
|
|---|
| 688 | }
|
|---|
| 689 |
|
|---|
| 690 | $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password);
|
|---|
| 691 | $a2 = md5($this->request->getMethod() . ':' . $url);
|
|---|
| 692 |
|
|---|
| 693 | if (empty($challenge['qop'])) {
|
|---|
| 694 | $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2);
|
|---|
| 695 | } else {
|
|---|
| 696 | $challenge['cnonce'] = 'Req2.' . rand();
|
|---|
| 697 | if (empty($challenge['nc'])) {
|
|---|
| 698 | $challenge['nc'] = 1;
|
|---|
| 699 | }
|
|---|
| 700 | $nc = sprintf('%08x', $challenge['nc']++);
|
|---|
| 701 | $digest = md5(
|
|---|
| 702 | $a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' .
|
|---|
| 703 | $challenge['cnonce'] . ':auth:' . $a2
|
|---|
| 704 | );
|
|---|
| 705 | }
|
|---|
| 706 | return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' .
|
|---|
| 707 | 'realm="' . $challenge['realm'] . '", ' .
|
|---|
| 708 | 'nonce="' . $challenge['nonce'] . '", ' .
|
|---|
| 709 | 'uri="' . $url . '", ' .
|
|---|
| 710 | 'response="' . $digest . '"' .
|
|---|
| 711 | (!empty($challenge['opaque'])?
|
|---|
| 712 | ', opaque="' . $challenge['opaque'] . '"':
|
|---|
| 713 | '') .
|
|---|
| 714 | (!empty($challenge['qop'])?
|
|---|
| 715 | ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"':
|
|---|
| 716 | '');
|
|---|
| 717 | }
|
|---|
| 718 |
|
|---|
| 719 | /**
|
|---|
| 720 | * Adds 'Authorization' header (if needed) to request headers array
|
|---|
| 721 | *
|
|---|
| 722 | * @param array &$headers request headers
|
|---|
| 723 | * @param string $requestHost request host (needed for digest authentication)
|
|---|
| 724 | * @param string $requestUrl request URL (needed for digest authentication)
|
|---|
| 725 | *
|
|---|
| 726 | * @throws HTTP_Request2_NotImplementedException
|
|---|
| 727 | */
|
|---|
| 728 | protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)
|
|---|
| 729 | {
|
|---|
| 730 | if (!($auth = $this->request->getAuth())) {
|
|---|
| 731 | return;
|
|---|
| 732 | }
|
|---|
| 733 | switch ($auth['scheme']) {
|
|---|
| 734 | case HTTP_Request2::AUTH_BASIC:
|
|---|
| 735 | $headers['authorization'] = 'Basic ' . base64_encode(
|
|---|
| 736 | $auth['user'] . ':' . $auth['password']
|
|---|
| 737 | );
|
|---|
| 738 | break;
|
|---|
| 739 |
|
|---|
| 740 | case HTTP_Request2::AUTH_DIGEST:
|
|---|
| 741 | unset($this->serverChallenge);
|
|---|
| 742 | $fullUrl = ('/' == $requestUrl[0])?
|
|---|
| 743 | $this->request->getUrl()->getScheme() . '://' .
|
|---|
| 744 | $requestHost . $requestUrl:
|
|---|
| 745 | $requestUrl;
|
|---|
| 746 | foreach (array_keys(self::$challenges) as $key) {
|
|---|
| 747 | if ($key == substr($fullUrl, 0, strlen($key))) {
|
|---|
| 748 | $headers['authorization'] = $this->createDigestResponse(
|
|---|
| 749 | $auth['user'], $auth['password'],
|
|---|
| 750 | $requestUrl, self::$challenges[$key]
|
|---|
| 751 | );
|
|---|
| 752 | $this->serverChallenge =& self::$challenges[$key];
|
|---|
| 753 | break;
|
|---|
| 754 | }
|
|---|
| 755 | }
|
|---|
| 756 | break;
|
|---|
| 757 |
|
|---|
| 758 | default:
|
|---|
| 759 | throw new HTTP_Request2_NotImplementedException(
|
|---|
| 760 | "Unknown HTTP authentication scheme '{$auth['scheme']}'"
|
|---|
| 761 | );
|
|---|
| 762 | }
|
|---|
| 763 | }
|
|---|
| 764 |
|
|---|
| 765 | /**
|
|---|
| 766 | * Adds 'Proxy-Authorization' header (if needed) to request headers array
|
|---|
| 767 | *
|
|---|
| 768 | * @param array &$headers request headers
|
|---|
| 769 | * @param string $requestUrl request URL (needed for digest authentication)
|
|---|
| 770 | *
|
|---|
| 771 | * @throws HTTP_Request2_NotImplementedException
|
|---|
| 772 | */
|
|---|
| 773 | protected function addProxyAuthorizationHeader(&$headers, $requestUrl)
|
|---|
| 774 | {
|
|---|
| 775 | if (!$this->request->getConfig('proxy_host')
|
|---|
| 776 | || !($user = $this->request->getConfig('proxy_user'))
|
|---|
| 777 | || (0 == strcasecmp('https', $this->request->getUrl()->getScheme())
|
|---|
| 778 | && HTTP_Request2::METHOD_CONNECT != $this->request->getMethod())
|
|---|
| 779 | ) {
|
|---|
| 780 | return;
|
|---|
| 781 | }
|
|---|
| 782 |
|
|---|
| 783 | $password = $this->request->getConfig('proxy_password');
|
|---|
| 784 | switch ($this->request->getConfig('proxy_auth_scheme')) {
|
|---|
| 785 | case HTTP_Request2::AUTH_BASIC:
|
|---|
| 786 | $headers['proxy-authorization'] = 'Basic ' . base64_encode(
|
|---|
| 787 | $user . ':' . $password
|
|---|
| 788 | );
|
|---|
| 789 | break;
|
|---|
| 790 |
|
|---|
| 791 | case HTTP_Request2::AUTH_DIGEST:
|
|---|
| 792 | unset($this->proxyChallenge);
|
|---|
| 793 | $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') .
|
|---|
| 794 | ':' . $this->request->getConfig('proxy_port');
|
|---|
| 795 | if (!empty(self::$challenges[$proxyUrl])) {
|
|---|
| 796 | $headers['proxy-authorization'] = $this->createDigestResponse(
|
|---|
| 797 | $user, $password,
|
|---|
| 798 | $requestUrl, self::$challenges[$proxyUrl]
|
|---|
| 799 | );
|
|---|
| 800 | $this->proxyChallenge =& self::$challenges[$proxyUrl];
|
|---|
| 801 | }
|
|---|
| 802 | break;
|
|---|
| 803 |
|
|---|
| 804 | default:
|
|---|
| 805 | throw new HTTP_Request2_NotImplementedException(
|
|---|
| 806 | "Unknown HTTP authentication scheme '" .
|
|---|
| 807 | $this->request->getConfig('proxy_auth_scheme') . "'"
|
|---|
| 808 | );
|
|---|
| 809 | }
|
|---|
| 810 | }
|
|---|
| 811 |
|
|---|
| 812 |
|
|---|
| 813 | /**
|
|---|
| 814 | * Creates the string with the Request-Line and request headers
|
|---|
| 815 | *
|
|---|
| 816 | * @return string
|
|---|
| 817 | * @throws HTTP_Request2_Exception
|
|---|
| 818 | */
|
|---|
| 819 | protected function prepareHeaders()
|
|---|
| 820 | {
|
|---|
| 821 | $headers = $this->request->getHeaders();
|
|---|
| 822 | $url = $this->request->getUrl();
|
|---|
| 823 | $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
|
|---|
| 824 | $host = $url->getHost();
|
|---|
| 825 |
|
|---|
| 826 | $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80;
|
|---|
| 827 | if (($port = $url->getPort()) && $port != $defaultPort || $connect) {
|
|---|
| 828 | $host .= ':' . (empty($port)? $defaultPort: $port);
|
|---|
| 829 | }
|
|---|
| 830 | // Do not overwrite explicitly set 'Host' header, see bug #16146
|
|---|
| 831 | if (!isset($headers['host'])) {
|
|---|
| 832 | $headers['host'] = $host;
|
|---|
| 833 | }
|
|---|
| 834 |
|
|---|
| 835 | if ($connect) {
|
|---|
| 836 | $requestUrl = $host;
|
|---|
| 837 |
|
|---|
| 838 | } else {
|
|---|
| 839 | if (!$this->request->getConfig('proxy_host')
|
|---|
| 840 | || 'http' != $this->request->getConfig('proxy_type')
|
|---|
| 841 | || 0 == strcasecmp($url->getScheme(), 'https')
|
|---|
| 842 | ) {
|
|---|
| 843 | $requestUrl = '';
|
|---|
| 844 | } else {
|
|---|
| 845 | $requestUrl = $url->getScheme() . '://' . $host;
|
|---|
| 846 | }
|
|---|
| 847 | $path = $url->getPath();
|
|---|
| 848 | $query = $url->getQuery();
|
|---|
| 849 | $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query);
|
|---|
| 850 | }
|
|---|
| 851 |
|
|---|
| 852 | if ('1.1' == $this->request->getConfig('protocol_version')
|
|---|
| 853 | && extension_loaded('zlib') && !isset($headers['accept-encoding'])
|
|---|
| 854 | ) {
|
|---|
| 855 | $headers['accept-encoding'] = 'gzip, deflate';
|
|---|
| 856 | }
|
|---|
| 857 | if (($jar = $this->request->getCookieJar())
|
|---|
| 858 | && ($cookies = $jar->getMatching($this->request->getUrl(), true))
|
|---|
| 859 | ) {
|
|---|
| 860 | $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
|
|---|
| 861 | }
|
|---|
| 862 |
|
|---|
| 863 | $this->addAuthorizationHeader($headers, $host, $requestUrl);
|
|---|
| 864 | $this->addProxyAuthorizationHeader($headers, $requestUrl);
|
|---|
| 865 | $this->calculateRequestLength($headers);
|
|---|
| 866 |
|
|---|
| 867 | $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' .
|
|---|
| 868 | $this->request->getConfig('protocol_version') . "\r\n";
|
|---|
| 869 | foreach ($headers as $name => $value) {
|
|---|
| 870 | $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
|
|---|
| 871 | $headersStr .= $canonicalName . ': ' . $value . "\r\n";
|
|---|
| 872 | }
|
|---|
| 873 | return $headersStr . "\r\n";
|
|---|
| 874 | }
|
|---|
| 875 |
|
|---|
| 876 | /**
|
|---|
| 877 | * Sends the request body
|
|---|
| 878 | *
|
|---|
| 879 | * @throws HTTP_Request2_MessageException
|
|---|
| 880 | */
|
|---|
| 881 | protected function writeBody()
|
|---|
| 882 | {
|
|---|
| 883 | if (in_array($this->request->getMethod(), self::$bodyDisallowed)
|
|---|
| 884 | || 0 == $this->contentLength
|
|---|
| 885 | ) {
|
|---|
| 886 | return;
|
|---|
| 887 | }
|
|---|
| 888 |
|
|---|
| 889 | $position = 0;
|
|---|
| 890 | $bufferSize = $this->request->getConfig('buffer_size');
|
|---|
| 891 | while ($position < $this->contentLength) {
|
|---|
| 892 | if (is_string($this->requestBody)) {
|
|---|
| 893 | $str = substr($this->requestBody, $position, $bufferSize);
|
|---|
| 894 | } elseif (is_resource($this->requestBody)) {
|
|---|
| 895 | $str = fread($this->requestBody, $bufferSize);
|
|---|
| 896 | } else {
|
|---|
| 897 | $str = $this->requestBody->read($bufferSize);
|
|---|
| 898 | }
|
|---|
| 899 | $this->socket->write($str);
|
|---|
| 900 | // Provide the length of written string to the observer, request #7630
|
|---|
| 901 | $this->request->setLastEvent('sentBodyPart', strlen($str));
|
|---|
| 902 | $position += strlen($str);
|
|---|
| 903 | }
|
|---|
| 904 | $this->request->setLastEvent('sentBody', $this->contentLength);
|
|---|
| 905 | }
|
|---|
| 906 |
|
|---|
| 907 | /**
|
|---|
| 908 | * Reads the remote server's response
|
|---|
| 909 | *
|
|---|
| 910 | * @return HTTP_Request2_Response
|
|---|
| 911 | * @throws HTTP_Request2_Exception
|
|---|
| 912 | */
|
|---|
| 913 | protected function readResponse()
|
|---|
| 914 | {
|
|---|
| 915 | $bufferSize = $this->request->getConfig('buffer_size');
|
|---|
| 916 |
|
|---|
| 917 | do {
|
|---|
| 918 | $response = new HTTP_Request2_Response(
|
|---|
| 919 | $this->socket->readLine($bufferSize), true, $this->request->getUrl()
|
|---|
| 920 | );
|
|---|
| 921 | do {
|
|---|
| 922 | $headerLine = $this->socket->readLine($bufferSize);
|
|---|
| 923 | $response->parseHeaderLine($headerLine);
|
|---|
| 924 | } while ('' != $headerLine);
|
|---|
| 925 | } while (in_array($response->getStatus(), array(100, 101)));
|
|---|
| 926 |
|
|---|
| 927 | $this->request->setLastEvent('receivedHeaders', $response);
|
|---|
| 928 |
|
|---|
| 929 | // No body possible in such responses
|
|---|
| 930 | if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod()
|
|---|
| 931 | || (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod()
|
|---|
| 932 | && 200 <= $response->getStatus() && 300 > $response->getStatus())
|
|---|
| 933 | || in_array($response->getStatus(), array(204, 304))
|
|---|
| 934 | ) {
|
|---|
| 935 | return $response;
|
|---|
| 936 | }
|
|---|
| 937 |
|
|---|
| 938 | $chunked = 'chunked' == $response->getHeader('transfer-encoding');
|
|---|
| 939 | $length = $response->getHeader('content-length');
|
|---|
| 940 | $hasBody = false;
|
|---|
| 941 | if ($chunked || null === $length || 0 < intval($length)) {
|
|---|
| 942 | // RFC 2616, section 4.4:
|
|---|
| 943 | // 3. ... If a message is received with both a
|
|---|
| 944 | // Transfer-Encoding header field and a Content-Length header field,
|
|---|
| 945 | // the latter MUST be ignored.
|
|---|
| 946 | $toRead = ($chunked || null === $length)? null: $length;
|
|---|
| 947 | $this->chunkLength = 0;
|
|---|
| 948 |
|
|---|
| 949 | while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) {
|
|---|
| 950 | if ($chunked) {
|
|---|
| 951 | $data = $this->readChunked($bufferSize);
|
|---|
| 952 | } elseif (is_null($toRead)) {
|
|---|
| 953 | $data = $this->socket->read($bufferSize);
|
|---|
| 954 | } else {
|
|---|
| 955 | $data = $this->socket->read(min($toRead, $bufferSize));
|
|---|
| 956 | $toRead -= strlen($data);
|
|---|
| 957 | }
|
|---|
| 958 | if ('' == $data && (!$this->chunkLength || $this->socket->eof())) {
|
|---|
| 959 | break;
|
|---|
| 960 | }
|
|---|
| 961 |
|
|---|
| 962 | $hasBody = true;
|
|---|
| 963 | if ($this->request->getConfig('store_body')) {
|
|---|
| 964 | $response->appendBody($data);
|
|---|
| 965 | }
|
|---|
| 966 | if (!in_array($response->getHeader('content-encoding'), array('identity', null))) {
|
|---|
| 967 | $this->request->setLastEvent('receivedEncodedBodyPart', $data);
|
|---|
| 968 | } else {
|
|---|
| 969 | $this->request->setLastEvent('receivedBodyPart', $data);
|
|---|
| 970 | }
|
|---|
| 971 | }
|
|---|
| 972 | }
|
|---|
| 973 |
|
|---|
| 974 | if ($hasBody) {
|
|---|
| 975 | $this->request->setLastEvent('receivedBody', $response);
|
|---|
| 976 | }
|
|---|
| 977 | return $response;
|
|---|
| 978 | }
|
|---|
| 979 |
|
|---|
| 980 | /**
|
|---|
| 981 | * Reads a part of response body encoded with chunked Transfer-Encoding
|
|---|
| 982 | *
|
|---|
| 983 | * @param int $bufferSize buffer size to use for reading
|
|---|
| 984 | *
|
|---|
| 985 | * @return string
|
|---|
| 986 | * @throws HTTP_Request2_MessageException
|
|---|
| 987 | */
|
|---|
| 988 | protected function readChunked($bufferSize)
|
|---|
| 989 | {
|
|---|
| 990 | // at start of the next chunk?
|
|---|
| 991 | if (0 == $this->chunkLength) {
|
|---|
| 992 | $line = $this->socket->readLine($bufferSize);
|
|---|
| 993 | if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
|
|---|
| 994 | throw new HTTP_Request2_MessageException(
|
|---|
| 995 | "Cannot decode chunked response, invalid chunk length '{$line}'",
|
|---|
| 996 | HTTP_Request2_Exception::DECODE_ERROR
|
|---|
| 997 | );
|
|---|
| 998 | } else {
|
|---|
| 999 | $this->chunkLength = hexdec($matches[1]);
|
|---|
| 1000 | // Chunk with zero length indicates the end
|
|---|
| 1001 | if (0 == $this->chunkLength) {
|
|---|
| 1002 | $this->socket->readLine($bufferSize);
|
|---|
| 1003 | return '';
|
|---|
| 1004 | }
|
|---|
| 1005 | }
|
|---|
| 1006 | }
|
|---|
| 1007 | $data = $this->socket->read(min($this->chunkLength, $bufferSize));
|
|---|
| 1008 | $this->chunkLength -= strlen($data);
|
|---|
| 1009 | if (0 == $this->chunkLength) {
|
|---|
| 1010 | $this->socket->readLine($bufferSize); // Trailing CRLF
|
|---|
| 1011 | }
|
|---|
| 1012 | return $data;
|
|---|
| 1013 | }
|
|---|
| 1014 | }
|
|---|
| 1015 |
|
|---|
| 1016 | ?> |
|---|