From 3f4a3c819da970a0f786be56c141bf8d2a8eb9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2023 23:59:34 +0100 Subject: [PATCH] Fix cancelling happy eyeballs when IPv6 resolution is pending --- src/HappyEyeBallsConnectionBuilder.php | 21 ++++++++++---------- tests/HappyEyeBallsConnectionBuilderTest.php | 4 +++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/HappyEyeBallsConnectionBuilder.php b/src/HappyEyeBallsConnectionBuilder.php index 65e0718f..d4f05e85 100644 --- a/src/HappyEyeBallsConnectionBuilder.php +++ b/src/HappyEyeBallsConnectionBuilder.php @@ -65,9 +65,8 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector, public function connect() { - $timer = null; $that = $this; - return new Promise\Promise(function ($resolve, $reject) use ($that, &$timer) { + return new Promise\Promise(function ($resolve, $reject) use ($that) { $lookupResolve = function ($type) use ($that, $resolve, $reject) { return function (array $ips) use ($that, $type, $resolve, $reject) { unset($that->resolverPromises[$type]); @@ -83,26 +82,29 @@ public function connect() }; $that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA)); - $that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function (array $ips) use ($that, &$timer) { + $that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function (array $ips) use ($that) { // happy path: IPv6 has resolved already (or could not resolve), continue with IPv4 addresses if ($that->resolved[Message::TYPE_AAAA] === true || !$ips) { return $ips; } // Otherwise delay processing IPv4 lookup until short timer passes or IPv6 resolves in the meantime - $deferred = new Promise\Deferred(); + $deferred = new Promise\Deferred(function () use (&$ips) { + // discard all IPv4 addresses if cancelled + $ips = array(); + }); $timer = $that->loop->addTimer($that::RESOLUTION_DELAY, function () use ($deferred, $ips) { $deferred->resolve($ips); }); - $that->resolverPromises[Message::TYPE_AAAA]->then(function () use ($that, $timer, $deferred, $ips) { + $that->resolverPromises[Message::TYPE_AAAA]->then(function () use ($that, $timer, $deferred, &$ips) { $that->loop->cancelTimer($timer); $deferred->resolve($ips); }); return $deferred->promise(); })->then($lookupResolve(Message::TYPE_A)); - }, function ($_, $reject) use ($that, &$timer) { + }, function ($_, $reject) use ($that) { $reject(new \RuntimeException( 'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : '') . ' (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 @@ -110,9 +112,6 @@ public function connect() $_ = $reject = null; $that->cleanUp(); - if ($timer instanceof TimerInterface) { - $that->loop->cancelTimer($timer); - } }); } @@ -247,13 +246,15 @@ public function cleanUp() // clear list of outstanding IPs to avoid creating new connections $this->connectQueue = array(); + // cancel pending connection attempts foreach ($this->connectionPromises as $connectionPromise) { if ($connectionPromise instanceof PromiseInterface && \method_exists($connectionPromise, 'cancel')) { $connectionPromise->cancel(); } } - foreach ($this->resolverPromises as $resolverPromise) { + // cancel pending DNS resolution (cancel IPv4 first in case it is awaiting IPv6 resolution delay) + foreach (\array_reverse($this->resolverPromises) as $resolverPromise) { if ($resolverPromise instanceof PromiseInterface && \method_exists($resolverPromise, 'cancel')) { $resolverPromise->cancel(); } diff --git a/tests/HappyEyeBallsConnectionBuilderTest.php b/tests/HappyEyeBallsConnectionBuilderTest.php index 59b1c1fd..581d8836 100644 --- a/tests/HappyEyeBallsConnectionBuilderTest.php +++ b/tests/HappyEyeBallsConnectionBuilderTest.php @@ -695,7 +695,9 @@ public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6LookupAndC array('reactphp.org', Message::TYPE_AAAA), array('reactphp.org', Message::TYPE_A) )->willReturnOnConsecutiveCalls( - new Promise(function () { }, $this->expectCallableOnce()), + new Promise(function () { }, function () { + throw new \RuntimeException('DNS cancelled'); + }), \React\Promise\resolve(array('127.0.0.1')) );