PluginProbe
Media Cloud Sync / 1.3.11
Media Cloud Sync v1.3.11
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / sdk / s3 / GuzzleHttp / Handler / CurlFactory.php

CurlFactory.php in Media Cloud Sync 1.3.11, at includes/sdk/s3/GuzzleHttp/Handler/CurlFactory.php

568 lines 26.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\s3\GuzzleHttp\Handler;
4
5 use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\ConnectException;
6 use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\RequestException;
7 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise as P;
8 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\FulfilledPromise;
9 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\PromiseInterface;
10 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\LazyOpenStream;
11 use Dudlewebs\WPMCS\s3\GuzzleHttp\TransferStats;
12 use Dudlewebs\WPMCS\s3\GuzzleHttp\Utils;
13 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
14 use Dudlewebs\WPMCS\s3\Psr\Http\Message\UriInterface;
15 /**
16 * Creates curl resources from a request
17 *
18 * @final
19 */
20 class CurlFactory implements CurlFactoryInterface
21 {
22 public const CURL_VERSION_STR = 'curl_version';
23 /**
24 * @deprecated
25 */
26 public const LOW_CURL_VERSION_NUMBER = '7.21.2';
27 /**
28 * @var resource[]|\CurlHandle[]
29 */
30 private $handles = [];
31 /**
32 * @var int Total number of idle handles to keep in cache
33 */
34 private $maxHandles;
35 /**
36 * @param int $maxHandles Maximum number of idle handles.
37 */
38 public function __construct(int $maxHandles)
39 {
40 $this->maxHandles = $maxHandles;
41 }
42 public function create(RequestInterface $request, array $options) : EasyHandle
43 {
44 $protocolVersion = $request->getProtocolVersion();
45 if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
46 if (!self::supportsHttp2()) {
47 throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request);
48 }
49 } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
50 throw new ConnectException(\sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request);
51 }
52 if (isset($options['curl']['body_as_string'])) {
53 $options['_body_as_string'] = $options['curl']['body_as_string'];
54 unset($options['curl']['body_as_string']);
55 }
56 $easy = new EasyHandle();
57 $easy->request = $request;
58 $easy->options = $options;
59 $conf = $this->getDefaultConf($easy);
60 $this->applyMethod($easy, $conf);
61 $this->applyHandlerOptions($easy, $conf);
62 $this->applyHeaders($easy, $conf);
63 unset($conf['_headers']);
64 // Add handler options from the request configuration options
65 if (isset($options['curl'])) {
66 $conf = \array_replace($conf, $options['curl']);
67 }
68 $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
69 $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
70 \curl_setopt_array($easy->handle, $conf);
71 return $easy;
72 }
73 private static function supportsHttp2() : bool
74 {
75 static $supportsHttp2 = null;
76 if (null === $supportsHttp2) {
77 $supportsHttp2 = self::supportsTls12() && \defined('CURL_VERSION_HTTP2') && \CURL_VERSION_HTTP2 & \curl_version()['features'];
78 }
79 return $supportsHttp2;
80 }
81 private static function supportsTls12() : bool
82 {
83 static $supportsTls12 = null;
84 if (null === $supportsTls12) {
85 $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features'];
86 }
87 return $supportsTls12;
88 }
89 private static function supportsTls13() : bool
90 {
91 static $supportsTls13 = null;
92 if (null === $supportsTls13) {
93 $supportsTls13 = \defined('CURL_SSLVERSION_TLSv1_3') && \CURL_SSLVERSION_TLSv1_3 & \curl_version()['features'];
94 }
95 return $supportsTls13;
96 }
97 public function release(EasyHandle $easy) : void
98 {
99 $resource = $easy->handle;
100 unset($easy->handle);
101 if (\count($this->handles) >= $this->maxHandles) {
102 if (\PHP_VERSION_ID < 80000) {
103 \curl_close($resource);
104 }
105 } else {
106 // Remove all callback functions as they can hold onto references
107 // and are not cleaned up by curl_reset. Using curl_setopt_array
108 // does not work for some reason, so removing each one
109 // individually.
110 \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
111 \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
112 \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
113 \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
114 \curl_reset($resource);
115 $this->handles[] = $resource;
116 }
117 }
118 /**
119 * Completes a cURL transaction, either returning a response promise or a
120 * rejected promise.
121 *
122 * @param callable(RequestInterface, array): PromiseInterface $handler
123 * @param CurlFactoryInterface $factory Dictates how the handle is released
124 */
125 public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory) : PromiseInterface
126 {
127 if (isset($easy->options['on_stats'])) {
128 self::invokeStats($easy);
129 }
130 if (!$easy->response || $easy->errno) {
131 return self::finishError($handler, $easy, $factory);
132 }
133 // Return the response if it is present and there is no error.
134 $factory->release($easy);
135 // Rewind the body of the response if possible.
136 $body = $easy->response->getBody();
137 if ($body->isSeekable()) {
138 $body->rewind();
139 }
140 return new FulfilledPromise($easy->response);
141 }
142 private static function invokeStats(EasyHandle $easy) : void
143 {
144 $curlStats = \curl_getinfo($easy->handle);
145 $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
146 $stats = new TransferStats($easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats);
147 $easy->options['on_stats']($stats);
148 }
149 /**
150 * @param callable(RequestInterface, array): PromiseInterface $handler
151 */
152 private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory) : PromiseInterface
153 {
154 // Get error information and release the handle to the factory.
155 $ctx = ['errno' => $easy->errno, 'error' => \curl_error($easy->handle), 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME)] + \curl_getinfo($easy->handle);
156 $ctx[self::CURL_VERSION_STR] = self::getCurlVersion();
157 $factory->release($easy);
158 // Retry when nothing is present or when curl failed to rewind.
159 if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
160 return self::retryFailedRewind($handler, $easy, $ctx);
161 }
162 return self::createRejection($easy, $ctx);
163 }
164 private static function getCurlVersion() : string
165 {
166 static $curlVersion = null;
167 if (null === $curlVersion) {
168 $curlVersion = \curl_version()['version'];
169 }
170 return $curlVersion;
171 }
172 private static function createRejection(EasyHandle $easy, array $ctx) : PromiseInterface
173 {
174 static $connectionErrors = [\CURLE_OPERATION_TIMEOUTED => \true, \CURLE_COULDNT_RESOLVE_HOST => \true, \CURLE_COULDNT_CONNECT => \true, \CURLE_SSL_CONNECT_ERROR => \true, \CURLE_GOT_NOTHING => \true];
175 if ($easy->createResponseException) {
176 return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $easy->request, $easy->response, $easy->createResponseException, $ctx));
177 }
178 // If an exception was encountered during the onHeaders event, then
179 // return a rejected promise that wraps that exception.
180 if ($easy->onHeadersException) {
181 return P\Create::rejectionFor(new RequestException('An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx));
182 }
183 $uri = $easy->request->getUri();
184 $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri);
185 $message = \sprintf('cURL error %s: %s (%s)', $ctx['errno'], $sanitizedError, 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html');
186 if ('' !== $sanitizedError) {
187 $redactedUriString = \Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString();
188 if ($redactedUriString !== '' && \false === \strpos($sanitizedError, $redactedUriString)) {
189 $message .= \sprintf(' for %s', $redactedUriString);
190 }
191 }
192 // Create a connection exception if it was a specific error code.
193 $error = isset($connectionErrors[$easy->errno]) ? new ConnectException($message, $easy->request, null, $ctx) : new RequestException($message, $easy->request, $easy->response, null, $ctx);
194 return P\Create::rejectionFor($error);
195 }
196 private static function sanitizeCurlError(string $error, UriInterface $uri) : string
197 {
198 if ('' === $error) {
199 return $error;
200 }
201 $baseUri = $uri->withQuery('')->withFragment('');
202 $baseUriString = $baseUri->__toString();
203 if ('' === $baseUriString) {
204 return $error;
205 }
206 $redactedUriString = \Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString();
207 return \str_replace($baseUriString, $redactedUriString, $error);
208 }
209 /**
210 * @return array<int|string, mixed>
211 */
212 private function getDefaultConf(EasyHandle $easy) : array
213 {
214 $conf = ['_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => \false, \CURLOPT_HEADER => \false, \CURLOPT_CONNECTTIMEOUT => 300];
215 if (\defined('CURLOPT_PROTOCOLS')) {
216 $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
217 }
218 $version = $easy->request->getProtocolVersion();
219 if ('2' === $version || '2.0' === $version) {
220 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
221 } elseif ('1.1' === $version) {
222 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
223 } else {
224 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
225 }
226 return $conf;
227 }
228 private function applyMethod(EasyHandle $easy, array &$conf) : void
229 {
230 $body = $easy->request->getBody();
231 $size = $body->getSize();
232 if ($size === null || $size > 0) {
233 $this->applyBody($easy->request, $easy->options, $conf);
234 return;
235 }
236 $method = $easy->request->getMethod();
237 if ($method === 'PUT' || $method === 'POST') {
238 // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
239 if (!$easy->request->hasHeader('Content-Length')) {
240 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
241 }
242 } elseif ($method === 'HEAD') {
243 $conf[\CURLOPT_NOBODY] = \true;
244 unset($conf[\CURLOPT_WRITEFUNCTION], $conf[\CURLOPT_READFUNCTION], $conf[\CURLOPT_FILE], $conf[\CURLOPT_INFILE]);
245 }
246 }
247 private function applyBody(RequestInterface $request, array $options, array &$conf) : void
248 {
249 $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null;
250 // Send the body as a string if the size is less than 1MB OR if the
251 // [curl][body_as_string] request value is set.
252 if ($size !== null && $size < 1000000 || !empty($options['_body_as_string'])) {
253 $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
254 // Don't duplicate the Content-Length header
255 $this->removeHeader('Content-Length', $conf);
256 $this->removeHeader('Transfer-Encoding', $conf);
257 } else {
258 $conf[\CURLOPT_UPLOAD] = \true;
259 if ($size !== null) {
260 $conf[\CURLOPT_INFILESIZE] = $size;
261 $this->removeHeader('Content-Length', $conf);
262 }
263 $body = $request->getBody();
264 if ($body->isSeekable()) {
265 $body->rewind();
266 }
267 $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use($body) {
268 return $body->read($length);
269 };
270 }
271 // If the Expect header is not present, prevent curl from adding it
272 if (!$request->hasHeader('Expect')) {
273 $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
274 }
275 // cURL sometimes adds a content-type by default. Prevent this.
276 if (!$request->hasHeader('Content-Type')) {
277 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
278 }
279 }
280 private function applyHeaders(EasyHandle $easy, array &$conf) : void
281 {
282 foreach ($conf['_headers'] as $name => $values) {
283 foreach ($values as $value) {
284 $value = (string) $value;
285 if ($value === '') {
286 // cURL requires a special format for empty headers.
287 // See https://github.com/guzzle/guzzle/issues/1882 for more details.
288 $conf[\CURLOPT_HTTPHEADER][] = "{$name};";
289 } else {
290 $conf[\CURLOPT_HTTPHEADER][] = "{$name}: {$value}";
291 }
292 }
293 }
294 // Remove the Accept header if one was not set
295 if (!$easy->request->hasHeader('Accept')) {
296 $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
297 }
298 }
299 /**
300 * Remove a header from the options array.
301 *
302 * @param string $name Case-insensitive header to remove
303 * @param array $options Array of options to modify
304 */
305 private function removeHeader(string $name, array &$options) : void
306 {
307 foreach (\array_keys($options['_headers']) as $key) {
308 if (!\strcasecmp($key, $name)) {
309 unset($options['_headers'][$key]);
310 return;
311 }
312 }
313 }
314 private function applyHandlerOptions(EasyHandle $easy, array &$conf) : void
315 {
316 $options = $easy->options;
317 if (isset($options['verify'])) {
318 if ($options['verify'] === \false) {
319 unset($conf[\CURLOPT_CAINFO]);
320 $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
321 $conf[\CURLOPT_SSL_VERIFYPEER] = \false;
322 } else {
323 $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
324 $conf[\CURLOPT_SSL_VERIFYPEER] = \true;
325 if (\is_string($options['verify'])) {
326 // Throw an error if the file/folder/link path is not valid or doesn't exist.
327 if (!\file_exists($options['verify'])) {
328 throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
329 }
330 // If it's a directory or a link to a directory use CURLOPT_CAPATH.
331 // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
332 if (\is_dir($options['verify']) || \is_link($options['verify']) === \true && ($verifyLink = \readlink($options['verify'])) !== \false && \is_dir($verifyLink)) {
333 $conf[\CURLOPT_CAPATH] = $options['verify'];
334 } else {
335 $conf[\CURLOPT_CAINFO] = $options['verify'];
336 }
337 }
338 }
339 }
340 if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
341 $accept = $easy->request->getHeaderLine('Accept-Encoding');
342 if ($accept) {
343 $conf[\CURLOPT_ENCODING] = $accept;
344 } else {
345 // The empty string enables all available decoders and implicitly
346 // sets a matching 'Accept-Encoding' header.
347 $conf[\CURLOPT_ENCODING] = '';
348 // But as the user did not specify any encoding preference,
349 // let's leave it up to server by preventing curl from sending
350 // the header, which will be interpreted as 'Accept-Encoding: *'.
351 // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding
352 $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
353 }
354 }
355 if (!isset($options['sink'])) {
356 // Use a default temp stream if no sink was set.
357 $options['sink'] = \Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
358 }
359 $sink = $options['sink'];
360 if (!\is_string($sink)) {
361 $sink = \Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\Utils::streamFor($sink);
362 } elseif (!\is_dir(\dirname($sink))) {
363 // Ensure that the directory exists before failing in curl.
364 throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
365 } else {
366 $sink = new LazyOpenStream($sink, 'w+');
367 }
368 $easy->sink = $sink;
369 $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use($sink) : int {
370 return $sink->write($write);
371 };
372 $timeoutRequiresNoSignal = \false;
373 if (isset($options['timeout'])) {
374 $timeoutRequiresNoSignal |= $options['timeout'] < 1;
375 $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
376 }
377 // CURL default value is CURL_IPRESOLVE_WHATEVER
378 if (isset($options['force_ip_resolve'])) {
379 if ('v4' === $options['force_ip_resolve']) {
380 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
381 } elseif ('v6' === $options['force_ip_resolve']) {
382 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
383 }
384 }
385 if (isset($options['connect_timeout'])) {
386 $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
387 $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
388 }
389 if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
390 $conf[\CURLOPT_NOSIGNAL] = \true;
391 }
392 if (isset($options['proxy'])) {
393 if (!\is_array($options['proxy'])) {
394 $conf[\CURLOPT_PROXY] = $options['proxy'];
395 } else {
396 $scheme = $easy->request->getUri()->getScheme();
397 if (isset($options['proxy'][$scheme])) {
398 $host = $easy->request->getUri()->getHost();
399 if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
400 unset($conf[\CURLOPT_PROXY]);
401 } else {
402 $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
403 }
404 }
405 }
406 }
407 if (isset($options['crypto_method'])) {
408 $protocolVersion = $easy->request->getProtocolVersion();
409 // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2
410 if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
411 if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
412 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
413 } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
414 if (!self::supportsTls13()) {
415 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
416 }
417 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
418 } else {
419 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
420 }
421 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
422 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
423 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
424 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
425 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
426 if (!self::supportsTls12()) {
427 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
428 }
429 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
430 } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
431 if (!self::supportsTls13()) {
432 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
433 }
434 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
435 } else {
436 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
437 }
438 }
439 if (isset($options['cert'])) {
440 $cert = $options['cert'];
441 if (\is_array($cert)) {
442 $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
443 $cert = $cert[0];
444 }
445 if (!\file_exists($cert)) {
446 throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
447 }
448 // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
449 // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
450 $ext = \pathinfo($cert, \PATHINFO_EXTENSION);
451 if (\preg_match('#^(der|p12)$#i', $ext)) {
452 $conf[\CURLOPT_SSLCERTTYPE] = \strtoupper($ext);
453 }
454 $conf[\CURLOPT_SSLCERT] = $cert;
455 }
456 if (isset($options['ssl_key'])) {
457 if (\is_array($options['ssl_key'])) {
458 if (\count($options['ssl_key']) === 2) {
459 [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
460 } else {
461 [$sslKey] = $options['ssl_key'];
462 }
463 }
464 $sslKey = $sslKey ?? $options['ssl_key'];
465 if (!\file_exists($sslKey)) {
466 throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
467 }
468 $conf[\CURLOPT_SSLKEY] = $sslKey;
469 }
470 if (isset($options['progress'])) {
471 $progress = $options['progress'];
472 if (!\is_callable($progress)) {
473 throw new \InvalidArgumentException('progress client option must be callable');
474 }
475 $conf[\CURLOPT_NOPROGRESS] = \false;
476 $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use($progress) {
477 $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
478 };
479 }
480 if (!empty($options['debug'])) {
481 $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
482 $conf[\CURLOPT_VERBOSE] = \true;
483 }
484 }
485 /**
486 * This function ensures that a response was set on a transaction. If one
487 * was not set, then the request is retried if possible. This error
488 * typically means you are sending a payload, curl encountered a
489 * "Connection died, retrying a fresh connect" error, tried to rewind the
490 * stream, and then encountered a "necessary data rewind wasn't possible"
491 * error, causing the request to be sent through curl_multi_info_read()
492 * without an error status.
493 *
494 * @param callable(RequestInterface, array): PromiseInterface $handler
495 */
496 private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx) : PromiseInterface
497 {
498 try {
499 // Only rewind if the body has been read from.
500 $body = $easy->request->getBody();
501 if ($body->tell() > 0) {
502 $body->rewind();
503 }
504 } catch (\RuntimeException $e) {
505 $ctx['error'] = 'The connection unexpectedly failed without ' . 'providing an error. The request would have been retried, ' . 'but attempting to rewind the request body failed. ' . 'Exception: ' . $e;
506 return self::createRejection($easy, $ctx);
507 }
508 // Retry no more than 3 times before giving up.
509 if (!isset($easy->options['_curl_retries'])) {
510 $easy->options['_curl_retries'] = 1;
511 } elseif ($easy->options['_curl_retries'] == 2) {
512 $ctx['error'] = 'The cURL request was retried 3 times ' . 'and did not succeed. The most likely reason for the failure ' . 'is that cURL was unable to rewind the body of the request ' . 'and subsequent retries resulted in the same error. Turn on ' . 'the debug option to see what went wrong. See ' . 'https://bugs.php.net/bug.php?id=47204 for more information.';
513 return self::createRejection($easy, $ctx);
514 } else {
515 ++$easy->options['_curl_retries'];
516 }
517 return $handler($easy->request, $easy->options);
518 }
519 private function createHeaderFn(EasyHandle $easy) : callable
520 {
521 if (isset($easy->options['on_headers'])) {
522 $onHeaders = $easy->options['on_headers'];
523 if (!\is_callable($onHeaders)) {
524 throw new \InvalidArgumentException('on_headers must be callable');
525 }
526 } else {
527 $onHeaders = null;
528 }
529 return static function ($ch, $h) use($onHeaders, $easy, &$startingResponse) {
530 $value = \trim($h);
531 if ($value === '') {
532 $startingResponse = \true;
533 try {
534 $easy->createResponse();
535 } catch (\Exception $e) {
536 $easy->createResponseException = $e;
537 return -1;
538 }
539 if ($onHeaders !== null) {
540 try {
541 $onHeaders($easy->response);
542 } catch (\Exception $e) {
543 // Associate the exception with the handle and trigger
544 // a curl header write error by returning 0.
545 $easy->onHeadersException = $e;
546 return -1;
547 }
548 }
549 } elseif ($startingResponse) {
550 $startingResponse = \false;
551 $easy->headers = [$value];
552 } else {
553 $easy->headers[] = $value;
554 }
555 return \strlen($h);
556 };
557 }
558 public function __destruct()
559 {
560 foreach ($this->handles as $id => $handle) {
561 if (\PHP_VERSION_ID < 80000) {
562 \curl_close($handle);
563 }
564 unset($this->handles[$id]);
565 }
566 }
567 }
568