PageRenderTime 149ms CodeModel.GetById 61ms app.highlight 68ms RepoModel.GetById 12ms app.codeStats 0ms

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

https://bitbucket.org/baruffaldi/website-insaneminds
PHP | 841 lines | 409 code | 84 blank | 348 comment | 112 complexity | d01a7c6c696aa6642eb57e367d16a285 MD5 | raw file
  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-2008 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 8964 2008-03-21 17:53:14Z thomas $
 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-2008 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        if (!in_array($clientScheme, $this->_supportedSchemes)) {
391            $this->_response->setHttpResponseCode(400);
392            return new Zend_Auth_Result(
393                Zend_Auth_Result::FAILURE_UNCATEGORIZED,
394                array(),
395                array('Client requested an unsupported authentication scheme')
396            );
397        }
398
399        // The server can issue multiple challenges, but the client should
400        // answer with only one selected auth scheme.
401        switch ($clientScheme) {
402            case 'basic':
403                $result = $this->_basicAuth($authHeader);
404                break;
405            case 'digest':
406                $result = $this->_digestAuth($authHeader);
407                break;
408            default:
409                /**
410                 * @see Zend_Auth_Adapter_Exception
411                 */
412                require_once 'Zend/Auth/Adapter/Exception.php';
413                throw new Zend_Auth_Adapter_Exception('Unsupported authentication scheme');
414        }
415
416        return $result;
417    }
418
419    /**
420     * Challenge Client
421     *
422     * Sets a 401 or 407 Unauthorized response code, and creates the
423     * appropriate Authenticate header(s) to prompt for credentials.
424     *
425     * @return Zend_Auth_Result Always returns a non-identity Auth result
426     */
427    protected function _challengeClient()
428    {
429        if ($this->_imaProxy) {
430            $statusCode = 407;
431            $headerName = 'Proxy-Authenticate';
432        } else {
433            $statusCode = 401;
434            $headerName = 'WWW-Authenticate';
435        }
436
437        $this->_response->setHttpResponseCode($statusCode);
438
439        // Send a challenge in each acceptable authentication scheme
440        if (in_array('basic', $this->_acceptSchemes)) {
441            $this->_response->setHeader($headerName, $this->_basicHeader());
442        }
443        if (in_array('digest', $this->_acceptSchemes)) {
444            $this->_response->setHeader($headerName, $this->_digestHeader());
445        }
446        return new Zend_Auth_Result(
447            Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
448            array(),
449            array('Invalid or absent credentials; challenging client')
450        );
451    }
452
453    /**
454     * Basic Header
455     *
456     * Generates a Proxy- or WWW-Authenticate header value in the Basic
457     * authentication scheme.
458     *
459     * @return string Authenticate header value
460     */
461    protected function _basicHeader()
462    {
463        return 'Basic realm="' . $this->_realm . '"';
464    }
465
466    /**
467     * Digest Header
468     *
469     * Generates a Proxy- or WWW-Authenticate header value in the Digest
470     * authentication scheme.
471     *
472     * @return string Authenticate header value
473     */
474    protected function _digestHeader()
475    {
476        $wwwauth = 'Digest realm="' . $this->_realm . '", '
477                 . 'domain="' . $this->_domains . '", '
478                 . 'nonce="' . $this->_calcNonce() . '", '
479                 . ($this->_useOpaque ? 'opaque="' . $this->_calcOpaque() . '", ' : '')
480                 . 'algorithm="' . $this->_algo . '", '
481                 . 'qop="' . implode(',', $this->_supportedQops) . '"';
482
483        return $wwwauth;
484    }
485
486    /**
487     * Basic Authentication
488     *
489     * @param  string $header Client's Authorization header
490     * @throws Zend_Auth_Adapter_Exception
491     * @return Zend_Auth_Result
492     */
493    protected function _basicAuth($header)
494    {
495        if (empty($header)) {
496            /**
497             * @see Zend_Auth_Adapter_Exception
498             */
499            require_once 'Zend/Auth/Adapter/Exception.php';
500            throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required');
501        }
502        if (empty($this->_basicResolver)) {
503            /**
504             * @see Zend_Auth_Adapter_Exception
505             */
506            require_once 'Zend/Auth/Adapter/Exception.php';
507            throw new Zend_Auth_Adapter_Exception('A basicResolver object must be set before doing Basic '
508                                                . 'authentication');
509        }
510
511        // Decode the Authorization header
512        $auth = substr($header, strlen('Basic '));
513        $auth = base64_decode($auth);
514        if (!$auth) {
515            /**
516             * @see Zend_Auth_Adapter_Exception
517             */
518            require_once 'Zend/Auth/Adapter/Exception.php';
519            throw new Zend_Auth_Adapter_Exception('Unable to base64_decode Authorization header value');
520        }
521
522        // See ZF-1253. Validate the credentials the same way the digest
523        // implementation does. If invalid credentials are detected,
524        // re-challenge the client.
525        if (!ctype_print($auth)) {
526            return $this->_challengeClient();
527        }
528        // Fix for ZF-1515: Now re-challenges on empty username or password
529        $creds = array_filter(explode(':', $auth));
530        if (count($creds) != 2) {
531            return $this->_challengeClient();
532        }
533
534        $password = $this->_basicResolver->resolve($creds[0], $this->_realm);
535        if ($password && $password == $creds[1]) {
536            $identity = array('username'=>$creds[0], 'realm'=>$this->_realm);
537            return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity);
538        } else {
539            return $this->_challengeClient();
540        }
541    }
542
543    /**
544     * Digest Authentication
545     *
546     * @param  string $header Client's Authorization header
547     * @throws Zend_Auth_Adapter_Exception
548     * @return Zend_Auth_Result Valid auth result only on successful auth
549     */
550    protected function _digestAuth($header)
551    {
552        if (empty($header)) {
553            /**
554             * @see Zend_Auth_Adapter_Exception
555             */
556            require_once 'Zend/Auth/Adapter/Exception.php';
557            throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required');
558        }
559        if (empty($this->_digestResolver)) {
560            /**
561             * @see Zend_Auth_Adapter_Exception
562             */
563            require_once 'Zend/Auth/Adapter/Exception.php';
564            throw new Zend_Auth_Adapter_Exception('A digestResolver object must be set before doing Digest authentication');
565        }
566
567        $data = $this->_parseDigestAuth($header);
568        if ($data === false) {
569            $this->_response->setHttpResponseCode(400);
570            return new Zend_Auth_Result(
571                Zend_Auth_Result::FAILURE_UNCATEGORIZED,
572                array(),
573                array('Invalid Authorization header format')
574            );
575        }
576
577        // See ZF-1052. This code was a bit too unforgiving of invalid
578        // usernames. Now, if the username is bad, we re-challenge the client.
579        if ('::invalid::' == $data['username']) {
580            return $this->_challengeClient();
581        }
582
583        // Verify that the client sent back the same nonce
584        if ($this->_calcNonce() != $data['nonce']) {
585            return $this->_challengeClient();
586        }
587        // The opaque value is also required to match, but of course IE doesn't
588        // play ball.
589        if (!$this->_ieNoOpaque && $this->_calcOpaque() != $data['opaque']) {
590            return $this->_challengeClient();
591        }
592
593        // Look up the user's password hash. If not found, deny access.
594        // This makes no assumptions about how the password hash was
595        // constructed beyond that it must have been built in such a way as
596        // to be recreatable with the current settings of this object.
597        $ha1 = $this->_digestResolver->resolve($data['username'], $data['realm']);
598        if ($ha1 === false) {
599            return $this->_challengeClient();
600        }
601
602        // If MD5-sess is used, a1 value is made of the user's password
603        // hash with the server and client nonce appended, separated by
604        // colons.
605        if ($this->_algo == 'MD5-sess') {
606            $ha1 = hash('md5', $ha1 . ':' . $data['nonce'] . ':' . $data['cnonce']);
607        }
608
609        // Calculate h(a2). The value of this hash depends on the qop
610        // option selected by the client and the supported hash functions
611        switch ($data['qop']) {
612            case 'auth':
613                $a2 = $this->_request->getMethod() . ':' . $data['uri'];
614                break;
615            case 'auth-int':
616                // Should be REQUEST_METHOD . ':' . uri . ':' . hash(entity-body),
617                // but this isn't supported yet, so fall through to default case
618            default:
619                /**
620                 * @see Zend_Auth_Adapter_Exception
621                 */
622                require_once 'Zend/Auth/Adapter/Exception.php';
623                throw new Zend_Auth_Adapter_Exception('Client requested an unsupported qop option');
624        }
625        // Using hash() should make parameterizing the hash algorithm
626        // easier
627        $ha2 = hash('md5', $a2);
628
629
630        // Calculate the server's version of the request-digest. This must
631        // match $data['response']. See RFC 2617, section 3.2.2.1
632        $message = $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $ha2;
633        $digest  = hash('md5', $ha1 . ':' . $message);
634
635        // If our digest matches the client's let them in, otherwise return
636        // a 401 code and exit to prevent access to the protected resource.
637        if ($digest == $data['response']) {
638            $identity = array('username'=>$data['username'], 'realm'=>$data['realm']);
639            return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity);
640        } else {
641            return $this->_challengeClient();
642        }
643    }
644
645    /**
646     * Calculate Nonce
647     *
648     * @return string The nonce value
649     */
650    protected function _calcNonce()
651    {
652        // Once subtle consequence of this timeout calculation is that it
653        // actually divides all of time into _nonceTimeout-sized sections, such
654        // that the value of timeout is the point in time of the next
655        // approaching "boundary" of a section. This allows the server to
656        // consistently generate the same timeout (and hence the same nonce
657        // value) across requests, but only as long as one of those
658        // "boundaries" is not crossed between requests. If that happens, the
659        // nonce will change on its own, and effectively log the user out. This
660        // would be surprising if the user just logged in.
661        $timeout = ceil(time() / $this->_nonceTimeout) * $this->_nonceTimeout;
662
663        $nonce = hash('md5', $timeout . ':' . $this->_request->getServer('HTTP_USER_AGENT') . ':' . __CLASS__);
664        return $nonce;
665    }
666
667    /**
668     * Calculate Opaque
669     *
670     * The opaque string can be anything; the client must return it exactly as
671     * it was sent. It may be useful to store data in this string in some
672     * applications. Ideally, a new value for this would be generated each time
673     * a WWW-Authenticate header is sent (in order to reduce predictability),
674     * but we would have to be able to create the same exact value across at
675     * least two separate requests from the same client.
676     *
677     * @return string The opaque value
678     */
679    protected function _calcOpaque()
680    {
681        return hash('md5', 'Opaque Data:' . __CLASS__);
682    }
683
684    /**
685     * Parse Digest Authorization header
686     *
687     * @param  string $header Client's Authorization: HTTP header
688     * @return array|false Data elements from header, or false if any part of
689     *         the header is invalid
690     */
691    protected function _parseDigestAuth($header)
692    {
693        $temp = null;
694        $data = array();
695
696        // See ZF-1052. Detect invalid usernames instead of just returning a
697        // 400 code.
698        $ret = preg_match('/username="([^"]+)"/', $header, $temp);
699        if (!$ret || empty($temp[1])
700                  || !ctype_print($temp[1])
701                  || strpos($temp[1], ':') !== false) {
702            $data['username'] = '::invalid::';
703        } else {
704            $data['username'] = $temp[1];
705        }
706        $temp = null;
707
708        $ret = preg_match('/realm="([^"]+)"/', $header, $temp);
709        if (!$ret || empty($temp[1])) {
710            return false;
711        }
712        if (!ctype_print($temp[1]) || strpos($temp[1], ':') !== false) {
713            return false;
714        } else {
715            $data['realm'] = $temp[1];
716        }
717        $temp = null;
718
719        $ret = preg_match('/nonce="([^"]+)"/', $header, $temp);
720        if (!$ret || empty($temp[1])) {
721            return false;
722        }
723        if (!ctype_xdigit($temp[1])) {
724            return false;
725        } else {
726            $data['nonce'] = $temp[1];
727        }
728        $temp = null;
729
730        $ret = preg_match('/uri="([^"]+)"/', $header, $temp);
731        if (!$ret || empty($temp[1])) {
732            return false;
733        }
734        // Section 3.2.2.5 in RFC 2617 says the authenticating server must
735        // verify that the URI field in the Authorization header is for the
736        // same resource requested in the Request Line.
737        $rUri = @parse_url($this->_request->getRequestUri());
738        $cUri = @parse_url($temp[1]);
739        if (false === $rUri || false === $cUri) {
740            return false;
741        } else {
742            // Make sure the path portion of both URIs is the same
743            if ($rUri['path'] != $cUri['path']) {
744                return false;
745            }
746            // Section 3.2.2.5 seems to suggest that the value of the URI
747            // Authorization field should be made into an absolute URI if the
748            // Request URI is absolute, but it's vague, and that's a bunch of
749            // code I don't want to write right now.
750            $data['uri'] = $temp[1];
751        }
752        $temp = null;
753
754        $ret = preg_match('/response="([^"]+)"/', $header, $temp);
755        if (!$ret || empty($temp[1])) {
756            return false;
757        }
758        if (32 != strlen($temp[1]) || !ctype_xdigit($temp[1])) {
759            return false;
760        } else {
761            $data['response'] = $temp[1];
762        }
763        $temp = null;
764
765        // The spec says this should default to MD5 if omitted. OK, so how does
766        // that square with the algo we send out in the WWW-Authenticate header,
767        // if it can easily be overridden by the client?
768        $ret = preg_match('/algorithm="?(' . $this->_algo . ')"?/', $header, $temp);
769        if ($ret && !empty($temp[1])
770                 && in_array($temp[1], $this->_supportedAlgos)) {
771            $data['algorithm'] = $temp[1];
772        } else {
773            $data['algorithm'] = 'MD5';  // = $this->_algo; ?
774        }
775        $temp = null;
776
777        // Not optional in this implementation
778        $ret = preg_match('/cnonce="([^"]+)"/', $header, $temp);
779        if (!$ret || empty($temp[1])) {
780            return false;
781        }
782        if (!ctype_print($temp[1])) {
783            return false;
784        } else {
785            $data['cnonce'] = $temp[1];
786        }
787        $temp = null;
788
789        // If the server sent an opaque value, the client must send it back
790        if ($this->_useOpaque) {
791            $ret = preg_match('/opaque="([^"]+)"/', $header, $temp);
792            if (!$ret || empty($temp[1])) {
793
794                // Big surprise: IE isn't RFC 2617-compliant.
795                if (false !== strpos($this->_request->getHeader('User-Agent'), 'MSIE')) {
796                    $temp[1] = '';
797                    $this->_ieNoOpaque = true;
798                } else {
799                    return false;
800                }
801            }
802            // This implementation only sends MD5 hex strings in the opaque value
803            if (!$this->_ieNoOpaque &&
804                (32 != strlen($temp[1]) || !ctype_xdigit($temp[1]))) {
805                return false;
806            } else {
807                $data['opaque'] = $temp[1];
808            }
809            $temp = null;
810        }
811
812        // Not optional in this implementation, but must be one of the supported
813        // qop types
814        $ret = preg_match('/qop="?(' . implode('|', $this->_supportedQops) . ')"?/', $header, $temp);
815        if (!$ret || empty($temp[1])) {
816            return false;
817        }
818        if (!in_array($temp[1], $this->_supportedQops)) {
819            return false;
820        } else {
821            $data['qop'] = $temp[1];
822        }
823        $temp = null;
824
825        // Not optional in this implementation. The spec says this value
826        // shouldn't be a quoted string, but apparently some implementations
827        // quote it anyway. See ZF-1544.
828        $ret = preg_match('/nc="?([0-9A-Fa-f]{8})"?/', $header, $temp);
829        if (!$ret || empty($temp[1])) {
830            return false;
831        }
832        if (8 != strlen($temp[1]) || !ctype_xdigit($temp[1])) {
833            return false;
834        } else {
835            $data['nc'] = $temp[1];
836        }
837        $temp = null;
838
839        return $data;
840    }
841}