PageRenderTime 183ms CodeModel.GetById 97ms app.highlight 46ms RepoModel.GetById 27ms app.codeStats 0ms

/common/libraries/plugin/pear/HTTP/Request2/Adapter/Socket.php

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