CurlHttpClient.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpClient;
  11. use Psr\Log\LoggerAwareInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
  14. use Symfony\Component\HttpClient\Exception\TransportException;
  15. use Symfony\Component\HttpClient\Internal\CurlClientState;
  16. use Symfony\Component\HttpClient\Internal\PushedResponse;
  17. use Symfony\Component\HttpClient\Response\CurlResponse;
  18. use Symfony\Component\HttpClient\Response\ResponseStream;
  19. use Symfony\Contracts\HttpClient\HttpClientInterface;
  20. use Symfony\Contracts\HttpClient\ResponseInterface;
  21. use Symfony\Contracts\HttpClient\ResponseStreamInterface;
  22. use Symfony\Contracts\Service\ResetInterface;
  23. /**
  24. * A performant implementation of the HttpClientInterface contracts based on the curl extension.
  25. *
  26. * This provides fully concurrent HTTP requests, with transparent
  27. * HTTP/2 push when a curl version that supports it is installed.
  28. *
  29. * @author Nicolas Grekas <p@tchwork.com>
  30. */
  31. final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
  32. {
  33. use HttpClientTrait;
  34. public const OPTIONS_DEFAULTS = HttpClientInterface::OPTIONS_DEFAULTS + [
  35. 'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
  36. ];
  37. /**
  38. * @var mixed[]
  39. */
  40. private $defaultOptions = self::OPTIONS_DEFAULTS + [
  41. 'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
  42. // password as the second one; or string like username:password - enabling NTLM auth
  43. 'extra' => [
  44. 'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
  45. ],
  46. ];
  47. /**
  48. * @var mixed[]
  49. */
  50. private static $emptyDefaults = self::OPTIONS_DEFAULTS + ['auth_ntlm' => null];
  51. /**
  52. * @var \Psr\Log\LoggerInterface|null
  53. */
  54. private $logger;
  55. /**
  56. * An internal object to share state between the client and its responses.
  57. * @var \Symfony\Component\HttpClient\Internal\CurlClientState
  58. */
  59. private $multi;
  60. /**
  61. * @param array $defaultOptions Default request's options
  62. * @param int $maxHostConnections The maximum number of connections to a single host
  63. * @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
  64. *
  65. * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
  66. */
  67. public function __construct($defaultOptions = [], $maxHostConnections = 6, $maxPendingPushes = 50)
  68. {
  69. if (!\extension_loaded('curl')) {
  70. throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
  71. }
  72. $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([self::class, 'shouldBuffer']);
  73. if ($defaultOptions) {
  74. [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
  75. }
  76. $this->multi = new CurlClientState($maxHostConnections, $maxPendingPushes);
  77. }
  78. /**
  79. * @param \Psr\Log\LoggerInterface $logger
  80. */
  81. public function setLogger($logger)
  82. {
  83. $this->logger = $this->multi->logger = $logger;
  84. }
  85. /**
  86. * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
  87. * @param string $method
  88. * @param string $url
  89. * @param mixed[] $options
  90. */
  91. public function request($method, $url, $options = [])
  92. {
  93. [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
  94. $scheme = $url['scheme'];
  95. $authority = $url['authority'];
  96. $host = parse_url($authority, \PHP_URL_HOST);
  97. $port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
  98. $proxy = self::getProxyUrl($options['proxy'], $url);
  99. $url = implode('', $url);
  100. if (!isset($options['normalized_headers']['user-agent'])) {
  101. $options['headers'][] = 'User-Agent: Symfony HttpClient (Curl)';
  102. }
  103. $curlopts = [
  104. \CURLOPT_URL => $url,
  105. \CURLOPT_TCP_NODELAY => true,
  106. \CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
  107. \CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
  108. \CURLOPT_FOLLOWLOCATION => true,
  109. \CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
  110. \CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
  111. \CURLOPT_TIMEOUT => 0,
  112. \CURLOPT_PROXY => $proxy,
  113. \CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
  114. \CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
  115. \CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
  116. \CURLOPT_CAINFO => $options['cafile'],
  117. \CURLOPT_CAPATH => $options['capath'],
  118. \CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
  119. \CURLOPT_SSLCERT => $options['local_cert'],
  120. \CURLOPT_SSLKEY => $options['local_pk'],
  121. \CURLOPT_KEYPASSWD => $options['passphrase'],
  122. \CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
  123. \CURLOPT_SSLVERSION => (function () use ($options) {
  124. switch ($options['crypto_method']) {
  125. case \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT:
  126. return \CURL_SSLVERSION_TLSv1_3;
  127. case \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT:
  128. return \CURL_SSLVERSION_TLSv1_2;
  129. case \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT:
  130. return \CURL_SSLVERSION_TLSv1_1;
  131. case \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT:
  132. return \CURL_SSLVERSION_TLSv1_0;
  133. }
  134. })(),
  135. ];
  136. if (1.0 === (float) $options['http_version']) {
  137. $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
  138. } elseif (1.1 === (float) $options['http_version']) {
  139. $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
  140. } elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & CurlClientState::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
  141. $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
  142. }
  143. if (isset($options['auth_ntlm'])) {
  144. $curlopts[\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
  145. $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
  146. if (\is_array($options['auth_ntlm'])) {
  147. $count = \count($options['auth_ntlm']);
  148. if ($count <= 0 || $count > 2) {
  149. throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %d given.', $count));
  150. }
  151. $options['auth_ntlm'] = implode(':', $options['auth_ntlm']);
  152. }
  153. if (!\is_string($options['auth_ntlm'])) {
  154. throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be a string or an array, "%s" given.', get_debug_type($options['auth_ntlm'])));
  155. }
  156. $curlopts[\CURLOPT_USERPWD] = $options['auth_ntlm'];
  157. }
  158. if (!\ZEND_THREAD_SAFE) {
  159. $curlopts[\CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
  160. }
  161. if (\defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE')) {
  162. $curlopts[\CURLOPT_HEADEROPT] = \CURLHEADER_SEPARATE;
  163. }
  164. // curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map
  165. if (isset($this->multi->dnsCache->hostnames[$host])) {
  166. $options['resolve'] += [$host => $this->multi->dnsCache->hostnames[$host]];
  167. }
  168. if ($options['resolve'] || $this->multi->dnsCache->evictions) {
  169. // First reset any old DNS cache entries then add the new ones
  170. $resolve = $this->multi->dnsCache->evictions;
  171. $this->multi->dnsCache->evictions = [];
  172. if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
  173. // DNS cache removals require curl 7.42 or higher
  174. $this->multi->reset();
  175. }
  176. foreach ($options['resolve'] as $host => $ip) {
  177. $resolve[] = null === $ip ? "-$host:$port" : "$host:$port:$ip";
  178. $this->multi->dnsCache->hostnames[$host] = $ip;
  179. $this->multi->dnsCache->removals["-$host:$port"] = "-$host:$port";
  180. }
  181. $curlopts[\CURLOPT_RESOLVE] = $resolve;
  182. }
  183. if ('POST' === $method) {
  184. // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
  185. $curlopts[\CURLOPT_POST] = true;
  186. } elseif ('HEAD' === $method) {
  187. $curlopts[\CURLOPT_NOBODY] = true;
  188. } else {
  189. $curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
  190. }
  191. if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
  192. $curlopts[\CURLOPT_NOSIGNAL] = true;
  193. }
  194. if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
  195. $options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
  196. }
  197. $body = $options['body'];
  198. foreach ($options['headers'] as $i => $header) {
  199. if (\is_string($body) && '' !== $body && 0 === stripos($header, 'Content-Length: ')) {
  200. // Let curl handle Content-Length headers
  201. unset($options['headers'][$i]);
  202. continue;
  203. }
  204. if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
  205. // curl requires a special syntax to send empty headers
  206. $curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
  207. } else {
  208. $curlopts[\CURLOPT_HTTPHEADER][] = $header;
  209. }
  210. }
  211. // Prevent curl from sending its default Accept and Expect headers
  212. foreach (['accept', 'expect'] as $header) {
  213. if (!isset($options['normalized_headers'][$header][0])) {
  214. $curlopts[\CURLOPT_HTTPHEADER][] = $header.':';
  215. }
  216. }
  217. if (!\is_string($body)) {
  218. if (\is_resource($body)) {
  219. $curlopts[\CURLOPT_INFILE] = $body;
  220. } else {
  221. $curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
  222. static $eof = false;
  223. static $buffer = '';
  224. return self::readRequestBody($length, $body, $buffer, $eof);
  225. };
  226. }
  227. if (isset($options['normalized_headers']['content-length'][0])) {
  228. $curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
  229. }
  230. if (!isset($options['normalized_headers']['transfer-encoding'])) {
  231. $curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' : ' chunked');
  232. }
  233. if ('POST' !== $method) {
  234. $curlopts[\CURLOPT_UPLOAD] = true;
  235. if (!isset($options['normalized_headers']['content-type']) && 0 !== ($curlopts[\CURLOPT_INFILESIZE] ?? null)) {
  236. $curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
  237. }
  238. }
  239. } elseif ('' !== $body || 'POST' === $method) {
  240. $curlopts[\CURLOPT_POSTFIELDS] = $body;
  241. }
  242. if ($options['peer_fingerprint']) {
  243. if (!isset($options['peer_fingerprint']['pin-sha256'])) {
  244. throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
  245. }
  246. $curlopts[\CURLOPT_PINNEDPUBLICKEY] = 'sha256//'.implode(';sha256//', $options['peer_fingerprint']['pin-sha256']);
  247. }
  248. if ($options['bindto']) {
  249. if (file_exists($options['bindto'])) {
  250. $curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
  251. } elseif (strncmp($options['bindto'], 'if!', strlen('if!')) !== 0 && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
  252. $curlopts[\CURLOPT_INTERFACE] = $matches[1];
  253. $curlopts[\CURLOPT_LOCALPORT] = $matches[2];
  254. } else {
  255. $curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
  256. }
  257. }
  258. if (0 < $options['max_duration']) {
  259. $curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
  260. }
  261. if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
  262. $this->validateExtraCurlOptions($options['extra']['curl']);
  263. $curlopts += $options['extra']['curl'];
  264. }
  265. if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
  266. unset($this->multi->pushedResponses[$url]);
  267. if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
  268. ($logger = $this->logger) ? $logger->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url)) : null;
  269. // Reinitialize the pushed response with request's options
  270. $ch = $pushedResponse->handle;
  271. $pushedResponse = $pushedResponse->response;
  272. $pushedResponse->__construct($this->multi, $url, $options, $this->logger);
  273. } else {
  274. ($logger = $this->logger) ? $logger->debug(sprintf('Rejecting pushed response: "%s"', $url)) : null;
  275. $pushedResponse = null;
  276. }
  277. }
  278. if (!$pushedResponse) {
  279. $ch = curl_init();
  280. ($logger = $this->logger) ? $logger->info(sprintf('Request: "%s %s"', $method, $url)) : null;
  281. $curlopts += [\CURLOPT_SHARE => $this->multi->share];
  282. }
  283. foreach ($curlopts as $opt => $value) {
  284. if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
  285. $constantName = $this->findConstantName($opt);
  286. throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
  287. }
  288. }
  289. return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host, $port), CurlClientState::$curlVersion['version_number'], $url);
  290. }
  291. /**
  292. * @param \Symfony\Contracts\HttpClient\ResponseInterface|mixed[] $responses
  293. * @param float|null $timeout
  294. */
  295. public function stream($responses, $timeout = null)
  296. {
  297. if ($responses instanceof CurlResponse) {
  298. $responses = [$responses];
  299. }
  300. if (is_resource($this->multi->handle) || $this->multi->handle instanceof \CurlMultiHandle) {
  301. $active = 0;
  302. while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)) {
  303. }
  304. }
  305. return new ResponseStream(CurlResponse::stream($responses, $timeout));
  306. }
  307. public function reset()
  308. {
  309. $this->multi->reset();
  310. }
  311. /**
  312. * Accepts pushed responses only if their headers related to authentication match the request.
  313. * @param string $method
  314. * @param mixed[] $options
  315. * @param \Symfony\Component\HttpClient\Internal\PushedResponse $pushedResponse
  316. */
  317. private static function acceptPushForRequest($method, $options, $pushedResponse)
  318. {
  319. if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
  320. return false;
  321. }
  322. foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) {
  323. if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
  324. return false;
  325. }
  326. }
  327. foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
  328. $normalizedHeaders = $options['normalized_headers'][$k] ?? [];
  329. foreach ($normalizedHeaders as $i => $v) {
  330. $normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
  331. }
  332. if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
  333. return false;
  334. }
  335. }
  336. return true;
  337. }
  338. /**
  339. * Wraps the request's body callback to allow it to return strings longer than curl requested.
  340. * @param int $length
  341. * @param \Closure $body
  342. * @param string $buffer
  343. * @param bool $eof
  344. */
  345. private static function readRequestBody($length, $body, &$buffer, &$eof)
  346. {
  347. if (!$eof && \strlen($buffer) < $length) {
  348. if (!\is_string($data = $body($length))) {
  349. throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.', get_debug_type($data)));
  350. }
  351. $buffer .= $data;
  352. $eof = '' === $data;
  353. }
  354. $data = substr($buffer, 0, $length);
  355. $buffer = substr($buffer, $length);
  356. return $data;
  357. }
  358. /**
  359. * Resolves relative URLs on redirects and deals with authentication headers.
  360. *
  361. * Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
  362. * @param mixed[] $options
  363. * @param string $host
  364. * @param int $port
  365. */
  366. private static function createRedirectResolver($options, $host, $port)
  367. {
  368. $redirectHeaders = [];
  369. if (0 < $options['max_redirects']) {
  370. $redirectHeaders['host'] = $host;
  371. $redirectHeaders['port'] = $port;
  372. $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
  373. return 0 !== stripos($h, 'Host:');
  374. });
  375. if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
  376. $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
  377. return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
  378. });
  379. }
  380. }
  381. return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
  382. try {
  383. $location = self::parseUrl($location);
  384. } catch (InvalidArgumentException $exception) {
  385. return null;
  386. }
  387. if ($noContent && $redirectHeaders) {
  388. $filterContentHeaders = static function ($h) {
  389. return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
  390. };
  391. $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
  392. $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
  393. }
  394. if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
  395. $port = parse_url('http:'.$location['authority'], \PHP_URL_PORT) ?: ('http:' === $location['scheme'] ? 80 : 443);
  396. $requestHeaders = $redirectHeaders['host'] === $host && $redirectHeaders['port'] === $port ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
  397. curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
  398. } elseif ($noContent && $redirectHeaders) {
  399. curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);
  400. }
  401. $url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
  402. $url = self::resolveUrl($location, $url);
  403. curl_setopt($ch, \CURLOPT_PROXY, self::getProxyUrl($options['proxy'], $url));
  404. return implode('', $url);
  405. };
  406. }
  407. /**
  408. * @param int $opt
  409. */
  410. private function findConstantName($opt)
  411. {
  412. $constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
  413. return $v === $opt && 'C' === $k[0] && (strncmp($k, 'CURLOPT_', strlen('CURLOPT_')) === 0 || strncmp($k, 'CURLINFO_', strlen('CURLINFO_')) === 0);
  414. }, \ARRAY_FILTER_USE_BOTH);
  415. return key($constants);
  416. }
  417. /**
  418. * Prevents overriding options that are set internally throughout the request.
  419. * @param mixed[] $options
  420. */
  421. private function validateExtraCurlOptions($options)
  422. {
  423. $curloptsToConfig = [
  424. // options used in CurlHttpClient
  425. \CURLOPT_HTTPAUTH => 'auth_ntlm',
  426. \CURLOPT_USERPWD => 'auth_ntlm',
  427. \CURLOPT_RESOLVE => 'resolve',
  428. \CURLOPT_NOSIGNAL => 'timeout',
  429. \CURLOPT_HTTPHEADER => 'headers',
  430. \CURLOPT_INFILE => 'body',
  431. \CURLOPT_READFUNCTION => 'body',
  432. \CURLOPT_INFILESIZE => 'body',
  433. \CURLOPT_POSTFIELDS => 'body',
  434. \CURLOPT_UPLOAD => 'body',
  435. \CURLOPT_INTERFACE => 'bindto',
  436. \CURLOPT_TIMEOUT_MS => 'max_duration',
  437. \CURLOPT_TIMEOUT => 'max_duration',
  438. \CURLOPT_MAXREDIRS => 'max_redirects',
  439. \CURLOPT_POSTREDIR => 'max_redirects',
  440. \CURLOPT_PROXY => 'proxy',
  441. \CURLOPT_NOPROXY => 'no_proxy',
  442. \CURLOPT_SSL_VERIFYPEER => 'verify_peer',
  443. \CURLOPT_SSL_VERIFYHOST => 'verify_host',
  444. \CURLOPT_CAINFO => 'cafile',
  445. \CURLOPT_CAPATH => 'capath',
  446. \CURLOPT_SSL_CIPHER_LIST => 'ciphers',
  447. \CURLOPT_SSLCERT => 'local_cert',
  448. \CURLOPT_SSLKEY => 'local_pk',
  449. \CURLOPT_KEYPASSWD => 'passphrase',
  450. \CURLOPT_CERTINFO => 'capture_peer_cert_chain',
  451. \CURLOPT_USERAGENT => 'normalized_headers',
  452. \CURLOPT_REFERER => 'headers',
  453. // options used in CurlResponse
  454. \CURLOPT_NOPROGRESS => 'on_progress',
  455. \CURLOPT_PROGRESSFUNCTION => 'on_progress',
  456. ];
  457. if (\defined('CURLOPT_UNIX_SOCKET_PATH')) {
  458. $curloptsToConfig[\CURLOPT_UNIX_SOCKET_PATH] = 'bindto';
  459. }
  460. if (\defined('CURLOPT_PINNEDPUBLICKEY')) {
  461. $curloptsToConfig[\CURLOPT_PINNEDPUBLICKEY] = 'peer_fingerprint';
  462. }
  463. $curloptsToCheck = [
  464. \CURLOPT_PRIVATE,
  465. \CURLOPT_HEADERFUNCTION,
  466. \CURLOPT_WRITEFUNCTION,
  467. \CURLOPT_VERBOSE,
  468. \CURLOPT_STDERR,
  469. \CURLOPT_RETURNTRANSFER,
  470. \CURLOPT_URL,
  471. \CURLOPT_FOLLOWLOCATION,
  472. \CURLOPT_HEADER,
  473. \CURLOPT_CONNECTTIMEOUT,
  474. \CURLOPT_CONNECTTIMEOUT_MS,
  475. \CURLOPT_HTTP_VERSION,
  476. \CURLOPT_PORT,
  477. \CURLOPT_DNS_USE_GLOBAL_CACHE,
  478. \CURLOPT_PROTOCOLS,
  479. \CURLOPT_REDIR_PROTOCOLS,
  480. \CURLOPT_COOKIEFILE,
  481. \CURLINFO_REDIRECT_COUNT,
  482. ];
  483. if (\defined('CURLOPT_HTTP09_ALLOWED')) {
  484. $curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED;
  485. }
  486. if (\defined('CURLOPT_HEADEROPT')) {
  487. $curloptsToCheck[] = \CURLOPT_HEADEROPT;
  488. }
  489. $methodOpts = [
  490. \CURLOPT_POST,
  491. \CURLOPT_PUT,
  492. \CURLOPT_CUSTOMREQUEST,
  493. \CURLOPT_HTTPGET,
  494. \CURLOPT_NOBODY,
  495. ];
  496. foreach ($options as $opt => $optValue) {
  497. if (isset($curloptsToConfig[$opt])) {
  498. $constName = $this->findConstantName($opt) ?? $opt;
  499. throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
  500. }
  501. if (\in_array($opt, $methodOpts)) {
  502. throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
  503. }
  504. if (\in_array($opt, $curloptsToCheck)) {
  505. $constName = $this->findConstantName($opt) ?? $opt;
  506. throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
  507. }
  508. }
  509. }
  510. }