First Commit

This commit is contained in:
Esteban
2026-07-03 16:27:34 +02:00
commit 7a7440daab
8955 changed files with 1117958 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
<?php
namespace GuzzleHttp\Handler;
use Psr\Http\Message\RequestInterface;
interface CurlFactoryInterface
{
/**
* Creates a cURL handle resource.
*
* @param RequestInterface $request Request
* @param array $options Transfer options
*
* @throws \RuntimeException when an option cannot be applied
*/
public function create(RequestInterface $request, array $options): EasyHandle;
/**
* Release an easy handle, allowing it to be reused or closed.
*
* This function must call unset on the easy handle's "handle" property.
*/
public function release(EasyHandle $easy): void;
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\TransportSharing;
use Psr\Http\Message\RequestInterface;
/**
* HTTP handler that uses cURL easy handles as a transport layer.
*
* When using the CurlHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the "client" key of the request.
*
* @final
*/
class CurlHandler
{
/**
* @var CurlFactoryInterface
*/
private $factory;
/**
* @var CurlShareHandleState|null
*/
private $shareHandleState;
/**
* Accepts an associative array of options:
*
* - handle_factory: Optional curl factory used to create cURL handles.
* - transport_sharing: Optional transport sharing mode.
*
* @param array{handle_factory?: ?CurlFactoryInterface, transport_sharing?: mixed} $options Array of options to use with the handler
*/
public function __construct(array $options = [])
{
CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlHandler');
$transportSharing = $options['transport_sharing'] ?? null;
$sharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
$this->shareHandleState = null;
$this->factory = $options['handle_factory'];
return;
}
$this->shareHandleState = $sharingMode !== TransportSharing::NONE
? CurlShareHandleState::fromOption($transportSharing)
: null;
$this->factory = $this->shareHandleState !== null
? new CurlFactory(3, $this->shareHandleState->mode, $this->shareHandleState->handle)
: new CurlFactory(3);
}
public function __invoke(RequestInterface $request, array $options): PromiseInterface
{
if (isset($options['delay'])) {
\usleep($options['delay'] * 1000);
}
$easy = $this->factory->create($request, $options);
\curl_exec($easy->handle);
$easy->errno = \curl_errno($easy->handle);
return CurlFactory::finish($this, $easy, $this->factory);
}
}
@@ -0,0 +1,534 @@
<?php
namespace GuzzleHttp\Handler;
use Closure;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\TransportSharing;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
/**
* Returns an asynchronous response using curl_multi_* functions.
*
* When using the CurlMultiHandler, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of the provided request options.
*
* @final
*/
class CurlMultiHandler
{
/**
* @var CurlFactoryInterface
*/
private $factory;
/**
* @var CurlShareHandleState|null
*/
private $shareHandleState;
/**
* @var int
*/
private $selectTimeout;
/**
* @var int Will be higher than 0 when `curl_multi_exec` is still running.
*/
private $active = 0;
/**
* @var array Request entry handles, indexed by handle id in `addRequest`.
*
* @see CurlMultiHandler::addRequest
*/
private $handles = [];
/**
* @var array<int, float> An array of delay times, indexed by handle id in `addRequest`.
*
* @see CurlMultiHandler::addRequest
*/
private $delays = [];
/**
* @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt()
*/
private $options = [];
/** @var resource|\CurlMultiHandle */
private $_mh;
/**
* @var bool
*/
private $executingMulti = false;
/**
* @var array<int, EasyHandle>
*/
private $deferredCancels = [];
/**
* @var string|null Owner signature of the proxy tunnels the multi handle's
* connection cache may hold
*/
private $proxyTunnelOwner;
/** @var array<string, int> Count of attached transfers per proxy tunnel signature. */
private $activeProxyTunnelSignatures = [];
/** @var array<int, string> Maps an attached handle id to its proxy tunnel signature. */
private $activeProxyTunnelHandles = [];
/**
* @var bool Guards against multi-handle recreation re-entrancy from
* processMessages (a retried transfer re-invokes the handler)
*/
private $processingMessages = false;
/**
* This handler accepts the following options:
*
* - handle_factory: An optional factory used to create curl handles
* - transport_sharing: Optional transport sharing mode.
* - select_timeout: Optional timeout (in seconds) to block before timing
* out while selecting curl handles. Defaults to 1 second.
* - options: An associative array of CURLMOPT_* options and
* corresponding values for curl_multi_setopt()
*/
public function __construct(array $options = [])
{
CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlMultiHandler');
$transportSharing = $options['transport_sharing'] ?? null;
$sharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
$this->shareHandleState = null;
$this->factory = $options['handle_factory'];
} else {
$this->shareHandleState = $sharingMode !== TransportSharing::NONE
? CurlShareHandleState::fromOption($transportSharing)
: null;
$this->factory = $this->shareHandleState !== null
? new CurlFactory(50, $this->shareHandleState->mode, $this->shareHandleState->handle)
: new CurlFactory(50);
}
if (isset($options['select_timeout'])) {
$this->selectTimeout = $options['select_timeout'];
} elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
\trigger_deprecation('guzzlehttp/guzzle', '7.2', 'The GUZZLE_CURL_SELECT_TIMEOUT environment variable is deprecated; use the "select_timeout" option instead.');
$this->selectTimeout = (int) $selectTimeout;
} else {
$this->selectTimeout = 1;
}
$this->options = $options['options'] ?? [];
// unsetting the property forces the first access to go through
// __get().
unset($this->_mh);
}
/**
* @param string $name
*
* @return resource|\CurlMultiHandle
*
* @throws \BadMethodCallException when another field as `_mh` will be gotten
* @throws \RuntimeException when curl can not initialize a multi handle
*/
public function __get($name)
{
if ($name !== '_mh') {
throw new \BadMethodCallException("Can not get other property as '_mh'.");
}
$multiHandle = \curl_multi_init();
if (false === $multiHandle) {
throw new \RuntimeException('Can not initialize curl multi handle.');
}
$this->_mh = $multiHandle;
foreach ($this->options as $option => $value) {
// A warning is raised in case of a wrong option.
curl_multi_setopt($this->_mh, $option, $value);
}
return $this->_mh;
}
public function __destruct()
{
if (isset($this->_mh)) {
try {
\curl_multi_close($this->_mh);
} catch (\Throwable $e) {
// Destructors must not throw.
} finally {
unset($this->_mh);
}
}
}
public function __invoke(RequestInterface $request, array $options): PromiseInterface
{
$easy = $this->factory->create($request, $options);
$this->applyProxyTunnelOwnership($easy);
$id = (int) $easy->handle;
$promise = new Promise(
[$this, 'execute'],
function () use ($id) {
return $this->cancel($id);
}
);
$this->addRequest(['easy' => $easy, 'deferred' => $promise]);
return $promise;
}
/**
* Isolates the connection cache when the request's proxy tunnel section
* differs from the one the multi handle's cache may already hold.
*/
private function applyProxyTunnelOwnership(EasyHandle $easy): void
{
$signature = $easy->proxyTunnelSignature;
if ($signature === null || $signature === $this->proxyTunnelOwner) {
return;
}
if ($this->proxyTunnelOwner === null) {
// No in-domain transfer has ever run on this multi handle: latch
// the owner without destroying pooled direct connections.
$this->proxyTunnelOwner = $signature;
return;
}
if (
$this->handles === []
&& !$this->executingMulti
&& !$this->processingMessages
&& $this->deferredCancels === []
) {
// Idle: hand the connection cache over by recreating the multi
// handle (unsetting re-arms the lazy __get initializer, which
// re-applies the CURLMOPT_* options).
if (isset($this->_mh)) {
\curl_multi_close($this->_mh);
unset($this->_mh);
}
$this->proxyTunnelOwner = $signature;
return;
}
// Busy: isolate this transfer from the owner's pooled tunnels.
$this->isolateProxyTunnelTransfer($easy);
}
private function addCurlHandle(EasyHandle $easy): void
{
$this->isolateFromForeignActiveProxyTunnel($easy);
\curl_multi_add_handle($this->_mh, $easy->handle);
$this->markProxyTunnelActive($easy);
}
/**
* @param resource|\CurlHandle $handle
*/
private function removeCompletedHandleFromMulti(int $id, $handle): void
{
\curl_multi_remove_handle($this->_mh, $handle);
$this->unmarkProxyTunnelActiveById($id);
}
private function isolateFromForeignActiveProxyTunnel(EasyHandle $easy): void
{
$signature = $easy->proxyTunnelSignature;
if ($signature === null || $this->activeProxyTunnelSignatures === []) {
return;
}
if (\count($this->activeProxyTunnelSignatures) === 1 && isset($this->activeProxyTunnelSignatures[$signature])) {
return;
}
$this->isolateProxyTunnelTransfer($easy);
}
private function isolateProxyTunnelTransfer(EasyHandle $easy): void
{
// Unqualified curl_setopt so the test bootstrap shadow records it.
curl_setopt($easy->handle, \CURLOPT_FRESH_CONNECT, true);
curl_setopt($easy->handle, \CURLOPT_FORBID_REUSE, true);
}
private function markProxyTunnelActive(EasyHandle $easy): void
{
$signature = $easy->proxyTunnelSignature;
if ($signature === null) {
return;
}
$id = (int) $easy->handle;
if (isset($this->activeProxyTunnelHandles[$id])) {
if ($this->activeProxyTunnelHandles[$id] === $signature) {
return;
}
$this->unmarkProxyTunnelActiveById($id);
}
$this->activeProxyTunnelHandles[$id] = $signature;
$this->activeProxyTunnelSignatures[$signature] = ($this->activeProxyTunnelSignatures[$signature] ?? 0) + 1;
}
private function unmarkProxyTunnelActive(EasyHandle $easy): void
{
$this->unmarkProxyTunnelActiveById((int) $easy->handle);
}
private function unmarkProxyTunnelActiveById(int $id): void
{
if (!isset($this->activeProxyTunnelHandles[$id])) {
return;
}
$signature = $this->activeProxyTunnelHandles[$id];
unset($this->activeProxyTunnelHandles[$id]);
if (!isset($this->activeProxyTunnelSignatures[$signature])) {
return;
}
--$this->activeProxyTunnelSignatures[$signature];
if ($this->activeProxyTunnelSignatures[$signature] <= 0) {
unset($this->activeProxyTunnelSignatures[$signature]);
}
}
/**
* Ticks the curl event loop.
*/
public function tick(): void
{
// Add any delayed handles if needed.
if ($this->delays) {
$currentTime = Utils::currentTime();
foreach ($this->delays as $id => $delay) {
if ($currentTime >= $delay) {
unset($this->delays[$id]);
$this->addCurlHandle($this->handles[$id]['easy']);
}
}
}
// Run curl_multi_exec in the queue to enable other async tasks to run
P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue']));
// Step through the task queue which may add additional requests.
P\Utils::queue()->run();
if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
\usleep(250);
}
do {
$this->executingMulti = true;
try {
$exec = \curl_multi_exec($this->_mh, $this->active);
} finally {
$this->executingMulti = false;
$this->cleanupDeferredCancels();
}
// Prevent busy looping for slow HTTP requests.
if ($exec === \CURLM_CALL_MULTI_PERFORM) {
\curl_multi_select($this->_mh, $this->selectTimeout);
}
} while ($exec === \CURLM_CALL_MULTI_PERFORM);
$this->processMessages();
}
/**
* Runs \curl_multi_exec() inside the event loop, to prevent busy looping
*/
private function tickInQueue(): void
{
$this->executingMulti = true;
try {
$exec = \curl_multi_exec($this->_mh, $this->active);
} finally {
$this->executingMulti = false;
$this->cleanupDeferredCancels();
}
if ($exec === \CURLM_CALL_MULTI_PERFORM) {
\curl_multi_select($this->_mh, 0);
P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue']));
}
}
/**
* Runs until all outstanding connections have completed.
*/
public function execute(): void
{
$queue = P\Utils::queue();
while ($this->handles || !$queue->isEmpty()) {
// If there are no transfers, then sleep for the next delay
if (!$this->active && $this->delays) {
\usleep($this->timeToNext());
}
$this->tick();
}
}
private function addRequest(array $entry): void
{
$easy = $entry['easy'];
$id = (int) $easy->handle;
$this->handles[$id] = $entry;
if (empty($easy->options['delay'])) {
$this->addCurlHandle($easy);
} else {
$this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
}
}
/**
* Cancels a handle from sending and removes references to it.
*
* @param int $id Handle ID to cancel and remove.
*
* @return bool True on success, false on failure.
*/
private function cancel($id): bool
{
if (!is_int($id)) {
\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
}
// Cannot cancel if it has been processed.
if (!isset($this->handles[$id])) {
return false;
}
$easy = $this->handles[$id]['easy'];
unset($this->delays[$id], $this->handles[$id]);
if ($this->executingMulti) {
$this->deferredCancels[$id] = $easy;
return true;
}
$this->cleanupCancelledHandle($easy);
return true;
}
private function cleanupDeferredCancels(): void
{
if ($this->deferredCancels === []) {
return;
}
$entries = $this->deferredCancels;
$this->deferredCancels = [];
foreach ($entries as $easy) {
$this->cleanupCancelledHandle($easy);
}
}
private function cleanupCancelledHandle(EasyHandle $easy): void
{
$handle = $easy->handle;
\curl_multi_remove_handle($this->_mh, $handle);
$this->unmarkProxyTunnelActive($easy);
if (PHP_VERSION_ID < 80000) {
\curl_close($handle);
}
}
private function processMessages(): void
{
// CurlFactory::finish can retry a transfer by re-invoking this handler
// from inside this loop; the guard keeps that re-entry from recreating
// the multi handle mid-iteration (see applyProxyTunnelOwnership).
$this->processingMessages = true;
try {
while ($done = \curl_multi_info_read($this->_mh)) {
if ($done['msg'] !== \CURLMSG_DONE) {
// if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
continue;
}
if (!isset($done['handle'])) {
// Work around a PHP issue where cancelled transfers may omit the handle.
// Remove this once we no longer support PHP versions before the fix in
// https://github.com/php/php-src/pull/16302.
continue;
}
$id = (int) $done['handle'];
$this->removeCompletedHandleFromMulti($id, $done['handle']);
if (!isset($this->handles[$id])) {
// Probably was cancelled.
continue;
}
$entry = $this->handles[$id];
unset($this->handles[$id], $this->delays[$id]);
$entry['easy']->errno = $done['result'];
try {
$result = CurlFactory::finish($this, $entry['easy'], $this->factory);
} catch (\Throwable $e) {
$entry['deferred']->reject($e);
continue;
}
$entry['deferred']->resolve($result);
}
} finally {
$this->processingMessages = false;
}
}
private function timeToNext(): int
{
$currentTime = Utils::currentTime();
$nextTime = \PHP_INT_MAX;
foreach ($this->delays as $time) {
if ($time < $nextTime) {
$nextTime = $time;
}
}
return ((int) \max(0, $nextTime - $currentTime)) * 1000000;
}
}
@@ -0,0 +1,182 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\TransportSharing;
/**
* @internal
*/
final class CurlShareHandleState
{
/**
* @var resource|\CurlShareHandle|null
*/
public $handle;
/**
* @var string
*/
public $mode;
/**
* @param resource|\CurlShareHandle|null $handle
*/
private function __construct(string $mode, $handle)
{
$this->mode = $mode;
$this->handle = $handle;
}
/**
* @param mixed $sharing
*/
public static function fromOption($sharing): ?self
{
if ($sharing instanceof self) {
return $sharing;
}
$mode = self::normalizeMode($sharing, 'transport_sharing');
if ($mode === TransportSharing::NONE) {
return null;
}
if ($mode === TransportSharing::HANDLER_PREFER) {
return self::createHandlerShareOrNull($mode);
}
return self::createHandlerShare($mode);
}
/**
* @param mixed $sharing
*/
public static function normalizeMode($sharing, string $option): string
{
if ($sharing instanceof self) {
return $sharing->mode;
}
if ($sharing === null || $sharing === TransportSharing::NONE) {
return TransportSharing::NONE;
}
if ($sharing === TransportSharing::HANDLER_PREFER || $sharing === TransportSharing::HANDLER_REQUIRE) {
return $sharing;
}
throw new \InvalidArgumentException(\sprintf(
'The "%s" option must be null or a GuzzleHttp\\TransportSharing::* constant; received %s.',
$option,
\get_debug_type($sharing)
));
}
public static function assertNoRequiredSharingCustomFactoryConflict(array $options, string $handlerName): void
{
if (!\array_key_exists('handle_factory', $options) || $options['handle_factory'] === null) {
return;
}
$mode = self::normalizeMode($options['transport_sharing'] ?? null, 'transport_sharing');
if ($mode !== TransportSharing::HANDLER_REQUIRE) {
return;
}
throw new \InvalidArgumentException(\sprintf(
'The "transport_sharing" %s option cannot require sharing with a custom "handle_factory" because Guzzle cannot ensure that the custom factory applies CURLOPT_SHARE.',
$handlerName
));
}
private static function createHandlerShareOrNull(string $mode): ?self
{
try {
return self::createHandlerShare($mode);
} catch (\Throwable $e) {
return null;
}
}
private static function createHandlerShare(string $mode): self
{
if (!\function_exists('curl_share_init') || !\function_exists('curl_share_setopt')) {
throw new \InvalidArgumentException('The "transport_sharing" option requires cURL share support.');
}
self::requireCurlConstant('CURLOPT_SHARE');
$shareOption = self::requireCurlConstant('CURLSHOPT_SHARE');
$locks = self::handlerLocks($mode);
$handle = curl_share_init();
try {
foreach ($locks as $lock) {
try {
$success = curl_share_setopt($handle, $shareOption, $lock);
} catch (\Throwable $e) {
throw new \InvalidArgumentException('Unable to configure cURL share handle: '.$e->getMessage(), 0, $e);
}
if (!$success) {
throw new \InvalidArgumentException(\sprintf('Unable to configure cURL share handle with lock data %d.', $lock));
}
}
} catch (\Throwable $e) {
self::closeHandlerShareHandleOnPhp7($handle);
throw $e;
}
return new self($mode, $handle);
}
/**
* @return int[]
*/
private static function handlerLocks(string $mode): array
{
CurlVersion::ensureHandlerSharingSupported();
if ($mode === TransportSharing::HANDLER_REQUIRE) {
CurlVersion::ensureSslSessionSharingSupported();
}
$locks = [
self::requireCurlConstant('CURL_LOCK_DATA_DNS'),
];
if (CurlVersion::supportsSslSessionSharing()) {
$locks[] = self::requireCurlConstant('CURL_LOCK_DATA_SSL_SESSION');
}
return $locks;
}
private static function requireCurlConstant(string $constant): int
{
if (!\defined($constant)) {
throw new \InvalidArgumentException(\sprintf(
'The "transport_sharing" option requires %s, but it is not available in the installed PHP cURL extension.',
$constant
));
}
$value = \constant($constant);
if (!\is_int($value)) {
throw new \InvalidArgumentException(\sprintf('The cURL constant %s must resolve to an integer.', $constant));
}
return $value;
}
/**
* @param resource|\CurlShareHandle $handle
*/
private static function closeHandlerShareHandleOnPhp7($handle): void
{
if (\PHP_VERSION_ID < 80000 && \is_resource($handle)) {
curl_share_close($handle);
}
}
}
+200
View File
@@ -0,0 +1,200 @@
<?php
namespace GuzzleHttp\Handler;
/**
* @internal
*/
final class CurlVersion
{
private const MIN_VERSION = '7.21.2';
private const TLS_12_VERSION = '7.34.0';
private const TLS_13_VERSION = '7.52.0';
// curl 7.52.0 introduced HTTPS proxy support, advertised by a feature bit
// (a build can meet the version yet lack the feature). Earlier libcurl
// mishandles an https:// proxy: before 7.50.2 it silently downgrades to a
// plaintext HTTP proxy, and 7.50.2 through 7.51 reject it at connect time.
private const HTTPS_PROXY_VERSION = '7.52.0';
private const HANDLER_SHARING_VERSION = '7.35.0';
private const SSL_SESSION_SHARING_VERSION = '8.6.0';
// curl 7.83.1 added proxy TLS-SRP to the connection-reuse match
// (CVE-2022-27782); the proxy client certificate was matched from 7.52.0,
// so proxy TLS credentials are trusted from 7.83.1 onwards.
private const PROXY_TLS_CREDENTIAL_REUSE_VERSION = '7.83.1';
// curl 8.19.0 fixed proxy tunnel reuse after credential changes
// (CVE-2026-3784), but related proxy credential leak flaws were only
// fixed in 8.20.0, so connection reuse is trusted from 8.20.0 onwards.
private const PROXY_CREDENTIAL_REUSE_VERSION = '8.20.0';
private const PROXY_HEADER_SEPARATION_VERSION = '7.37.0';
/**
* @var array{version: string, features: int}|false|null
*/
private static $versionInfo;
private function __construct()
{
}
public static function supportsCurlHandler(): bool
{
$version = self::getVersion();
return $version !== null && \version_compare($version, self::MIN_VERSION, '>=');
}
public static function supportsTls12(): bool
{
$version = self::getVersion();
return self::supportsSsl()
&& \defined('CURL_SSLVERSION_TLSv1_2')
&& $version !== null
&& \version_compare($version, self::TLS_12_VERSION, '>=');
}
public static function supportsTls13(): bool
{
$version = self::getVersion();
return self::supportsSsl()
&& \defined('CURL_SSLVERSION_TLSv1_3')
&& $version !== null
&& \version_compare($version, self::TLS_13_VERSION, '>=');
}
public static function supportsHttp2(): bool
{
$versionInfo = self::getVersionInfo();
return self::supportsTls12()
&& \defined('CURL_VERSION_HTTP2')
&& $versionInfo !== null
&& 0 !== (\CURL_VERSION_HTTP2 & $versionInfo['features']);
}
public static function supportsHttpsProxy(): bool
{
$versionInfo = self::getVersionInfo();
// CURL_VERSION_HTTPS_PROXY is not defined on every supported PHP
// version; fall back to the curl.h bit value.
$httpsProxyFeature = \defined('CURL_VERSION_HTTPS_PROXY') ? \CURL_VERSION_HTTPS_PROXY : (1 << 21);
return $versionInfo !== null
&& \version_compare($versionInfo['version'], self::HTTPS_PROXY_VERSION, '>=')
&& 0 !== ($httpsProxyFeature & $versionInfo['features']);
}
public static function supportsHandlerSharing(): bool
{
$version = self::getVersion();
return $version !== null && \version_compare($version, self::HANDLER_SHARING_VERSION, '>=');
}
public static function ensureHandlerSharingSupported(): void
{
if (!self::supportsHandlerSharing()) {
throw new \InvalidArgumentException(\sprintf(
'The "transport_sharing" option requires libcurl %s or higher for cURL share handles.',
self::HANDLER_SHARING_VERSION
));
}
}
public static function supportsSslSessionSharing(): bool
{
$version = self::getVersion();
return self::supportsSsl()
&& $version !== null
&& \version_compare($version, self::SSL_SESSION_SHARING_VERSION, '>=');
}
public static function ensureSslSessionSharingSupported(): void
{
if (!self::supportsSslSessionSharing()) {
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
));
}
}
public static function supportsProxyTlsCredentialAwareConnectionReuse(): bool
{
$version = self::getVersion();
return $version !== null
&& \version_compare($version, self::PROXY_TLS_CREDENTIAL_REUSE_VERSION, '>=');
}
public static function supportsProxyCredentialAwareConnectionReuse(): bool
{
$version = self::getVersion();
return $version !== null
&& \version_compare($version, self::PROXY_CREDENTIAL_REUSE_VERSION, '>=');
}
public static function supportsProxyHeaderSeparation(): bool
{
$version = self::getVersion();
return $version !== null
&& \version_compare($version, self::PROXY_HEADER_SEPARATION_VERSION, '>=')
&& \defined('CURLOPT_PROXYHEADER')
&& \defined('CURLOPT_HEADEROPT')
&& \defined('CURLHEADER_SEPARATE');
}
private static function supportsSsl(): bool
{
$versionInfo = self::getVersionInfo();
return \defined('CURL_VERSION_SSL')
&& $versionInfo !== null
&& 0 !== (\CURL_VERSION_SSL & $versionInfo['features']);
}
public static function getVersion(): ?string
{
$versionInfo = self::getVersionInfo();
return $versionInfo === null ? null : $versionInfo['version'];
}
/**
* @return array{version: string, features: int}|null
*/
private static function getVersionInfo(): ?array
{
if (self::$versionInfo === null) {
if (!\function_exists('curl_version')) {
self::$versionInfo = false;
} else {
$versionInfo = \curl_version();
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;
}
}
return self::$versionInfo === false ? null : self::$versionInfo;
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Represents a cURL easy handle and the data it populates.
*
* @internal
*/
final class EasyHandle
{
/**
* @var resource|\CurlHandle cURL resource
*/
public $handle;
/**
* @var StreamInterface Where data is being written
*/
public $sink;
/**
* @var array Received HTTP headers so far
*/
public $headers = [];
/**
* @var ResponseInterface|null Received response (if any)
*/
public $response;
/**
* @var RequestInterface Request being sent
*/
public $request;
/**
* @var array Request options
*/
public $options = [];
/**
* @var int cURL error number (if any)
*/
public $errno = 0;
/**
* @var string|null Effective CURLOPT_PROXY value the handle was created with (if any)
*/
public $effectiveProxy;
/**
* Proxy tunnel section signature for connection-reuse isolation, or
* null when the request does not require sectioning.
*
* @var string|null
*/
public $proxyTunnelSignature;
/**
* @var \Throwable|null Exception during on_headers (if any)
*/
public $onHeadersException;
/**
* @var \Throwable|null Exception during createResponse (if any)
*/
public $createResponseException;
/**
* Attach a response to the easy handle based on the received headers.
*
* @throws \RuntimeException if no headers have been received or the first
* header line is invalid.
*/
public function createResponse(): void
{
$this->response = null;
[$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($this->headers);
$normalizedKeys = Utils::normalizeHeaderKeys($headers);
if (isset($this->options['decode_content']) && $this->options['decode_content'] !== false && isset($normalizedKeys['content-encoding'])) {
$headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
unset($headers[$normalizedKeys['content-encoding']]);
if (isset($normalizedKeys['content-length'])) {
$headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
$bodyLength = (int) $this->sink->getSize();
if ($bodyLength) {
$headers[$normalizedKeys['content-length']] = [(string) $bodyLength];
} else {
unset($headers[$normalizedKeys['content-length']]);
}
}
}
// Attach a response to the easy handle with the parsed headers.
$this->response = new Response(
$status,
$headers,
$this->sink,
$ver,
$reason
);
}
/**
* @param string $name
*
* @return void
*
* @throws \BadMethodCallException
*/
public function __get($name)
{
$msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: '.$name;
throw new \BadMethodCallException($msg);
}
}
@@ -0,0 +1,77 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Utils;
/**
* @internal
*/
final class HeaderProcessor
{
/**
* Returns the HTTP version, status code, reason phrase, and headers.
*
* @param string[] $headers
*
* @return array{0:string, 1:int, 2:?string, 3:array}
*
* @throws \RuntimeException
*/
public static function parseHeaders(array $headers): array
{
if ($headers === []) {
throw new \RuntimeException('Expected a non-empty array of header data');
}
$headers = self::getLastHeaderBlock(\array_values($headers));
$statusLine = \array_shift($headers);
if ($statusLine === null) {
throw new \RuntimeException('Expected a non-empty array of header data');
}
$parts = \explode(' ', $statusLine, 3);
$version = \explode('/', $parts[0])[1] ?? null;
if ($version === null) {
throw new \RuntimeException('HTTP version missing from header data');
}
$status = $parts[1] ?? null;
if ($status === null) {
throw new \RuntimeException('HTTP status code missing from header data');
}
if (!\preg_match('/^\d{3}$/', $status)) {
throw new \RuntimeException('HTTP status code is invalid');
}
foreach ($headers as $header) {
if (\strpos($header, ':') === false) {
throw new \RuntimeException('HTTP header line is invalid');
}
}
return [$version, (int) $status, $parts[2] ?? null, Utils::headersFromLines($headers)];
}
/**
* @param non-empty-list<string> $headers
*
* @return list<string>
*/
private static function getLastHeaderBlock(array $headers): array
{
$lastStatusLine = 0;
foreach ($headers as $index => $line) {
if (\preg_match('/^HTTP\/\S+\s+/i', $line)) {
$lastStatusLine = $index;
}
}
return \array_slice($headers, $lastStatusLine);
}
}
+211
View File
@@ -0,0 +1,211 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\TransferStats;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Handler that returns responses or throw exceptions from a queue.
*
* @final
*/
class MockHandler implements \Countable
{
/**
* @var array
*/
private $queue = [];
/**
* @var RequestInterface|null
*/
private $lastRequest;
/**
* @var array
*/
private $lastOptions = [];
/**
* @var callable|null
*/
private $onFulfilled;
/**
* @var callable|null
*/
private $onRejected;
/**
* Creates a new MockHandler that uses the default handler stack list of
* middlewares.
*
* @param array|null $queue Array of responses, callables, or exceptions.
* @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled.
* @param callable|null $onRejected Callback to invoke when the return value is rejected.
*/
public static function createWithMiddleware(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null): HandlerStack
{
return HandlerStack::create(new self($queue, $onFulfilled, $onRejected));
}
/**
* The passed in value must be an array of
* {@see ResponseInterface} objects, Exceptions,
* callables, or Promises.
*
* @param array<int, mixed>|null $queue The parameters to be passed to the append function, as an indexed array.
* @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled.
* @param callable|null $onRejected Callback to invoke when the return value is rejected.
*/
public function __construct(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null)
{
$this->onFulfilled = $onFulfilled;
$this->onRejected = $onRejected;
if ($queue) {
// array_values included for BC
$this->append(...array_values($queue));
}
}
public function __invoke(RequestInterface $request, array $options): PromiseInterface
{
if (!$this->queue) {
throw new \OutOfBoundsException('Mock queue is empty');
}
if (isset($options['delay']) && \is_numeric($options['delay'])) {
\usleep((int) $options['delay'] * 1000);
}
$this->lastRequest = $request;
$this->lastOptions = $options;
$response = \array_shift($this->queue);
if (isset($options['on_headers'])) {
if (!\is_callable($options['on_headers'])) {
throw new \InvalidArgumentException('on_headers must be callable');
}
try {
$options['on_headers']($response);
} catch (\Exception $e) {
$msg = 'An error was encountered during the on_headers event';
$response = new RequestException($msg, $request, $response, $e);
}
}
if (\is_callable($response)) {
$response = $response($request, $options);
}
$response = $response instanceof \Throwable
? P\Create::rejectionFor($response)
: P\Create::promiseFor($response);
return $response->then(
function (?ResponseInterface $value) use ($request, $options) {
$this->invokeStats($request, $options, $value);
if ($this->onFulfilled) {
($this->onFulfilled)($value);
}
if ($value !== null && isset($options['sink'])) {
$contents = (string) $value->getBody();
$sink = $options['sink'];
if (\is_resource($sink)) {
\fwrite($sink, $contents);
} elseif (\is_string($sink)) {
\file_put_contents($sink, $contents);
} elseif ($sink instanceof StreamInterface) {
$sink->write($contents);
}
}
return $value;
},
function ($reason) use ($request, $options) {
$this->invokeStats($request, $options, null, $reason);
if ($this->onRejected) {
($this->onRejected)($reason);
}
return P\Create::rejectionFor($reason);
}
);
}
/**
* Adds one or more variadic requests, exceptions, callables, or promises
* to the queue.
*
* @param mixed ...$values
*/
public function append(...$values): void
{
foreach ($values as $value) {
if ($value instanceof ResponseInterface
|| $value instanceof \Throwable
|| $value instanceof PromiseInterface
|| \is_callable($value)
) {
$this->queue[] = $value;
} else {
throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.\get_debug_type($value));
}
}
}
/**
* Get the last received request.
*/
public function getLastRequest(): ?RequestInterface
{
return $this->lastRequest;
}
/**
* Get the last received request options.
*/
public function getLastOptions(): array
{
return $this->lastOptions;
}
/**
* Returns the number of remaining items in the queue.
*/
public function count(): int
{
return \count($this->queue);
}
public function reset(): void
{
$this->queue = [];
}
/**
* @param mixed $reason Promise or reason.
*/
private function invokeStats(
RequestInterface $request,
array $options,
?ResponseInterface $response = null,
$reason = null
): void {
if (isset($options['on_stats'])) {
$transferTime = $options['transfer_time'] ?? 0;
$stats = new TransferStats($request, $response, $transferTime, $reason);
($options['on_stats'])($stats);
}
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\RequestInterface;
/**
* Provides basic proxies for handlers.
*
* @final
*/
class Proxy
{
/**
* Sends synchronous requests to a specific handler while sending all other
* requests to another handler.
*
* @param callable(RequestInterface, array): PromiseInterface $default Handler used for normal responses
* @param callable(RequestInterface, array): PromiseInterface $sync Handler used for synchronous responses.
*
* @return callable(RequestInterface, array): PromiseInterface Returns the composed handler.
*/
public static function wrapSync(callable $default, callable $sync): callable
{
return static function (RequestInterface $request, array $options) use ($default, $sync): PromiseInterface {
return empty($options[RequestOptions::SYNCHRONOUS]) ? $default($request, $options) : $sync($request, $options);
};
}
/**
* Sends streaming requests to a streaming compatible handler while sending
* all other requests to a default handler.
*
* This, for example, could be useful for taking advantage of the
* performance benefits of curl while still supporting true streaming
* through the StreamHandler.
*
* @param callable(RequestInterface, array): PromiseInterface $default Handler used for non-streaming responses
* @param callable(RequestInterface, array): PromiseInterface $streaming Handler used for streaming responses
*
* @return callable(RequestInterface, array): PromiseInterface Returns the composed handler.
*/
public static function wrapStreaming(callable $default, callable $streaming): callable
{
return static function (RequestInterface $request, array $options) use ($default, $streaming): PromiseInterface {
return empty($options['stream']) ? $default($request, $options) : $streaming($request, $options);
};
}
/**
* Sends requests to a fallback handler when the default cURL handler cannot
* honor TLS 1.2 selection.
*
* @param callable(RequestInterface, array): PromiseInterface $default
* @param callable(RequestInterface, array): PromiseInterface $fallback
*
* @return callable(RequestInterface, array): PromiseInterface Returns the composed handler.
*/
public static function wrapTlsFallback(callable $default, callable $fallback): callable
{
return static function (RequestInterface $request, array $options) use ($default, $fallback): PromiseInterface {
if (self::requiresTls12Fallback($options)) {
return $fallback($request, $options);
}
return $default($request, $options);
};
}
/**
* @param array<string, mixed> $options
*/
private static function requiresTls12Fallback(array $options): bool
{
return isset($options[RequestOptions::CRYPTO_METHOD])
&& $options[RequestOptions::CRYPTO_METHOD] === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
&& !CurlVersion::supportsTls12();
}
}
@@ -0,0 +1,113 @@
<?php
namespace GuzzleHttp\Handler;
/**
* Resolves proxy configuration from the process environment with the same
* semantics libcurl applies, so the cURL handlers can pin CURLOPT_PROXY and
* CURLOPT_NOPROXY explicitly and libcurl never reads the environment itself.
*
* @internal
*/
final class ProxyEnvironment
{
private function __construct()
{
}
/**
* Resolves the proxy to use for the given request scheme.
*
* The lookup mirrors libcurl for the http and https schemes the handlers
* accept: the lowercase scheme-specific variable first, its uppercase
* variant next (except for "http", where uppercase HTTP_PROXY is never
* read), then all_proxy/ALL_PROXY.
*
* @return string|null The proxy to use; null when the environment
* configures none.
*/
public static function getProxyForScheme(string $scheme): ?string
{
$scheme = \strtolower($scheme);
$candidates = [$scheme.'_proxy'];
if ($scheme !== 'http') {
// Uppercase HTTP_PROXY is deliberately never consulted: a CGI
// request header "Proxy:" becomes HTTP_PROXY in the environment.
// See https://httpoxy.org for more information.
$candidates[] = \strtoupper($scheme).'_PROXY';
}
$candidates[] = 'all_proxy';
$candidates[] = 'ALL_PROXY';
foreach ($candidates as $name) {
$value = self::getenv($name);
if ($value !== null) {
return $value;
}
}
return null;
}
/**
* @return string|null The no-proxy list; null when nothing is set.
*/
public static function getNoProxy(): ?string
{
foreach (['no_proxy', 'NO_PROXY'] as $name) {
$value = self::getenv($name);
if ($value !== null) {
return $value;
}
}
return null;
}
/**
* Splits a no_proxy environment value into matchable entries.
*
* Mirrors libcurl's tokenization: entries may be separated by commas or
* blanks, and a single leading dot is ignored, so ".example.com" bypasses
* example.com and its subdomains exactly as a bare domain entry does.
*
* @return string[]
*/
public static function splitNoProxy(string $noProxy): array
{
$entries = [];
foreach (\preg_split('/[\s,]+/', $noProxy) ?: [] as $entry) {
if ($entry !== '' && $entry[0] === '.') {
$entry = \substr($entry, 1);
}
if ($entry !== '') {
$entries[] = $entry;
}
}
return $entries;
}
private static function getenv(string $name): ?string
{
// Windows environment variables are case-insensitive, so the
// lowercase-only httpoxy defence does not hold there. Outside the
// CLI SAPI on Windows, environment proxies are not resolved at all
// (a safe-side divergence from libcurl).
if (\PHP_OS_FAMILY === 'Windows' && \PHP_SAPI !== 'cli') {
return null;
}
// local_only: the OS environment and putenv() only - the same
// environ(7) libcurl reads. SAPI request environments such as
// fastcgi_param or SetEnv are deliberately excluded.
$value = \getenv($name, true);
// libcurl's GetEnv (lib/getenv.c) treats variables set to an empty
// string as unset on every version, so the lookup falls through to
// the next candidate.
return $value === false || $value === '' ? null : $value;
}
}
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace GuzzleHttp\Handler;
/**
* @internal
*/
final class TlsVersion
{
/**
* @param mixed $value
*/
public static function ordinal(string $option, $value): int
{
if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT) {
return 10;
}
if ($value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT) {
return 11;
}
if ($value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT) {
return 12;
}
if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) {
return 13;
}
throw new \InvalidArgumentException(\sprintf('Invalid %s request option: unknown version provided', $option));
}
/**
* @param mixed $min
* @param mixed $max
*/
public static function assertRange($min, $max): void
{
if ($min === null || $max === null) {
return;
}
if (self::ordinal('crypto_method_max', $max) < self::ordinal('crypto_method', $min)) {
throw new \InvalidArgumentException('Invalid crypto_method_max request option: maximum TLS version must be greater than or equal to crypto_method');
}
}
/**
* @param mixed $value
*/
public static function streamProtocolVersion(string $option, $value): int
{
if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT) {
return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_0', $option);
}
if ($value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT) {
return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_1', $option);
}
if ($value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT) {
return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_2', $option);
}
if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) {
return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_3', $option);
}
throw new \InvalidArgumentException(\sprintf('Invalid %s request option: unknown version provided', $option));
}
/**
* Resolves a STREAM_CRYPTO_PROTO_* constant. The ssl.max_proto_version
* context option and these constants were added in PHP 7.3.0 (TLS 1.3 in
* 7.4.0); on older runtimes the option cannot be honored, so reject loudly.
*/
private static function requireStreamProto(string $constant, string $option): int
{
if (\defined($constant)) {
/** @var int */
return \constant($constant);
}
throw new \InvalidArgumentException(\sprintf(
'Invalid %s request option: maximum TLS version control is not supported by your version of PHP',
$option
));
}
}