Curl.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. <?php
  2. /**
  3. * Adapter for HTTP_Request2 wrapping around cURL extension
  4. *
  5. * PHP version 5
  6. *
  7. * LICENSE
  8. *
  9. * This source file is subject to BSD 3-Clause License that is bundled
  10. * with this package in the file LICENSE and available at the URL
  11. * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
  12. *
  13. * @category HTTP
  14. * @package HTTP_Request2
  15. * @author Alexey Borzov <avb@php.net>
  16. * @copyright 2008-2014 Alexey Borzov <avb@php.net>
  17. * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
  18. * @link http://pear.php.net/package/HTTP_Request2
  19. */
  20. /**
  21. * Base class for HTTP_Request2 adapters
  22. */
  23. require_once 'HTTP/Request2/Adapter.php';
  24. /**
  25. * Adapter for HTTP_Request2 wrapping around cURL extension
  26. *
  27. * @category HTTP
  28. * @package HTTP_Request2
  29. * @author Alexey Borzov <avb@php.net>
  30. * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
  31. * @version Release: 2.2.1
  32. * @link http://pear.php.net/package/HTTP_Request2
  33. */
  34. class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
  35. {
  36. /**
  37. * Mapping of header names to cURL options
  38. * @var array
  39. */
  40. protected static $headerMap = array(
  41. 'accept-encoding' => CURLOPT_ENCODING,
  42. 'cookie' => CURLOPT_COOKIE,
  43. 'referer' => CURLOPT_REFERER,
  44. 'user-agent' => CURLOPT_USERAGENT
  45. );
  46. /**
  47. * Mapping of SSL context options to cURL options
  48. * @var array
  49. */
  50. protected static $sslContextMap = array(
  51. 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
  52. 'ssl_cafile' => CURLOPT_CAINFO,
  53. 'ssl_capath' => CURLOPT_CAPATH,
  54. 'ssl_local_cert' => CURLOPT_SSLCERT,
  55. 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD
  56. );
  57. /**
  58. * Mapping of CURLE_* constants to Exception subclasses and error codes
  59. * @var array
  60. */
  61. protected static $errorMap = array(
  62. CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',
  63. HTTP_Request2_Exception::NON_HTTP_REDIRECT),
  64. CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),
  65. CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),
  66. CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),
  67. // error returned from write callback
  68. CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',
  69. HTTP_Request2_Exception::NON_HTTP_REDIRECT),
  70. CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',
  71. HTTP_Request2_Exception::TIMEOUT),
  72. CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),
  73. CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),
  74. CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',
  75. HTTP_Request2_Exception::MISCONFIGURATION),
  76. CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',
  77. HTTP_Request2_Exception::MISCONFIGURATION),
  78. CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',
  79. HTTP_Request2_Exception::NON_HTTP_REDIRECT),
  80. CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',
  81. HTTP_Request2_Exception::TOO_MANY_REDIRECTS),
  82. CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),
  83. CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),
  84. CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',
  85. HTTP_Request2_Exception::MISCONFIGURATION),
  86. CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',
  87. HTTP_Request2_Exception::MISCONFIGURATION),
  88. CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),
  89. CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),
  90. CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',
  91. HTTP_Request2_Exception::INVALID_ARGUMENT),
  92. CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),
  93. CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),
  94. CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),
  95. );
  96. /**
  97. * Response being received
  98. * @var HTTP_Request2_Response
  99. */
  100. protected $response;
  101. /**
  102. * Whether 'sentHeaders' event was sent to observers
  103. * @var boolean
  104. */
  105. protected $eventSentHeaders = false;
  106. /**
  107. * Whether 'receivedHeaders' event was sent to observers
  108. * @var boolean
  109. */
  110. protected $eventReceivedHeaders = false;
  111. /**
  112. * Position within request body
  113. * @var integer
  114. * @see callbackReadBody()
  115. */
  116. protected $position = 0;
  117. /**
  118. * Information about last transfer, as returned by curl_getinfo()
  119. * @var array
  120. */
  121. protected $lastInfo;
  122. /**
  123. * Creates a subclass of HTTP_Request2_Exception from curl error data
  124. *
  125. * @param resource $ch curl handle
  126. *
  127. * @return HTTP_Request2_Exception
  128. */
  129. protected static function wrapCurlError($ch)
  130. {
  131. $nativeCode = curl_errno($ch);
  132. $message = 'Curl error: ' . curl_error($ch);
  133. if (!isset(self::$errorMap[$nativeCode])) {
  134. return new HTTP_Request2_Exception($message, 0, $nativeCode);
  135. } else {
  136. $class = self::$errorMap[$nativeCode][0];
  137. $code = empty(self::$errorMap[$nativeCode][1])
  138. ? 0 : self::$errorMap[$nativeCode][1];
  139. return new $class($message, $code, $nativeCode);
  140. }
  141. }
  142. /**
  143. * Sends request to the remote server and returns its response
  144. *
  145. * @param HTTP_Request2 $request HTTP request message
  146. *
  147. * @return HTTP_Request2_Response
  148. * @throws HTTP_Request2_Exception
  149. */
  150. public function sendRequest(HTTP_Request2 $request)
  151. {
  152. if (!extension_loaded('curl')) {
  153. throw new HTTP_Request2_LogicException(
  154. 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION
  155. );
  156. }
  157. $this->request = $request;
  158. $this->response = null;
  159. $this->position = 0;
  160. $this->eventSentHeaders = false;
  161. $this->eventReceivedHeaders = false;
  162. try {
  163. if (false === curl_exec($ch = $this->createCurlHandle())) {
  164. $e = self::wrapCurlError($ch);
  165. }
  166. } catch (Exception $e) {
  167. }
  168. if (isset($ch)) {
  169. $this->lastInfo = curl_getinfo($ch);
  170. curl_close($ch);
  171. }
  172. $response = $this->response;
  173. unset($this->request, $this->requestBody, $this->response);
  174. if (!empty($e)) {
  175. throw $e;
  176. }
  177. if ($jar = $request->getCookieJar()) {
  178. $jar->addCookiesFromResponse($response, $request->getUrl());
  179. }
  180. if (0 < $this->lastInfo['size_download']) {
  181. $request->setLastEvent('receivedBody', $response);
  182. }
  183. return $response;
  184. }
  185. /**
  186. * Returns information about last transfer
  187. *
  188. * @return array associative array as returned by curl_getinfo()
  189. */
  190. public function getInfo()
  191. {
  192. return $this->lastInfo;
  193. }
  194. /**
  195. * Creates a new cURL handle and populates it with data from the request
  196. *
  197. * @return resource a cURL handle, as created by curl_init()
  198. * @throws HTTP_Request2_LogicException
  199. * @throws HTTP_Request2_NotImplementedException
  200. */
  201. protected function createCurlHandle()
  202. {
  203. $ch = curl_init();
  204. curl_setopt_array($ch, array(
  205. // setup write callbacks
  206. CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
  207. CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
  208. // buffer size
  209. CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
  210. // connection timeout
  211. CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
  212. // save full outgoing headers, in case someone is interested
  213. CURLINFO_HEADER_OUT => true,
  214. // request url
  215. CURLOPT_URL => $this->request->getUrl()->getUrl()
  216. ));
  217. // set up redirects
  218. if (!$this->request->getConfig('follow_redirects')) {
  219. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  220. } else {
  221. if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {
  222. throw new HTTP_Request2_LogicException(
  223. 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',
  224. HTTP_Request2_Exception::MISCONFIGURATION
  225. );
  226. }
  227. curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
  228. // limit redirects to http(s), works in 5.2.10+
  229. if (defined('CURLOPT_REDIR_PROTOCOLS')) {
  230. curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
  231. }
  232. // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571
  233. if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {
  234. curl_setopt($ch, CURLOPT_POSTREDIR, 3);
  235. }
  236. }
  237. // set local IP via CURLOPT_INTERFACE (request #19515)
  238. if ($ip = $this->request->getConfig('local_ip')) {
  239. curl_setopt($ch, CURLOPT_INTERFACE, $ip);
  240. }
  241. // request timeout
  242. if ($timeout = $this->request->getConfig('timeout')) {
  243. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
  244. }
  245. // set HTTP version
  246. switch ($this->request->getConfig('protocol_version')) {
  247. case '1.0':
  248. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
  249. break;
  250. case '1.1':
  251. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  252. }
  253. // set request method
  254. switch ($this->request->getMethod()) {
  255. case HTTP_Request2::METHOD_GET:
  256. curl_setopt($ch, CURLOPT_HTTPGET, true);
  257. break;
  258. case HTTP_Request2::METHOD_POST:
  259. curl_setopt($ch, CURLOPT_POST, true);
  260. break;
  261. case HTTP_Request2::METHOD_HEAD:
  262. curl_setopt($ch, CURLOPT_NOBODY, true);
  263. break;
  264. case HTTP_Request2::METHOD_PUT:
  265. curl_setopt($ch, CURLOPT_UPLOAD, true);
  266. break;
  267. default:
  268. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
  269. }
  270. // set proxy, if needed
  271. if ($host = $this->request->getConfig('proxy_host')) {
  272. if (!($port = $this->request->getConfig('proxy_port'))) {
  273. throw new HTTP_Request2_LogicException(
  274. 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE
  275. );
  276. }
  277. curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
  278. if ($user = $this->request->getConfig('proxy_user')) {
  279. curl_setopt(
  280. $ch, CURLOPT_PROXYUSERPWD,
  281. $user . ':' . $this->request->getConfig('proxy_password')
  282. );
  283. switch ($this->request->getConfig('proxy_auth_scheme')) {
  284. case HTTP_Request2::AUTH_BASIC:
  285. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
  286. break;
  287. case HTTP_Request2::AUTH_DIGEST:
  288. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
  289. }
  290. }
  291. if ($type = $this->request->getConfig('proxy_type')) {
  292. switch ($type) {
  293. case 'http':
  294. curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
  295. break;
  296. case 'socks5':
  297. curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
  298. break;
  299. default:
  300. throw new HTTP_Request2_NotImplementedException(
  301. "Proxy type '{$type}' is not supported"
  302. );
  303. }
  304. }
  305. }
  306. // set authentication data
  307. if ($auth = $this->request->getAuth()) {
  308. curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
  309. switch ($auth['scheme']) {
  310. case HTTP_Request2::AUTH_BASIC:
  311. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
  312. break;
  313. case HTTP_Request2::AUTH_DIGEST:
  314. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
  315. }
  316. }
  317. // set SSL options
  318. foreach ($this->request->getConfig() as $name => $value) {
  319. if ('ssl_verify_host' == $name && null !== $value) {
  320. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
  321. } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
  322. curl_setopt($ch, self::$sslContextMap[$name], $value);
  323. }
  324. }
  325. $headers = $this->request->getHeaders();
  326. // make cURL automagically send proper header
  327. if (!isset($headers['accept-encoding'])) {
  328. $headers['accept-encoding'] = '';
  329. }
  330. if (($jar = $this->request->getCookieJar())
  331. && ($cookies = $jar->getMatching($this->request->getUrl(), true))
  332. ) {
  333. $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
  334. }
  335. // set headers having special cURL keys
  336. foreach (self::$headerMap as $name => $option) {
  337. if (isset($headers[$name])) {
  338. curl_setopt($ch, $option, $headers[$name]);
  339. unset($headers[$name]);
  340. }
  341. }
  342. $this->calculateRequestLength($headers);
  343. if (isset($headers['content-length']) || isset($headers['transfer-encoding'])) {
  344. $this->workaroundPhpBug47204($ch, $headers);
  345. }
  346. // set headers not having special keys
  347. $headersFmt = array();
  348. foreach ($headers as $name => $value) {
  349. $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
  350. $headersFmt[] = $canonicalName . ': ' . $value;
  351. }
  352. curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
  353. return $ch;
  354. }
  355. /**
  356. * Workaround for PHP bug #47204 that prevents rewinding request body
  357. *
  358. * The workaround consists of reading the entire request body into memory
  359. * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
  360. * file uploads, use Socket adapter instead.
  361. *
  362. * @param resource $ch cURL handle
  363. * @param array &$headers Request headers
  364. */
  365. protected function workaroundPhpBug47204($ch, &$headers)
  366. {
  367. // no redirects, no digest auth -> probably no rewind needed
  368. if (!$this->request->getConfig('follow_redirects')
  369. && (!($auth = $this->request->getAuth())
  370. || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
  371. ) {
  372. curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
  373. } else {
  374. // rewind may be needed, read the whole body into memory
  375. if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
  376. $this->requestBody = $this->requestBody->__toString();
  377. } elseif (is_resource($this->requestBody)) {
  378. $fp = $this->requestBody;
  379. $this->requestBody = '';
  380. while (!feof($fp)) {
  381. $this->requestBody .= fread($fp, 16384);
  382. }
  383. }
  384. // curl hangs up if content-length is present
  385. unset($headers['content-length']);
  386. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
  387. }
  388. }
  389. /**
  390. * Callback function called by cURL for reading the request body
  391. *
  392. * @param resource $ch cURL handle
  393. * @param resource $fd file descriptor (not used)
  394. * @param integer $length maximum length of data to return
  395. *
  396. * @return string part of the request body, up to $length bytes
  397. */
  398. protected function callbackReadBody($ch, $fd, $length)
  399. {
  400. if (!$this->eventSentHeaders) {
  401. $this->request->setLastEvent(
  402. 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
  403. );
  404. $this->eventSentHeaders = true;
  405. }
  406. if (in_array($this->request->getMethod(), self::$bodyDisallowed)
  407. || 0 == $this->contentLength || $this->position >= $this->contentLength
  408. ) {
  409. return '';
  410. }
  411. if (is_string($this->requestBody)) {
  412. $string = substr($this->requestBody, $this->position, $length);
  413. } elseif (is_resource($this->requestBody)) {
  414. $string = fread($this->requestBody, $length);
  415. } else {
  416. $string = $this->requestBody->read($length);
  417. }
  418. $this->request->setLastEvent('sentBodyPart', strlen($string));
  419. $this->position += strlen($string);
  420. return $string;
  421. }
  422. /**
  423. * Callback function called by cURL for saving the response headers
  424. *
  425. * @param resource $ch cURL handle
  426. * @param string $string response header (with trailing CRLF)
  427. *
  428. * @return integer number of bytes saved
  429. * @see HTTP_Request2_Response::parseHeaderLine()
  430. */
  431. protected function callbackWriteHeader($ch, $string)
  432. {
  433. // we may receive a second set of headers if doing e.g. digest auth
  434. if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {
  435. // don't bother with 100-Continue responses (bug #15785)
  436. if (!$this->eventSentHeaders
  437. || $this->response->getStatus() >= 200
  438. ) {
  439. $this->request->setLastEvent(
  440. 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
  441. );
  442. }
  443. $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
  444. // if body wasn't read by a callback, send event with total body size
  445. if ($upload > $this->position) {
  446. $this->request->setLastEvent(
  447. 'sentBodyPart', $upload - $this->position
  448. );
  449. $this->position = $upload;
  450. }
  451. if ($upload && (!$this->eventSentHeaders
  452. || $this->response->getStatus() >= 200)
  453. ) {
  454. $this->request->setLastEvent('sentBody', $upload);
  455. }
  456. $this->eventSentHeaders = true;
  457. // we'll need a new response object
  458. if ($this->eventReceivedHeaders) {
  459. $this->eventReceivedHeaders = false;
  460. $this->response = null;
  461. }
  462. }
  463. if (empty($this->response)) {
  464. $this->response = new HTTP_Request2_Response(
  465. $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)
  466. );
  467. } else {
  468. $this->response->parseHeaderLine($string);
  469. if ('' == trim($string)) {
  470. // don't bother with 100-Continue responses (bug #15785)
  471. if (200 <= $this->response->getStatus()) {
  472. $this->request->setLastEvent('receivedHeaders', $this->response);
  473. }
  474. if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {
  475. $redirectUrl = new Net_URL2($this->response->getHeader('location'));
  476. // for versions lower than 5.2.10, check the redirection URL protocol
  477. if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()
  478. && !in_array($redirectUrl->getScheme(), array('http', 'https'))
  479. ) {
  480. return -1;
  481. }
  482. if ($jar = $this->request->getCookieJar()) {
  483. $jar->addCookiesFromResponse($this->response, $this->request->getUrl());
  484. if (!$redirectUrl->isAbsolute()) {
  485. $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);
  486. }
  487. if ($cookies = $jar->getMatching($redirectUrl, true)) {
  488. curl_setopt($ch, CURLOPT_COOKIE, $cookies);
  489. }
  490. }
  491. }
  492. $this->eventReceivedHeaders = true;
  493. }
  494. }
  495. return strlen($string);
  496. }
  497. /**
  498. * Callback function called by cURL for saving the response body
  499. *
  500. * @param resource $ch cURL handle (not used)
  501. * @param string $string part of the response body
  502. *
  503. * @return integer number of bytes saved
  504. * @throws HTTP_Request2_MessageException
  505. * @see HTTP_Request2_Response::appendBody()
  506. */
  507. protected function callbackWriteBody($ch, $string)
  508. {
  509. // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
  510. // response doesn't start with proper HTTP status line (see bug #15716)
  511. if (empty($this->response)) {
  512. throw new HTTP_Request2_MessageException(
  513. "Malformed response: {$string}",
  514. HTTP_Request2_Exception::MALFORMED_RESPONSE
  515. );
  516. }
  517. if ($this->request->getConfig('store_body')) {
  518. $this->response->appendBody($string);
  519. }
  520. $this->request->setLastEvent('receivedBodyPart', $string);
  521. return strlen($string);
  522. }
  523. }
  524. ?>