3 * Adapter for HTTP_Request2 wrapping around cURL extension
\r
9 * This source file is subject to BSD 3-Clause License that is bundled
\r
10 * with this package in the file LICENSE and available at the URL
\r
11 * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
\r
14 * @package HTTP_Request2
\r
15 * @author Alexey Borzov <avb@php.net>
\r
16 * @copyright 2008-2014 Alexey Borzov <avb@php.net>
\r
17 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
\r
18 * @link http://pear.php.net/package/HTTP_Request2
\r
22 * Base class for HTTP_Request2 adapters
\r
24 require_once 'HTTP/Request2/Adapter.php';
\r
27 * Adapter for HTTP_Request2 wrapping around cURL extension
\r
30 * @package HTTP_Request2
\r
31 * @author Alexey Borzov <avb@php.net>
\r
32 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
\r
33 * @version Release: 2.2.1
\r
34 * @link http://pear.php.net/package/HTTP_Request2
\r
36 class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
\r
39 * Mapping of header names to cURL options
\r
42 protected static $headerMap = array(
\r
43 'accept-encoding' => CURLOPT_ENCODING,
\r
44 'cookie' => CURLOPT_COOKIE,
\r
45 'referer' => CURLOPT_REFERER,
\r
46 'user-agent' => CURLOPT_USERAGENT
\r
50 * Mapping of SSL context options to cURL options
\r
53 protected static $sslContextMap = array(
\r
54 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
\r
55 'ssl_cafile' => CURLOPT_CAINFO,
\r
56 'ssl_capath' => CURLOPT_CAPATH,
\r
57 'ssl_local_cert' => CURLOPT_SSLCERT,
\r
58 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD
\r
62 * Mapping of CURLE_* constants to Exception subclasses and error codes
\r
65 protected static $errorMap = array(
\r
66 CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',
\r
67 HTTP_Request2_Exception::NON_HTTP_REDIRECT),
\r
68 CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),
\r
69 CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),
\r
70 CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),
\r
71 // error returned from write callback
\r
72 CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',
\r
73 HTTP_Request2_Exception::NON_HTTP_REDIRECT),
\r
74 CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',
\r
75 HTTP_Request2_Exception::TIMEOUT),
\r
76 CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),
\r
77 CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),
\r
78 CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',
\r
79 HTTP_Request2_Exception::MISCONFIGURATION),
\r
80 CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',
\r
81 HTTP_Request2_Exception::MISCONFIGURATION),
\r
82 CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',
\r
83 HTTP_Request2_Exception::NON_HTTP_REDIRECT),
\r
84 CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',
\r
85 HTTP_Request2_Exception::TOO_MANY_REDIRECTS),
\r
86 CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),
\r
87 CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),
\r
88 CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',
\r
89 HTTP_Request2_Exception::MISCONFIGURATION),
\r
90 CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',
\r
91 HTTP_Request2_Exception::MISCONFIGURATION),
\r
92 CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),
\r
93 CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),
\r
94 CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',
\r
95 HTTP_Request2_Exception::INVALID_ARGUMENT),
\r
96 CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),
\r
97 CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),
\r
98 CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),
\r
102 * Response being received
\r
103 * @var HTTP_Request2_Response
\r
105 protected $response;
\r
108 * Whether 'sentHeaders' event was sent to observers
\r
111 protected $eventSentHeaders = false;
\r
114 * Whether 'receivedHeaders' event was sent to observers
\r
117 protected $eventReceivedHeaders = false;
\r
120 * Position within request body
\r
122 * @see callbackReadBody()
\r
124 protected $position = 0;
\r
127 * Information about last transfer, as returned by curl_getinfo()
\r
130 protected $lastInfo;
\r
133 * Creates a subclass of HTTP_Request2_Exception from curl error data
\r
135 * @param resource $ch curl handle
\r
137 * @return HTTP_Request2_Exception
\r
139 protected static function wrapCurlError($ch)
\r
141 $nativeCode = curl_errno($ch);
\r
142 $message = 'Curl error: ' . curl_error($ch);
\r
143 if (!isset(self::$errorMap[$nativeCode])) {
\r
144 return new HTTP_Request2_Exception($message, 0, $nativeCode);
\r
146 $class = self::$errorMap[$nativeCode][0];
\r
147 $code = empty(self::$errorMap[$nativeCode][1])
\r
148 ? 0 : self::$errorMap[$nativeCode][1];
\r
149 return new $class($message, $code, $nativeCode);
\r
154 * Sends request to the remote server and returns its response
\r
156 * @param HTTP_Request2 $request HTTP request message
\r
158 * @return HTTP_Request2_Response
\r
159 * @throws HTTP_Request2_Exception
\r
161 public function sendRequest(HTTP_Request2 $request)
\r
163 if (!extension_loaded('curl')) {
\r
164 throw new HTTP_Request2_LogicException(
\r
165 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION
\r
169 $this->request = $request;
\r
170 $this->response = null;
\r
171 $this->position = 0;
\r
172 $this->eventSentHeaders = false;
\r
173 $this->eventReceivedHeaders = false;
\r
176 if (false === curl_exec($ch = $this->createCurlHandle())) {
\r
177 $e = self::wrapCurlError($ch);
\r
179 } catch (Exception $e) {
\r
182 $this->lastInfo = curl_getinfo($ch);
\r
186 $response = $this->response;
\r
187 unset($this->request, $this->requestBody, $this->response);
\r
193 if ($jar = $request->getCookieJar()) {
\r
194 $jar->addCookiesFromResponse($response, $request->getUrl());
\r
197 if (0 < $this->lastInfo['size_download']) {
\r
198 $request->setLastEvent('receivedBody', $response);
\r
204 * Returns information about last transfer
\r
206 * @return array associative array as returned by curl_getinfo()
\r
208 public function getInfo()
\r
210 return $this->lastInfo;
\r
214 * Creates a new cURL handle and populates it with data from the request
\r
216 * @return resource a cURL handle, as created by curl_init()
\r
217 * @throws HTTP_Request2_LogicException
\r
218 * @throws HTTP_Request2_NotImplementedException
\r
220 protected function createCurlHandle()
\r
224 curl_setopt_array($ch, array(
\r
225 // setup write callbacks
\r
226 CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
\r
227 CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
\r
229 CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
\r
230 // connection timeout
\r
231 CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
\r
232 // save full outgoing headers, in case someone is interested
\r
233 CURLINFO_HEADER_OUT => true,
\r
235 CURLOPT_URL => $this->request->getUrl()->getUrl()
\r
238 // set up redirects
\r
239 if (!$this->request->getConfig('follow_redirects')) {
\r
240 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
\r
242 if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {
\r
243 throw new HTTP_Request2_LogicException(
\r
244 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',
\r
245 HTTP_Request2_Exception::MISCONFIGURATION
\r
248 curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
\r
249 // limit redirects to http(s), works in 5.2.10+
\r
250 if (defined('CURLOPT_REDIR_PROTOCOLS')) {
\r
251 curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
\r
253 // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571
\r
254 if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {
\r
255 curl_setopt($ch, CURLOPT_POSTREDIR, 3);
\r
259 // set local IP via CURLOPT_INTERFACE (request #19515)
\r
260 if ($ip = $this->request->getConfig('local_ip')) {
\r
261 curl_setopt($ch, CURLOPT_INTERFACE, $ip);
\r
265 if ($timeout = $this->request->getConfig('timeout')) {
\r
266 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
\r
269 // set HTTP version
\r
270 switch ($this->request->getConfig('protocol_version')) {
\r
272 curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
\r
275 curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
\r
278 // set request method
\r
279 switch ($this->request->getMethod()) {
\r
280 case HTTP_Request2::METHOD_GET:
\r
281 curl_setopt($ch, CURLOPT_HTTPGET, true);
\r
283 case HTTP_Request2::METHOD_POST:
\r
284 curl_setopt($ch, CURLOPT_POST, true);
\r
286 case HTTP_Request2::METHOD_HEAD:
\r
287 curl_setopt($ch, CURLOPT_NOBODY, true);
\r
289 case HTTP_Request2::METHOD_PUT:
\r
290 curl_setopt($ch, CURLOPT_UPLOAD, true);
\r
293 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
\r
296 // set proxy, if needed
\r
297 if ($host = $this->request->getConfig('proxy_host')) {
\r
298 if (!($port = $this->request->getConfig('proxy_port'))) {
\r
299 throw new HTTP_Request2_LogicException(
\r
300 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE
\r
303 curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
\r
304 if ($user = $this->request->getConfig('proxy_user')) {
\r
306 $ch, CURLOPT_PROXYUSERPWD,
\r
307 $user . ':' . $this->request->getConfig('proxy_password')
\r
309 switch ($this->request->getConfig('proxy_auth_scheme')) {
\r
310 case HTTP_Request2::AUTH_BASIC:
\r
311 curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
\r
313 case HTTP_Request2::AUTH_DIGEST:
\r
314 curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
\r
317 if ($type = $this->request->getConfig('proxy_type')) {
\r
320 curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
\r
323 curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
\r
326 throw new HTTP_Request2_NotImplementedException(
\r
327 "Proxy type '{$type}' is not supported"
\r
333 // set authentication data
\r
334 if ($auth = $this->request->getAuth()) {
\r
335 curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
\r
336 switch ($auth['scheme']) {
\r
337 case HTTP_Request2::AUTH_BASIC:
\r
338 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
\r
340 case HTTP_Request2::AUTH_DIGEST:
\r
341 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
\r
346 foreach ($this->request->getConfig() as $name => $value) {
\r
347 if ('ssl_verify_host' == $name && null !== $value) {
\r
348 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
\r
349 } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
\r
350 curl_setopt($ch, self::$sslContextMap[$name], $value);
\r
354 $headers = $this->request->getHeaders();
\r
355 // make cURL automagically send proper header
\r
356 if (!isset($headers['accept-encoding'])) {
\r
357 $headers['accept-encoding'] = '';
\r
360 if (($jar = $this->request->getCookieJar())
\r
361 && ($cookies = $jar->getMatching($this->request->getUrl(), true))
\r
363 $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
\r
366 // set headers having special cURL keys
\r
367 foreach (self::$headerMap as $name => $option) {
\r
368 if (isset($headers[$name])) {
\r
369 curl_setopt($ch, $option, $headers[$name]);
\r
370 unset($headers[$name]);
\r
374 $this->calculateRequestLength($headers);
\r
375 if (isset($headers['content-length']) || isset($headers['transfer-encoding'])) {
\r
376 $this->workaroundPhpBug47204($ch, $headers);
\r
379 // set headers not having special keys
\r
380 $headersFmt = array();
\r
381 foreach ($headers as $name => $value) {
\r
382 $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
\r
383 $headersFmt[] = $canonicalName . ': ' . $value;
\r
385 curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
\r
391 * Workaround for PHP bug #47204 that prevents rewinding request body
\r
393 * The workaround consists of reading the entire request body into memory
\r
394 * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
\r
395 * file uploads, use Socket adapter instead.
\r
397 * @param resource $ch cURL handle
\r
398 * @param array &$headers Request headers
\r
400 protected function workaroundPhpBug47204($ch, &$headers)
\r
402 // no redirects, no digest auth -> probably no rewind needed
\r
403 if (!$this->request->getConfig('follow_redirects')
\r
404 && (!($auth = $this->request->getAuth())
\r
405 || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
\r
407 curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
\r
410 // rewind may be needed, read the whole body into memory
\r
411 if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
\r
412 $this->requestBody = $this->requestBody->__toString();
\r
414 } elseif (is_resource($this->requestBody)) {
\r
415 $fp = $this->requestBody;
\r
416 $this->requestBody = '';
\r
417 while (!feof($fp)) {
\r
418 $this->requestBody .= fread($fp, 16384);
\r
421 // curl hangs up if content-length is present
\r
422 unset($headers['content-length']);
\r
423 curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
\r
428 * Callback function called by cURL for reading the request body
\r
430 * @param resource $ch cURL handle
\r
431 * @param resource $fd file descriptor (not used)
\r
432 * @param integer $length maximum length of data to return
\r
434 * @return string part of the request body, up to $length bytes
\r
436 protected function callbackReadBody($ch, $fd, $length)
\r
438 if (!$this->eventSentHeaders) {
\r
439 $this->request->setLastEvent(
\r
440 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
\r
442 $this->eventSentHeaders = true;
\r
444 if (in_array($this->request->getMethod(), self::$bodyDisallowed)
\r
445 || 0 == $this->contentLength || $this->position >= $this->contentLength
\r
449 if (is_string($this->requestBody)) {
\r
450 $string = substr($this->requestBody, $this->position, $length);
\r
451 } elseif (is_resource($this->requestBody)) {
\r
452 $string = fread($this->requestBody, $length);
\r
454 $string = $this->requestBody->read($length);
\r
456 $this->request->setLastEvent('sentBodyPart', strlen($string));
\r
457 $this->position += strlen($string);
\r
462 * Callback function called by cURL for saving the response headers
\r
464 * @param resource $ch cURL handle
\r
465 * @param string $string response header (with trailing CRLF)
\r
467 * @return integer number of bytes saved
\r
468 * @see HTTP_Request2_Response::parseHeaderLine()
\r
470 protected function callbackWriteHeader($ch, $string)
\r
472 // we may receive a second set of headers if doing e.g. digest auth
\r
473 if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {
\r
474 // don't bother with 100-Continue responses (bug #15785)
\r
475 if (!$this->eventSentHeaders
\r
476 || $this->response->getStatus() >= 200
\r
478 $this->request->setLastEvent(
\r
479 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
\r
482 $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
\r
483 // if body wasn't read by a callback, send event with total body size
\r
484 if ($upload > $this->position) {
\r
485 $this->request->setLastEvent(
\r
486 'sentBodyPart', $upload - $this->position
\r
488 $this->position = $upload;
\r
490 if ($upload && (!$this->eventSentHeaders
\r
491 || $this->response->getStatus() >= 200)
\r
493 $this->request->setLastEvent('sentBody', $upload);
\r
495 $this->eventSentHeaders = true;
\r
496 // we'll need a new response object
\r
497 if ($this->eventReceivedHeaders) {
\r
498 $this->eventReceivedHeaders = false;
\r
499 $this->response = null;
\r
502 if (empty($this->response)) {
\r
503 $this->response = new HTTP_Request2_Response(
\r
504 $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)
\r
507 $this->response->parseHeaderLine($string);
\r
508 if ('' == trim($string)) {
\r
509 // don't bother with 100-Continue responses (bug #15785)
\r
510 if (200 <= $this->response->getStatus()) {
\r
511 $this->request->setLastEvent('receivedHeaders', $this->response);
\r
514 if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {
\r
515 $redirectUrl = new Net_URL2($this->response->getHeader('location'));
\r
517 // for versions lower than 5.2.10, check the redirection URL protocol
\r
518 if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()
\r
519 && !in_array($redirectUrl->getScheme(), array('http', 'https'))
\r
524 if ($jar = $this->request->getCookieJar()) {
\r
525 $jar->addCookiesFromResponse($this->response, $this->request->getUrl());
\r
526 if (!$redirectUrl->isAbsolute()) {
\r
527 $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);
\r
529 if ($cookies = $jar->getMatching($redirectUrl, true)) {
\r
530 curl_setopt($ch, CURLOPT_COOKIE, $cookies);
\r
534 $this->eventReceivedHeaders = true;
\r
537 return strlen($string);
\r
541 * Callback function called by cURL for saving the response body
\r
543 * @param resource $ch cURL handle (not used)
\r
544 * @param string $string part of the response body
\r
546 * @return integer number of bytes saved
\r
547 * @throws HTTP_Request2_MessageException
\r
548 * @see HTTP_Request2_Response::appendBody()
\r
550 protected function callbackWriteBody($ch, $string)
\r
552 // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
\r
553 // response doesn't start with proper HTTP status line (see bug #15716)
\r
554 if (empty($this->response)) {
\r
555 throw new HTTP_Request2_MessageException(
\r
556 "Malformed response: {$string}",
\r
557 HTTP_Request2_Exception::MALFORMED_RESPONSE
\r
560 if ($this->request->getConfig('store_body')) {
\r
561 $this->response->appendBody($string);
\r
563 $this->request->setLastEvent('receivedBodyPart', $string);
\r
564 return strlen($string);
\r