PageRenderTime 332ms CodeModel.GetById 120ms app.highlight 72ms RepoModel.GetById 99ms app.codeStats 1ms

/monica/monica/vendor/zendframework/zendframework/library/Zend/Authentication/Adapter/Http.php

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