Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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