Skip to content

Commit

Permalink
[7.9] Improve protocol version checks to provide useful feedback to u…
Browse files Browse the repository at this point in the history
…sers when trying to use an unsupported protocol version for their system (#3175)

* Improve protocol version checks to provide useful feedback to users when trying to use an unsupported protocol version for their system

* Fixes

* Update CurlFactory.php

* Update phpstan-baseline.neon

* Update phpstan-baseline.neon
  • Loading branch information
GrahamCampbell committed Mar 31, 2024
1 parent 53f5a4e commit fcac7ba
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 15 deletions.
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ parameters:
count: 1
path: src/Exception/RequestException.php

-
message: "#^Cannot access offset 'features' on array\\|false\\.$#"
count: 3
path: src/Handler/CurlFactory.php

-
message: "#^Cannot access offset 'version' on array\\|false\\.$#"
count: 1
Expand Down
94 changes: 82 additions & 12 deletions src/Handler/CurlFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ public function __construct(int $maxHandles)

public function create(RequestInterface $request, array $options): EasyHandle
{
$protocolVersion = $request->getProtocolVersion();

if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
if (!self::supportsHttp2()) {
throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request);
}
} elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
throw new ConnectException(sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request);
}

if (isset($options['curl']['body_as_string'])) {
$options['_body_as_string'] = $options['curl']['body_as_string'];
unset($options['curl']['body_as_string']);
Expand All @@ -72,6 +82,42 @@ public function create(RequestInterface $request, array $options): EasyHandle
return $easy;
}

private static function supportsHttp2(): bool
{
static $supportsHttp2 = null;

if (null === $supportsHttp2) {
$supportsHttp2 = self::supportsTls12()
&& defined('CURL_VERSION_HTTP2')
&& (\CURL_VERSION_HTTP2 & \curl_version()['features']);
}

return $supportsHttp2;
}

private static function supportsTls12(): bool
{
static $supportsTls12 = null;

if (null === $supportsTls12) {
$supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features'];
}

return $supportsTls12;
}

private static function supportsTls13(): bool
{
static $supportsTls13 = null;

if (null === $supportsTls13) {
$supportsTls13 = defined('CURL_SSLVERSION_TLSv1_3')
&& (\CURL_SSLVERSION_TLSv1_3 & \curl_version()['features']);
}

return $supportsTls13;
}

public function release(EasyHandle $easy): void
{
$resource = $easy->handle;
Expand Down Expand Up @@ -147,7 +193,7 @@ private static function finishError(callable $handler, EasyHandle $easy, CurlFac
'error' => \curl_error($easy->handle),
'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME),
] + \curl_getinfo($easy->handle);
$ctx[self::CURL_VERSION_STR] = \curl_version()['version'];
$ctx[self::CURL_VERSION_STR] = self::getCurlVersion();
$factory->release($easy);

// Retry when nothing is present or when curl failed to rewind.
Expand All @@ -158,6 +204,17 @@ private static function finishError(callable $handler, EasyHandle $easy, CurlFac
return self::createRejection($easy, $ctx);
}

private static function getCurlVersion(): string
{
static $curlVersion = null;

if (null === $curlVersion) {
$curlVersion = \curl_version()['version'];
}

return $curlVersion;
}

private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
{
static $connectionErrors = [
Expand Down Expand Up @@ -232,10 +289,11 @@ private function getDefaultConf(EasyHandle $easy): array
}

$version = $easy->request->getProtocolVersion();
if ($version == 1.1) {
$conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
} elseif ($version == 2.0) {

if ('2' === $version || '2.0' === $version) {
$conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
} elseif ('1.1' === $version) {
$conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
} else {
$conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
}
Expand Down Expand Up @@ -455,23 +513,35 @@ private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
}

if (isset($options['crypto_method'])) {
if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
if (!defined('CURL_SSLVERSION_TLSv1_0')) {
throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.0 not supported by your version of cURL');
$protocolVersion = $easy->request->getProtocolVersion();

// If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2
if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
if (
\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']
|| \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']
|| \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']
) {
$conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
} elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
if (!self::supportsTls12()) {
throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
}
$conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
} else {
throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
}
} elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
$conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
} elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
if (!defined('CURL_SSLVERSION_TLSv1_1')) {
throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.1 not supported by your version of cURL');
}
$conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
} elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
if (!defined('CURL_SSLVERSION_TLSv1_2')) {
if (!self::supportsTls12()) {
throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
}
$conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
} elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
if (!defined('CURL_SSLVERSION_TLSv1_3')) {
if (!self::supportsTls13()) {
throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
}
$conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
Expand Down
8 changes: 7 additions & 1 deletion src/Handler/StreamHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte
\usleep($options['delay'] * 1000);
}

$protocolVersion = $request->getProtocolVersion();

if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
throw new ConnectException(sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
}

$startTime = isset($options['on_stats']) ? Utils::currentTime() : null;

try {
Expand Down Expand Up @@ -273,7 +279,7 @@ private function createStream(RequestInterface $request, array $options)

// HTTP/1.1 streams using the PHP stream wrapper require a
// Connection: close header
if ($request->getProtocolVersion() == '1.1'
if ($request->getProtocolVersion() === '1.1'
&& !$request->hasHeader('Connection')
) {
$request = $request->withHeader('Connection', 'close');
Expand Down
4 changes: 2 additions & 2 deletions src/PrepareBodyMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ private function addExpectHeader(RequestInterface $request, array $options, arra

$expect = $options['expect'] ?? null;

// Return if disabled or if you're not using HTTP/1.1 or HTTP/2.0
if ($expect === false || $request->getProtocolVersion() < 1.1) {
// Return if disabled or using HTTP/1.0
if ($expect === false || $request->getProtocolVersion() === '1.0') {
return;
}

Expand Down

0 comments on commit fcac7ba

Please sign in to comment.