Headers[$k] ?? []) !== $normalizedHeaders) { return false; } } return true; } /** * Wraps the request's body callback to allow it to return strings longer than curl requested. */ private static function readRequestBody(int $length, \Closure $body, string &$buffer, bool &$eof): string { if (!$eof && \strlen($buffer) < $length) { if (!\is_string($data = $body($length))) { throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.', get_debug_type($data))); } $buffer .= $data; $eof = '' === $data; } $data = substr($buffer, 0, $length); $buffer = substr($buffer, $length); return $data; } /** * Resolves relative URLs on redirects and deals with authentication headers. * * Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64 */ private static function createRedirectResolver(array $options, string $host): \Closure { $redirectHeaders = []; if (0 < $options['max_redirects']) { $redirectHeaders['host'] = $host; $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) { return 0 !== stripos($h, 'Host:'); }); if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) { $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) { return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'); }); } } return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) { try { $location = self::parseUrl($location); $url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)); $url = self::resolveUrl($location, $url); } catch (InvalidArgumentException $e) { return null; } if ($noContent && $redirectHeaders) { $filterContentHeaders = static function ($h) { return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:'); }; $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders); $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders); } if ($redirectHeaders && isset($location['authority'])) { $requestHeaders = parse_url($location['authority'], \PHP_URL_HOST) === $redirectHeaders['host'] ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth']; curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders); } elseif ($noContent && $redirectHeaders) { curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']); } curl_setopt($ch, \CURLOPT_PROXY, self::getProxyUrl($options['proxy'], $url)); return implode('', $url); }; } private function ensureState(): CurlClientState { if (!isset($this->multi)) { $this->multi = new CurlClientState($this->maxHostConnections, $this->maxPendingPushes); $this->multi->logger = $this->logger; } return $this->multi; } private function findConstantName(int $opt): ?string { $constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) { return $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_')); }, \ARRAY_FILTER_USE_BOTH); return key($constants); } /** * Prevents overriding options that are set internally throughout the request. */ private function validateExtraCurlOptions(array $options): void { $curloptsToConfig = [ // options used in CurlHttpClient \CURLOPT_HTTPAUTH => 'auth_ntlm', \CURLOPT_USERPWD => 'auth_ntlm', \CURLOPT_RESOLVE => 'resolve', \CURLOPT_NOSIGNAL => 'timeout', \CURLOPT_HTTPHEADER => 'headers', \CURLOPT_INFILE => 'body', \CURLOPT_READFUNCTION => 'body', \CURLOPT_INFILESIZE => 'body', \CURLOPT_POSTFIELDS => 'body', \CURLOPT_UPLOAD => 'body', \CURLOPT_INTERFACE => 'bindto', \CURLOPT_TIMEOUT_MS => 'max_duration', \CURLOPT_TIMEOUT => 'max_duration', \CURLOPT_MAXREDIRS => 'max_redirects', \CURLOPT_POSTREDIR => 'max_redirects', \CURLOPT_PROXY => 'proxy', \CURLOPT_NOPROXY => 'no_proxy', \CURLOPT_SSL_VERIFYPEER => 'verify_peer', \CURLOPT_SSL_VERIFYHOST => 'verify_host', \CURLOPT_CAINFO => 'cafile', \CURLOPT_CAPATH => 'capath', \CURLOPT_SSL_CIPHER_LIST => 'ciphers', \CURLOPT_SSLCERT => 'local_cert', \CURLOPT_SSLKEY => 'local_pk', \CURLOPT_KEYPASSWD => 'passphrase', \CURLOPT_CERTINFO => 'capture_peer_cert_chain', \CURLOPT_USERAGENT => 'normalized_headers', \CURLOPT_REFERER => 'headers', // options used in CurlResponse \CURLOPT_NOPROGRESS => 'on_progress', \CURLOPT_PROGRESSFUNCTION => 'on_progress', ]; if (\defined('CURLOPT_UNIX_SOCKET_PATH')) { $curloptsToConfig[\CURLOPT_UNIX_SOCKET_PATH] = 'bindto'; } if (\defined('CURLOPT_PINNEDPUBLICKEY')) { $curloptsToConfig[\CURLOPT_PINNEDPUBLICKEY] = 'peer_fingerprint'; } $curloptsToCheck = [ \CURLOPT_PRIVATE, \CURLOPT_HEADERFUNCTION, \CURLOPT_WRITEFUNCTION, \CURLOPT_VERBOSE, \CURLOPT_STDERR, \CURLOPT_RETURNTRANSFER, \CURLOPT_URL, \CURLOPT_FOLLOWLOCATION, \CURLOPT_HEADER, \CURLOPT_CONNECTTIMEOUT, \CURLOPT_CONNECTTIMEOUT_MS, \CURLOPT_HTTP_VERSION, \CURLOPT_PORT, \CURLOPT_DNS_USE_GLOBAL_CACHE, \CURLOPT_PROTOCOLS, \CURLOPT_REDIR_PROTOCOLS, \CURLOPT_COOKIEFILE, \CURLINFO_REDIRECT_COUNT, ]; if (\defined('CURLOPT_HTTP09_ALLOWED')) { $curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED; } if (\defined('CURLOPT_HEADEROPT')) { $curloptsToCheck[] = \CURLOPT_HEADEROPT; } $methodOpts = [ \CURLOPT_POST, \CURLOPT_PUT, \CURLOPT_CUSTOMREQUEST, \CURLOPT_HTTPGET, \CURLOPT_NOBODY, ]; foreach ($options as $opt => $optValue) { if (isset($curloptsToConfig[$opt])) { $constName = $this->findConstantName($opt) ?? $opt; throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt])); } if (\in_array($opt, $methodOpts)) { throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".'); } if (\in_array($opt, $curloptsToCheck)) { $constName = $this->findConstantName($opt) ?? $opt; throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName)); } } } }