From b400a57fd8718b6d891a232cbc2d62b8634dbb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 9 Mar 2020 17:03:12 +0100 Subject: [PATCH 1/2] Fix cancelling happy eyeballs to stop timer and fix rejection reason --- composer.json | 2 +- src/HappyEyeBallsConnectionBuilder.php | 12 +- tests/HappyEyeBallsConnectionBuilderTest.php | 233 +++++++++++++++++++ tests/HappyEyeBallsConnectorTest.php | 48 ---- 4 files changed, 239 insertions(+), 56 deletions(-) diff --git a/composer.json b/composer.json index 355c62e1..676d3e77 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.0", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/dns": "^1.1", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/event-loop": "^1.0 || ^0.5", "react/promise": "^2.6.0 || ^1.2.1", "react/promise-timer": "^1.4.0", "react/stream": "^1.1" diff --git a/src/HappyEyeBallsConnectionBuilder.php b/src/HappyEyeBallsConnectionBuilder.php index fa10224c..09380685 100644 --- a/src/HappyEyeBallsConnectionBuilder.php +++ b/src/HappyEyeBallsConnectionBuilder.php @@ -48,8 +48,9 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector, public function connect() { + $timer = null; $that = $this; - return new Promise\Promise(function ($resolve, $reject) use ($that) { + return new Promise\Promise(function ($resolve, $reject) use ($that, &$timer) { $lookupResolve = function ($type) use ($that, $resolve, $reject) { return function (array $ips) use ($that, $type, $resolve, $reject) { unset($that->resolverPromises[$type]); @@ -66,7 +67,6 @@ public function connect() }; $ipv4Deferred = null; - $timer = null; $that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA))->then(function () use (&$ipv4Deferred) { if ($ipv4Deferred instanceof Promise\Deferred) { $ipv4Deferred->resolve(); @@ -99,15 +99,13 @@ public function connect() return $deferred->promise(); })->then($lookupResolve(Message::TYPE_A)); }, function ($_, $reject) use ($that, &$timer) { - $that->cleanUp(); + $reject(new \RuntimeException('Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : ''))); + $_ = $reject = null; + $that->cleanUp(); if ($timer instanceof TimerInterface) { $that->loop->cancelTimer($timer); } - - $reject(new \RuntimeException('Connection to ' . $that->uri . ' cancelled during DNS lookup')); - - $_ = $reject = null; }); } diff --git a/tests/HappyEyeBallsConnectionBuilderTest.php b/tests/HappyEyeBallsConnectionBuilderTest.php index 4be5e7b5..2c5eb29d 100644 --- a/tests/HappyEyeBallsConnectionBuilderTest.php +++ b/tests/HappyEyeBallsConnectionBuilderTest.php @@ -4,9 +4,242 @@ use React\Promise\Promise; use React\Socket\HappyEyeBallsConnectionBuilder; +use React\Dns\Model\Message; +use React\Promise\Deferred; class HappyEyeBallsConnectionBuilderTest extends TestCase { + public function testConnectWillResolveTwiceViaResolver() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); + $resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive( + array('reactphp.org', Message::TYPE_AAAA), + array('reactphp.org', Message::TYPE_A) + )->willReturn(new Promise(function () { })); + + $uri = 'tcp://reactphp.org:80'; + $host = 'reactphp.org'; + $parts = parse_url($uri); + + $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); + + $builder->connect(); + } + + public function testConnectWillStartTimerWhenIpv4ResolvesAndIpv6IsPending() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer'); + $loop->expects($this->never())->method('cancelTimer'); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); + $resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive( + array('reactphp.org', Message::TYPE_AAAA), + array('reactphp.org', Message::TYPE_A) + )->willReturnOnConsecutiveCalls( + new Promise(function () { }), + \React\Promise\resolve(array('127.0.0.1')) + ); + + $uri = 'tcp://reactphp.org:80'; + $host = 'reactphp.org'; + $parts = parse_url($uri); + + $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); + + $builder->connect(); + } + + public function testConnectWillStartConnectingWithoutTimerWhenIpv6ResolvesAndIpv4IsPending() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tcp://[::1]:80?hostname=reactphp.org')->willReturn(new Promise(function () { })); + + $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); + $resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive( + array('reactphp.org', Message::TYPE_AAAA), + array('reactphp.org', Message::TYPE_A) + )->willReturnOnConsecutiveCalls( + \React\Promise\resolve(array('::1')), + new Promise(function () { }) + ); + + $uri = 'tcp://reactphp.org:80'; + $host = 'reactphp.org'; + $parts = parse_url($uri); + + $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); + + $builder->connect(); + } + + public function testConnectWillStartTimerAndCancelTimerWhenIpv4ResolvesAndIpv6ResolvesAfterwardsAndStartConnectingToIpv6() + { + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->once())->method('addPeriodicTimer')->willReturn($this->getMockBuilder('React\EventLoop\TimerInterface')->getMock()); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tcp://[::1]:80?hostname=reactphp.org')->willReturn(new Promise(function () { })); + + $deferred = new Deferred(); + $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); + $resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive( + array('reactphp.org', Message::TYPE_AAAA), + array('reactphp.org', Message::TYPE_A) + )->willReturnOnConsecutiveCalls( + $deferred->promise(), + \React\Promise\resolve(array('127.0.0.1')) + ); + + $uri = 'tcp://reactphp.org:80'; + $host = 'reactphp.org'; + $parts = parse_url($uri); + + $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); + + $builder->connect(); + $deferred->resolve(array('::1')); + } + + public function testCancelConnectWillRejectPromiseAndCancelBothDnsLookups() + { + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $cancelled = 0; + $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); + $resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive( + array('reactphp.org', Message::TYPE_AAAA), + array('reactphp.org', Message::TYPE_A) + )->willReturnOnConsecutiveCalls( + new Promise(function () { }, function () use (&$cancelled) { + ++$cancelled; + throw new \RuntimeException(); + }), + new Promise(function () { }, function () use (&$cancelled) { + ++$cancelled; + throw new \RuntimeException(); + }) + ); + + $uri = 'tcp://reactphp.org:80'; + $host = 'reactphp.org'; + $parts = parse_url($uri); + + $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); + + $promise = $builder->connect(); + $promise->cancel(); + + $this->assertEquals(2, $cancelled); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + } + + public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6LookupAndCancelTimer() + { + $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->never())->method('connect'); + + $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); + $resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive( + array('reactphp.org', Message::TYPE_AAAA), + array('reactphp.org', Message::TYPE_A) + )->willReturnOnConsecutiveCalls( + new Promise(function () { }, $this->expectCallableOnce()), + \React\Promise\resolve(array('127.0.0.1')) + ); + + $uri = 'tcp://reactphp.org:80'; + $host = 'reactphp.org'; + $parts = parse_url($uri); + + $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); + + $promise = $builder->connect(); + $promise->cancel(); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled during DNS lookup', $exception->getMessage()); + } + + public function testCancelConnectWillRejectPromiseAndCancelPendingIpv6ConnectionAttemptAndPendingIpv4Lookup() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $cancelled = 0; + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('tcp://[::1]:80?hostname=reactphp.org')->willReturn(new Promise(function () { }, function () use (&$cancelled) { + ++$cancelled; + throw new \RuntimeException('Ignored message'); + })); + + $resolver = $this->getMockBuilder('React\Dns\Resolver\ResolverInterface')->getMock(); + $resolver->expects($this->exactly(2))->method('resolveAll')->withConsecutive( + array('reactphp.org', Message::TYPE_AAAA), + array('reactphp.org', Message::TYPE_A) + )->willReturnOnConsecutiveCalls( + \React\Promise\resolve(array('::1')), + new Promise(function () { }, $this->expectCallableOnce()) + ); + + $uri = 'tcp://reactphp.org:80'; + $host = 'reactphp.org'; + $parts = parse_url($uri); + + $builder = new HappyEyeBallsConnectionBuilder($loop, $connector, $resolver, $uri, $host, $parts); + + $promise = $builder->connect(); + $promise->cancel(); + + $this->assertEquals(1, $cancelled); + + $exception = null; + $promise->then(null, function ($e) use (&$exception) { + $exception = $e; + }); + + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('Connection to tcp://reactphp.org:80 cancelled', $exception->getMessage()); + } + public function testAttemptConnectionWillConnectViaConnectorToGivenIpWithPortAndHostnameFromUriParts() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); diff --git a/tests/HappyEyeBallsConnectorTest.php b/tests/HappyEyeBallsConnectorTest.php index cb7a0aa2..0ab49aea 100644 --- a/tests/HappyEyeBallsConnectorTest.php +++ b/tests/HappyEyeBallsConnectorTest.php @@ -414,54 +414,6 @@ public function testCancelDuringTcpConnectionCancelsTcpConnectionIfGivenIp() $this->loop->run(); } - /** - * @dataProvider provideIpvAddresses - */ - public function testCancelDuringTcpConnectionCancelsTcpConnectionAfterDnsIsResolved(array $ipv6, array $ipv4) - { - $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); - $this->resolver->expects($this->at(0))->method('resolveAll')->with($this->equalTo('example.com'), $this->anything())->willReturn(Promise\resolve($ipv6)); - $this->resolver->expects($this->at(1))->method('resolveAll')->with($this->equalTo('example.com'), $this->anything())->willReturn(Promise\resolve($ipv4)); - $this->tcp->expects($this->any())->method('connect')->with($this->stringContains(':80?hostname=example.com'))->willReturn($pending); - - $promise = $this->connector->connect('example.com:80'); - $this->loop->addTimer(0.06 * (count($ipv4) + count($ipv6)), function () use ($promise) { - $promise->cancel(); - }); - - $this->loop->run(); - } - - /** - * @expectedException RuntimeException - * @expectedExceptionMessage All attempts to connect to "example.com" have failed - * @dataProvider provideIpvAddresses - */ - public function testCancelDuringTcpConnectionCancelsTcpConnectionWithTcpRejectionAfterDnsIsResolved(array $ipv6, array $ipv4) - { - $first = new Deferred(); - $second = new Deferred(); - $this->resolver->expects($this->at(0))->method('resolveAll')->with($this->equalTo('example.com'), Message::TYPE_AAAA)->willReturn($first->promise()); - $this->resolver->expects($this->at(1))->method('resolveAll')->with($this->equalTo('example.com'), Message::TYPE_A)->willReturn($second->promise()); - $pending = new Promise\Promise(function () { }, function () { - throw new \RuntimeException('Connection cancelled'); - }); - $this->tcp->expects($this->exactly(count($ipv6) + count($ipv4)))->method('connect')->with($this->stringContains(':80?hostname=example.com'))->willReturn($pending); - - $promise = $this->connector->connect('example.com:80'); - $first->resolve($ipv6); - $second->resolve($ipv4); - - $that = $this; - $this->loop->addTimer(0.8, function () use ($promise, $that) { - $promise->cancel(); - - $that->throwRejection($promise); - }); - - $this->loop->run(); - } - /** * @dataProvider provideIpvAddresses */ From 511f85d7d8e791d7352e241435e3ff43d34af764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 10 Mar 2020 16:21:44 +0100 Subject: [PATCH 2/2] Fix resolution delay (50ms) and simplify related timer logic --- src/HappyEyeBallsConnectionBuilder.php | 72 ++++++++++---------- tests/FunctionalConnectorTest.php | 48 ++++++------- tests/HappyEyeBallsConnectionBuilderTest.php | 4 +- 3 files changed, 61 insertions(+), 63 deletions(-) diff --git a/src/HappyEyeBallsConnectionBuilder.php b/src/HappyEyeBallsConnectionBuilder.php index 09380685..57d6150c 100644 --- a/src/HappyEyeBallsConnectionBuilder.php +++ b/src/HappyEyeBallsConnectionBuilder.php @@ -14,8 +14,21 @@ */ final class HappyEyeBallsConnectionBuilder { - const CONNECT_INTERVAL = 0.1; - const RESOLVE_WAIT = 0.5; + /** + * As long as we haven't connected yet keep popping an IP address of the connect queue until one of them + * succeeds or they all fail. We will wait 100ms between connection attempts as per RFC. + * + * @link https://tools.ietf.org/html/rfc8305#section-5 + */ + const CONNECTION_ATTEMPT_DELAY = 0.1; + + /** + * Delay `A` lookup by 50ms sending out connection to IPv4 addresses when IPv6 records haven't + * resolved yet as per RFC. + * + * @link https://tools.ietf.org/html/rfc8305#section-3 + */ + const RESOLUTION_DELAY = 0.05; public $loop; public $connector; @@ -29,7 +42,7 @@ final class HappyEyeBallsConnectionBuilder public $resolverPromises = array(); public $connectionPromises = array(); public $connectQueue = array(); - public $timer; + public $nextAttemptTimer; public $parts; public $ipsCount = 0; public $failureCount = 0; @@ -58,7 +71,7 @@ public function connect() $that->mixIpsIntoConnectQueue($ips); - if ($that->timer instanceof TimerInterface) { + if ($that->nextAttemptTimer instanceof TimerInterface) { return; } @@ -66,32 +79,20 @@ public function connect() }; }; - $ipv4Deferred = null; - $that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA))->then(function () use (&$ipv4Deferred) { - if ($ipv4Deferred instanceof Promise\Deferred) { - $ipv4Deferred->resolve(); - } - }); - $that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function ($ips) use ($that, &$ipv4Deferred, &$timer) { + $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 ($ips) use ($that, &$timer) { + // happy path: IPv6 has resolved already, continue with IPv4 addresses if ($that->resolved[Message::TYPE_AAAA] === true) { - return Promise\resolve($ips); + return $ips; } - /** - * Delay A lookup by 50ms sending out connection to IPv4 addresses when IPv6 records haven't - * resolved yet as per RFC. - * - * @link https://tools.ietf.org/html/rfc8305#section-3 - */ - $ipv4Deferred = new Promise\Deferred(); + // Otherwise delay processing IPv4 lookup until short timer passes or IPv6 resolves in the meantime $deferred = new Promise\Deferred(); - - $timer = $that->loop->addTimer($that::RESOLVE_WAIT, function () use ($deferred, $ips) { - $ipv4Deferred = null; + $timer = $that->loop->addTimer($that::RESOLUTION_DELAY, function () use ($deferred, $ips) { $deferred->resolve($ips); }); - $ipv4Deferred->promise()->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); }); @@ -124,7 +125,6 @@ public function resolve($type, $reject) } if ($that->ipsCount === 0) { - $that->resolved = null; $that->resolverPromises = null; $reject(new \RuntimeException('Connection to ' . $that->uri . ' failed during DNS lookup: DNS error')); } @@ -136,9 +136,9 @@ public function resolve($type, $reject) */ public function check($resolve, $reject) { - if (\count($this->connectQueue) === 0 && $this->resolved[Message::TYPE_A] === true && $this->resolved[Message::TYPE_AAAA] === true && $this->timer instanceof TimerInterface) { - $this->loop->cancelTimer($this->timer); - $this->timer = null; + if (\count($this->connectQueue) === 0 && $this->resolved[Message::TYPE_A] === true && $this->resolved[Message::TYPE_AAAA] === true && $this->nextAttemptTimer instanceof TimerInterface) { + $this->loop->cancelTimer($this->nextAttemptTimer); + $this->nextAttemptTimer = null; } if (\count($this->connectQueue) === 0) { @@ -154,7 +154,7 @@ public function check($resolve, $reject) $that->cleanUp(); $resolve($connection); - }, function () use ($that, $ip, $resolve, $reject) { + }, function () use ($that, $ip, $reject) { unset($that->connectionPromises[$ip]); $that->failureCount++; @@ -176,8 +176,8 @@ public function check($resolve, $reject) * * @link https://tools.ietf.org/html/rfc8305#section-5 */ - if ((\count($this->connectQueue) > 0 || ($this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false)) && $this->timer === null) { - $this->timer = $this->loop->addPeriodicTimer(self::CONNECT_INTERVAL, function () use ($that, $resolve, $reject) { + if ((\count($this->connectQueue) > 0 || ($this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false)) && $this->nextAttemptTimer === null) { + $this->nextAttemptTimer = $this->loop->addPeriodicTimer(self::CONNECTION_ATTEMPT_DELAY, function () use ($that, $resolve, $reject) { $that->check($resolve, $reject); }); } @@ -238,23 +238,21 @@ public function attemptConnection($ip) */ public function cleanUp() { - /** @var CancellablePromiseInterface $promise */ - foreach ($this->connectionPromises as $index => $connectionPromise) { + foreach ($this->connectionPromises as $connectionPromise) { if ($connectionPromise instanceof CancellablePromiseInterface) { $connectionPromise->cancel(); } } - /** @var CancellablePromiseInterface $promise */ - foreach ($this->resolverPromises as $index => $resolverPromise) { + foreach ($this->resolverPromises as $resolverPromise) { if ($resolverPromise instanceof CancellablePromiseInterface) { $resolverPromise->cancel(); } } - if ($this->timer instanceof TimerInterface) { - $this->loop->cancelTimer($this->timer); - $this->timer = null; + if ($this->nextAttemptTimer instanceof TimerInterface) { + $this->loop->cancelTimer($this->nextAttemptTimer); + $this->nextAttemptTimer = null; } } diff --git a/tests/FunctionalConnectorTest.php b/tests/FunctionalConnectorTest.php index 57ef8d5b..11ef8093 100644 --- a/tests/FunctionalConnectorTest.php +++ b/tests/FunctionalConnectorTest.php @@ -56,15 +56,16 @@ public function connectionToRemoteTCP4n6ServerShouldResultInOurIP() */ public function connectionToRemoteTCP4ServerShouldResultInOurIP() { - if ($this->ipv4() === false) { - $this->markTestSkipped('IPv4 connection not supported on this system'); - } - $loop = Factory::create(); $connector = new Connector($loop, array('happy_eyeballs' => true)); - $ip = Block\await($this->request('ipv4.tlund.se', $connector), $loop, self::TIMEOUT); + try { + $ip = Block\await($this->request('ipv4.tlund.se', $connector), $loop, self::TIMEOUT); + } catch (\Exception $e) { + $this->checkIpv4(); + throw $e; + } $this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip); $this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip); @@ -76,15 +77,16 @@ public function connectionToRemoteTCP4ServerShouldResultInOurIP() */ public function connectionToRemoteTCP6ServerShouldResultInOurIP() { - if ($this->ipv6() === false) { - $this->markTestSkipped('IPv6 connection not supported on this system'); - } - $loop = Factory::create(); $connector = new Connector($loop, array('happy_eyeballs' => true)); - $ip = Block\await($this->request('ipv6.tlund.se', $connector), $loop, self::TIMEOUT); + try { + $ip = Block\await($this->request('ipv6.tlund.se', $connector), $loop, self::TIMEOUT); + } catch (\Exception $e) { + $this->checkIpv6(); + throw $e; + } $this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip); $this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip); @@ -105,7 +107,7 @@ private function request($host, ConnectorInterface $connector) { $that = $this; return $connector->connect($host . ':80')->then(function (ConnectionInterface $connection) use ($host) { - $connection->write("GET / HTTP/1.1\r\nHost: " . $host . "\r\n\r\n"); + $connection->write("GET / HTTP/1.1\r\nHost: " . $host . "\r\nConnection: close\r\n\r\n"); return \React\Promise\Stream\buffer($connection); })->then(function ($response) use ($that) { @@ -113,25 +115,25 @@ private function request($host, ConnectorInterface $connector) }); } - private function ipv4() + private function checkIpv4() { - if ($this->ipv4 !== null) { - return $this->ipv4; + if ($this->ipv4 === null) { + $this->ipv4 = !!@file_get_contents('http://ipv4.tlund.se/'); } - $this->ipv4 = !!@file_get_contents('http://ipv4.tlund.se/'); - - return $this->ipv4; + if (!$this->ipv4) { + $this->markTestSkipped('IPv4 connection not supported on this system'); + } } - private function ipv6() + private function checkIpv6() { - if ($this->ipv6 !== null) { - return $this->ipv6; + if ($this->ipv6 === null) { + $this->ipv6 = !!@file_get_contents('http://ipv6.tlund.se/'); } - $this->ipv6 = !!@file_get_contents('http://ipv6.tlund.se/'); - - return $this->ipv6; + if (!$this->ipv6) { + $this->markTestSkipped('IPv6 connection not supported on this system'); + } } } diff --git a/tests/HappyEyeBallsConnectionBuilderTest.php b/tests/HappyEyeBallsConnectionBuilderTest.php index 2c5eb29d..000e42e4 100644 --- a/tests/HappyEyeBallsConnectionBuilderTest.php +++ b/tests/HappyEyeBallsConnectionBuilderTest.php @@ -118,10 +118,8 @@ public function testConnectWillStartTimerAndCancelTimerWhenIpv4ResolvesAndIpv6Re public function testCancelConnectWillRejectPromiseAndCancelBothDnsLookups() { - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->willReturn($timer); - $loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->never())->method('addTimer'); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->never())->method('connect');