_NOSIGNAL] = true; } // Always pin CURLOPT_PROXY (and CURLOPT_NOPROXY when available) so // that libcurl never falls back to reading proxy environment // variables itself. When the proxy request option makes no decision, // the environment is resolved here with libcurl's own semantics. [$proxyConf, $noProxyConf] = self::resolveProxy($easy->request, $options); self::assertResolvedProxySupported($easy->request, $proxyConf); $conf[\CURLOPT_PROXY] = $proxyConf; if (\defined('CURLOPT_NOPROXY')) { $conf[(int) \constant('CURLOPT_NOPROXY')] = $noProxyConf; } $this->applyTlsVersionRange($easy, $conf); $certType = null; if (isset($options['cert_type'])) { $certType = self::normalizeTlsFileType('cert_type', $options['cert_type']); $conf[\CURLOPT_SSLCERTTYPE] = $certType; } if (isset($options['cert'])) { $cert = $options['cert']; if (\is_array($cert)) { if (!isset($cert[0]) || !\is_string($cert[0])) { throw new \InvalidArgumentException('Invalid cert request option'); } if (isset($cert[1])) { if (!\is_string($cert[1])) { throw new \InvalidArgumentException('Invalid cert request option'); } $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; } $cert = $cert[0]; } if (!\is_string($cert)) { throw new \InvalidArgumentException('Invalid cert request option'); } if (!\file_exists($cert)) { throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); } // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html $ext = pathinfo($cert, \PATHINFO_EXTENSION); if ($certType === null && preg_match('#^(der|p12)$#iD', $ext)) { $conf[\CURLOPT_SSLCERTTYPE] = Psr7\Utils::asciiToUpper($ext); } $conf[\CURLOPT_SSLCERT] = $cert; } $sslKeyType = null; if (isset($options['ssl_key_type'])) { $sslKeyType = self::normalizeTlsFileType('ssl_key_type', $options['ssl_key_type']); $conf[\CURLOPT_SSLKEYTYPE] = $sslKeyType; } if (isset($options['ssl_key'])) { if (\is_array($options['ssl_key'])) { if (!isset($options['ssl_key'][0]) || !\is_string($options['ssl_key'][0])) { throw new \InvalidArgumentException('Invalid ssl_key request option'); } if (isset($options['ssl_key'][1])) { if (!\is_string($options['ssl_key'][1])) { throw new \InvalidArgumentException('Invalid ssl_key request option'); } $conf[\CURLOPT_SSLKEYPASSWD] = $options['ssl_key'][1]; } $sslKey = $options['ssl_key'][0]; } $sslKey = $sslKey ?? $options['ssl_key']; if (!\is_string($sslKey)) { throw new \InvalidArgumentException('Invalid ssl_key request option'); } if (self::shouldValidateSslKeyFile($sslKeyType) && !\file_exists($sslKey)) { throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); } $conf[\CURLOPT_SSLKEY] = $sslKey; } if (isset($options['progress'])) { $progress = $options['progress']; if (!\is_callable($progress)) { throw new \InvalidArgumentException('progress client option must be callable'); } $conf[\CURLOPT_NOPROGRESS] = false; $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) { $progress($downloadSize, $downloaded, $uploadSize, $uploaded); }; } if (!empty($options['debug'])) { $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']); $conf[\CURLOPT_VERBOSE] = true; } } private function applyTlsVersionRange(EasyHandle $easy, array &$conf): void { $options = $easy->options; $cryptoMethod = $options['crypto_method'] ?? null; $cryptoMethodMax = $options['crypto_method_max'] ?? null; if ($cryptoMethod === null && $cryptoMethodMax === null) { return; } $protocolVersion = $easy->request->getProtocolVersion(); $isHttp2 = '2' === $protocolVersion || '2.0' === $protocolVersion; if ($isHttp2 && $cryptoMethodMax !== null && TlsVersion::ordinal('crypto_method_max', $cryptoMethodMax) < 12) { throw new \InvalidArgumentException( 'Invalid crypto_method_max request option: HTTP/2 requires TLS 1.2 or higher' ); } if ($isHttp2 && $cryptoMethod !== null && TlsVersion::ordinal('crypto_method', $cryptoMethod) < 12) { $cryptoMethod = \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; } TlsVersion::assertRange($cryptoMethod, $cryptoMethodMax); $sslVersion = $cryptoMethod === null ? \CURL_SSLVERSION_DEFAULT : self::curlMinSslVersion($cryptoMethod); if ($cryptoMethodMax !== null) { $sslVersion |= self::curlMaxSslVersion($cryptoMethodMax); } $conf[\CURLOPT_SSLVERSION] = $sslVersion; } /** * @param mixed $value */ private static function curlMinSslVersion($value): int { if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT) { return \CURL_SSLVERSION_TLSv1_0; } if ($value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT) { return \CURL_SSLVERSION_TLSv1_1; } if ($value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT) { if (!CurlVersion::supportsTls12()) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); } return \CURL_SSLVERSION_TLSv1_2; } if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) { if (!CurlVersion::supportsTls13()) { throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); } return \CURL_SSLVERSION_TLSv1_3; } throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); } /** * @param mixed $value */ private static function curlMaxSslVersion($value): int { if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT) { return self::requireCurlMaxSslVersion('CURL_SSLVERSION_MAX_TLSv1_0'); } if ($value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT) { return self::requireCurlMaxSslVersion('CURL_SSLVERSION_MAX_TLSv1_1'); } if ($value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT) { return self::requireCurlMaxSslVersion('CURL_SSLVERSION_MAX_TLSv1_2'); } if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) { return self::requireCurlMaxSslVersion('CURL_SSLVERSION_MAX_TLSv1_3'); } throw new \InvalidArgumentException('Invalid crypto_method_max request option: unknown version provided'); } private static function requireCurlMaxSslVersion(string $constant): int { if (\defined($constant)) { /** @var int */ return \constant($constant); } throw new \InvalidArgumentException( 'Invalid crypto_method_max request option: maximum TLS version control is not supported by your version of cURL' ); } private static function validateRequestUriScheme(RequestInterface $request): void { $scheme = $request->getUri()->getScheme(); if ($scheme === '') { throw new RequestException('URI must include a scheme and host. Use an absolute URI, a network-path reference starting with //, or configure a base_uri.', $request); } if (!\in_array($scheme, ['http', 'https'], true)) { throw new RequestException(\sprintf("The scheme '%s' is not supported.", $scheme), $request); } } /** * This function ensures that a response was set on a transaction. If one * was not set, then the request is retried if possible. This error * typically means you are sending a payload, curl encountered a * "Connection died, retrying a fresh connect" error, tried to rewind the * stream, and then encountered a "necessary data rewind wasn't possible" * error, causing the request to be sent through curl_multi_info_read() * without an error status. * * @param callable(RequestInterface, array): PromiseInterface $handler */ private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface { try { // Only rewind if the body has been read from. $body = $easy->request->getBody(); if ($body->tell() > 0) { $body->rewind(); } } catch (\RuntimeException $e) { $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; return self::createRejection($easy, $ctx); } // Retry no more than 3 times before giving up. if (!isset($easy->options['_curl_retries'])) { $easy->options['_curl_retries'] = 1; } elseif ($easy->options['_curl_retries'] == 2) { $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.'; return self::createRejection($easy, $ctx); } else { ++$easy->options['_curl_retries']; } return $handler($easy->request, $easy->options); } /** * Parses validated trailer field lines into an associative array keyed by * lowercased field name, preserving first-occurrence key order and wire * value order. */ private static function headersFromTrailerLines(array $lines): array { $headers = []; foreach ($lines as $line) { [$name, $value] = \explode(':', $line, 2); $name = Psr7\Utils::asciiToLower(\trim($name, " \n\r\t\0\x0B")); $headers[$name][] = \trim($value, " \n\r\t\0\x0B"); } return $headers; } private function createHeaderFn(EasyHandle $easy): callable { if (isset($easy->options['on_headers'])) { $onHeaders = $easy->options['on_headers']; if (!\is_callable($onHeaders)) { throw new \InvalidArgumentException('on_headers must be callable'); } } else { $onHeaders = null; } $startingResponse = false; $collectingTrailers = false; $retainTrailers = isset($easy->options['on_trailers']); return static function ($ch, $h) use ( $onHeaders, $easy, &$startingResponse, &$collectingTrailers, $retainTrailers ) { $value = \trim($h, " \n\r\t\0\x0B"); if ($h === "\r\n" || $h === "\n" || $h === "\r" || $h === '') { if ($collectingTrailers) { // A blank line ends the trailer section; the response has // already been created. return \strlen($h); } $startingResponse = true; try { $easy->createResponse(); } catch (\Throwable $e) { $easy->response = null; $easy->createResponseException = $e; return -1; } if ($onHeaders !== null) { try { $onHeaders($easy->response); } catch (\Throwable $e) { // Associate the exception with the handle and trigger // a curl header write error by returning 0. $easy->onHeadersException = $e; return -1; } } } elseif ($startingResponse || $collectingTrailers) { if ($easy->response !== null && !HeaderProcessor::isStatusLineCandidate($h)) { // Trailer fields arrive through the header callback after // the body; a new header block always begins with a status // line. $collectingTrailers = true; if ($retainTrailers && HeaderProcessor::isValidHeaderFieldLine($h)) { $easy->trailers[] = $value; } } else { $collectingTrailers = false; $easy->trailers = []; $easy->headers = [$value]; } $startingResponse = false; } else { $easy->headers[] = $value; } return \strlen($h); }; } public function __destruct() { $this->discardIdleHandles(); } }