Skip to content

Commit e88598d

Browse files
committed
Make happy eyeballs available in Connector with a flag
We'll set it to off until version 2.0.0 to ensure backwards compatibility.
1 parent ca817ad commit e88598d

File tree

4 files changed

+170
-3
lines changed

4 files changed

+170
-3
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,13 @@ $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+
> Aside from defaulting to the `DnsConnector` the `Connector` also supports the
1098+
`HappyEyeBallsConnector` by setting the `happy_eyeballs` option to `true`. It's
1099+
set to false by default to be backwards compatible with older systems only
1100+
supporting IPv4. By setting the `happy_eyeballs` option to `true` new connection
1101+
attempts are made over both IPv6 and IPv4 with the priority for IPv6. And return
1102+
the connection attempt that connects first.
1103+
10971104
### Advanced client usage
10981105

10991106
#### 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' => false,
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/FunctionalConnectorTest.php

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@
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 = false;
17+
private $ipv6 = false;
18+
19+
public function __construct()
20+
{
21+
parent::__construct();
22+
$this->ipv4 = !!@file_get_contents('http://ipv4.tlund.se/');
23+
$this->ipv6 = !!@file_get_contents('http://ipv6.tlund.se/');
24+
}
1325

1426
/** @test */
1527
public function connectionToTcpServerShouldSucceedWithLocalhost()
@@ -29,4 +41,146 @@ public function connectionToTcpServerShouldSucceedWithLocalhost()
2941
$connection->close();
3042
$server->close();
3143
}
44+
45+
/**
46+
* @test
47+
*/
48+
public function connectionToRemoteTCP4n6ServerShouldResultInOurIP()
49+
{
50+
$loop = Factory::create();
51+
52+
$connector = new Connector($loop, array('happy_eyeballs' => true));
53+
54+
$ip = Block\await($this->request('dual.tlund.se', $connector), $loop, self::TIMEOUT);
55+
56+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6), $ip);
57+
}
58+
59+
/**
60+
* @test
61+
*/
62+
public function connectionToRemoteTCP4ServerShouldResultInOurIP()
63+
{
64+
if ($this->ipv4 === false) {
65+
$this->markTestSkipped('IPv4 not supported on this system');
66+
}
67+
68+
$loop = Factory::create();
69+
70+
$connector = new Connector($loop, array('happy_eyeballs' => true));
71+
72+
$ip = Block\await($this->request('ipv4.tlund.se', $connector), $loop, self::TIMEOUT);
73+
74+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip);
75+
$this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip);
76+
}
77+
78+
/**
79+
* @test
80+
*/
81+
public function connectionToRemoteTCP6ServerShouldResultInOurIP()
82+
{
83+
if ($this->ipv6 === false) {
84+
$this->markTestSkipped('IPv6 not supported on this system');
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+
*
100+
* @expectedException \RuntimeException
101+
* @expectedExceptionMessageRegExp /Connection to ipv6.tlund.se:80 failed/
102+
*/
103+
public function tryingToConnectToAnIPv6OnlyHostWithOutHappyEyeBallsShouldResultInFailure()
104+
{
105+
$loop = Factory::create();
106+
107+
$connector = new Connector($loop);
108+
109+
$ip = Block\await($this->request('ipv6.tlund.se', $connector), $loop, self::TIMEOUT);
110+
111+
$this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip);
112+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip);
113+
}
114+
115+
/**
116+
* @test
117+
*
118+
* @expectedException \RuntimeException
119+
* @expectedExceptionMessageRegExp /Connection to tcp:\/\/193.15.228.195:80 failed:/
120+
*/
121+
public function connectingDirectlyToAnIPv4AddressShouldFailWhenIPv4IsntAvailable()
122+
{
123+
if ($this->ipv4 === true) {
124+
$this->markTestSkipped('IPv4 supported on this system');
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+
$ip = Block\await($this->request($host, $connector), $loop, self::TIMEOUT);
134+
135+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip);
136+
$this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip);
137+
}
138+
139+
/**
140+
* @test
141+
*
142+
* @expectedException \RuntimeException
143+
* @expectedExceptionMessageRegExp /Connection to tcp:\/\/\[2a00:801:f::195\]:80 failed:/
144+
*/
145+
public function connectingDirectlyToAnIPv6AddressShouldFailWhenIPv6IsntAvailable()
146+
{
147+
if ($this->ipv6 === true) {
148+
$this->markTestSkipped('IPv6 supported on this system');
149+
}
150+
151+
$loop = Factory::create();
152+
153+
$connector = new Connector($loop);
154+
155+
$host = current(dns_get_record('ipv6.tlund.se', DNS_AAAA));
156+
$host = $host['ipv6'];
157+
$host = '[' . $host . ']';
158+
$ip = Block\await($this->request($host, $connector), $loop, self::TIMEOUT);
159+
160+
$this->assertFalse(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), $ip);
161+
$this->assertSame($ip, filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), $ip);
162+
}
163+
164+
/**
165+
* @internal
166+
*/
167+
public function parseIpFromPage($body)
168+
{
169+
$ex = explode('title="Look up on bgp.he.net">', $body);
170+
$ex = explode('<', $ex[1]);
171+
172+
return $ex[0];
173+
}
174+
175+
private function request($host, ConnectorInterface $connector)
176+
{
177+
$that = $this;
178+
return $connector->connect($host . ':80')->then(function (ConnectionInterface $connection) use ($host) {
179+
$connection->write("GET / HTTP/1.1\r\nHost: " . $host . "\r\n\r\n");
180+
181+
return \React\Promise\Stream\buffer($connection);
182+
})->then(function ($response) use ($that) {
183+
return $that->parseIpFromPage($response);
184+
});
185+
}
32186
}

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