Proyectos de Subversion Moodle

Rev

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