FunctionalSecureServerTest.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. <?php
  2. namespace React\Tests\Socket;
  3. use React\Promise\Promise;
  4. use React\Socket\ConnectionInterface;
  5. use React\Socket\SecureConnector;
  6. use React\Socket\ServerInterface;
  7. use React\Socket\SecureServer;
  8. use React\Socket\TcpConnector;
  9. use React\Socket\TcpServer;
  10. class FunctionalSecureServerTest extends TestCase
  11. {
  12. const TIMEOUT = 2;
  13. /**
  14. * @before
  15. */
  16. public function setUpSkipTest()
  17. {
  18. if (defined('HHVM_VERSION')) {
  19. $this->markTestSkipped('Not supported on legacy HHVM');
  20. }
  21. }
  22. public function testClientCanConnectToServer()
  23. {
  24. $server = new TcpServer(0);
  25. $server = new SecureServer($server, null, array(
  26. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  27. ));
  28. $connector = new SecureConnector(new TcpConnector(), null, array(
  29. 'verify_peer' => false
  30. ));
  31. $promise = $connector->connect($server->getAddress());
  32. /* @var ConnectionInterface $client */
  33. $client = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  34. $this->assertInstanceOf('React\Socket\ConnectionInterface', $client);
  35. $this->assertEquals($server->getAddress(), $client->getRemoteAddress());
  36. $client->close();
  37. $server->close();
  38. }
  39. public function testClientUsesTls13ByDefaultWhenSupportedByOpenSSL()
  40. {
  41. if (PHP_VERSION_ID < 70000 || (PHP_VERSION_ID >= 70300 && PHP_VERSION_ID < 70400) || !$this->supportsTls13()) {
  42. // @link https://github.com/php/php-src/pull/3909 explicitly adds TLS 1.3 on PHP 7.4
  43. // @link https://github.com/php/php-src/pull/3317 implicitly limits to TLS 1.2 on PHP 7.3
  44. // all older PHP versions support TLS 1.3 (provided OpenSSL supports it), but only PHP 7 allows checking the version
  45. $this->markTestSkipped('Test requires PHP 7+ for crypto meta data (but excludes PHP 7.3 because it implicitly limits to TLS 1.2) and OpenSSL 1.1.1+ for TLS 1.3');
  46. }
  47. $server = new TcpServer(0);
  48. $server = new SecureServer($server, null, array(
  49. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  50. ));
  51. $connector = new SecureConnector(new TcpConnector(), null, array(
  52. 'verify_peer' => false
  53. ));
  54. $promise = $connector->connect($server->getAddress());
  55. /* @var ConnectionInterface $client */
  56. $client = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  57. $this->assertInstanceOf('React\Socket\Connection', $client);
  58. $this->assertTrue(isset($client->stream));
  59. $meta = stream_get_meta_data($client->stream);
  60. $this->assertTrue(isset($meta['crypto']['protocol']));
  61. if ($meta['crypto']['protocol'] === 'UNKNOWN') {
  62. // TLSv1.3 protocol will only be added via https://github.com/php/php-src/pull/3700
  63. // prior to merging that PR, this info is still available in the cipher version by OpenSSL
  64. $this->assertTrue(isset($meta['crypto']['cipher_version']));
  65. $this->assertEquals('TLSv1.3', $meta['crypto']['cipher_version']);
  66. } else {
  67. $this->assertEquals('TLSv1.3', $meta['crypto']['protocol']);
  68. }
  69. $client->close();
  70. $server->close();
  71. }
  72. public function testClientUsesTls12WhenCryptoMethodIsExplicitlyConfiguredByClient()
  73. {
  74. if (PHP_VERSION_ID < 70000) {
  75. $this->markTestSkipped('Test requires PHP 7+ for crypto meta data');
  76. }
  77. $server = new TcpServer(0);
  78. $server = new SecureServer($server, null, array(
  79. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  80. ));
  81. $connector = new SecureConnector(new TcpConnector(), null, array(
  82. 'verify_peer' => false,
  83. 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
  84. ));
  85. $promise = $connector->connect($server->getAddress());
  86. /* @var ConnectionInterface $client */
  87. $client = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  88. $this->assertInstanceOf('React\Socket\Connection', $client);
  89. $this->assertTrue(isset($client->stream));
  90. $meta = stream_get_meta_data($client->stream);
  91. $this->assertTrue(isset($meta['crypto']['protocol']));
  92. $this->assertEquals('TLSv1.2', $meta['crypto']['protocol']);
  93. $client->close();
  94. $server->close();
  95. }
  96. public function testClientUsesTls12WhenCryptoMethodIsExplicitlyConfiguredByServer()
  97. {
  98. if (PHP_VERSION_ID < 70000) {
  99. $this->markTestSkipped('Test requires PHP 7+ for crypto meta data');
  100. }
  101. $server = new TcpServer(0);
  102. $server = new SecureServer($server, null, array(
  103. 'local_cert' => __DIR__ . '/../examples/localhost.pem',
  104. 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
  105. ));
  106. $connector = new SecureConnector(new TcpConnector(), null, array(
  107. 'verify_peer' => false
  108. ));
  109. $promise = $connector->connect($server->getAddress());
  110. /* @var ConnectionInterface $client */
  111. $client = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  112. $this->assertInstanceOf('React\Socket\Connection', $client);
  113. $this->assertTrue(isset($client->stream));
  114. $meta = stream_get_meta_data($client->stream);
  115. $this->assertTrue(isset($meta['crypto']['protocol']));
  116. $this->assertEquals('TLSv1.2', $meta['crypto']['protocol']);
  117. $client->close();
  118. $server->close();
  119. }
  120. public function testClientUsesTls10WhenCryptoMethodIsExplicitlyConfiguredByClient()
  121. {
  122. if (PHP_VERSION_ID < 70000) {
  123. $this->markTestSkipped('Test requires PHP 7+ for crypto meta data');
  124. }
  125. $server = new TcpServer(0);
  126. $server = new SecureServer($server, null, array(
  127. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  128. ));
  129. $connector = new SecureConnector(new TcpConnector(), null, array(
  130. 'verify_peer' => false,
  131. 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
  132. ));
  133. $promise = $connector->connect($server->getAddress());
  134. /* @var ConnectionInterface $client */
  135. try {
  136. $client = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  137. } catch (\RuntimeException $e) {
  138. // legacy TLS 1.0 would be considered insecure by today's standards, so skip test if connection fails
  139. // OpenSSL error messages are version/platform specific
  140. // […] no protocols available
  141. // […] routines:state_machine:internal error
  142. // SSL operation failed with code 1. OpenSSL Error messages: error:0A000438:SSL routines::tlsv1 alert internal error
  143. // Connection lost during TLS handshake (ECONNRESET)
  144. $server->close();
  145. $this->markTestSkipped('TLS 1.0 not available on this system (' . $e->getMessage() . ')');
  146. }
  147. $this->assertInstanceOf('React\Socket\Connection', $client);
  148. $this->assertTrue(isset($client->stream));
  149. $meta = stream_get_meta_data($client->stream);
  150. $this->assertTrue(isset($meta['crypto']['protocol']));
  151. $this->assertEquals('TLSv1', $meta['crypto']['protocol']);
  152. $client->close();
  153. $server->close();
  154. }
  155. public function testServerEmitsConnectionForClientConnection()
  156. {
  157. $server = new TcpServer(0);
  158. $server = new SecureServer($server, null, array(
  159. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  160. ));
  161. $peer = new Promise(function ($resolve, $reject) use ($server) {
  162. $server->on('connection', $resolve);
  163. $server->on('error', $reject);
  164. });
  165. $connector = new SecureConnector(new TcpConnector(), null, array(
  166. 'verify_peer' => false
  167. ));
  168. $client = $connector->connect($server->getAddress());
  169. // await both client and server side end of connection
  170. /* @var ConnectionInterface[] $both */
  171. $both = \React\Async\await(\React\Promise\Timer\timeout(\React\Promise\all(array($peer, $client)), self::TIMEOUT));
  172. // both ends of the connection are represented by different instances of ConnectionInterface
  173. $this->assertCount(2, $both);
  174. $this->assertInstanceOf('React\Socket\ConnectionInterface', $both[0]);
  175. $this->assertInstanceOf('React\Socket\ConnectionInterface', $both[1]);
  176. $this->assertNotSame($both[0], $both[1]);
  177. // server side end has local server address and client end has remote server address
  178. $this->assertEquals($server->getAddress(), $both[0]->getLocalAddress());
  179. $this->assertEquals($server->getAddress(), $both[1]->getRemoteAddress());
  180. // clean up all connections and server again
  181. $both[0]->close();
  182. $both[1]->close();
  183. $server->close();
  184. }
  185. public function testClientEmitsDataEventOnceForDataWrittenFromServer()
  186. {
  187. $server = new TcpServer(0);
  188. $server = new SecureServer($server, null, array(
  189. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  190. ));
  191. $server->on('connection', function (ConnectionInterface $conn) {
  192. $conn->write('foo');
  193. });
  194. $connector = new SecureConnector(new TcpConnector(), null, array(
  195. 'verify_peer' => false
  196. ));
  197. $connecting = $connector->connect($server->getAddress());
  198. $promise = new Promise(function ($resolve, $reject) use ($connecting) {
  199. $connecting->then(function (ConnectionInterface $connection) use ($resolve) {
  200. $connection->on('data', $resolve);
  201. }, $reject);
  202. });
  203. $data = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  204. $this->assertEquals('foo', $data);
  205. $server->close();
  206. $connecting->then(function (ConnectionInterface $connection) {
  207. $connection->close();
  208. });
  209. }
  210. public function testWritesDataInMultipleChunksToConnection()
  211. {
  212. $server = new TcpServer(0);
  213. $server = new SecureServer($server, null, array(
  214. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  215. ));
  216. $server->on('connection', $this->expectCallableOnce());
  217. $server->on('connection', function (ConnectionInterface $conn) {
  218. $conn->write(str_repeat('*', 400000));
  219. });
  220. $connector = new SecureConnector(new TcpConnector(), null, array(
  221. 'verify_peer' => false
  222. ));
  223. $connecting = $connector->connect($server->getAddress());
  224. $promise = new Promise(function ($resolve, $reject) use ($connecting) {
  225. $connecting->then(function (ConnectionInterface $connection) use ($resolve) {
  226. $received = 0;
  227. $connection->on('data', function ($chunk) use (&$received, $resolve, $connection) {
  228. $received += strlen($chunk);
  229. if ($received >= 400000) {
  230. $resolve($received);
  231. }
  232. });
  233. }, $reject);
  234. });
  235. $received = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  236. $this->assertEquals(400000, $received);
  237. $server->close();
  238. $connecting->then(function (ConnectionInterface $connection) {
  239. $connection->close();
  240. });
  241. }
  242. public function testWritesMoreDataInMultipleChunksToConnection()
  243. {
  244. $server = new TcpServer(0);
  245. $server = new SecureServer($server, null, array(
  246. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  247. ));
  248. $server->on('connection', $this->expectCallableOnce());
  249. $server->on('connection', function (ConnectionInterface $conn) {
  250. $conn->write(str_repeat('*', 2000000));
  251. });
  252. $connector = new SecureConnector(new TcpConnector(), null, array(
  253. 'verify_peer' => false
  254. ));
  255. $connecting = $connector->connect($server->getAddress());
  256. $promise = new Promise(function ($resolve, $reject) use ($connecting) {
  257. $connecting->then(function (ConnectionInterface $connection) use ($resolve) {
  258. $received = 0;
  259. $connection->on('data', function ($chunk) use (&$received, $resolve) {
  260. $received += strlen($chunk);
  261. if ($received >= 2000000) {
  262. $resolve($received);
  263. }
  264. });
  265. }, $reject);
  266. });
  267. $received = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  268. $this->assertEquals(2000000, $received);
  269. $server->close();
  270. $connecting->then(function (ConnectionInterface $connection) {
  271. $connection->close();
  272. });
  273. }
  274. public function testEmitsDataFromConnection()
  275. {
  276. $server = new TcpServer(0);
  277. $server = new SecureServer($server, null, array(
  278. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  279. ));
  280. $server->on('connection', $this->expectCallableOnce());
  281. $promise = new Promise(function ($resolve, $reject) use ($server) {
  282. $server->on('connection', function (ConnectionInterface $connection) use ($resolve) {
  283. $connection->on('data', $resolve);
  284. });
  285. });
  286. $connector = new SecureConnector(new TcpConnector(), null, array(
  287. 'verify_peer' => false
  288. ));
  289. $connecting = $connector->connect($server->getAddress());
  290. $connecting->then(function (ConnectionInterface $connection) {
  291. $connection->write('foo');
  292. });
  293. $data = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  294. $this->assertEquals('foo', $data);
  295. $server->close();
  296. $connecting->then(function (ConnectionInterface $connection) {
  297. $connection->close();
  298. });
  299. }
  300. public function testEmitsDataInMultipleChunksFromConnection()
  301. {
  302. $server = new TcpServer(0);
  303. $server = new SecureServer($server, null, array(
  304. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  305. ));
  306. $server->on('connection', $this->expectCallableOnce());
  307. $promise = new Promise(function ($resolve, $reject) use ($server) {
  308. $server->on('connection', function (ConnectionInterface $connection) use ($resolve) {
  309. $received = 0;
  310. $connection->on('data', function ($chunk) use (&$received, $resolve) {
  311. $received += strlen($chunk);
  312. if ($received >= 400000) {
  313. $resolve($received);
  314. }
  315. });
  316. });
  317. });
  318. $connector = new SecureConnector(new TcpConnector(), null, array(
  319. 'verify_peer' => false
  320. ));
  321. $connecting = $connector->connect($server->getAddress());
  322. $connecting->then(function (ConnectionInterface $connection) {
  323. $connection->write(str_repeat('*', 400000));
  324. });
  325. $received = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  326. $this->assertEquals(400000, $received);
  327. $server->close();
  328. $connecting->then(function (ConnectionInterface $connection) {
  329. $connection->close();
  330. });
  331. }
  332. public function testPipesDataBackInMultipleChunksFromConnection()
  333. {
  334. $server = new TcpServer(0);
  335. $server = new SecureServer($server, null, array(
  336. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  337. ));
  338. $server->on('connection', $this->expectCallableOnce());
  339. $server->on('connection', function (ConnectionInterface $conn) {
  340. $conn->pipe($conn);
  341. });
  342. $connector = new SecureConnector(new TcpConnector(), null, array(
  343. 'verify_peer' => false
  344. ));
  345. $connecting = $connector->connect($server->getAddress());
  346. $promise = new Promise(function ($resolve, $reject) use ($connecting) {
  347. $connecting->then(function (ConnectionInterface $connection) use ($resolve) {
  348. $received = 0;
  349. $connection->on('data', function ($chunk) use (&$received, $resolve) {
  350. $received += strlen($chunk);
  351. if ($received >= 400000) {
  352. $resolve($received);
  353. }
  354. });
  355. $connection->write(str_repeat('*', 400000));
  356. }, $reject);
  357. });
  358. $received = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  359. $this->assertEquals(400000, $received);
  360. $server->close();
  361. $connecting->then(function (ConnectionInterface $connection) {
  362. $connection->close();
  363. });
  364. }
  365. /**
  366. * @requires PHP 5.6
  367. * @depends testClientUsesTls10WhenCryptoMethodIsExplicitlyConfiguredByClient
  368. */
  369. public function testEmitsConnectionForNewTlsv11Connection()
  370. {
  371. $server = new TcpServer(0);
  372. $server = new SecureServer($server, null, array(
  373. 'local_cert' => __DIR__ . '/../examples/localhost.pem',
  374. 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER
  375. ));
  376. $server->on('connection', $this->expectCallableOnce());
  377. $connector = new SecureConnector(new TcpConnector(), null, array(
  378. 'verify_peer' => false,
  379. 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
  380. ));
  381. $promise = $connector->connect($server->getAddress());
  382. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  383. $server->close();
  384. $promise->then(function (ConnectionInterface $connection) {
  385. $connection->close();
  386. });
  387. }
  388. /**
  389. * @requires PHP 5.6
  390. * @depends testClientUsesTls10WhenCryptoMethodIsExplicitlyConfiguredByClient
  391. */
  392. public function testEmitsErrorForClientWithTlsVersionMismatch()
  393. {
  394. $server = new TcpServer(0);
  395. $server = new SecureServer($server, null, array(
  396. 'local_cert' => __DIR__ . '/../examples/localhost.pem',
  397. 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER|STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
  398. ));
  399. $server->on('connection', $this->expectCallableNever());
  400. $server->on('error', $this->expectCallableOnce());
  401. $connector = new SecureConnector(new TcpConnector(), null, array(
  402. 'verify_peer' => false,
  403. 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
  404. ));
  405. $promise = $connector->connect($server->getAddress());
  406. $this->setExpectedException('RuntimeException', 'handshake');
  407. try {
  408. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  409. } catch (\Exception $e) {
  410. $server->close();
  411. throw $e;
  412. }
  413. }
  414. public function testServerEmitsConnectionForNewConnectionWithEncryptedCertificate()
  415. {
  416. $server = new TcpServer(0);
  417. $server = new SecureServer($server, null, array(
  418. 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem',
  419. 'passphrase' => 'swordfish'
  420. ));
  421. $peer = new Promise(function ($resolve, $reject) use ($server) {
  422. $server->on('connection', $resolve);
  423. $server->on('error', $reject);
  424. });
  425. $connector = new SecureConnector(new TcpConnector(), null, array(
  426. 'verify_peer' => false
  427. ));
  428. $connector->connect($server->getAddress());
  429. $connection = \React\Async\await(\React\Promise\Timer\timeout($peer, self::TIMEOUT));
  430. $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection);
  431. $server->close();
  432. $connection->close();
  433. }
  434. public function testClientRejectsWithErrorForServerWithInvalidCertificate()
  435. {
  436. $server = new TcpServer(0);
  437. $server = new SecureServer($server, null, array(
  438. 'local_cert' => 'invalid.pem'
  439. ));
  440. $connector = new SecureConnector(new TcpConnector(), null, array(
  441. 'verify_peer' => false
  442. ));
  443. $promise = $connector->connect($server->getAddress());
  444. $this->setExpectedException('RuntimeException', 'handshake');
  445. try {
  446. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  447. } catch (\Exception $e) {
  448. $server->close();
  449. throw $e;
  450. }
  451. }
  452. public function testServerEmitsErrorForClientWithInvalidCertificate()
  453. {
  454. $server = new TcpServer(0);
  455. $server = new SecureServer($server, null, array(
  456. 'local_cert' => 'invalid.pem'
  457. ));
  458. $peer = new Promise(function ($resolve, $reject) use ($server) {
  459. $server->on('connection', function () use ($reject) {
  460. $reject(new \RuntimeException('Did not expect connection to succeed'));
  461. });
  462. $server->on('error', $reject);
  463. });
  464. $connector = new SecureConnector(new TcpConnector(), null, array(
  465. 'verify_peer' => false
  466. ));
  467. $promise = $connector->connect($server->getAddress());
  468. try {
  469. \React\Async\await($promise);
  470. } catch (\RuntimeException $e) {
  471. // ignore client-side exception
  472. }
  473. $this->setExpectedException('RuntimeException', 'handshake');
  474. try {
  475. \React\Async\await(\React\Promise\Timer\timeout($peer, self::TIMEOUT));
  476. } catch (\Exception $e) {
  477. $server->close();
  478. throw $e;
  479. }
  480. }
  481. public function testEmitsErrorForServerWithEncryptedCertificateMissingPassphrase()
  482. {
  483. if (DIRECTORY_SEPARATOR === '\\') {
  484. $this->markTestSkipped('Not supported on Windows');
  485. }
  486. $server = new TcpServer(0);
  487. $server = new SecureServer($server, null, array(
  488. 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem'
  489. ));
  490. $server->on('connection', $this->expectCallableNever());
  491. $server->on('error', $this->expectCallableOnce());
  492. $connector = new SecureConnector(new TcpConnector(), null, array(
  493. 'verify_peer' => false
  494. ));
  495. $promise = $connector->connect($server->getAddress());
  496. $this->setExpectedException('RuntimeException', 'handshake');
  497. try {
  498. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  499. } catch (\Exception $e) {
  500. $server->close();
  501. throw $e;
  502. }
  503. }
  504. public function testEmitsErrorForServerWithEncryptedCertificateWithInvalidPassphrase()
  505. {
  506. if (DIRECTORY_SEPARATOR === '\\') {
  507. $this->markTestSkipped('Not supported on Windows');
  508. }
  509. $server = new TcpServer(0);
  510. $server = new SecureServer($server, null, array(
  511. 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem',
  512. 'passphrase' => 'nope'
  513. ));
  514. $server->on('connection', $this->expectCallableNever());
  515. $server->on('error', $this->expectCallableOnce());
  516. $connector = new SecureConnector(new TcpConnector(), null, array(
  517. 'verify_peer' => false
  518. ));
  519. $promise = $connector->connect($server->getAddress());
  520. $this->setExpectedException('RuntimeException', 'handshake');
  521. try {
  522. \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  523. } catch (\Exception $e) {
  524. $server->close();
  525. throw $e;
  526. }
  527. }
  528. public function testEmitsErrorForConnectionWithPeerVerification()
  529. {
  530. $server = new TcpServer(0);
  531. $server = new SecureServer($server, null, array(
  532. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  533. ));
  534. $server->on('connection', $this->expectCallableNever());
  535. $errorEvent = $this->createPromiseForServerError($server);
  536. $connector = new SecureConnector(new TcpConnector(), null, array(
  537. 'verify_peer' => true
  538. ));
  539. $promise = $connector->connect($server->getAddress());
  540. $promise->then(null, $this->expectCallableOnce());
  541. \React\Async\await(\React\Promise\Timer\timeout($errorEvent, self::TIMEOUT));
  542. $server->close();
  543. }
  544. public function testEmitsErrorIfConnectionIsCancelled()
  545. {
  546. if (PHP_OS !== 'Linux') {
  547. $this->markTestSkipped('Linux only (OS is ' . PHP_OS . ')');
  548. }
  549. $server = new TcpServer(0);
  550. $server = new SecureServer($server, null, array(
  551. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  552. ));
  553. $server->on('connection', $this->expectCallableNever());
  554. $errorEvent = $this->createPromiseForServerError($server);
  555. $connector = new SecureConnector(new TcpConnector(), null, array(
  556. 'verify_peer' => false
  557. ));
  558. $promise = $connector->connect($server->getAddress());
  559. $promise->cancel();
  560. $promise->then(null, $this->expectCallableOnce());
  561. \React\Async\await(\React\Promise\Timer\timeout($errorEvent, self::TIMEOUT));
  562. $server->close();
  563. }
  564. public function testEmitsErrorIfConnectionIsClosedBeforeHandshake()
  565. {
  566. $server = new TcpServer(0);
  567. $server = new SecureServer($server, null, array(
  568. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  569. ));
  570. $server->on('connection', $this->expectCallableNever());
  571. $errorEvent = $this->createPromiseForServerError($server);
  572. $connector = new TcpConnector();
  573. $promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
  574. $promise->then(function (ConnectionInterface $stream) {
  575. $stream->close();
  576. });
  577. $error = \React\Async\await(\React\Promise\Timer\timeout($errorEvent, self::TIMEOUT));
  578. // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)
  579. $this->assertInstanceOf('RuntimeException', $error);
  580. $this->assertStringStartsWith('Connection from tcp://', $error->getMessage());
  581. $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)', $error->getMessage());
  582. $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode());
  583. $this->assertNull($error->getPrevious());
  584. $server->close();
  585. }
  586. public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake()
  587. {
  588. $server = new TcpServer(0);
  589. $server = new SecureServer($server, null, array(
  590. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  591. ));
  592. $server->on('connection', $this->expectCallableNever());
  593. $errorEvent = $this->createPromiseForServerError($server);
  594. $connector = new TcpConnector();
  595. $promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
  596. $promise->then(function (ConnectionInterface $stream) {
  597. $stream->end("\x1e");
  598. });
  599. $error = \React\Async\await(\React\Promise\Timer\timeout($errorEvent, self::TIMEOUT));
  600. // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)
  601. $this->assertInstanceOf('RuntimeException', $error);
  602. $this->assertStringStartsWith('Connection from tcp://', $error->getMessage());
  603. $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake (ECONNRESET)', $error->getMessage());
  604. $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104, $error->getCode());
  605. $this->assertNull($error->getPrevious());
  606. $server->close();
  607. }
  608. public function testEmitsNothingIfPlaintextConnectionIsIdle()
  609. {
  610. $server = new TcpServer(0);
  611. $server = new SecureServer($server, null, array(
  612. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  613. ));
  614. $server->on('connection', $this->expectCallableNever());
  615. $server->on('error', $this->expectCallableNever());
  616. $connector = new TcpConnector();
  617. $promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
  618. $connection = \React\Async\await(\React\Promise\Timer\timeout($promise, self::TIMEOUT));
  619. $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection);
  620. $server->close();
  621. $promise->then(function (ConnectionInterface $connection) {
  622. $connection->close();
  623. });
  624. }
  625. public function testEmitsErrorIfConnectionIsHttpInsteadOfSecureHandshake()
  626. {
  627. $server = new TcpServer(0);
  628. $server = new SecureServer($server, null, array(
  629. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  630. ));
  631. $server->on('connection', $this->expectCallableNever());
  632. $errorEvent = $this->createPromiseForServerError($server);
  633. $connector = new TcpConnector();
  634. $promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
  635. $promise->then(function (ConnectionInterface $stream) {
  636. $stream->write("GET / HTTP/1.0\r\n\r\n");
  637. });
  638. $error = \React\Async\await(\React\Promise\Timer\timeout($errorEvent, self::TIMEOUT));
  639. $this->assertInstanceOf('RuntimeException', $error);
  640. // OpenSSL error messages are version/platform specific
  641. // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:SSL3_GET_RECORD:http request
  642. // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:ssl3_get_record:wrong version number
  643. // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:func(143):reason(267)
  644. // Unable to complete TLS handshake: Failed setting RSA key
  645. $server->close();
  646. }
  647. public function testEmitsErrorIfConnectionIsUnknownProtocolInsteadOfSecureHandshake()
  648. {
  649. $server = new TcpServer(0);
  650. $server = new SecureServer($server, null, array(
  651. 'local_cert' => __DIR__ . '/../examples/localhost.pem'
  652. ));
  653. $server->on('connection', $this->expectCallableNever());
  654. $errorEvent = $this->createPromiseForServerError($server);
  655. $connector = new TcpConnector();
  656. $promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
  657. $promise->then(function (ConnectionInterface $stream) {
  658. $stream->write("Hello world!\n");
  659. });
  660. $error = \React\Async\await(\React\Promise\Timer\timeout($errorEvent, self::TIMEOUT));
  661. $this->assertInstanceOf('RuntimeException', $error);
  662. // OpenSSL error messages are version/platform specific
  663. // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:SSL3_GET_RECORD:unknown protocol
  664. // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:ssl3_get_record:wrong version number
  665. // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:func(143):reason(267)
  666. // Unable to complete TLS handshake: Failed setting RSA key
  667. $server->close();
  668. }
  669. private function createPromiseForServerError(ServerInterface $server)
  670. {
  671. return new Promise(function ($resolve) use ($server) {
  672. $server->on('error', function ($arg) use ($resolve) {
  673. $resolve($arg);
  674. });
  675. });
  676. }
  677. }