PageRenderTime 41ms CodeModel.GetById 24ms app.highlight 13ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Zend/Auth/Adapter/Http.php

https://bitbucket.org/hamidrezas/melobit
PHP | 869 lines | 423 code | 86 blank | 360 comment | 114 complexity | af6d34d6128985a103edbd84ef19b24a MD5 | raw file
Possible License(s): AGPL-1.0
  1<?php
  2/**
  3 * Zend Framework
  4 *
  5 * LICENSE
  6 *
  7 * This source file is subject to the new BSD license that is bundled
  8 * with this package in the file LICENSE.txt.
  9 * It is also available through the world-wide-web at this URL:
 10 * http://framework.zend.com/license/new-bsd
 11 * If you did not receive a copy of the license and are unable to
 12 * obtain it through the world-wide-web, please send an email
 13 * to license@zend.com so we can send you a copy immediately.
 14 *
 15 * @category   Zend
 16 * @package    Zend_Auth
 17 * @subpackage Zend_Auth_Adapter_Http
 18 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 19 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 20 * @version    $Id: Http.php 24594 2012-01-05 21:27:01Z matthew $
 21 */
 22
 23
 24/**
 25 * @see Zend_Auth_Adapter_Interface
 26 */
 27require_once 'Zend/Auth/Adapter/Interface.php';
 28
 29
 30/**
 31 * HTTP Authentication Adapter
 32 *
 33 * Implements a pretty good chunk of RFC 2617.
 34 *
 35 * @category   Zend
 36 * @package    Zend_Auth
 37 * @subpackage Zend_Auth_Adapter_Http
 38 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 39 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 40 * @todo       Support auth-int
 41 * @todo       Track nonces, nonce-count, opaque for replay protection and stale support
 42 * @todo       Support Authentication-Info header
 43 */
 44class Zend_Auth_Adapter_Http implements Zend_Auth_Adapter_Interface
 45{
 46    /**
 47     * Reference to the HTTP Request object
 48     *
 49     * @var Zend_Controller_Request_Http
 50     */
 51    protected $_request;
 52
 53    /**
 54     * Reference to the HTTP Response object
 55     *
 56     * @var Zend_Controller_Response_Http
 57     */
 58    protected $_response;
 59
 60    /**
 61     * Object that looks up user credentials for the Basic scheme
 62     *
 63     * @var Zend_Auth_Adapter_Http_Resolver_Interface
 64     */
 65    protected $_basicResolver;
 66
 67    /**
 68     * Object that looks up user credentials for the Digest scheme
 69     *
 70     * @var Zend_Auth_Adapter_Http_Resolver_Interface
 71     */
 72    protected $_digestResolver;
 73
 74    /**
 75     * List of authentication schemes supported by this class
 76     *
 77     * @var array
 78     */
 79    protected $_supportedSchemes = array('basic', 'digest');
 80
 81    /**
 82     * List of schemes this class will accept from the client
 83     *
 84     * @var array
 85     */
 86    protected $_acceptSchemes;
 87
 88    /**
 89     * Space-delimited list of protected domains for Digest Auth
 90     *
 91     * @var string
 92     */
 93    protected $_domains;
 94
 95    /**
 96     * The protection realm to use
 97     *
 98     * @var string
 99     */
100    protected $_realm;
101
102    /**
103     * Nonce timeout period
104     *
105     * @var integer
106     */
107    protected $_nonceTimeout;
108
109    /**
110     * Whether to send the opaque value in the header. True by default
111     *
112     * @var boolean
113     */
114    protected $_useOpaque;
115
116    /**
117     * List of the supported digest algorithms. I want to support both MD5 and
118     * MD5-sess, but MD5-sess won't make it into the first version.
119     *
120     * @var array
121     */
122    protected $_supportedAlgos = array('MD5');
123
124    /**
125     * The actual algorithm to use. Defaults to MD5
126     *
127     * @var string
128     */
129    protected $_algo;
130
131    /**
132     * List of supported qop options. My intetion is to support both 'auth' and
133     * 'auth-int', but 'auth-int' won't make it into the first version.
134     *
135     * @var array
136     */
137    protected $_supportedQops = array('auth');
138
139    /**
140     * Whether or not to do Proxy Authentication instead of origin server
141     * authentication (send 407's instead of 401's). Off by default.
142     *
143     * @var boolean
144     */
145    protected $_imaProxy;
146
147    /**
148     * Flag indicating the client is IE and didn't bother to return the opaque string
149     *
150     * @var boolean
151     */
152    protected $_ieNoOpaque;
153
154    /**
155     * Constructor
156     *
157     * @param  array $config Configuration settings:
158     *    'accept_schemes' => 'basic'|'digest'|'basic digest'
159     *    'realm' => <string>
160     *    'digest_domains' => <string> Space-delimited list of URIs
161     *    'nonce_timeout' => <int>
162     *    'use_opaque' => <bool> Whether to send the opaque value in the header
163     *    'alogrithm' => <string> See $_supportedAlgos. Default: MD5
164     *    'proxy_auth' => <bool> Whether to do authentication as a Proxy
165     * @throws Zend_Auth_Adapter_Exception
166     * @return void
167     */
168    public function __construct(array $config)
169    {
170        if (!extension_loaded('hash')) {
171            /**
172             * @see Zend_Auth_Adapter_Exception
173             */
174            require_once 'Zend/Auth/Adapter/Exception.php';
175            throw new Zend_Auth_Adapter_Exception(__CLASS__  . ' requires the \'hash\' extension');
176        }
177
178        $this->_request  = null;
179        $this->_response = null;
180        $this->_ieNoOpaque = false;
181
182
183        if (empty($config['accept_schemes'])) {
184            /**
185             * @see Zend_Auth_Adapter_Exception
186             */
187            require_once 'Zend/Auth/Adapter/Exception.php';
188            throw new Zend_Auth_Adapter_Exception('Config key \'accept_schemes\' is required');
189        }
190
191        $schemes = explode(' ', $config['accept_schemes']);
192        $this->_acceptSchemes = array_intersect($schemes, $this->_supportedSchemes);
193        if (empty($this->_acceptSchemes)) {
194            /**
195             * @see Zend_Auth_Adapter_Exception
196             */
197            require_once 'Zend/Auth/Adapter/Exception.php';
198            throw new Zend_Auth_Adapter_Exception('No supported schemes given in \'accept_schemes\'. Valid values: '
199                                                . implode(', ', $this->_supportedSchemes));
200        }
201
202        // Double-quotes are used to delimit the realm string in the HTTP header,
203        // and colons are field delimiters in the password file.
204        if (empty($config['realm']) ||
205            !ctype_print($config['realm']) ||
206            strpos($config['realm'], ':') !== false ||
207            strpos($config['realm'], '"') !== false) {
208            /**
209             * @see Zend_Auth_Adapter_Exception
210             */
211            require_once 'Zend/Auth/Adapter/Exception.php';
212            throw new Zend_Auth_Adapter_Exception('Config key \'realm\' is required, and must contain only printable '
213                                                . 'characters, excluding quotation marks and colons');
214        } else {
215            $this->_realm = $config['realm'];
216        }
217
218        if (in_array('digest', $this->_acceptSchemes)) {
219            if (empty($config['digest_domains']) ||
220                !ctype_print($config['digest_domains']) ||
221                strpos($config['digest_domains'], '"') !== false) {
222                /**
223                 * @see Zend_Auth_Adapter_Exception
224                 */
225                require_once 'Zend/Auth/Adapter/Exception.php';
226                throw new Zend_Auth_Adapter_Exception('Config key \'digest_domains\' is required, and must contain '
227                                                    . 'only printable characters, excluding quotation marks');
228            } else {
229                $this->_domains = $config['digest_domains'];
230            }
231
232            if (empty($config['nonce_timeout']) ||
233                !is_numeric($config['nonce_timeout'])) {
234                /**
235                 * @see Zend_Auth_Adapter_Exception
236                 */
237                require_once 'Zend/Auth/Adapter/Exception.php';
238                throw new Zend_Auth_Adapter_Exception('Config key \'nonce_timeout\' is required, and must be an '
239                                                    . 'integer');
240            } else {
241                $this->_nonceTimeout = (int) $config['nonce_timeout'];
242            }
243
244            // We use the opaque value unless explicitly told not to
245            if (isset($config['use_opaque']) && false == (bool) $config['use_opaque']) {
246                $this->_useOpaque = false;
247            } else {
248                $this->_useOpaque = true;
249            }
250
251            if (isset($config['algorithm']) && in_array($config['algorithm'], $this->_supportedAlgos)) {
252                $this->_algo = $config['algorithm'];
253            } else {
254                $this->_algo = 'MD5';
255            }
256        }
257
258        // Don't be a proxy unless explicitly told to do so
259        if (isset($config['proxy_auth']) && true == (bool) $config['proxy_auth']) {
260            $this->_imaProxy = true;  // I'm a Proxy
261        } else {
262            $this->_imaProxy = false;
263        }
264    }
265
266    /**
267     * Setter for the _basicResolver property
268     *
269     * @param  Zend_Auth_Adapter_Http_Resolver_Interface $resolver
270     * @return Zend_Auth_Adapter_Http Provides a fluent interface
271     */
272    public function setBasicResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver)
273    {
274        $this->_basicResolver = $resolver;
275
276        return $this;
277    }
278
279    /**
280     * Getter for the _basicResolver property
281     *
282     * @return Zend_Auth_Adapter_Http_Resolver_Interface
283     */
284    public function getBasicResolver()
285    {
286        return $this->_basicResolver;
287    }
288
289    /**
290     * Setter for the _digestResolver property
291     *
292     * @param  Zend_Auth_Adapter_Http_Resolver_Interface $resolver
293     * @return Zend_Auth_Adapter_Http Provides a fluent interface
294     */
295    public function setDigestResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver)
296    {
297        $this->_digestResolver = $resolver;
298
299        return $this;
300    }
301
302    /**
303     * Getter for the _digestResolver property
304     *
305     * @return Zend_Auth_Adapter_Http_Resolver_Interface
306     */
307    public function getDigestResolver()
308    {
309        return $this->_digestResolver;
310    }
311
312    /**
313     * Setter for the Request object
314     *
315     * @param  Zend_Controller_Request_Http $request
316     * @return Zend_Auth_Adapter_Http Provides a fluent interface
317     */
318    public function setRequest(Zend_Controller_Request_Http $request)
319    {
320        $this->_request = $request;
321
322        return $this;
323    }
324
325    /**
326     * Getter for the Request object
327     *
328     * @return Zend_Controller_Request_Http
329     */
330    public function getRequest()
331    {
332        return $this->_request;
333    }
334
335    /**
336     * Setter for the Response object
337     *
338     * @param  Zend_Controller_Response_Http $response
339     * @return Zend_Auth_Adapter_Http Provides a fluent interface
340     */
341    public function setResponse(Zend_Controller_Response_Http $response)
342    {
343        $this->_response = $response;
344
345        return $this;
346    }
347
348    /**
349     * Getter for the Response object
350     *
351     * @return Zend_Controller_Response_Http
352     */
353    public function getResponse()
354    {
355        return $this->_response;
356    }
357
358    /**
359     * Authenticate
360     *
361     * @throws Zend_Auth_Adapter_Exception
362     * @return Zend_Auth_Result
363     */
364    public function authenticate()
365    {
366        if (empty($this->_request) ||
367            empty($this->_response)) {
368            /**
369             * @see Zend_Auth_Adapter_Exception
370             */
371            require_once 'Zend/Auth/Adapter/Exception.php';
372            throw new Zend_Auth_Adapter_Exception('Request and Response objects must be set before calling '
373                                                . 'authenticate()');
374        }
375
376        if ($this->_imaProxy) {
377            $getHeader = 'Proxy-Authorization';
378        } else {
379            $getHeader = 'Authorization';
380        }
381
382        $authHeader = $this->_request->getHeader($getHeader);
383        if (!$authHeader) {
384            return $this->_challengeClient();
385        }
386
387        list($clientScheme) = explode(' ', $authHeader);
388        $clientScheme = strtolower($clientScheme);
389
390        // The server can issue multiple challenges, but the client should
391        // answer with only the selected auth scheme.
392        if (!in_array($clientScheme, $this->_supportedSchemes)) {
393            $this->_response->setHttpResponseCode(400);
394            return new Zend_Auth_Result(
395                Zend_Auth_Result::FAILURE_UNCATEGORIZED,
396                array(),
397                array('Client requested an incorrect or unsupported authentication scheme')
398            );
399        }
400
401        // client sent a scheme that is not the one required
402        if (!in_array($clientScheme, $this->_acceptSchemes)) {
403            // challenge again the client
404            return $this->_challengeClient();
405        }
406
407        switch ($clientScheme) {
408            case 'basic':
409                $result = $this->_basicAuth($authHeader);
410                break;
411            case 'digest':
412                $result = $this->_digestAuth($authHeader);
413            break;
414            default:
415                /**
416                 * @see Zend_Auth_Adapter_Exception
417                 */
418                require_once 'Zend/Auth/Adapter/Exception.php';
419                throw new Zend_Auth_Adapter_Exception('Unsupported authentication scheme');
420        }
421
422        return $result;
423    }
424
425    /**
426     * Challenge Client
427     *
428     * Sets a 401 or 407 Unauthorized response code, and creates the
429     * appropriate Authenticate header(s) to prompt for credentials.
430     *
431     * @return Zend_Auth_Result Always returns a non-identity Auth result
432     */
433    protected function _challengeClient()
434    {
435        if ($this->_imaProxy) {
436            $statusCode = 407;
437            $headerName = 'Proxy-Authenticate';
438        } else {
439            $statusCode = 401;
440            $headerName = 'WWW-Authenticate';
441        }
442
443        $this->_response->setHttpResponseCode($statusCode);
444
445        // Send a challenge in each acceptable authentication scheme
446        if (in_array('basic', $this->_acceptSchemes)) {
447            $this->_response->setHeader($headerName, $this->_basicHeader());
448        }
449        if (in_array('digest', $this->_acceptSchemes)) {
450            $this->_response->setHeader($headerName, $this->_digestHeader());
451        }
452        return new Zend_Auth_Result(
453            Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
454            array(),
455            array('Invalid or absent credentials; challenging client')
456        );
457    }
458
459    /**
460     * Basic Header
461     *
462     * Generates a Proxy- or WWW-Authenticate header value in the Basic
463     * authentication scheme.
464     *
465     * @return string Authenticate header value
466     */
467    protected function _basicHeader()
468    {
469        return 'Basic realm="' . $this->_realm . '"';
470    }
471
472    /**
473     * Digest Header
474     *
475     * Generates a Proxy- or WWW-Authenticate header value in the Digest
476     * authentication scheme.
477     *
478     * @return string Authenticate header value
479     */
480    protected function _digestHeader()
481    {
482        $wwwauth = 'Digest realm="' . $this->_realm . '", '
483                 . 'domain="' . $this->_domains . '", '
484                 . 'nonce="' . $this->_calcNonce() . '", '
485                 . ($this->_useOpaque ? 'opaque="' . $this->_calcOpaque() . '", ' : '')
486                 . 'algorithm="' . $this->_algo . '", '
487                 . 'qop="' . implode(',', $this->_supportedQops) . '"';
488
489        return $wwwauth;
490    }
491
492    /**
493     * Basic Authentication
494     *
495     * @param  string $header Client's Authorization header
496     * @throws Zend_Auth_Adapter_Exception
497     * @return Zend_Auth_Result
498     */
499    protected function _basicAuth($header)
500    {
501        if (empty($header)) {
502            /**
503             * @see Zend_Auth_Adapter_Exception
504             */
505            require_once 'Zend/Auth/Adapter/Exception.php';
506            throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required');
507        }
508        if (empty($this->_basicResolver)) {
509            /**
510             * @see Zend_Auth_Adapter_Exception
511             */
512            require_once 'Zend/Auth/Adapter/Exception.php';
513            throw new Zend_Auth_Adapter_Exception('A basicResolver object must be set before doing Basic '
514                                                . 'authentication');
515        }
516
517        // Decode the Authorization header
518        $auth = substr($header, strlen('Basic '));
519        $auth = base64_decode($auth);
520        if (!$auth) {
521            /**
522             * @see Zend_Auth_Adapter_Exception
523             */
524            require_once 'Zend/Auth/Adapter/Exception.php';
525            throw new Zend_Auth_Adapter_Exception('Unable to base64_decode Authorization header value');
526        }
527
528        // See ZF-1253. Validate the credentials the same way the digest
529        // implementation does. If invalid credentials are detected,
530        // re-challenge the client.
531        if (!ctype_print($auth)) {
532            return $this->_challengeClient();
533        }
534        // Fix for ZF-1515: Now re-challenges on empty username or password
535        $creds = array_filter(explode(':', $auth));
536        if (count($creds) != 2) {
537            return $this->_challengeClient();
538        }
539
540        $password = $this->_basicResolver->resolve($creds[0], $this->_realm);
541        if ($password && $this->_secureStringCompare($password, $creds[1])) {
542            $identity = array('username'=>$creds[0], 'realm'=>$this->_realm);
543            return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity);
544        } else {
545            return $this->_challengeClient();
546        }
547    }
548
549    /**
550     * Digest Authentication
551     *
552     * @param  string $header Client's Authorization header
553     * @throws Zend_Auth_Adapter_Exception
554     * @return Zend_Auth_Result Valid auth result only on successful auth
555     */
556    protected function _digestAuth($header)
557    {
558        if (empty($header)) {
559            /**
560             * @see Zend_Auth_Adapter_Exception
561             */
562            require_once 'Zend/Auth/Adapter/Exception.php';
563            throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required');
564        }
565        if (empty($this->_digestResolver)) {
566            /**
567             * @see Zend_Auth_Adapter_Exception
568             */
569            require_once 'Zend/Auth/Adapter/Exception.php';
570            throw new Zend_Auth_Adapter_Exception('A digestResolver object must be set before doing Digest authentication');
571        }
572
573        $data = $this->_parseDigestAuth($header);
574        if ($data === false) {
575            $this->_response->setHttpResponseCode(400);
576            return new Zend_Auth_Result(
577                Zend_Auth_Result::FAILURE_UNCATEGORIZED,
578                array(),
579                array('Invalid Authorization header format')
580            );
581        }
582
583        // See ZF-1052. This code was a bit too unforgiving of invalid
584        // usernames. Now, if the username is bad, we re-challenge the client.
585        if ('::invalid::' == $data['username']) {
586            return $this->_challengeClient();
587        }
588
589        // Verify that the client sent back the same nonce
590        if ($this->_calcNonce() != $data['nonce']) {
591            return $this->_challengeClient();
592        }
593        // The opaque value is also required to match, but of course IE doesn't
594        // play ball.
595        if (!$this->_ieNoOpaque && $this->_calcOpaque() != $data['opaque']) {
596            return $this->_challengeClient();
597        }
598
599        // Look up the user's password hash. If not found, deny access.
600        // This makes no assumptions about how the password hash was
601        // constructed beyond that it must have been built in such a way as
602        // to be recreatable with the current settings of this object.
603        $ha1 = $this->_digestResolver->resolve($data['username'], $data['realm']);
604        if ($ha1 === false) {
605            return $this->_challengeClient();
606        }
607
608        // If MD5-sess is used, a1 value is made of the user's password
609        // hash with the server and client nonce appended, separated by
610        // colons.
611        if ($this->_algo == 'MD5-sess') {
612            $ha1 = hash('md5', $ha1 . ':' . $data['nonce'] . ':' . $data['cnonce']);
613        }
614
615        // Calculate h(a2). The value of this hash depends on the qop
616        // option selected by the client and the supported hash functions
617        switch ($data['qop']) {
618            case 'auth':
619                $a2 = $this->_request->getMethod() . ':' . $data['uri'];
620                break;
621            case 'auth-int':
622                // Should be REQUEST_METHOD . ':' . uri . ':' . hash(entity-body),
623                // but this isn't supported yet, so fall through to default case
624            default:
625                /**
626                 * @see Zend_Auth_Adapter_Exception
627                 */
628                require_once 'Zend/Auth/Adapter/Exception.php';
629                throw new Zend_Auth_Adapter_Exception('Client requested an unsupported qop option');
630        }
631        // Using hash() should make parameterizing the hash algorithm
632        // easier
633        $ha2 = hash('md5', $a2);
634
635
636        // Calculate the server's version of the request-digest. This must
637        // match $data['response']. See RFC 2617, section 3.2.2.1
638        $message = $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $ha2;
639        $digest  = hash('md5', $ha1 . ':' . $message);
640
641        // If our digest matches the client's let them in, otherwise return
642        // a 401 code and exit to prevent access to the protected resource.
643        if ($this->_secureStringCompare($digest, $data['response'])) {
644            $identity = array('username'=>$data['username'], 'realm'=>$data['realm']);
645            return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity);
646        } else {
647            return $this->_challengeClient();
648        }
649    }
650
651    /**
652     * Calculate Nonce
653     *
654     * @return string The nonce value
655     */
656    protected function _calcNonce()
657    {
658        // Once subtle consequence of this timeout calculation is that it
659        // actually divides all of time into _nonceTimeout-sized sections, such
660        // that the value of timeout is the point in time of the next
661        // approaching "boundary" of a section. This allows the server to
662        // consistently generate the same timeout (and hence the same nonce
663        // value) across requests, but only as long as one of those
664        // "boundaries" is not crossed between requests. If that happens, the
665        // nonce will change on its own, and effectively log the user out. This
666        // would be surprising if the user just logged in.
667        $timeout = ceil(time() / $this->_nonceTimeout) * $this->_nonceTimeout;
668
669        $nonce = hash('md5', $timeout . ':' . $this->_request->getServer('HTTP_USER_AGENT') . ':' . __CLASS__);
670        return $nonce;
671    }
672
673    /**
674     * Calculate Opaque
675     *
676     * The opaque string can be anything; the client must return it exactly as
677     * it was sent. It may be useful to store data in this string in some
678     * applications. Ideally, a new value for this would be generated each time
679     * a WWW-Authenticate header is sent (in order to reduce predictability),
680     * but we would have to be able to create the same exact value across at
681     * least two separate requests from the same client.
682     *
683     * @return string The opaque value
684     */
685    protected function _calcOpaque()
686    {
687        return hash('md5', 'Opaque Data:' . __CLASS__);
688    }
689
690    /**
691     * Parse Digest Authorization header
692     *
693     * @param  string $header Client's Authorization: HTTP header
694     * @return array|false Data elements from header, or false if any part of
695     *         the header is invalid
696     */
697    protected function _parseDigestAuth($header)
698    {
699        $temp = null;
700        $data = array();
701
702        // See ZF-1052. Detect invalid usernames instead of just returning a
703        // 400 code.
704        $ret = preg_match('/username="([^"]+)"/', $header, $temp);
705        if (!$ret || empty($temp[1])
706                  || !ctype_print($temp[1])
707                  || strpos($temp[1], ':') !== false) {
708            $data['username'] = '::invalid::';
709        } else {
710            $data['username'] = $temp[1];
711        }
712        $temp = null;
713
714        $ret = preg_match('/realm="([^"]+)"/', $header, $temp);
715        if (!$ret || empty($temp[1])) {
716            return false;
717        }
718        if (!ctype_print($temp[1]) || strpos($temp[1], ':') !== false) {
719            return false;
720        } else {
721            $data['realm'] = $temp[1];
722        }
723        $temp = null;
724
725        $ret = preg_match('/nonce="([^"]+)"/', $header, $temp);
726        if (!$ret || empty($temp[1])) {
727            return false;
728        }
729        if (!ctype_xdigit($temp[1])) {
730            return false;
731        } else {
732            $data['nonce'] = $temp[1];
733        }
734        $temp = null;
735
736        $ret = preg_match('/uri="([^"]+)"/', $header, $temp);
737        if (!$ret || empty($temp[1])) {
738            return false;
739        }
740        // Section 3.2.2.5 in RFC 2617 says the authenticating server must
741        // verify that the URI field in the Authorization header is for the
742        // same resource requested in the Request Line.
743        $rUri = @parse_url($this->_request->getRequestUri());
744        $cUri = @parse_url($temp[1]);
745        if (false === $rUri || false === $cUri) {
746            return false;
747        } else {
748            // Make sure the path portion of both URIs is the same
749            if ($rUri['path'] != $cUri['path']) {
750                return false;
751            }
752            // Section 3.2.2.5 seems to suggest that the value of the URI
753            // Authorization field should be made into an absolute URI if the
754            // Request URI is absolute, but it's vague, and that's a bunch of
755            // code I don't want to write right now.
756            $data['uri'] = $temp[1];
757        }
758        $temp = null;
759
760        $ret = preg_match('/response="([^"]+)"/', $header, $temp);
761        if (!$ret || empty($temp[1])) {
762            return false;
763        }
764        if (32 != strlen($temp[1]) || !ctype_xdigit($temp[1])) {
765            return false;
766        } else {
767            $data['response'] = $temp[1];
768        }
769        $temp = null;
770
771        // The spec says this should default to MD5 if omitted. OK, so how does
772        // that square with the algo we send out in the WWW-Authenticate header,
773        // if it can easily be overridden by the client?
774        $ret = preg_match('/algorithm="?(' . $this->_algo . ')"?/', $header, $temp);
775        if ($ret && !empty($temp[1])
776                 && in_array($temp[1], $this->_supportedAlgos)) {
777            $data['algorithm'] = $temp[1];
778        } else {
779            $data['algorithm'] = 'MD5';  // = $this->_algo; ?
780        }
781        $temp = null;
782
783        // Not optional in this implementation
784        $ret = preg_match('/cnonce="([^"]+)"/', $header, $temp);
785        if (!$ret || empty($temp[1])) {
786            return false;
787        }
788        if (!ctype_print($temp[1])) {
789            return false;
790        } else {
791            $data['cnonce'] = $temp[1];
792        }
793        $temp = null;
794
795        // If the server sent an opaque value, the client must send it back
796        if ($this->_useOpaque) {
797            $ret = preg_match('/opaque="([^"]+)"/', $header, $temp);
798            if (!$ret || empty($temp[1])) {
799
800                // Big surprise: IE isn't RFC 2617-compliant.
801                if (false !== strpos($this->_request->getHeader('User-Agent'), 'MSIE')) {
802                    $temp[1] = '';
803                    $this->_ieNoOpaque = true;
804                } else {
805                    return false;
806                }
807            }
808            // This implementation only sends MD5 hex strings in the opaque value
809            if (!$this->_ieNoOpaque &&
810                (32 != strlen($temp[1]) || !ctype_xdigit($temp[1]))) {
811                return false;
812            } else {
813                $data['opaque'] = $temp[1];
814            }
815            $temp = null;
816        }
817
818        // Not optional in this implementation, but must be one of the supported
819        // qop types
820        $ret = preg_match('/qop="?(' . implode('|', $this->_supportedQops) . ')"?/', $header, $temp);
821        if (!$ret || empty($temp[1])) {
822            return false;
823        }
824        if (!in_array($temp[1], $this->_supportedQops)) {
825            return false;
826        } else {
827            $data['qop'] = $temp[1];
828        }
829        $temp = null;
830
831        // Not optional in this implementation. The spec says this value
832        // shouldn't be a quoted string, but apparently some implementations
833        // quote it anyway. See ZF-1544.
834        $ret = preg_match('/nc="?([0-9A-Fa-f]{8})"?/', $header, $temp);
835        if (!$ret || empty($temp[1])) {
836            return false;
837        }
838        if (8 != strlen($temp[1]) || !ctype_xdigit($temp[1])) {
839            return false;
840        } else {
841            $data['nc'] = $temp[1];
842        }
843        $temp = null;
844
845        return $data;
846    }
847
848    /**
849     * Securely compare two strings for equality while avoided C level memcmp()
850     * optimisations capable of leaking timing information useful to an attacker
851     * attempting to iteratively guess the unknown string (e.g. password) being
852     * compared against.
853     *
854     * @param string $a
855     * @param string $b
856     * @return bool
857     */
858    protected function _secureStringCompare($a, $b)
859    {
860        if (strlen($a) !== strlen($b)) {
861            return false;
862        }
863        $result = 0;
864        for ($i = 0; $i < strlen($a); $i++) {
865            $result |= ord($a[$i]) ^ ord($b[$i]);
866        }
867        return $result == 0;
868    }
869}