Skip to content

Commit 4d9ea52

Browse files
committed
Make happy eyeballs available in Connector with a flag
1 parent ca817ad commit 4d9ea52

File tree

5 files changed

+200
-6
lines changed

5 files changed

+200
-6
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,7 @@ pass an instance implementing the `ConnectorInterface` like this:
10611061
```php
10621062
$dnsResolverFactory = new React\Dns\Resolver\Factory();
10631063
$resolver = $dnsResolverFactory->createCached('127.0.1.1', $loop);
1064-
$tcp = new React\Socket\DnsConnector(new React\Socket\TcpConnector($loop), $resolver);
1064+
$tcp = new React\Socket\HappyEyeBallsConnector($loop, new React\Socket\TcpConnector($loop), $resolver);
10651065

10661066
$tls = new React\Socket\SecureConnector($tcp, $loop);
10671067

@@ -1094,6 +1094,17 @@ $connector->connect('google.com:80')->then(function (React\Socket\ConnectionInte
10941094
Internally, the `tcp://` and `tls://` connectors will always be wrapped by
10951095
`TimeoutConnector`, unless you disable timeouts like in the above example.
10961096

1097+
> Internally the `HappyEyeBallsConnector` has replaced the `DnsConnector` as default
1098+
resolving connector. It is still available as `Connector` has a new option, namely
1099+
`happy_eyeballs`, to control which of the two will be used. By default it's `true`
1100+
and will use `HappyEyeBallsConnector`, when set to `false` `DnsConnector` is used.
1101+
We only recommend doing so when there are any backwards compatible issues on older
1102+
systems only supporting IPv4. The `HappyEyeBallsConnector` implements most of
1103+
RFC6555 and RFC8305 and will use concurrency to connect to the remote host by
1104+
attempting to connect over both IPv4 and IPv6 with a priority for IPv6 when
1105+
available. Which ever connection attempt succeeds first will be used, the rest
1106+
connection attempts will be canceled.
1107+
10971108
### Advanced client usage
10981109

10991110
#### TcpConnector

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
},
1515
"require-dev": {
1616
"clue/block-react": "^1.2",
17-
"phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35"
17+
"phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35",
18+
"react/promise-stream": "^1.2"
1819
},
1920
"autoload": {
2021
"psr-4": {

src/Connector.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function __construct(LoopInterface $loop, array $options = array())
3636

3737
'dns' => true,
3838
'timeout' => true,
39+
'happy_eyeballs' => \PHP_VERSION_ID < 70000 ? false : true,
3940
);
4041

4142
if ($options['timeout'] === true) {
@@ -70,7 +71,11 @@ public function __construct(LoopInterface $loop, array $options = array())
7071
);
7172
}
7273

73-
$tcp = new DnsConnector($tcp, $resolver);
74+
if ($options['happy_eyeballs'] === true) {
75+
$tcp = new HappyEyeBallsConnector($loop, $tcp, $resolver);
76+
} else {
77+
$tcp = new DnsConnector($tcp, $resolver);
78+
}
7479
}
7580

