PageRenderTime 62ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/common/libraries/plugin/pear/HTTP/Request2/Adapter/Socket.php

https://bitbucket.org/renaatdemuynck/chamilo
PHP | 1064 lines | 661 code | 83 blank | 320 comment | 198 complexity | b0c0e6e6c8ab5770e1f4d1fb30c6add1 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1, LGPL-3.0, GPL-3.0, MIT, GPL-2.0
  1. <?php
  2. /**
  3. * Socket-based adapter for HTTP_Request2
  4. *
  5. * PHP version 5
  6. *
  7. * LICENSE:
  8. *
  9. * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions
  14. * are met:
  15. *
  16. * * Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. * * Redistributions in binary form must reproduce the above copyright
  19. * notice, this list of conditions and the following disclaimer in the
  20. * documentation and/or other materials provided with the distribution.
  21. * * The names of the authors may not be used to endorse or promote products
  22. * derived from this software without specific prior written permission.
  23. *
  24. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  25. * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  26. * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  27. * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  28. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  29. * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  30. * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  31. * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
  32. * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  33. * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  34. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  35. *
  36. * @category HTTP
  37. * @package HTTP_Request2
  38. * @author Alexey Borzov <avb@php.net>
  39. * @license http://opensource.org/licenses/bsd-license.php New BSD License
  40. * @version SVN: $Id: Socket.php 290921 2009-11-18 17:31:58Z avb $
  41. * @link http://pear.php.net/package/HTTP_Request2
  42. */
  43. /**
  44. * Base class for HTTP_Request2 adapters
  45. */
  46. require_once 'HTTP/Request2/Adapter.php';
  47. /**
  48. * Socket-based adapter for HTTP_Request2
  49. *
  50. * This adapter uses only PHP sockets and will work on almost any PHP
  51. * environment. Code is based on original HTTP_Request PEAR package.
  52. *
  53. * @category HTTP
  54. * @package HTTP_Request2
  55. * @author Alexey Borzov <avb@php.net>
  56. * @version Release: 0.5.2
  57. */
  58. class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
  59. {
  60. /**
  61. * Regular expression for 'token' rule from RFC 2616
  62. */
  63. const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';
  64. /**
  65. * Regular expression for 'quoted-string' rule from RFC 2616
  66. */
  67. const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"';
  68. /**
  69. * Connected sockets, needed for Keep-Alive support
  70. * @var array
  71. * @see connect()
  72. */
  73. protected static $sockets = array();
  74. /**
  75. * Data for digest authentication scheme
  76. *
  77. * The keys for the array are URL prefixes.
  78. *
  79. * The values are associative arrays with data (realm, nonce, nonce-count,
  80. * opaque...) needed for digest authentication. Stored here to prevent making
  81. * duplicate requests to digest-protected resources after we have already
  82. * received the challenge.
  83. *
  84. * @var array
  85. */
  86. protected static $challenges = array();
  87. /**
  88. * Connected socket
  89. * @var resource
  90. * @see connect()
  91. */
  92. protected $socket;
  93. /**
  94. * Challenge used for server digest authentication
  95. * @var array
  96. */
  97. protected $serverChallenge;
  98. /**
  99. * Challenge used for proxy digest authentication
  100. * @var array
  101. */
  102. protected $proxyChallenge;
  103. /**
  104. * Sum of start time and global timeout, exception will be thrown if request continues past this time
  105. * @var integer
  106. */
  107. protected $deadline = null;
  108. /**
  109. * Remaining length of the current chunk, when reading chunked response
  110. * @var integer
  111. * @see readChunked()
  112. */
  113. protected $chunkLength = 0;
  114. /**
  115. * Remaining amount of redirections to follow
  116. *
  117. * Starts at 'max_redirects' configuration parameter and is reduced on each
  118. * subsequent redirect. An Exception will be thrown once it reaches zero.
  119. *
  120. * @var integer
  121. */
  122. protected $redirectCountdown = null;
  123. /**
  124. * Sends request to the remote server and returns its response
  125. *
  126. * @param HTTP_Request2
  127. * @return HTTP_Request2_Response
  128. * @throws HTTP_Request2_Exception
  129. */
  130. public function sendRequest(HTTP_Request2 $request)
  131. {
  132. $this->request = $request;
  133. // Use global request timeout if given, see feature requests #5735, #8964
  134. if ($timeout = $request->getConfig('timeout'))
  135. {
  136. $this->deadline = time() + $timeout;
  137. }
  138. else
  139. {
  140. $this->deadline = null;
  141. }
  142. try
  143. {
  144. $keepAlive = $this->connect();
  145. $headers = $this->prepareHeaders();
  146. if (false === @fwrite($this->socket, $headers, strlen($headers)))
  147. {
  148. throw new HTTP_Request2_Exception('Error writing request');
  149. }
  150. // provide request headers to the observer, see request #7633
  151. $this->request->setLastEvent('sentHeaders', $headers);
  152. $this->writeBody();
  153. if ($this->deadline && time() > $this->deadline)
  154. {
  155. throw new HTTP_Request2_Exception('Request timed out after ' . $request->getConfig('timeout') . ' second(s)');
  156. }
  157. $response = $this->readResponse();
  158. if (! $this->canKeepAlive($keepAlive, $response))
  159. {
  160. $this->disconnect();
  161. }
  162. if ($this->shouldUseProxyDigestAuth($response))
  163. {
  164. return $this->sendRequest($request);
  165. }
  166. if ($this->shouldUseServerDigestAuth($response))
  167. {
  168. return $this->sendRequest($request);
  169. }
  170. if ($authInfo = $response->getHeader('authentication-info'))
  171. {
  172. $this->updateChallenge($this->serverChallenge, $authInfo);
  173. }
  174. if ($proxyInfo = $response->getHeader('proxy-authentication-info'))
  175. {
  176. $this->updateChallenge($this->proxyChallenge, $proxyInfo);
  177. }
  178. }
  179. catch (Exception $e)
  180. {
  181. $this->disconnect();
  182. }
  183. unset($this->request, $this->requestBody);
  184. if (! empty($e))
  185. {
  186. throw $e;
  187. }
  188. if (! $request->getConfig('follow_redirects') || ! $response->isRedirect())
  189. {
  190. return $response;
  191. }
  192. else
  193. {
  194. return $this->handleRedirect($request, $response);
  195. }
  196. }
  197. /**
  198. * Connects to the remote server
  199. *
  200. * @return bool whether the connection can be persistent
  201. * @throws HTTP_Request2_Exception
  202. */
  203. protected function connect()
  204. {
  205. $secure = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https');
  206. $tunnel = HTTP_Request2 :: METHOD_CONNECT == $this->request->getMethod();
  207. $headers = $this->request->getHeaders();
  208. $reqHost = $this->request->getUrl()->getHost();
  209. if (! ($reqPort = $this->request->getUrl()->getPort()))
  210. {
  211. $reqPort = $secure ? 443 : 80;
  212. }
  213. if ($host = $this->request->getConfig('proxy_host'))
  214. {
  215. if (! ($port = $this->request->getConfig('proxy_port')))
  216. {
  217. throw new HTTP_Request2_Exception('Proxy port not provided');
  218. }
  219. $proxy = true;
  220. }
  221. else
  222. {
  223. $host = $reqHost;
  224. $port = $reqPort;
  225. $proxy = false;
  226. }
  227. if ($tunnel && ! $proxy)
  228. {
  229. throw new HTTP_Request2_Exception("Trying to perform CONNECT request without proxy");
  230. }
  231. if ($secure && ! in_array('ssl', stream_get_transports()))
  232. {
  233. throw new HTTP_Request2_Exception('Need OpenSSL support for https:// requests');
  234. }
  235. // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
  236. // connection token to a proxy server...
  237. if ($proxy && ! $secure && ! empty($headers['connection']) && 'Keep-Alive' == $headers['connection'])
  238. {
  239. $this->request->setHeader('connection');
  240. }
  241. $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && empty($headers['connection'])) || (! empty($headers['connection']) && 'Keep-Alive' == $headers['connection']);
  242. $host = ((! $secure || $proxy) ? 'tcp://' : 'ssl://') . $host;
  243. $options = array();
  244. if ($secure || $tunnel)
  245. {
  246. foreach ($this->request->getConfig() as $name => $value)
  247. {
  248. if ('ssl_' == substr($name, 0, 4) && null !== $value)
  249. {
  250. if ('ssl_verify_host' == $name)
  251. {
  252. if ($value)
  253. {
  254. $options['CN_match'] = $reqHost;
  255. }
  256. }
  257. else
  258. {
  259. $options[substr($name, 4)] = $value;
  260. }
  261. }
  262. }
  263. ksort($options);
  264. }
  265. // Changing SSL context options after connection is established does *not*
  266. // work, we need a new connection if options change
  267. $remote = $host . ':' . $port;
  268. $socketKey = $remote . (($secure && $proxy) ? "->{$reqHost}:{$reqPort}" : '') . (empty($options) ? '' : ':' . serialize($options));
  269. unset($this->socket);
  270. // We use persistent connections and have a connected socket?
  271. // Ensure that the socket is still connected, see bug #16149
  272. if ($keepAlive && ! empty(self :: $sockets[$socketKey]) && ! feof(self :: $sockets[$socketKey]))
  273. {
  274. $this->socket = & self :: $sockets[$socketKey];
  275. }
  276. elseif ($secure && $proxy && ! $tunnel)
  277. {
  278. $this->establishTunnel();
  279. $this->request->setLastEvent('connect', "ssl://{$reqHost}:{$reqPort} via {$host}:{$port}");
  280. self :: $sockets[$socketKey] = & $this->socket;
  281. }
  282. else
  283. {
  284. // Set SSL context options if doing HTTPS request or creating a tunnel
  285. $context = stream_context_create();
  286. foreach ($options as $name => $value)
  287. {
  288. if (! stream_context_set_option($context, 'ssl', $name, $value))
  289. {
  290. throw new HTTP_Request2_Exception("Error setting SSL context option '{$name}'");
  291. }
  292. }
  293. $this->socket = @stream_socket_client($remote, $errno, $errstr, $this->request->getConfig('connect_timeout'), STREAM_CLIENT_CONNECT, $context);
  294. if (! $this->socket)
  295. {
  296. throw new HTTP_Request2_Exception("Unable to connect to {$remote}. Error #{$errno}: {$errstr}");
  297. }
  298. $this->request->setLastEvent('connect', $remote);
  299. self :: $sockets[$socketKey] = & $this->socket;
  300. }
  301. return $keepAlive;
  302. }
  303. /**
  304. * Establishes a tunnel to a secure remote server via HTTP CONNECT request
  305. *
  306. * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP
  307. * sees that we are connected to a proxy server (duh!) rather than the server
  308. * that presents its certificate.
  309. *
  310. * @link http://tools.ietf.org/html/rfc2817#section-5.2
  311. * @throws HTTP_Request2_Exception
  312. */
  313. protected function establishTunnel()
  314. {
  315. $donor = new self();
  316. $connect = new HTTP_Request2($this->request->getUrl(), HTTP_Request2 :: METHOD_CONNECT, array_merge($this->request->getConfig(), array(
  317. 'adapter' => $donor)));
  318. $response = $connect->send();
  319. // Need any successful (2XX) response
  320. if (200 > $response->getStatus() || 300 <= $response->getStatus())
  321. {
  322. throw new HTTP_Request2_Exception('Failed to connect via HTTPS proxy. Proxy response: ' . $response->getStatus() . ' ' . $response->getReasonPhrase());
  323. }
  324. $this->socket = $donor->socket;
  325. $modes = array(STREAM_CRYPTO_METHOD_TLS_CLIENT, STREAM_CRYPTO_METHOD_SSLv3_CLIENT,
  326. STREAM_CRYPTO_METHOD_SSLv23_CLIENT, STREAM_CRYPTO_METHOD_SSLv2_CLIENT);
  327. foreach ($modes as $mode)
  328. {
  329. if (stream_socket_enable_crypto($this->socket, true, $mode))
  330. {
  331. return;
  332. }
  333. }
  334. throw new HTTP_Request2_Exception('Failed to enable secure connection when connecting through proxy');
  335. }
  336. /**
  337. * Checks whether current connection may be reused or should be closed
  338. *
  339. * @param boolean whether connection could be persistent
  340. * in the first place
  341. * @param HTTP_Request2_Response response object to check
  342. * @return boolean
  343. */
  344. protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response)
  345. {
  346. // Do not close socket on successful CONNECT request
  347. if (HTTP_Request2 :: METHOD_CONNECT == $this->request->getMethod() && 200 <= $response->getStatus() && 300 > $response->getStatus())
  348. {
  349. return true;
  350. }
  351. $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) || null !== $response->getHeader('content-length');
  352. $persistent = 'keep-alive' == strtolower($response->getHeader('connection')) || (null === $response->getHeader('connection') && '1.1' == $response->getVersion());
  353. return $requestKeepAlive && $lengthKnown && $persistent;
  354. }
  355. /**
  356. * Disconnects from the remote server
  357. */
  358. protected function disconnect()
  359. {
  360. if (is_resource($this->socket))
  361. {
  362. fclose($this->socket);
  363. $this->socket = null;
  364. $this->request->setLastEvent('disconnect');
  365. }
  366. }
  367. /**
  368. * Handles HTTP redirection
  369. *
  370. * This method will throw an Exception if redirect to a non-HTTP(S) location
  371. * is attempted, also if number of redirects performed already is equal to
  372. * 'max_redirects' configuration parameter.
  373. *
  374. * @param HTTP_Request2 Original request
  375. * @param HTTP_Request2_Response Response containing redirect
  376. * @return HTTP_Request2_Response Response from a new location
  377. * @throws HTTP_Request2_Exception
  378. */
  379. protected function handleRedirect(HTTP_Request2 $request, HTTP_Request2_Response $response)
  380. {
  381. if (is_null($this->redirectCountdown))
  382. {
  383. $this->redirectCountdown = $request->getConfig('max_redirects');
  384. }
  385. if (0 == $this->redirectCountdown)
  386. {
  387. // Copying cURL behaviour
  388. throw new HTTP_Request2_Exception('Maximum (' . $request->getConfig('max_redirects') . ') redirects followed');
  389. }
  390. $redirectUrl = new Net_URL2($response->getHeader('location'), array(
  391. Net_URL2 :: OPTION_USE_BRACKETS => $request->getConfig('use_brackets')));
  392. // refuse non-HTTP redirect
  393. if ($redirectUrl->isAbsolute() && ! in_array($redirectUrl->getScheme(), array('http', 'https')))
  394. {
  395. throw new HTTP_Request2_Exception('Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString());
  396. }
  397. // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),
  398. // but in practice it is often not
  399. if (! $redirectUrl->isAbsolute())
  400. {
  401. $redirectUrl = $request->getUrl()->resolve($redirectUrl);
  402. }
  403. $redirect = clone $request;
  404. $redirect->setUrl($redirectUrl);
  405. if (303 == $response->getStatus() || (! $request->getConfig('strict_redirects') && in_array($response->getStatus(), array(
  406. 301, 302))))
  407. {
  408. $redirect->setMethod(HTTP_Request2 :: METHOD_GET);
  409. $redirect->setBody('');
  410. }
  411. if (0 < $this->redirectCountdown)
  412. {
  413. $this->redirectCountdown --;
  414. }
  415. return $this->sendRequest($redirect);
  416. }
  417. /**
  418. * Checks whether another request should be performed with server digest auth
  419. *
  420. * Several conditions should be satisfied for it to return true:
  421. * - response status should be 401
  422. * - auth credentials should be set in the request object
  423. * - response should contain WWW-Authenticate header with digest challenge
  424. * - there is either no challenge stored for this URL or new challenge
  425. * contains stale=true parameter (in other case we probably just failed
  426. * due to invalid username / password)
  427. *
  428. * The method stores challenge values in $challenges static property
  429. *
  430. * @param HTTP_Request2_Response response to check
  431. * @return boolean whether another request should be performed
  432. * @throws HTTP_Request2_Exception in case of unsupported challenge parameters
  433. */
  434. protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response)
  435. {
  436. // no sense repeating a request if we don't have credentials
  437. if (401 != $response->getStatus() || ! $this->request->getAuth())
  438. {
  439. return false;
  440. }
  441. if (! $challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate')))
  442. {
  443. return false;
  444. }
  445. $url = $this->request->getUrl();
  446. $scheme = $url->getScheme();
  447. $host = $scheme . '://' . $url->getHost();
  448. if ($port = $url->getPort())
  449. {
  450. if ((0 == strcasecmp($scheme, 'http') && 80 != $port) || (0 == strcasecmp($scheme, 'https') && 443 != $port))
  451. {
  452. $host .= ':' . $port;
  453. }
  454. }
  455. if (! empty($challenge['domain']))
  456. {
  457. $prefixes = array();
  458. foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix)
  459. {
  460. // don't bother with different servers
  461. if ('/' == substr($prefix, 0, 1))
  462. {
  463. $prefixes[] = $host . $prefix;
  464. }
  465. }
  466. }
  467. if (empty($prefixes))
  468. {
  469. $prefixes = array($host . '/');
  470. }
  471. $ret = true;
  472. foreach ($prefixes as $prefix)
  473. {
  474. if (! empty(self :: $challenges[$prefix]) && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])))
  475. {
  476. // probably credentials are invalid
  477. $ret = false;
  478. }
  479. self :: $challenges[$prefix] = & $challenge;
  480. }
  481. return $ret;
  482. }
  483. /**
  484. * Checks whether another request should be performed with proxy digest auth
  485. *
  486. * Several conditions should be satisfied for it to return true:
  487. * - response status should be 407
  488. * - proxy auth credentials should be set in the request object
  489. * - response should contain Proxy-Authenticate header with digest challenge
  490. * - there is either no challenge stored for this proxy or new challenge
  491. * contains stale=true parameter (in other case we probably just failed
  492. * due to invalid username / password)
  493. *
  494. * The method stores challenge values in $challenges static property
  495. *
  496. * @param HTTP_Request2_Response response to check
  497. * @return boolean whether another request should be performed
  498. * @throws HTTP_Request2_Exception in case of unsupported challenge parameters
  499. */
  500. protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response)
  501. {
  502. if (407 != $response->getStatus() || ! $this->request->getConfig('proxy_user'))
  503. {
  504. return false;
  505. }
  506. if (! ($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate'))))
  507. {
  508. return false;
  509. }
  510. $key = 'proxy://' . $this->request->getConfig('proxy_host') . ':' . $this->request->getConfig('proxy_port');
  511. if (! empty(self :: $challenges[$key]) && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])))
  512. {
  513. $ret = false;
  514. }
  515. else
  516. {
  517. $ret = true;
  518. }
  519. self :: $challenges[$key] = $challenge;
  520. return $ret;
  521. }
  522. /**
  523. * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value
  524. *
  525. * There is a problem with implementation of RFC 2617: several of the parameters
  526. * are defined as quoted-string there and thus may contain backslash escaped
  527. * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as
  528. * just value of quoted-string X without surrounding quotes, it doesn't speak
  529. * about removing backslash escaping.
  530. *
  531. * Now realm parameter is user-defined and human-readable, strange things
  532. * happen when it contains quotes:
  533. * - Apache allows quotes in realm, but apparently uses realm value without
  534. * backslashes for digest computation
  535. * - Squid allows (manually escaped) quotes there, but it is impossible to
  536. * authorize with either escaped or unescaped quotes used in digest,
  537. * probably it can't parse the response (?)
  538. * - Both IE and Firefox display realm value with backslashes in
  539. * the password popup and apparently use the same value for digest
  540. *
  541. * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in
  542. * quoted-string handling, unfortunately that means failure to authorize
  543. * sometimes
  544. *
  545. * @param string value of WWW-Authenticate or Proxy-Authenticate header
  546. * @return mixed associative array with challenge parameters, false if
  547. * no challenge is present in header value
  548. * @throws HTTP_Request2_Exception in case of unsupported challenge parameters
  549. */
  550. protected function parseDigestChallenge($headerValue)
  551. {
  552. $authParam = '(' . self :: REGEXP_TOKEN . ')\\s*=\\s*(' . self :: REGEXP_TOKEN . '|' . self :: REGEXP_QUOTED_STRING . ')';
  553. $challenge = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!";
  554. if (! preg_match($challenge, $headerValue, $matches))
  555. {
  556. return false;
  557. }
  558. preg_match_all('!' . $authParam . '!', $matches[0], $params);
  559. $paramsAry = array();
  560. $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale', 'algorithm', 'qop');
  561. for($i = 0; $i < count($params[0]); $i ++)
  562. {
  563. // section 3.2.1: Any unrecognized directive MUST be ignored.
  564. if (in_array($params[1][$i], $knownParams))
  565. {
  566. if ('"' == substr($params[2][$i], 0, 1))
  567. {
  568. $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, - 1);
  569. }
  570. else
  571. {
  572. $paramsAry[$params[1][$i]] = $params[2][$i];
  573. }
  574. }
  575. }
  576. // we only support qop=auth
  577. if (! empty($paramsAry['qop']) && ! in_array('auth', array_map('trim', explode(',', $paramsAry['qop']))))
  578. {
  579. throw new HTTP_Request2_Exception("Only 'auth' qop is currently supported in digest authentication, " . "server requested '{$paramsAry['qop']}'");
  580. }
  581. // we only support algorithm=MD5
  582. if (! empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm'])
  583. {
  584. throw new HTTP_Request2_Exception("Only 'MD5' algorithm is currently supported in digest authentication, " . "server requested '{$paramsAry['algorithm']}'");
  585. }
  586. return $paramsAry;
  587. }
  588. /**
  589. * Parses [Proxy-]Authentication-Info header value and updates challenge
  590. *
  591. * @param array challenge to update
  592. * @param string value of [Proxy-]Authentication-Info header
  593. * @todo validate server rspauth response
  594. */
  595. protected function updateChallenge(&$challenge, $headerValue)
  596. {
  597. $authParam = '!(' . self :: REGEXP_TOKEN . ')\\s*=\\s*(' . self :: REGEXP_TOKEN . '|' . self :: REGEXP_QUOTED_STRING . ')!';
  598. $paramsAry = array();
  599. preg_match_all($authParam, $headerValue, $params);
  600. for($i = 0; $i < count($params[0]); $i ++)
  601. {
  602. if ('"' == substr($params[2][$i], 0, 1))
  603. {
  604. $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, - 1);
  605. }
  606. else
  607. {
  608. $paramsAry[$params[1][$i]] = $params[2][$i];
  609. }
  610. }
  611. // for now, just update the nonce value
  612. if (! empty($paramsAry['nextnonce']))
  613. {
  614. $challenge['nonce'] = $paramsAry['nextnonce'];
  615. $challenge['nc'] = 1;
  616. }
  617. }
  618. /**
  619. * Creates a value for [Proxy-]Authorization header when using digest authentication
  620. *
  621. * @param string user name
  622. * @param string password
  623. * @param string request URL
  624. * @param array digest challenge parameters
  625. * @return string value of [Proxy-]Authorization request header
  626. * @link http://tools.ietf.org/html/rfc2617#section-3.2.2
  627. */
  628. protected function createDigestResponse($user, $password, $url, &$challenge)
  629. {
  630. if (false !== ($q = strpos($url, '?')) && $this->request->getConfig('digest_compat_ie'))
  631. {
  632. $url = substr($url, 0, $q);
  633. }
  634. $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password);
  635. $a2 = md5($this->request->getMethod() . ':' . $url);
  636. if (empty($challenge['qop']))
  637. {
  638. $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2);
  639. }
  640. else
  641. {
  642. $challenge['cnonce'] = 'Req2.' . rand();
  643. if (empty($challenge['nc']))
  644. {
  645. $challenge['nc'] = 1;
  646. }
  647. $nc = sprintf('%08x', $challenge['nc'] ++);
  648. $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' . $challenge['cnonce'] . ':auth:' . $a2);
  649. }
  650. return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' . 'realm="' . $challenge['realm'] . '", ' . 'nonce="' . $challenge['nonce'] . '", ' . 'uri="' . $url . '", ' . 'response="' . $digest . '"' . (! empty($challenge['opaque']) ? ', opaque="' . $challenge['opaque'] . '"' : '') . (! empty($challenge['qop']) ? ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"' : '');
  651. }
  652. /**
  653. * Adds 'Authorization' header (if needed) to request headers array
  654. *
  655. * @param array request headers
  656. * @param string request host (needed for digest authentication)
  657. * @param string request URL (needed for digest authentication)
  658. * @throws HTTP_Request2_Exception
  659. */
  660. protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)
  661. {
  662. if (! ($auth = $this->request->getAuth()))
  663. {
  664. return;
  665. }
  666. switch ($auth['scheme'])
  667. {
  668. case HTTP_Request2 :: AUTH_BASIC :
  669. $headers['authorization'] = 'Basic ' . base64_encode($auth['user'] . ':' . $auth['password']);
  670. break;
  671. case HTTP_Request2 :: AUTH_DIGEST :
  672. unset($this->serverChallenge);
  673. $fullUrl = ('/' == $requestUrl[0]) ? $this->request->getUrl()->getScheme() . '://' . $requestHost . $requestUrl : $requestUrl;
  674. foreach (array_keys(self :: $challenges) as $key)
  675. {
  676. if ($key == substr($fullUrl, 0, strlen($key)))
  677. {
  678. $headers['authorization'] = $this->createDigestResponse($auth['user'], $auth['password'], $requestUrl, self :: $challenges[$key]);
  679. $this->serverChallenge = & self :: $challenges[$key];
  680. break;
  681. }
  682. }
  683. break;
  684. default :
  685. throw new HTTP_Request2_Exception("Unknown HTTP authentication scheme '{$auth['scheme']}'");
  686. }
  687. }
  688. /**
  689. * Adds 'Proxy-Authorization' header (if needed) to request headers array
  690. *
  691. * @param array request headers
  692. * @param string request URL (needed for digest authentication)
  693. * @throws HTTP_Request2_Exception
  694. */
  695. protected function addProxyAuthorizationHeader(&$headers, $requestUrl)
  696. {
  697. if (! $this->request->getConfig('proxy_host') || ! ($user = $this->request->getConfig('proxy_user')) || (0 == strcasecmp('https', $this->request->getUrl()->getScheme()) && HTTP_Request2 :: METHOD_CONNECT != $this->request->getMethod()))
  698. {
  699. return;
  700. }
  701. $password = $this->request->getConfig('proxy_password');
  702. switch ($this->request->getConfig('proxy_auth_scheme'))
  703. {
  704. case HTTP_Request2 :: AUTH_BASIC :
  705. $headers['proxy-authorization'] = 'Basic ' . base64_encode($user . ':' . $password);
  706. break;
  707. case HTTP_Request2 :: AUTH_DIGEST :
  708. unset($this->proxyChallenge);
  709. $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') . ':' . $this->request->getConfig('proxy_port');
  710. if (! empty(self :: $challenges[$proxyUrl]))
  711. {
  712. $headers['proxy-authorization'] = $this->createDigestResponse($user, $password, $requestUrl, self :: $challenges[$proxyUrl]);
  713. $this->proxyChallenge = & self :: $challenges[$proxyUrl];
  714. }
  715. break;
  716. default :
  717. throw new HTTP_Request2_Exception("Unknown HTTP authentication scheme '" . $this->request->getConfig('proxy_auth_scheme') . "'");
  718. }
  719. }
  720. /**
  721. * Creates the string with the Request-Line and request headers
  722. *
  723. * @return string
  724. * @throws HTTP_Request2_Exception
  725. */
  726. protected function prepareHeaders()
  727. {
  728. $headers = $this->request->getHeaders();
  729. $url = $this->request->getUrl();
  730. $connect = HTTP_Request2 :: METHOD_CONNECT == $this->request->getMethod();
  731. $host = $url->getHost();
  732. $defaultPort = 0 == strcasecmp($url->getScheme(), 'https') ? 443 : 80;
  733. if (($port = $url->getPort()) && $port != $defaultPort || $connect)
  734. {
  735. $host .= ':' . (empty($port) ? $defaultPort : $port);
  736. }
  737. // Do not overwrite explicitly set 'Host' header, see bug #16146
  738. if (! isset($headers['host']))
  739. {
  740. $headers['host'] = $host;
  741. }
  742. if ($connect)
  743. {
  744. $requestUrl = $host;
  745. }
  746. else
  747. {
  748. if (! $this->request->getConfig('proxy_host') || 0 == strcasecmp($url->getScheme(), 'https'))
  749. {
  750. $requestUrl = '';
  751. }
  752. else
  753. {
  754. $requestUrl = $url->getScheme() . '://' . $host;
  755. }
  756. $path = $url->getPath();
  757. $query = $url->getQuery();
  758. $requestUrl .= (empty($path) ? '/' : $path) . (empty($query) ? '' : '?' . $query);
  759. }
  760. if ('1.1' == $this->request->getConfig('protocol_version') && extension_loaded('zlib') && ! isset($headers['accept-encoding']))
  761. {
  762. $headers['accept-encoding'] = 'gzip, deflate';
  763. }
  764. $this->addAuthorizationHeader($headers, $host, $requestUrl);
  765. $this->addProxyAuthorizationHeader($headers, $requestUrl);
  766. $this->calculateRequestLength($headers);
  767. $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' . $this->request->getConfig('protocol_version') . "\r\n";
  768. foreach ($headers as $name => $value)
  769. {
  770. $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
  771. $headersStr .= $canonicalName . ': ' . $value . "\r\n";
  772. }
  773. return $headersStr . "\r\n";
  774. }
  775. /**
  776. * Sends the request body
  777. *
  778. * @throws HTTP_Request2_Exception
  779. */
  780. protected function writeBody()
  781. {
  782. if (in_array($this->request->getMethod(), self :: $bodyDisallowed) || 0 == $this->contentLength)
  783. {
  784. return;
  785. }
  786. $position = 0;
  787. $bufferSize = $this->request->getConfig('buffer_size');
  788. while ($position < $this->contentLength)
  789. {
  790. if (is_string($this->requestBody))
  791. {
  792. $str = substr($this->requestBody, $position, $bufferSize);
  793. }
  794. elseif (is_resource($this->requestBody))
  795. {
  796. $str = fread($this->requestBody, $bufferSize);
  797. }
  798. else
  799. {
  800. $str = $this->requestBody->read($bufferSize);
  801. }
  802. if (false === @fwrite($this->socket, $str, strlen($str)))
  803. {
  804. throw new HTTP_Request2_Exception('Error writing request');
  805. }
  806. // Provide the length of written string to the observer, request #7630
  807. $this->request->setLastEvent('sentBodyPart', strlen($str));
  808. $position += strlen($str);
  809. }
  810. }
  811. /**
  812. * Reads the remote server's response
  813. *
  814. * @return HTTP_Request2_Response
  815. * @throws HTTP_Request2_Exception
  816. */
  817. protected function readResponse()
  818. {
  819. $bufferSize = $this->request->getConfig('buffer_size');
  820. do
  821. {
  822. $response = new HTTP_Request2_Response($this->readLine($bufferSize), true);
  823. do
  824. {
  825. $headerLine = $this->readLine($bufferSize);
  826. $response->parseHeaderLine($headerLine);
  827. }
  828. while ('' != $headerLine);
  829. }
  830. while (in_array($response->getStatus(), array(100, 101)));
  831. $this->request->setLastEvent('receivedHeaders', $response);
  832. // No body possible in such responses
  833. if (HTTP_Request2 :: METHOD_HEAD == $this->request->getMethod() || (HTTP_Request2 :: METHOD_CONNECT == $this->request->getMethod() && 200 <= $response->getStatus() && 300 > $response->getStatus()) || in_array($response->getStatus(), array(
  834. 204, 304)))
  835. {
  836. return $response;
  837. }
  838. $chunked = 'chunked' == $response->getHeader('transfer-encoding');
  839. $length = $response->getHeader('content-length');
  840. $hasBody = false;
  841. if ($chunked || null === $length || 0 < intval($length))
  842. {
  843. // RFC 2616, section 4.4:
  844. // 3. ... If a message is received with both a
  845. // Transfer-Encoding header field and a Content-Length header field,
  846. // the latter MUST be ignored.
  847. $toRead = ($chunked || null === $length) ? null : $length;
  848. $this->chunkLength = 0;
  849. while (! feof($this->socket) && (is_null($toRead) || 0 < $toRead))
  850. {
  851. if ($chunked)
  852. {
  853. $data = $this->readChunked($bufferSize);
  854. }
  855. elseif (is_null($toRead))
  856. {
  857. $data = $this->fread($bufferSize);
  858. }
  859. else
  860. {
  861. $data = $this->fread(min($toRead, $bufferSize));
  862. $toRead -= strlen($data);
  863. }
  864. if ('' == $data && (! $this->chunkLength || feof($this->socket)))
  865. {
  866. break;
  867. }
  868. $hasBody = true;
  869. if ($this->request->getConfig('store_body'))
  870. {
  871. $response->appendBody($data);
  872. }
  873. if (! in_array($response->getHeader('content-encoding'), array('identity', null)))
  874. {
  875. $this->request->setLastEvent('receivedEncodedBodyPart', $data);
  876. }
  877. else
  878. {
  879. $this->request->setLastEvent('receivedBodyPart', $data);
  880. }
  881. }
  882. }
  883. if ($hasBody)
  884. {
  885. $this->request->setLastEvent('receivedBody', $response);
  886. }
  887. return $response;
  888. }
  889. /**
  890. * Reads until either the end of the socket or a newline, whichever comes first
  891. *
  892. * Strips the trailing newline from the returned data, handles global
  893. * request timeout. Method idea borrowed from Net_Socket PEAR package.
  894. *
  895. * @param int buffer size to use for reading
  896. * @return Available data up to the newline (not including newline)
  897. * @throws HTTP_Request2_Exception In case of timeout
  898. */
  899. protected function readLine($bufferSize)
  900. {
  901. $line = '';
  902. while (! feof($this->socket))
  903. {
  904. if ($this->deadline)
  905. {
  906. stream_set_timeout($this->socket, max($this->deadline - time(), 1));
  907. }
  908. $line .= @fgets($this->socket, $bufferSize);
  909. $info = stream_get_meta_data($this->socket);
  910. if ($info['timed_out'] || $this->deadline && time() > $this->deadline)
  911. {
  912. $reason = $this->deadline ? 'after ' . $this->request->getConfig('timeout') . ' second(s)' : 'due to default_socket_timeout php.ini setting';
  913. throw new HTTP_Request2_Exception("Request timed out {$reason}");
  914. }
  915. if (substr($line, - 1) == "\n")
  916. {
  917. return rtrim($line, "\r\n");
  918. }
  919. }
  920. return $line;
  921. }
  922. /**
  923. * Wrapper around fread(), handles global request timeout
  924. *
  925. * @param int Reads up to this number of bytes
  926. * @return Data read from socket
  927. * @throws HTTP_Request2_Exception In case of timeout
  928. */
  929. protected function fread($length)
  930. {
  931. if ($this->deadline)
  932. {
  933. stream_set_timeout($this->socket, max($this->deadline - time(), 1));
  934. }
  935. $data = fread($this->socket, $length);
  936. $info = stream_get_meta_data($this->socket);
  937. if ($info['timed_out'] || $this->deadline && time() > $this->deadline)
  938. {
  939. $reason = $this->deadline ? 'after ' . $this->request->getConfig('timeout') . ' second(s)' : 'due to default_socket_timeout php.ini setting';
  940. throw new HTTP_Request2_Exception("Request timed out {$reason}");
  941. }
  942. return $data;
  943. }
  944. /**
  945. * Reads a part of response body encoded with chunked Transfer-Encoding
  946. *
  947. * @param int buffer size to use for reading
  948. * @return string
  949. * @throws HTTP_Request2_Exception
  950. */
  951. protected function readChunked($bufferSize)
  952. {
  953. // at start of the next chunk?
  954. if (0 == $this->chunkLength)
  955. {
  956. $line = $this->readLine($bufferSize);
  957. if (! preg_match('/^([0-9a-f]+)/i', $line, $matches))
  958. {
  959. throw new HTTP_Request2_Exception("Cannot decode chunked response, invalid chunk length '{$line}'");
  960. }
  961. else
  962. {
  963. $this->chunkLength = hexdec($matches[1]);
  964. // Chunk with zero length indicates the end
  965. if (0 == $this->chunkLength)
  966. {
  967. $this->readLine($bufferSize);
  968. return '';
  969. }
  970. }
  971. }
  972. $data = $this->fread(min($this->chunkLength, $bufferSize));
  973. $this->chunkLength -= strlen($data);
  974. if (0 == $this->chunkLength)
  975. {
  976. $this->readLine($bufferSize); // Trailing CRLF
  977. }
  978. return $data;
  979. }
  980. }
  981. ?>