PageRenderTime 49ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Zend/Authentication/Adapter/Http.php

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