7681
if ($options['tcp'] !== false) {

tests/ConnectorTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ public function testConnectorUsesGivenResolverInstance()
100100
$resolver->expects($this->once())->method('resolve')->with('google.com')->willReturn($promise);
101101

102102
$connector = new Connector($loop, array(
103-
'dns' => $resolver
103+
'dns' => $resolver,
104+
'happy_eyeballs' => false,
104105
));
105106

106107
$connector->connect('google.com:80');
@@ -120,7 +121,8 @@ public function testConnectorUsesResolvedHostnameIfDnsIsUsed()
120121

121122
$connector = new Connector($loop, array(
122123
'tcp' => $tcp,
123-
'dns' => $resolver
124+
'dns' => $resolver,
125+
'happy_eyeballs' => false,
124126
));
125127

126128
$connector->connect('tcp://google.com:80');

tests/FunctionalConnectorTest.php

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
use Clue\React\Block;
66
use React\EventLoop\Factory;
7+
use React\Socket\ConnectionInterface;
78
use React\Socket\Connector;
9+
use React\Socket\ConnectorInterface;
810
use React\Socket\TcpServer;
911

1012
class FunctionalConnectorTest extends TestCase
1113
{
12-
const TIMEOUT = 1.0;
14+
const TIMEOUT = 30.0;
15+
16+
private $ipv4;
17+
private $ipv6;
1318

1419
/** @test */
1520
public function connectionToTcpServerShouldSucceedWithLocalhost()
@@ -29,4 +34,174 @@ public function connectionToTcpServerShouldSucceedWithLocalhost()
2934
$connection->close();
3035
$server->close();
3136
}
37+
38+
/**
39+
* @test
40+
* @group internet
41+
*/
42+
public function connectionToRemoteTCP4n6ServerShouldResultInOurIP()
43+
{
44+
$loop = Factory::create();
45+
46+
$connector = new Connector($loop, array('happy_eyeballs' => true));
47+
48+
$ip = Block\await($this->request('dual.tlund.se', $connector), $loop, self::TIMEOUT);
49+
50+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6), $ip);
51+
}
52+
53+
/**
54+
* @test
55+
* @group internet
56+
*/
57+
public function connectionToRemoteTCP4ServerShouldResultInOurIP()
58+
{
59+
if ($this->ipv4() === false) {
60+
// IPv4 not supported on this system
61+
$this->assertFalse($this->ipv4());
62+
return;
63+
}
64+
65+
$loop = Factory::create();
66+
67+
$connector = new Connector($loop, array('happy_eyeballs' => true));
68+
69+
$ip = Block\await($this->request('ipv4.tlund.se', $connector), $loop, self::TIMEOUT);
70+
71+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip);
72+
$this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip);
73+
}
74+
75+
/**
76+
* @test
77+
* @group internet
78+
*/
79+
public function connectionToRemoteTCP6ServerShouldResultInOurIP()
80+
{
81+
if ($this->ipv6() === false) {
82+
// IPv6 not supported on this system
83+
$this->assertFalse($this->ipv6());
84+
return;
85+
}
86+
87+
$loop = Factory::create();
88+
89+
$connector = new Connector($loop, array('happy_eyeballs' => true));
90+
91+
$ip = Block\await($this->request('ipv6.tlund.se', $connector), $loop, self::TIMEOUT);
92+
93+
$this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip);
94+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip);
95+
}
96+
97+
/**
98+
* @test
99+
* @group internet
100+
*
101+
* @expectedException \RuntimeException
102+
* @expectedExceptionMessageRegExp /Connection to ipv6.tlund.se:80 failed/
103+
*/
104+
public function tryingToConnectToAnIPv6OnlyHostWithOutHappyEyeBallsShouldResultInFailure()
105+
{
106+
$loop = Factory::create();
107+
108+
$connector = new Connector($loop, array('happy_eyeballs' => false));
109+
110+
Block\await($this->request('ipv6.tlund.se', $connector), $loop, self::TIMEOUT);
111+
}
112+
113+
/**
114+
* @test
115+
* @group internet
116+
*
117+
* @expectedException \RuntimeException
118+
* @expectedExceptionMessageRegExp /Connection to tcp:\/\/193.15.228.195:80 failed:/
119+
*/
120+
public function connectingDirectlyToAnIPv4AddressShouldFailWhenIPv4IsntAvailable()
121+
{
122+
if ($this->ipv4() === true) {
123+
// IPv4 supported on this system
124+
throw new \RuntimeException('Connection to tcp://193.15.228.195:80 failed:');
125+
}
126+
127+
$loop = Factory::create();
128+
129+
$connector = new Connector($loop);
130+
131+
$host = current(dns_get_record('ipv4.tlund.se', DNS_A));
132+
$host = $host['ip'];
133+
Block\await($this->request($host, $connector), $loop, self::TIMEOUT);
134+
}
135+
136+
/**
137+
* @test
138+
* @group internet
139+
*
140+
* @expectedException \RuntimeException
141+
* @expectedExceptionMessageRegExp /Connection to tcp:\/\/\[2a00:801:f::195\]:80 failed:/
142+
*/
143+
public function connectingDirectlyToAnIPv6AddressShouldFailWhenIPv6IsntAvailable()
144+
{
145+
if ($this->ipv6() === true) {
146+
// IPv6 supported on this system
147+
throw new \RuntimeException('Connection to tcp://[2a00:801:f::195]:80 failed:');
148+
}
149+
150+
$loop = Factory::create();
151+
152+
$connector = new Connector($loop);
153+
154+
$host = current(dns_get_record('ipv6.tlund.se', DNS_AAAA));
155+
$host = $host['ipv6'];
156+
$host = '[' . $host . ']';
157+
$ip = Block\await($this->request($host, $connector), $loop, self::TIMEOUT);
158+
159+
$this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip);
160+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip);
161+
}
162+
163+
/**
164+
* @internal
165+
*/
166+
public function parseIpFromPage($body)
167+
{
168+
$ex = explode('title="Look up on bgp.he.net">', $body);
169+
$ex = explode('<', $ex[1]);
170+
171+
return $ex[0];
172+
}
173+
174+
private function request($host, ConnectorInterface $connector)
175+
{
176+
$that = $this;
177+
return $connector->connect($host . ':80')->then(function (ConnectionInterface $connection) use ($host) {
178+
$connection->write("GET / HTTP/1.1\r\nHost: " . $host . "\r\n\r\n");
179+
180+
return \React\Promise\Stream\buffer($connection);
181+
})->then(function ($response) use ($that) {
182+
return $that->parseIpFromPage($response);
183+
});
184+
}
185+
186+
private function ipv4()
187+
{
188+
if ($this->ipv4 !== null) {
189+
return $this->ipv4;
190+
}
191+
192+
$this->ipv4 = !!@file_get_contents('http://ipv4.tlund.se/');
193+
194+
return $this->ipv4;
195+
}
196+
197+
private function ipv6()
198+
{
199+
if ($this->ipv6 !== null) {
200+
return $this->ipv6;
201+
}
202+
203+
$this->ipv6 = !!@file_get_contents('http://ipv6.tlund.se/');
204+
205+
return $this->ipv6;
206+
}
32207
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy