Skip to content

Commit 770727f

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 770727f

File tree

3 files changed

+163
-3
lines changed

3 files changed

+163
-3
lines changed

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)