First Commit
This commit is contained in:
+1959
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
+1046
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user