TcpConnectorTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. namespace React\Tests\Socket;
  3. use React\EventLoop\Loop;
  4. use React\Socket\ConnectionInterface;
  5. use React\Socket\TcpConnector;
  6. use React\Socket\TcpServer;
  7. use React\Promise\Promise;
  8. class TcpConnectorTest extends TestCase
  9. {
  10. const TIMEOUT = 5.0;
  11. public function testConstructWithoutLoopAssignsLoopAutomatically()
  12. {
  13. $connector = new TcpConnector();
  14. $ref = new \ReflectionProperty($connector, 'loop');
  15. $ref->setAccessible(true);
  16. $loop = $ref->getValue($connector);
  17. $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
  18. }
  19. /** @test */
  20. public function connectionToEmptyPortShouldFailWithoutCallingCustomErrorHandler()
  21. {
  22. $connector = new TcpConnector();
  23. $promise = $connector->connect('127.0.0.1:9999');
  24. $error = null;
  25. set_error_handler(function ($_, $errstr) use (&$error) {
  26. $error = $errstr;
  27. });
  28. $this->setExpectedException(
  29. 'RuntimeException',
  30. 'Connection to tcp://127.0.0.1:9999 failed: Connection refused' . (function_exists('socket_import_stream') ? ' (ECONNREFUSED)' : ''),
  31. defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
  32. );
  33. try {
  34. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  35. restore_error_handler();
  36. } catch (\Exception $e) {
  37. restore_error_handler();
  38. $this->assertNull($error);
  39. throw $e;
  40. }
  41. }
  42. /** @test */
  43. public function connectionToTcpServerShouldAddResourceToLoop()
  44. {
  45. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  46. $connector = new TcpConnector($loop);
  47. $server = new TcpServer(0, $loop);
  48. $valid = false;
  49. $loop->expects($this->once())->method('addWriteStream')->with($this->callback(function ($arg) use (&$valid) {
  50. $valid = is_resource($arg);
  51. return true;
  52. }));
  53. $connector->connect($server->getAddress());
  54. $this->assertTrue($valid);
  55. }
  56. /** @test */
  57. public function connectionToTcpServerShouldSucceed()
  58. {
  59. $server = new TcpServer(9999);
  60. $connector = new TcpConnector();
  61. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  62. $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection);
  63. $connection->close();
  64. $server->close();
  65. }
  66. /** @test */
  67. public function connectionToTcpServerShouldFailIfFileDescriptorsAreExceeded()
  68. {
  69. $connector = new TcpConnector();
  70. /** @var string[] $_ */
  71. /** @var int $exit */
  72. $ulimit = exec('ulimit -n 2>&1', $_, $exit);
  73. if ($exit !== 0 || $ulimit < 1) {
  74. $this->markTestSkipped('Unable to determine limit of open files (ulimit not available?)');
  75. }
  76. $memory = ini_get('memory_limit');
  77. if ($memory === '-1') {
  78. $memory = PHP_INT_MAX;
  79. } elseif (preg_match('/^\d+G$/i', $memory)) {
  80. $memory = ((int) $memory) * 1024 * 1024 * 1024;
  81. } elseif (preg_match('/^\d+M$/i', $memory)) {
  82. $memory = ((int) $memory) * 1024 * 1024;
  83. } elseif (preg_match('/^\d+K$/i', $memory)) {
  84. $memory = ((int) $memory) * 1024;
  85. }
  86. // each file descriptor takes ~600 bytes of memory, so skip test if this would exceed memory_limit
  87. if ($ulimit * 600 > $memory || $ulimit > 100000) {
  88. $this->markTestSkipped('Test requires ~' . round($ulimit * 600 / 1024 / 1024) . '/' . round($memory / 1024 / 1024) . ' MiB memory with ' . $ulimit . ' file descriptors');
  89. }
  90. // dummy rejected promise to make sure autoloader has initialized all classes
  91. class_exists('React\Socket\SocketServer', true);
  92. class_exists('PHPUnit\Framework\Error\Warning', true);
  93. $promise = new Promise(function () { throw new \RuntimeException('dummy'); });
  94. $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection
  95. unset($promise);
  96. // keep creating dummy file handles until all file descriptors are exhausted
  97. $fds = array();
  98. for ($i = 0; $i < $ulimit; ++$i) {
  99. $fd = @fopen('/dev/null', 'r');
  100. if ($fd === false) {
  101. break;
  102. }
  103. $fds[] = $fd;
  104. }
  105. $this->setExpectedException('RuntimeException');
  106. \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  107. }
  108. /** @test */
  109. public function connectionToInvalidNetworkShouldFailWithUnreachableError()
  110. {
  111. if (PHP_OS !== 'Linux' && !function_exists('socket_import_stream')) {
  112. $this->markTestSkipped('Test requires either Linux or ext-sockets on PHP 5.4+');
  113. }
  114. $enetunreach = defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101;
  115. // try to find an unreachable network by trying a couple of private network addresses
  116. $errno = 0;
  117. $errstr = '';
  118. for ($i = 0; $i < 20 && $errno !== $enetunreach; ++$i) {
  119. $address = 'tcp://192.168.' . mt_rand(0, 255) . '.' . mt_rand(1, 254) . ':8123';
  120. $client = @stream_socket_client($address, $errno, $errstr, 0.1);
  121. }
  122. if ($client || $errno !== $enetunreach) {
  123. $this->markTestSkipped('Expected error ' . $enetunreach . ' but got ' . $errno . ' (' . $errstr . ') for ' . $address);
  124. }
  125. $connector = new TcpConnector();
  126. $promise = $connector->connect($address);
  127. $this->setExpectedException(
  128. 'RuntimeException',
  129. 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) . ' (ENETUNREACH)' : 'Network is unreachable'),
  130. $enetunreach
  131. );
  132. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  133. }
  134. /** @test */
  135. public function connectionToTcpServerShouldSucceedWithRemoteAdressSameAsTarget()
  136. {
  137. $server = new TcpServer(9999);
  138. $connector = new TcpConnector();
  139. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  140. /* @var $connection ConnectionInterface */
  141. $this->assertEquals('tcp://127.0.0.1:9999', $connection->getRemoteAddress());
  142. $connection->close();
  143. $server->close();
  144. }
  145. /** @test */
  146. public function connectionToTcpServerShouldSucceedWithLocalAdressOnLocalhost()
  147. {
  148. $server = new TcpServer(9999);
  149. $connector = new TcpConnector();
  150. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  151. /* @var $connection ConnectionInterface */
  152. $this->assertContainsString('tcp://127.0.0.1:', $connection->getLocalAddress());
  153. $this->assertNotEquals('tcp://127.0.0.1:9999', $connection->getLocalAddress());
  154. $connection->close();
  155. $server->close();
  156. }
  157. /** @test */
  158. public function connectionToTcpServerShouldSucceedWithNullAddressesAfterConnectionClosed()
  159. {
  160. $server = new TcpServer(9999);
  161. $connector = new TcpConnector();
  162. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('127.0.0.1:9999'), self::TIMEOUT));
  163. /* @var $connection ConnectionInterface */
  164. $server->close();
  165. $connection->close();
  166. $this->assertNull($connection->getRemoteAddress());
  167. $this->assertNull($connection->getLocalAddress());
  168. }
  169. /** @test */
  170. public function connectionToTcpServerWillCloseWhenOtherSideCloses()
  171. {
  172. // immediately close connection and server once connection is in
  173. $server = new TcpServer(0);
  174. $server->on('connection', function (ConnectionInterface $conn) use ($server) {
  175. $conn->close();
  176. $server->close();
  177. });
  178. $once = $this->expectCallableOnce();
  179. $connector = new TcpConnector();
  180. $connector->connect($server->getAddress())->then(function (ConnectionInterface $conn) use ($once) {
  181. $conn->write('hello');
  182. $conn->on('close', $once);
  183. });
  184. Loop::run();
  185. }
  186. /** @test
  187. * @group test
  188. */
  189. public function connectionToEmptyIp6PortShouldFail()
  190. {
  191. $connector = new TcpConnector();
  192. $connector
  193. ->connect('[::1]:9999')
  194. ->then($this->expectCallableNever(), $this->expectCallableOnce());
  195. Loop::run();
  196. }
  197. /** @test */
  198. public function connectionToIp6TcpServerShouldSucceed()
  199. {
  200. try {
  201. $server = new TcpServer('[::1]:9999');
  202. } catch (\Exception $e) {
  203. $this->markTestSkipped('Unable to start IPv6 server socket (IPv6 not supported on this system?)');
  204. }
  205. $connector = new TcpConnector();
  206. $connection = \React\Async\await(\React\Promise\Timer\timeout($connector->connect('[::1]:9999'), self::TIMEOUT));
  207. /* @var $connection ConnectionInterface */
  208. $this->assertEquals('tcp://[::1]:9999', $connection->getRemoteAddress());
  209. $this->assertContainsString('tcp://[::1]:', $connection->getLocalAddress());
  210. $this->assertNotEquals('tcp://[::1]:9999', $connection->getLocalAddress());
  211. $connection->close();
  212. $server->close();
  213. }
  214. /** @test */
  215. public function connectionToHostnameShouldFailImmediately()
  216. {
  217. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  218. $connector = new TcpConnector($loop);
  219. $promise = $connector->connect('www.google.com:80');
  220. $promise->then(null, $this->expectCallableOnceWithException(
  221. 'InvalidArgumentException',
  222. 'Given URI "tcp://www.google.com:80" does not contain a valid host IP (EINVAL)',
  223. defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
  224. ));
  225. }
  226. /** @test */
  227. public function connectionToInvalidPortShouldFailImmediately()
  228. {
  229. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  230. $connector = new TcpConnector($loop);
  231. $promise = $connector->connect('255.255.255.255:12345678');
  232. $promise->then(null, $this->expectCallableOnceWithException(
  233. 'InvalidArgumentException',
  234. 'Given URI "tcp://255.255.255.255:12345678" is invalid (EINVAL)',
  235. defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
  236. ));
  237. }
  238. /** @test */
  239. public function connectionToInvalidSchemeShouldFailImmediately()
  240. {
  241. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  242. $connector = new TcpConnector($loop);
  243. $connector->connect('tls://google.com:443')->then(
  244. $this->expectCallableNever(),
  245. $this->expectCallableOnce()
  246. );
  247. }
  248. /** @test */
  249. public function cancellingConnectionShouldRemoveResourceFromLoopAndCloseResource()
  250. {
  251. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  252. $connector = new TcpConnector($loop);
  253. $server = new TcpServer(0, $loop);
  254. $server->on('connection', $this->expectCallableNever());
  255. $loop->expects($this->once())->method('addWriteStream');
  256. $promise = $connector->connect($server->getAddress());
  257. $resource = null;
  258. $valid = false;
  259. $loop->expects($this->once())->method('removeWriteStream')->with($this->callback(function ($arg) use (&$resource, &$valid) {
  260. $resource = $arg;
  261. $valid = is_resource($arg);
  262. return true;
  263. }));
  264. $promise->cancel();
  265. // ensure that this was a valid resource during the removeWriteStream() call
  266. $this->assertTrue($valid);
  267. // ensure that this resource should now be closed after the cancel() call
  268. $this->assertFalse(is_resource($resource));
  269. }
  270. /** @test */
  271. public function cancellingConnectionShouldRejectPromise()
  272. {
  273. $connector = new TcpConnector();
  274. $server = new TcpServer(0);
  275. $promise = $connector->connect($server->getAddress());
  276. $promise->cancel();
  277. $this->setExpectedException(
  278. 'RuntimeException',
  279. 'Connection to ' . $server->getAddress() . ' cancelled during TCP/IP handshake (ECONNABORTED)',
  280. defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
  281. );
  282. try {
  283. \React\Async\await($promise);
  284. } catch (\Exception $e) {
  285. $server->close();
  286. throw $e;
  287. }
  288. }
  289. public function testCancelDuringConnectionShouldNotCreateAnyGarbageReferences()
  290. {
  291. if (class_exists('React\Promise\When')) {
  292. $this->markTestSkipped('Not supported on legacy Promise v1 API');
  293. }
  294. while (gc_collect_cycles()) {
  295. // collect all garbage cycles
  296. }
  297. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  298. $connector = new TcpConnector($loop);
  299. $promise = $connector->connect('127.0.0.1:9999');
  300. $promise->cancel();
  301. unset($promise);
  302. $this->assertEquals(0, gc_collect_cycles());
  303. }
  304. }