File indexing completed on 2024-12-22 05:36:28

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Auth
0017  * @subpackage Zend_Auth_Adapter_Http
0018  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0019  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0020  * @version    $Id$
0021  */
0022 
0023 
0024 /**
0025  * @see Zend_Auth_Adapter_Interface
0026  */
0027 // require_once 'Zend/Auth/Adapter/Interface.php';
0028 
0029 
0030 /**
0031  * HTTP Authentication Adapter
0032  *
0033  * Implements a pretty good chunk of RFC 2617.
0034  *
0035  * @category   Zend
0036  * @package    Zend_Auth
0037  * @subpackage Zend_Auth_Adapter_Http
0038  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0039  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0040  * @todo       Support auth-int
0041  * @todo       Track nonces, nonce-count, opaque for replay protection and stale support
0042  * @todo       Support Authentication-Info header
0043  */
0044 class Zend_Auth_Adapter_Http implements Zend_Auth_Adapter_Interface
0045 {
0046     /**
0047      * Reference to the HTTP Request object
0048      *
0049      * @var Zend_Controller_Request_Http
0050      */
0051     protected $_request;
0052 
0053     /**
0054      * Reference to the HTTP Response object
0055      *
0056      * @var Zend_Controller_Response_Http
0057      */
0058     protected $_response;
0059 
0060     /**
0061      * Object that looks up user credentials for the Basic scheme
0062      *
0063      * @var Zend_Auth_Adapter_Http_Resolver_Interface
0064      */
0065     protected $_basicResolver;
0066 
0067     /**
0068      * Object that looks up user credentials for the Digest scheme
0069      *
0070      * @var Zend_Auth_Adapter_Http_Resolver_Interface
0071      */
0072     protected $_digestResolver;
0073 
0074     /**
0075      * List of authentication schemes supported by this class
0076      *
0077      * @var array
0078      */
0079     protected $_supportedSchemes = array('basic', 'digest');
0080 
0081     /**
0082      * List of schemes this class will accept from the client
0083      *
0084      * @var array
0085      */
0086     protected $_acceptSchemes;
0087 
0088     /**
0089      * Space-delimited list of protected domains for Digest Auth
0090      *
0091      * @var string
0092      */
0093     protected $_domains;
0094 
0095     /**
0096      * The protection realm to use
0097      *
0098      * @var string
0099      */
0100     protected $_realm;
0101 
0102     /**
0103      * Nonce timeout period
0104      *
0105      * @var integer
0106      */
0107     protected $_nonceTimeout;
0108 
0109     /**
0110      * Whether to send the opaque value in the header. True by default
0111      *
0112      * @var boolean
0113      */
0114     protected $_useOpaque;
0115 
0116     /**
0117      * List of the supported digest algorithms. I want to support both MD5 and
0118      * MD5-sess, but MD5-sess won't make it into the first version.
0119      *
0120      * @var array
0121      */
0122     protected $_supportedAlgos = array('MD5');
0123 
0124     /**
0125      * The actual algorithm to use. Defaults to MD5
0126      *
0127      * @var string
0128      */
0129     protected $_algo;
0130 
0131     /**
0132      * List of supported qop options. My intetion is to support both 'auth' and
0133      * 'auth-int', but 'auth-int' won't make it into the first version.
0134      *
0135      * @var array
0136      */
0137     protected $_supportedQops = array('auth');
0138 
0139     /**
0140      * Whether or not to do Proxy Authentication instead of origin server
0141      * authentication (send 407's instead of 401's). Off by default.
0142      *
0143      * @var boolean
0144      */
0145     protected $_imaProxy;
0146 
0147     /**
0148      * Flag indicating the client is IE and didn't bother to return the opaque string
0149      *
0150      * @var boolean
0151      */
0152     protected $_ieNoOpaque;
0153 
0154     /**
0155      * Constructor
0156      *
0157      * @param  array $config Configuration settings:
0158      *    'accept_schemes' => 'basic'|'digest'|'basic digest'
0159      *    'realm' => <string>
0160      *    'digest_domains' => <string> Space-delimited list of URIs
0161      *    'nonce_timeout' => <int>
0162      *    'use_opaque' => <bool> Whether to send the opaque value in the header
0163      *    'alogrithm' => <string> See $_supportedAlgos. Default: MD5
0164      *    'proxy_auth' => <bool> Whether to do authentication as a Proxy
0165      * @throws Zend_Auth_Adapter_Exception
0166      */
0167     public function __construct(array $config)
0168     {
0169         if (!extension_loaded('hash')) {
0170             /**
0171              * @see Zend_Auth_Adapter_Exception
0172              */
0173             // require_once 'Zend/Auth/Adapter/Exception.php';
0174             throw new Zend_Auth_Adapter_Exception(__CLASS__  . ' requires the \'hash\' extension');
0175         }
0176 
0177         $this->_request  = null;
0178         $this->_response = null;
0179         $this->_ieNoOpaque = false;
0180 
0181 
0182         if (empty($config['accept_schemes'])) {
0183             /**
0184              * @see Zend_Auth_Adapter_Exception
0185              */
0186             // require_once 'Zend/Auth/Adapter/Exception.php';
0187             throw new Zend_Auth_Adapter_Exception('Config key \'accept_schemes\' is required');
0188         }
0189 
0190         $schemes = explode(' ', $config['accept_schemes']);
0191         $this->_acceptSchemes = array_intersect($schemes, $this->_supportedSchemes);
0192         if (empty($this->_acceptSchemes)) {
0193             /**
0194              * @see Zend_Auth_Adapter_Exception
0195              */
0196             // require_once 'Zend/Auth/Adapter/Exception.php';
0197             throw new Zend_Auth_Adapter_Exception('No supported schemes given in \'accept_schemes\'. Valid values: '
0198                                                 . implode(', ', $this->_supportedSchemes));
0199         }
0200 
0201         // Double-quotes are used to delimit the realm string in the HTTP header,
0202         // and colons are field delimiters in the password file.
0203         if (empty($config['realm']) ||
0204             !ctype_print($config['realm']) ||
0205             strpos($config['realm'], ':') !== false ||
0206             strpos($config['realm'], '"') !== false) {
0207             /**
0208              * @see Zend_Auth_Adapter_Exception
0209              */
0210             // require_once 'Zend/Auth/Adapter/Exception.php';
0211             throw new Zend_Auth_Adapter_Exception('Config key \'realm\' is required, and must contain only printable '
0212                                                 . 'characters, excluding quotation marks and colons');
0213         } else {
0214             $this->_realm = $config['realm'];
0215         }
0216 
0217         if (in_array('digest', $this->_acceptSchemes)) {
0218             if (empty($config['digest_domains']) ||
0219                 !ctype_print($config['digest_domains']) ||
0220                 strpos($config['digest_domains'], '"') !== false) {
0221                 /**
0222                  * @see Zend_Auth_Adapter_Exception
0223                  */
0224                 // require_once 'Zend/Auth/Adapter/Exception.php';
0225                 throw new Zend_Auth_Adapter_Exception('Config key \'digest_domains\' is required, and must contain '
0226                                                     . 'only printable characters, excluding quotation marks');
0227             } else {
0228                 $this->_domains = $config['digest_domains'];
0229             }
0230 
0231             if (empty($config['nonce_timeout']) ||
0232                 !is_numeric($config['nonce_timeout'])) {
0233                 /**
0234                  * @see Zend_Auth_Adapter_Exception
0235                  */
0236                 // require_once 'Zend/Auth/Adapter/Exception.php';
0237                 throw new Zend_Auth_Adapter_Exception('Config key \'nonce_timeout\' is required, and must be an '
0238                                                     . 'integer');
0239             } else {
0240                 $this->_nonceTimeout = (int) $config['nonce_timeout'];
0241             }
0242 
0243             // We use the opaque value unless explicitly told not to
0244             if (isset($config['use_opaque']) && false == (bool) $config['use_opaque']) {
0245                 $this->_useOpaque = false;
0246             } else {
0247                 $this->_useOpaque = true;
0248             }
0249 
0250             if (isset($config['algorithm']) && in_array($config['algorithm'], $this->_supportedAlgos)) {
0251                 $this->_algo = $config['algorithm'];
0252             } else {
0253                 $this->_algo = 'MD5';
0254             }
0255         }
0256 
0257         // Don't be a proxy unless explicitly told to do so
0258         if (isset($config['proxy_auth']) && true == (bool) $config['proxy_auth']) {
0259             $this->_imaProxy = true;  // I'm a Proxy
0260         } else {
0261             $this->_imaProxy = false;
0262         }
0263     }
0264 
0265     /**
0266      * Setter for the _basicResolver property
0267      *
0268      * @param  Zend_Auth_Adapter_Http_Resolver_Interface $resolver
0269      * @return Zend_Auth_Adapter_Http Provides a fluent interface
0270      */
0271     public function setBasicResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver)
0272     {
0273         $this->_basicResolver = $resolver;
0274 
0275         return $this;
0276     }
0277 
0278     /**
0279      * Getter for the _basicResolver property
0280      *
0281      * @return Zend_Auth_Adapter_Http_Resolver_Interface
0282      */
0283     public function getBasicResolver()
0284     {
0285         return $this->_basicResolver;
0286     }
0287 
0288     /**
0289      * Setter for the _digestResolver property
0290      *
0291      * @param  Zend_Auth_Adapter_Http_Resolver_Interface $resolver
0292      * @return Zend_Auth_Adapter_Http Provides a fluent interface
0293      */
0294     public function setDigestResolver(Zend_Auth_Adapter_Http_Resolver_Interface $resolver)
0295     {
0296         $this->_digestResolver = $resolver;
0297 
0298         return $this;
0299     }
0300 
0301     /**
0302      * Getter for the _digestResolver property
0303      *
0304      * @return Zend_Auth_Adapter_Http_Resolver_Interface
0305      */
0306     public function getDigestResolver()
0307     {
0308         return $this->_digestResolver;
0309     }
0310 
0311     /**
0312      * Setter for the Request object
0313      *
0314      * @param  Zend_Controller_Request_Http $request
0315      * @return Zend_Auth_Adapter_Http Provides a fluent interface
0316      */
0317     public function setRequest(Zend_Controller_Request_Http $request)
0318     {
0319         $this->_request = $request;
0320 
0321         return $this;
0322     }
0323 
0324     /**
0325      * Getter for the Request object
0326      *
0327      * @return Zend_Controller_Request_Http
0328      */
0329     public function getRequest()
0330     {
0331         return $this->_request;
0332     }
0333 
0334     /**
0335      * Setter for the Response object
0336      *
0337      * @param  Zend_Controller_Response_Http $response
0338      * @return Zend_Auth_Adapter_Http Provides a fluent interface
0339      */
0340     public function setResponse(Zend_Controller_Response_Http $response)
0341     {
0342         $this->_response = $response;
0343 
0344         return $this;
0345     }
0346 
0347     /**
0348      * Getter for the Response object
0349      *
0350      * @return Zend_Controller_Response_Http
0351      */
0352     public function getResponse()
0353     {
0354         return $this->_response;
0355     }
0356 
0357     /**
0358      * Authenticate
0359      *
0360      * @throws Zend_Auth_Adapter_Exception
0361      * @return Zend_Auth_Result
0362      */
0363     public function authenticate()
0364     {
0365         if (empty($this->_request) ||
0366             empty($this->_response)) {
0367             /**
0368              * @see Zend_Auth_Adapter_Exception
0369              */
0370             // require_once 'Zend/Auth/Adapter/Exception.php';
0371             throw new Zend_Auth_Adapter_Exception('Request and Response objects must be set before calling '
0372                                                 . 'authenticate()');
0373         }
0374 
0375         if ($this->_imaProxy) {
0376             $getHeader = 'Proxy-Authorization';
0377         } else {
0378             $getHeader = 'Authorization';
0379         }
0380 
0381         $authHeader = $this->_request->getHeader($getHeader);
0382         if (!$authHeader) {
0383             return $this->_challengeClient();
0384         }
0385 
0386         list($clientScheme) = explode(' ', $authHeader);
0387         $clientScheme = strtolower($clientScheme);
0388 
0389         // The server can issue multiple challenges, but the client should
0390         // answer with only the selected auth scheme.
0391         if (!in_array($clientScheme, $this->_supportedSchemes)) {
0392             $this->_response->setHttpResponseCode(400);
0393             return new Zend_Auth_Result(
0394                 Zend_Auth_Result::FAILURE_UNCATEGORIZED,
0395                 array(),
0396                 array('Client requested an incorrect or unsupported authentication scheme')
0397             );
0398         }
0399 
0400         // client sent a scheme that is not the one required
0401         if (!in_array($clientScheme, $this->_acceptSchemes)) {
0402             // challenge again the client
0403             return $this->_challengeClient();
0404         }
0405 
0406         switch ($clientScheme) {
0407             case 'basic':
0408                 $result = $this->_basicAuth($authHeader);
0409                 break;
0410             case 'digest':
0411                 $result = $this->_digestAuth($authHeader);
0412             break;
0413             default:
0414                 /**
0415                  * @see Zend_Auth_Adapter_Exception
0416                  */
0417                 // require_once 'Zend/Auth/Adapter/Exception.php';
0418                 throw new Zend_Auth_Adapter_Exception('Unsupported authentication scheme');
0419         }
0420 
0421         return $result;
0422     }
0423 
0424     /**
0425      * Challenge Client
0426      *
0427      * Sets a 401 or 407 Unauthorized response code, and creates the
0428      * appropriate Authenticate header(s) to prompt for credentials.
0429      *
0430      * @return Zend_Auth_Result Always returns a non-identity Auth result
0431      */
0432     protected function _challengeClient()
0433     {
0434         if ($this->_imaProxy) {
0435             $statusCode = 407;
0436             $headerName = 'Proxy-Authenticate';
0437         } else {
0438             $statusCode = 401;
0439             $headerName = 'WWW-Authenticate';
0440         }
0441 
0442         $this->_response->setHttpResponseCode($statusCode);
0443 
0444         // Send a challenge in each acceptable authentication scheme
0445         if (in_array('basic', $this->_acceptSchemes)) {
0446             $this->_response->setHeader($headerName, $this->_basicHeader());
0447         }
0448         if (in_array('digest', $this->_acceptSchemes)) {
0449             $this->_response->setHeader($headerName, $this->_digestHeader());
0450         }
0451         return new Zend_Auth_Result(
0452             Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
0453             array(),
0454             array('Invalid or absent credentials; challenging client')
0455         );
0456     }
0457 
0458     /**
0459      * Basic Header
0460      *
0461      * Generates a Proxy- or WWW-Authenticate header value in the Basic
0462      * authentication scheme.
0463      *
0464      * @return string Authenticate header value
0465      */
0466     protected function _basicHeader()
0467     {
0468         return 'Basic realm="' . $this->_realm . '"';
0469     }
0470 
0471     /**
0472      * Digest Header
0473      *
0474      * Generates a Proxy- or WWW-Authenticate header value in the Digest
0475      * authentication scheme.
0476      *
0477      * @return string Authenticate header value
0478      */
0479     protected function _digestHeader()
0480     {
0481         $wwwauth = 'Digest realm="' . $this->_realm . '", '
0482                  . 'domain="' . $this->_domains . '", '
0483                  . 'nonce="' . $this->_calcNonce() . '", '
0484                  . ($this->_useOpaque ? 'opaque="' . $this->_calcOpaque() . '", ' : '')
0485                  . 'algorithm="' . $this->_algo . '", '
0486                  . 'qop="' . implode(',', $this->_supportedQops) . '"';
0487 
0488         return $wwwauth;
0489     }
0490 
0491     /**
0492      * Basic Authentication
0493      *
0494      * @param  string $header Client's Authorization header
0495      * @throws Zend_Auth_Adapter_Exception
0496      * @return Zend_Auth_Result
0497      */
0498     protected function _basicAuth($header)
0499     {
0500         if (empty($header)) {
0501             /**
0502              * @see Zend_Auth_Adapter_Exception
0503              */
0504             // require_once 'Zend/Auth/Adapter/Exception.php';
0505             throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required');
0506         }
0507         if (empty($this->_basicResolver)) {
0508             /**
0509              * @see Zend_Auth_Adapter_Exception
0510              */
0511             // require_once 'Zend/Auth/Adapter/Exception.php';
0512             throw new Zend_Auth_Adapter_Exception('A basicResolver object must be set before doing Basic '
0513                                                 . 'authentication');
0514         }
0515 
0516         // Decode the Authorization header
0517         $auth = substr($header, strlen('Basic '));
0518         $auth = base64_decode($auth);
0519         if (!$auth) {
0520             /**
0521              * @see Zend_Auth_Adapter_Exception
0522              */
0523             // require_once 'Zend/Auth/Adapter/Exception.php';
0524             throw new Zend_Auth_Adapter_Exception('Unable to base64_decode Authorization header value');
0525         }
0526 
0527         // See ZF-1253. Validate the credentials the same way the digest
0528         // implementation does. If invalid credentials are detected,
0529         // re-challenge the client.
0530         if (!ctype_print($auth)) {
0531             return $this->_challengeClient();
0532         }
0533         // Fix for ZF-1515: Now re-challenges on empty username or password
0534         $creds = array_filter(explode(':', $auth));
0535         if (count($creds) != 2) {
0536             return $this->_challengeClient();
0537         }
0538 
0539         $password = $this->_basicResolver->resolve($creds[0], $this->_realm);
0540         if ($password && $this->_secureStringCompare($password, $creds[1])) {
0541             $identity = array('username'=>$creds[0], 'realm'=>$this->_realm);
0542             return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity);
0543         } else {
0544             return $this->_challengeClient();
0545         }
0546     }
0547 
0548     /**
0549      * Digest Authentication
0550      *
0551      * @param  string $header Client's Authorization header
0552      * @throws Zend_Auth_Adapter_Exception
0553      * @return Zend_Auth_Result Valid auth result only on successful auth
0554      */
0555     protected function _digestAuth($header)
0556     {
0557         if (empty($header)) {
0558             /**
0559              * @see Zend_Auth_Adapter_Exception
0560              */
0561             // require_once 'Zend/Auth/Adapter/Exception.php';
0562             throw new Zend_Auth_Adapter_Exception('The value of the client Authorization header is required');
0563         }
0564         if (empty($this->_digestResolver)) {
0565             /**
0566              * @see Zend_Auth_Adapter_Exception
0567              */
0568             // require_once 'Zend/Auth/Adapter/Exception.php';
0569             throw new Zend_Auth_Adapter_Exception('A digestResolver object must be set before doing Digest authentication');
0570         }
0571 
0572         $data = $this->_parseDigestAuth($header);
0573         if ($data === false) {
0574             $this->_response->setHttpResponseCode(400);
0575             return new Zend_Auth_Result(
0576                 Zend_Auth_Result::FAILURE_UNCATEGORIZED,
0577                 array(),
0578                 array('Invalid Authorization header format')
0579             );
0580         }
0581 
0582         // See ZF-1052. This code was a bit too unforgiving of invalid
0583         // usernames. Now, if the username is bad, we re-challenge the client.
0584         if ('::invalid::' == $data['username']) {
0585             return $this->_challengeClient();
0586         }
0587 
0588         // Verify that the client sent back the same nonce
0589         if ($this->_calcNonce() != $data['nonce']) {
0590             return $this->_challengeClient();
0591         }
0592         // The opaque value is also required to match, but of course IE doesn't
0593         // play ball.
0594         if (!$this->_ieNoOpaque && $this->_calcOpaque() != $data['opaque']) {
0595             return $this->_challengeClient();
0596         }
0597 
0598         // Look up the user's password hash. If not found, deny access.
0599         // This makes no assumptions about how the password hash was
0600         // constructed beyond that it must have been built in such a way as
0601         // to be recreatable with the current settings of this object.
0602         $ha1 = $this->_digestResolver->resolve($data['username'], $data['realm']);
0603         if ($ha1 === false) {
0604             return $this->_challengeClient();
0605         }
0606 
0607         // If MD5-sess is used, a1 value is made of the user's password
0608         // hash with the server and client nonce appended, separated by
0609         // colons.
0610         if ($this->_algo == 'MD5-sess') {
0611             $ha1 = hash('md5', $ha1 . ':' . $data['nonce'] . ':' . $data['cnonce']);
0612         }
0613 
0614         // Calculate h(a2). The value of this hash depends on the qop
0615         // option selected by the client and the supported hash functions
0616         switch ($data['qop']) {
0617             case 'auth':
0618                 $a2 = $this->_request->getMethod() . ':' . $data['uri'];
0619                 break;
0620             case 'auth-int':
0621                 // Should be REQUEST_METHOD . ':' . uri . ':' . hash(entity-body),
0622                 // but this isn't supported yet, so fall through to default case
0623             default:
0624                 /**
0625                  * @see Zend_Auth_Adapter_Exception
0626                  */
0627                 // require_once 'Zend/Auth/Adapter/Exception.php';
0628                 throw new Zend_Auth_Adapter_Exception('Client requested an unsupported qop option');
0629         }
0630         // Using hash() should make parameterizing the hash algorithm
0631         // easier
0632         $ha2 = hash('md5', $a2);
0633 
0634 
0635         // Calculate the server's version of the request-digest. This must
0636         // match $data['response']. See RFC 2617, section 3.2.2.1
0637         $message = $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $ha2;
0638         $digest  = hash('md5', $ha1 . ':' . $message);
0639 
0640         // If our digest matches the client's let them in, otherwise return
0641         // a 401 code and exit to prevent access to the protected resource.
0642         if ($this->_secureStringCompare($digest, $data['response'])) {
0643             $identity = array('username'=>$data['username'], 'realm'=>$data['realm']);
0644             return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $identity);
0645         } else {
0646             return $this->_challengeClient();
0647         }
0648     }
0649 
0650     /**
0651      * Calculate Nonce
0652      *
0653      * @return string The nonce value
0654      */
0655     protected function _calcNonce()
0656     {
0657         // Once subtle consequence of this timeout calculation is that it
0658         // actually divides all of time into _nonceTimeout-sized sections, such
0659         // that the value of timeout is the point in time of the next
0660         // approaching "boundary" of a section. This allows the server to
0661         // consistently generate the same timeout (and hence the same nonce
0662         // value) across requests, but only as long as one of those
0663         // "boundaries" is not crossed between requests. If that happens, the
0664         // nonce will change on its own, and effectively log the user out. This
0665         // would be surprising if the user just logged in.
0666         $timeout = ceil(time() / $this->_nonceTimeout) * $this->_nonceTimeout;
0667 
0668         $nonce = hash('md5', $timeout . ':' . $this->_request->getServer('HTTP_USER_AGENT') . ':' . __CLASS__);
0669         return $nonce;
0670     }
0671 
0672     /**
0673      * Calculate Opaque
0674      *
0675      * The opaque string can be anything; the client must return it exactly as
0676      * it was sent. It may be useful to store data in this string in some
0677      * applications. Ideally, a new value for this would be generated each time
0678      * a WWW-Authenticate header is sent (in order to reduce predictability),
0679      * but we would have to be able to create the same exact value across at
0680      * least two separate requests from the same client.
0681      *
0682      * @return string The opaque value
0683      */
0684     protected function _calcOpaque()
0685     {
0686         return hash('md5', 'Opaque Data:' . __CLASS__);
0687     }
0688 
0689     /**
0690      * Parse Digest Authorization header
0691      *
0692      * @param  string $header Client's Authorization: HTTP header
0693      * @return array|false Data elements from header, or false if any part of
0694      *         the header is invalid
0695      */
0696     protected function _parseDigestAuth($header)
0697     {
0698         $temp = null;
0699         $data = array();
0700 
0701         // See ZF-1052. Detect invalid usernames instead of just returning a
0702         // 400 code.
0703         $ret = preg_match('/username="([^"]+)"/', $header, $temp);
0704         if (!$ret || empty($temp[1])
0705                   || !ctype_print($temp[1])
0706                   || strpos($temp[1], ':') !== false) {
0707             $data['username'] = '::invalid::';
0708         } else {
0709             $data['username'] = $temp[1];
0710         }
0711         $temp = null;
0712 
0713         $ret = preg_match('/realm="([^"]+)"/', $header, $temp);
0714         if (!$ret || empty($temp[1])) {
0715             return false;
0716         }
0717         if (!ctype_print($temp[1]) || strpos($temp[1], ':') !== false) {
0718             return false;
0719         } else {
0720             $data['realm'] = $temp[1];
0721         }
0722         $temp = null;
0723 
0724         $ret = preg_match('/nonce="([^"]+)"/', $header, $temp);
0725         if (!$ret || empty($temp[1])) {
0726             return false;
0727         }
0728         if (!ctype_xdigit($temp[1])) {
0729             return false;
0730         } else {
0731             $data['nonce'] = $temp[1];
0732         }
0733         $temp = null;
0734 
0735         $ret = preg_match('/uri="([^"]+)"/', $header, $temp);
0736         if (!$ret || empty($temp[1])) {
0737             return false;
0738         }
0739         // Section 3.2.2.5 in RFC 2617 says the authenticating server must
0740         // verify that the URI field in the Authorization header is for the
0741         // same resource requested in the Request Line.
0742         $rUri = @parse_url($this->_request->getRequestUri());
0743         $cUri = @parse_url($temp[1]);
0744         if (false === $rUri || false === $cUri) {
0745             return false;
0746         } else {
0747             // Make sure the path portion of both URIs is the same
0748             if ($rUri['path'] != $cUri['path']) {
0749                 return false;
0750             }
0751             // Section 3.2.2.5 seems to suggest that the value of the URI
0752             // Authorization field should be made into an absolute URI if the
0753             // Request URI is absolute, but it's vague, and that's a bunch of
0754             // code I don't want to write right now.
0755             $data['uri'] = $temp[1];
0756         }
0757         $temp = null;
0758 
0759         $ret = preg_match('/response="([^"]+)"/', $header, $temp);
0760         if (!$ret || empty($temp[1])) {
0761             return false;
0762         }
0763         if (32 != strlen($temp[1]) || !ctype_xdigit($temp[1])) {
0764             return false;
0765         } else {
0766             $data['response'] = $temp[1];
0767         }
0768         $temp = null;
0769 
0770         // The spec says this should default to MD5 if omitted. OK, so how does
0771         // that square with the algo we send out in the WWW-Authenticate header,
0772         // if it can easily be overridden by the client?
0773         $ret = preg_match('/algorithm="?(' . $this->_algo . ')"?/', $header, $temp);
0774         if ($ret && !empty($temp[1])
0775                  && in_array($temp[1], $this->_supportedAlgos)) {
0776             $data['algorithm'] = $temp[1];
0777         } else {
0778             $data['algorithm'] = 'MD5';  // = $this->_algo; ?
0779         }
0780         $temp = null;
0781 
0782         // Not optional in this implementation
0783         $ret = preg_match('/cnonce="([^"]+)"/', $header, $temp);
0784         if (!$ret || empty($temp[1])) {
0785             return false;
0786         }
0787         if (!ctype_print($temp[1])) {
0788             return false;
0789         } else {
0790             $data['cnonce'] = $temp[1];
0791         }
0792         $temp = null;
0793 
0794         // If the server sent an opaque value, the client must send it back
0795         if ($this->_useOpaque) {
0796             $ret = preg_match('/opaque="([^"]+)"/', $header, $temp);
0797             if (!$ret || empty($temp[1])) {
0798 
0799                 // Big surprise: IE isn't RFC 2617-compliant.
0800                 if (false !== strpos($this->_request->getHeader('User-Agent'), 'MSIE')) {
0801                     $temp[1] = '';
0802                     $this->_ieNoOpaque = true;
0803                 } else {
0804                     return false;
0805                 }
0806             }
0807             // This implementation only sends MD5 hex strings in the opaque value
0808             if (!$this->_ieNoOpaque &&
0809                 (32 != strlen($temp[1]) || !ctype_xdigit($temp[1]))) {
0810                 return false;
0811             } else {
0812                 $data['opaque'] = $temp[1];
0813             }
0814             $temp = null;
0815         }
0816 
0817         // Not optional in this implementation, but must be one of the supported
0818         // qop types
0819         $ret = preg_match('/qop="?(' . implode('|', $this->_supportedQops) . ')"?/', $header, $temp);
0820         if (!$ret || empty($temp[1])) {
0821             return false;
0822         }
0823         if (!in_array($temp[1], $this->_supportedQops)) {
0824             return false;
0825         } else {
0826             $data['qop'] = $temp[1];
0827         }
0828         $temp = null;
0829 
0830         // Not optional in this implementation. The spec says this value
0831         // shouldn't be a quoted string, but apparently some implementations
0832         // quote it anyway. See ZF-1544.
0833         $ret = preg_match('/nc="?([0-9A-Fa-f]{8})"?/', $header, $temp);
0834         if (!$ret || empty($temp[1])) {
0835             return false;
0836         }
0837         if (8 != strlen($temp[1]) || !ctype_xdigit($temp[1])) {
0838             return false;
0839         } else {
0840             $data['nc'] = $temp[1];
0841         }
0842         $temp = null;
0843 
0844         return $data;
0845     }
0846 
0847     /**
0848      * Securely compare two strings for equality while avoided C level memcmp()
0849      * optimisations capable of leaking timing information useful to an attacker
0850      * attempting to iteratively guess the unknown string (e.g. password) being
0851      * compared against.
0852      *
0853      * @param string $a
0854      * @param string $b
0855      * @return bool
0856      */
0857     protected function _secureStringCompare($a, $b)
0858     {
0859         if (strlen($a) !== strlen($b)) {
0860             return false;
0861         }
0862         $result = 0;
0863         for ($i = 0; $i < strlen($a); $i++) {
0864             $result |= ord($a[$i]) ^ ord($b[$i]);
0865         }
0866         return $result == 0;
0867     }
0868 }