| 1 |
<?php |
| 2 |
|
| 3 |
namespace YoastSEO_Vendor\GuzzleHttp\Handler; |
| 4 |
|
| 5 |
/** |
| 6 |
* @internal |
| 7 |
*/ |
| 8 |
final class CurlVersion |
| 9 |
{ |
| 10 |
private const MIN_VERSION = '7.21.2'; |
| 11 |
private const TLS_12_VERSION = '7.34.0'; |
| 12 |
private const TLS_13_VERSION = '7.52.0'; |
| 13 |
private const CONNECTION_CAP_VERSION = '7.30.0'; |
| 14 |
// CURLOPT_PIPEWAIT exists since libcurl 7.43.0, and multi handles have |
| 15 |
// multiplexed by default since 7.62.0 - but a 7.65.0-7.65.1 regression |
| 16 |
// dropped that default, which 7.65.2 restored, so 7.65.2 is the floor at |
| 17 |
// which PIPEWAIT is reliably effective. |
| 18 |
private const MULTIPLEX_VERSION = '7.65.2'; |
| 19 |
// libcurl's connection matcher refuses to hand a transfer wanting |
| 20 |
// HTTP/1.x a pooled connection that already negotiated HTTP/2 or newer |
| 21 |
// from 7.77.0: ConnectionExists() in lib/url.c gained the check between |
| 22 |
// the 7.76.0 and 7.77.0 releases. The HTTP/2 branch of the check |
| 23 |
// regressed to a debug log in 8.11.0 (curl commit 433d730) and was |
| 24 |
// restored in 8.13.0 via the negotiation mask (curl commit db72b8d), so |
| 25 |
// 8.11.0 through 8.12.1 are vulnerable again. |
| 26 |
private const HTTP_VERSION_REUSE_MATCH_VERSION = '7.77.0'; |
| 27 |
private const HTTP_VERSION_REUSE_MATCH_REGRESSION = '8.11.0'; |
| 28 |
private const HTTP_VERSION_REUSE_MATCH_RESTORED = '8.13.0'; |
| 29 |
// CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE restricts the ALPN offer to h2 only |
| 30 |
// since libcurl 8.10.0, and connection reuse matching stopped handing |
| 31 |
// lower-version connections to prior-knowledge transfers in 8.14.0; below |
| 32 |
// that, a required request could silently be sent over a reused HTTP/1.1 |
| 33 |
// connection. |
| 34 |
private const REQUIRED_MULTIPLEX_VERSION = '8.14.0'; |
| 35 |
// curl 7.52.0 introduced HTTPS proxy support, advertised by a feature bit |
| 36 |
// (a build can meet the version yet lack the feature). Earlier libcurl |
| 37 |
// mishandles an https:// proxy: before 7.50.2 it silently downgrades to a |
| 38 |
// plaintext HTTP proxy, and 7.50.2 through 7.51 reject it at connect time. |
| 39 |
private const HTTPS_PROXY_VERSION = '7.52.0'; |
| 40 |
private const HANDLER_SHARING_VERSION = '7.35.0'; |
| 41 |
private const SSL_SESSION_SHARING_VERSION = '8.6.0'; |
| 42 |
// curl 7.57.0 added share-handle connection caches through |
| 43 |
// CURL_LOCK_DATA_CONNECT; older share objects can only hold DNS, TLS |
| 44 |
// session, and cookie data, never connections. |
| 45 |
private const SHARE_CONNECTION_CACHE_VERSION = '7.57.0'; |
| 46 |
// curl 7.83.1 added proxy TLS-SRP to the connection-reuse match |
| 47 |
// (CVE-2022-27782); the proxy client certificate was matched from 7.52.0, |
| 48 |
// so proxy TLS credentials are trusted from 7.83.1 onwards. |
| 49 |
private const PROXY_TLS_CREDENTIAL_REUSE_VERSION = '7.83.1'; |
| 50 |
// curl 8.19.0 fixed proxy tunnel reuse after credential changes |
| 51 |
// (CVE-2026-3784), but related proxy credential leak flaws were only |
| 52 |
// fixed in 8.20.0, so connection reuse is trusted from 8.20.0 onwards. |
| 53 |
private const PROXY_CREDENTIAL_REUSE_VERSION = '8.20.0'; |
| 54 |
// curl 7.69.0 started comparing SOCKS proxy credentials when matching |
| 55 |
// connections for reuse (curl #4835); older libcurl matches a SOCKS proxy |
| 56 |
// by type, host, and port only. |
| 57 |
private const SOCKS_PROXY_CREDENTIAL_REUSE_VERSION = '7.69.0'; |
| 58 |
private const PROXY_HEADER_SEPARATION_VERSION = '7.37.0'; |
| 59 |
/** |
| 60 |
* @var array{version: string, features: int}|false|null |
| 61 |
*/ |
| 62 |
private static $versionInfo; |
| 63 |
private function __construct() |
| 64 |
{ |
| 65 |
} |
| 66 |
public static function supportsCurlHandler() : bool |
| 67 |
{ |
| 68 |
$version = self::getVersion(); |
| 69 |
return $version !== null && \version_compare($version, self::MIN_VERSION, '>='); |
| 70 |
} |
| 71 |
public static function supportsTls12() : bool |
| 72 |
{ |
| 73 |
$version = self::getVersion(); |
| 74 |
return self::supportsSsl() && \defined('CURL_SSLVERSION_TLSv1_2') && $version !== null && \version_compare($version, self::TLS_12_VERSION, '>='); |
| 75 |
} |
| 76 |
public static function supportsTls13() : bool |
| 77 |
{ |
| 78 |
$version = self::getVersion(); |
| 79 |
return self::supportsSsl() && \defined('CURL_SSLVERSION_TLSv1_3') && $version !== null && \version_compare($version, self::TLS_13_VERSION, '>='); |
| 80 |
} |
| 81 |
public static function supportsHttp2() : bool |
| 82 |
{ |
| 83 |
$versionInfo = self::getVersionInfo(); |
| 84 |
return self::supportsTls12() && \defined('CURL_VERSION_HTTP2') && $versionInfo !== null && 0 !== (\CURL_VERSION_HTTP2 & $versionInfo['features']); |
| 85 |
} |
| 86 |
public static function supportsMultiplex() : bool |
| 87 |
{ |
| 88 |
$version = self::getVersion(); |
| 89 |
return \defined('CURLOPT_PIPEWAIT') && $version !== null && \version_compare($version, self::MULTIPLEX_VERSION, '>='); |
| 90 |
} |
| 91 |
public static function supportsHttpVersionReuseMatching() : bool |
| 92 |
{ |
| 93 |
$version = self::getVersion(); |
| 94 |
if ($version === null || \version_compare($version, self::HTTP_VERSION_REUSE_MATCH_VERSION, '<')) { |
| 95 |
return \false; |
| 96 |
} |
| 97 |
return \version_compare($version, self::HTTP_VERSION_REUSE_MATCH_REGRESSION, '<') || \version_compare($version, self::HTTP_VERSION_REUSE_MATCH_RESTORED, '>='); |
| 98 |
} |
| 99 |
public static function supportsConnectionCaps() : bool |
| 100 |
{ |
| 101 |
$version = self::getVersion(); |
| 102 |
return \defined('CURLMOPT_MAX_HOST_CONNECTIONS') && \defined('CURLMOPT_MAX_TOTAL_CONNECTIONS') && $version !== null && \version_compare($version, self::CONNECTION_CAP_VERSION, '>='); |
| 103 |
} |
| 104 |
public static function ensureConnectionCapsSupported(string $option) : void |
| 105 |
{ |
| 106 |
if (self::supportsConnectionCaps()) { |
| 107 |
return; |
| 108 |
} |
| 109 |
throw new \InvalidArgumentException(\sprintf('The "%s" option requires PHP cURL support for CURLMOPT_MAX_HOST_CONNECTIONS and CURLMOPT_MAX_TOTAL_CONNECTIONS with libcurl %s or newer.', $option, self::CONNECTION_CAP_VERSION)); |
| 110 |
} |
| 111 |
public static function supportsRequiredMultiplex() : bool |
| 112 |
{ |
| 113 |
$version = self::getVersion(); |
| 114 |
return \defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE') && $version !== null && self::supportsHttp2() && \version_compare($version, self::REQUIRED_MULTIPLEX_VERSION, '>='); |
| 115 |
} |
| 116 |
public static function supportsHttpsProxy() : bool |
| 117 |
{ |
| 118 |
$versionInfo = self::getVersionInfo(); |
| 119 |
// CURL_VERSION_HTTPS_PROXY is not defined on every supported PHP |
| 120 |
// version; fall back to the curl.h bit value. |
| 121 |
$httpsProxyFeature = \defined('CURL_VERSION_HTTPS_PROXY') ? \CURL_VERSION_HTTPS_PROXY : 1 << 21; |
| 122 |
return $versionInfo !== null && \version_compare($versionInfo['version'], self::HTTPS_PROXY_VERSION, '>=') && 0 !== ($httpsProxyFeature & $versionInfo['features']); |
| 123 |
} |
| 124 |
public static function supportsNtlm() : bool |
| 125 |
{ |
| 126 |
$versionInfo = self::getVersionInfo(); |
| 127 |
// CURL_VERSION_NTLM is not defined on every supported PHP version; fall |
| 128 |
// back to the curl.h bit value. |
| 129 |
$ntlmFeature = \defined('CURL_VERSION_NTLM') ? \CURL_VERSION_NTLM : 1 << 4; |
| 130 |
return \defined('CURLAUTH_NTLM') && $versionInfo !== null && 0 !== ($ntlmFeature & $versionInfo['features']); |
| 131 |
} |
| 132 |
public static function supportsHandlerSharing() : bool |
| 133 |
{ |
| 134 |
$version = self::getVersion(); |
| 135 |
return $version !== null && \version_compare($version, self::HANDLER_SHARING_VERSION, '>='); |
| 136 |
} |
| 137 |
public static function ensureHandlerSharingSupported() : void |
| 138 |
{ |
| 139 |
if (!self::supportsHandlerSharing()) { |
| 140 |
throw new \InvalidArgumentException(\sprintf('The "transport_sharing" option requires libcurl %s or higher for cURL share handles.', self::HANDLER_SHARING_VERSION)); |
| 141 |
} |
| 142 |
} |
| 143 |
public static function supportsSslSessionSharing() : bool |
| 144 |
{ |
| 145 |
$version = self::getVersion(); |
| 146 |
return self::supportsSsl() && $version !== null && \version_compare($version, self::SSL_SESSION_SHARING_VERSION, '>='); |
| 147 |
} |
| 148 |
public static function ensureSslSessionSharingSupported() : void |
| 149 |
{ |
| 150 |
if (!self::supportsSslSessionSharing()) { |
| 151 |
throw new \InvalidArgumentException(\sprintf('The "transport_sharing" option requires libcurl %s or higher with SSL support for SSL session sharing.', self::SSL_SESSION_SHARING_VERSION)); |
| 152 |
} |
| 153 |
} |
| 154 |
public static function supportsShareConnectionCaches() : bool |
| 155 |
{ |
| 156 |
$version = self::getVersion(); |
| 157 |
// An undetectable libcurl version is treated as capable so the |
| 158 |
// opaque share safeguards fail closed. |
| 159 |
return $version === null || \version_compare($version, self::SHARE_CONNECTION_CACHE_VERSION, '>='); |
| 160 |
} |
| 161 |
public static function supportsProxyTlsCredentialAwareConnectionReuse() : bool |
| 162 |
{ |
| 163 |
$version = self::getVersion(); |
| 164 |
return $version !== null && \version_compare($version, self::PROXY_TLS_CREDENTIAL_REUSE_VERSION, '>='); |
| 165 |
} |
| 166 |
public static function supportsProxyCredentialAwareConnectionReuse() : bool |
| 167 |
{ |
| 168 |
$version = self::getVersion(); |
| 169 |
return $version !== null && \version_compare($version, self::PROXY_CREDENTIAL_REUSE_VERSION, '>='); |
| 170 |
} |
| 171 |
public static function supportsSocksProxyCredentialAwareConnectionReuse() : bool |
| 172 |
{ |
| 173 |
$version = self::getVersion(); |
| 174 |
return $version !== null && \version_compare($version, self::SOCKS_PROXY_CREDENTIAL_REUSE_VERSION, '>='); |
| 175 |
} |
| 176 |
public static function supportsProxyHeaderSeparation() : bool |
| 177 |
{ |
| 178 |
$version = self::getVersion(); |
| 179 |
return $version !== null && \version_compare($version, self::PROXY_HEADER_SEPARATION_VERSION, '>=') && \defined('CURLOPT_PROXYHEADER') && \defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE'); |
| 180 |
} |
| 181 |
private static function supportsSsl() : bool |
| 182 |
{ |
| 183 |
$versionInfo = self::getVersionInfo(); |
| 184 |
return \defined('CURL_VERSION_SSL') && $versionInfo !== null && 0 !== (\CURL_VERSION_SSL & $versionInfo['features']); |
| 185 |
} |
| 186 |
public static function getVersion() : ?string |
| 187 |
{ |
| 188 |
$versionInfo = self::getVersionInfo(); |
| 189 |
return $versionInfo === null ? null : $versionInfo['version']; |
| 190 |
} |
| 191 |
/** |
| 192 |
* @return array{version: string, features: int}|null |
| 193 |
*/ |
| 194 |
private static function getVersionInfo() : ?array |
| 195 |
{ |
| 196 |
if (self::$versionInfo === null) { |
| 197 |
if (!\function_exists('curl_version')) { |
| 198 |
self::$versionInfo = \false; |
| 199 |
} else { |
| 200 |
$versionInfo = \curl_version(); |
| 201 |
self::$versionInfo = \is_array($versionInfo) && isset($versionInfo['version'], $versionInfo['features']) && \is_string($versionInfo['version']) && \is_int($versionInfo['features']) ? ['version' => $versionInfo['version'], 'features' => $versionInfo['features']] : \false; |
| 202 |
} |
| 203 |
} |
| 204 |
return self::$versionInfo === \false ? null : self::$versionInfo; |
| 205 |
} |
| 206 |
} |
| 207 |
|