From d874bac6940c918ee4752af72621958a785aeb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Aug 2018 18:57:33 +0200 Subject: [PATCH 1/4] Improve TLS error messages during connection --- src/SecureConnector.php | 11 ++++-- tests/SecureConnectorTest.php | 69 ++++++++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/SecureConnector.php b/src/SecureConnector.php index f04183d3..755fa0f7 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -40,7 +40,7 @@ public function connect($uri) $context = $this->context; $encryption = $this->streamEncryption; - return $this->connector->connect($uri)->then(function (ConnectionInterface $connection) use ($context, $encryption) { + return $this->connector->connect($uri)->then(function (ConnectionInterface $connection) use ($context, $encryption, $uri) { // (unencrypted) TCP/IP connection succeeded if (!$connection instanceof Connection) { @@ -54,10 +54,15 @@ public function connect($uri) } // try to enable encryption - return $encryption->enable($connection)->then(null, function ($error) use ($connection) { + return $encryption->enable($connection)->then(null, function ($error) use ($connection, $uri) { // establishing encryption failed => close invalid connection and return error $connection->close(); - throw $error; + + throw new \RuntimeException( + 'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(), + 0, + $error + ); }); }); } diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 0b3a7025..58bb2e53 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -49,18 +49,26 @@ public function testConnectionToInvalidSchemeWillReject() $promise->then(null, $this->expectCallableOnce()); } - public function testCancelDuringTcpConnectionCancelsTcpConnection() + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Connection cancelled + */ + public function testCancelDuringTcpConnectionCancelsTcpConnectionAndRejectsWithTcpRejection() { - $pending = new Promise\Promise(function () { }, function () { throw new \Exception(); }); + $pending = new Promise\Promise(function () { }, function () { throw new \RuntimeException('Connection cancelled'); }); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + $this->throwRejection($promise); } - public function testConnectionWillBeClosedAndRejectedIfConnectioIsNoStream() + /** + * @expectedException UnexpectedValueException + * @expectedExceptionMessage Base connector does not use internal Connection class exposing stream resource + */ + public function testConnectionWillBeClosedAndRejectedIfConnectionIsNoStream() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('close'); @@ -69,6 +77,57 @@ public function testConnectionWillBeClosedAndRejectedIfConnectioIsNoStream() $promise = $this->connector->connect('example.com:80'); - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + $this->throwRejection($promise); + } + + public function testStreamEncryptionWillBeEnabledAfterConnecting() + { + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock(); + + $encryption = $this->getMockBuilder('React\Socket\StreamEncryption')->disableOriginalConstructor()->getMock(); + $encryption->expects($this->once())->method('enable')->with($connection)->willReturn(new \React\Promise\Promise(function () { })); + + $ref = new \ReflectionProperty($this->connector, 'streamEncryption'); + $ref->setAccessible(true); + $ref->setValue($this->connector, $encryption); + + $pending = new Promise\Promise(function () { }, function () { throw new \RuntimeException('Connection cancelled'); }); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->willReturn(Promise\resolve($connection)); + + $promise = $this->connector->connect('example.com:80'); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Connection to example.com:80 failed during TLS handshake: TLS error + */ + public function testConnectionWillBeRejectedIfStreamEncryptionFailsAndClosesConnection() + { + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('close'); + + $encryption = $this->getMockBuilder('React\Socket\StreamEncryption')->disableOriginalConstructor()->getMock(); + $encryption->expects($this->once())->method('enable')->willReturn(Promise\reject(new \RuntimeException('TLS error'))); + + $ref = new \ReflectionProperty($this->connector, 'streamEncryption'); + $ref->setAccessible(true); + $ref->setValue($this->connector, $encryption); + + $pending = new Promise\Promise(function () { }, function () { throw new \RuntimeException('Connection cancelled'); }); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->willReturn(Promise\resolve($connection)); + + $promise = $this->connector->connect('example.com:80'); + + $this->throwRejection($promise); + } + + private function throwRejection($promise) + { + $ex = null; + $promise->then(null, function ($e) use (&$ex) { + $ex = $e; + }); + + throw $ex; } } From 9f493fd2571fbb4e751f9045fa5545bc169d8588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Aug 2018 19:34:57 +0200 Subject: [PATCH 2/4] Improve cancellation forwarding for TLS handshake after connecting --- src/SecureConnector.php | 20 ++++++++- src/StreamEncryption.php | 9 +++- tests/IntegrationTest.php | 44 ++++++++++++++++-- tests/SecureConnectorTest.php | 84 +++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 8 deletions(-) diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 755fa0f7..b6b5c223 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -40,8 +40,10 @@ public function connect($uri) $context = $this->context; $encryption = $this->streamEncryption; - return $this->connector->connect($uri)->then(function (ConnectionInterface $connection) use ($context, $encryption, $uri) { + $connected = false; + $promise = $this->connector->connect($uri)->then(function (ConnectionInterface $connection) use ($context, $encryption, $uri, &$promise, &$connected) { // (unencrypted) TCP/IP connection succeeded + $connected = true; if (!$connection instanceof Connection) { $connection->close(); @@ -54,7 +56,7 @@ public function connect($uri) } // try to enable encryption - return $encryption->enable($connection)->then(null, function ($error) use ($connection, $uri) { + return $promise = $encryption->enable($connection)->then(null, function ($error) use ($connection, $uri) { // establishing encryption failed => close invalid connection and return error $connection->close(); @@ -65,5 +67,19 @@ public function connect($uri) ); }); }); + + return new \React\Promise\Promise( + function ($resolve, $reject) use ($promise) { + $promise->then($resolve, $reject); + }, + function ($_, $reject) use (&$promise, $uri, &$connected) { + if ($connected) { + $reject(new \RuntimeException('Connection to ' . $uri . ' cancelled during TLS handshake')); + } + + $promise->cancel(); + $promise = null; + } + ); } } diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 06a0936a..5e482162 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -136,15 +136,20 @@ public function toggleCrypto($socket, Deferred $deferred, $toggle, $method) if (true === $result) { $deferred->resolve(); } else if (false === $result) { + // overwrite callback arguments for PHP7+ only, so they do not show + // up in the Exception trace and do not cause a possible cyclic reference. + $d = $deferred; + $deferred = null; + if (\feof($socket) || $error === null) { // EOF or failed without error => connection closed during handshake - $deferred->reject(new UnexpectedValueException( + $d->reject(new UnexpectedValueException( 'Connection lost during TLS handshake', \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 0 )); } else { // handshake failed with error message - $deferred->reject(new UnexpectedValueException( + $d->reject(new UnexpectedValueException( 'Unable to complete TLS handshake: ' . $error )); } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 0a048ce1..ae288026 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -280,6 +280,46 @@ function ($e) use (&$wait) { $this->assertEquals(0, gc_collect_cycles()); } + /** + * @requires PHP 7 + */ + public function testWaitingForInvalidTlsConnectionShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + $loop = Factory::create(); + $connector = new Connector($loop, array( + 'tls' => array( + 'verify_peer' => true + ) + )); + + gc_collect_cycles(); + + $wait = true; + $promise = $connector->connect('tls://self-signed.badssl.com:443')->then( + null, + function ($e) use (&$wait) { + $wait = false; + throw $e; + } + ); + + // run loop for short period to ensure we detect DNS error + Block\sleep(0.1, $loop); + if ($wait) { + Block\sleep(0.4, $loop); + if ($wait) { + $this->fail('Connection attempt did not fail'); + } + } + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + public function testWaitingForSuccessfullyClosedConnectionShouldNotCreateAnyGarbageReferences() { if (class_exists('React\Promise\When')) { @@ -303,10 +343,6 @@ function ($conn) { public function testConnectingFailsIfTimeoutIsTooSmall() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); - } - $loop = Factory::create(); $connector = new Connector($loop, array( diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 58bb2e53..56aff3dd 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -3,6 +3,7 @@ namespace React\Tests\Socket; use React\Promise; +use React\Promise\Deferred; use React\Socket\SecureConnector; class SecureConnectorTest extends TestCase @@ -49,6 +50,15 @@ public function testConnectionToInvalidSchemeWillReject() $promise->then(null, $this->expectCallableOnce()); } + public function testCancelDuringTcpConnectionCancelsTcpConnection() + { + $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); + $this->tcp->expects($this->once())->method('connect')->with('example.com:80')->willReturn($pending); + + $promise = $this->connector->connect('example.com:80'); + $promise->cancel(); + } + /** * @expectedException RuntimeException * @expectedExceptionMessage Connection cancelled @@ -121,6 +131,80 @@ public function testConnectionWillBeRejectedIfStreamEncryptionFailsAndClosesConn $this->throwRejection($promise); } + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Connection to example.com:80 cancelled during TLS handshake + */ + public function testCancelDuringStreamEncryptionCancelsEncryptionAndClosesConnection() + { + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock(); + $connection->expects($this->once())->method('close'); + + $pending = new Promise\Promise(function () { }, function () { + throw new \Exception('Ignored'); + }); + $encryption = $this->getMockBuilder('React\Socket\StreamEncryption')->disableOriginalConstructor()->getMock(); + $encryption->expects($this->once())->method('enable')->willReturn($pending); + + $ref = new \ReflectionProperty($this->connector, 'streamEncryption'); + $ref->setAccessible(true); + $ref->setValue($this->connector, $encryption); + + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->willReturn(Promise\resolve($connection)); + + $promise = $this->connector->connect('example.com:80'); + $promise->cancel(); + + $this->throwRejection($promise); + } + + public function testRejectionDuringConnectionShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + gc_collect_cycles(); + + $tcp = new Deferred(); + $this->tcp->expects($this->once())->method('connect')->willReturn($tcp->promise()); + + $promise = $this->connector->connect('example.com:80'); + $tcp->reject(new \RuntimeException()); + unset($promise, $tcp); + + $this->assertEquals(0, gc_collect_cycles()); + } + + public function testRejectionDuringTlsHandshakeShouldNotCreateAnyGarbageReferences() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + gc_collect_cycles(); + + $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock(); + + $tcp = new Deferred(); + $this->tcp->expects($this->once())->method('connect')->willReturn($tcp->promise()); + + $tls = new Deferred(); + $encryption = $this->getMockBuilder('React\Socket\StreamEncryption')->disableOriginalConstructor()->getMock(); + $encryption->expects($this->once())->method('enable')->willReturn($tls->promise()); + + $ref = new \ReflectionProperty($this->connector, 'streamEncryption'); + $ref->setAccessible(true); + $ref->setValue($this->connector, $encryption); + + $promise = $this->connector->connect('example.com:80'); + $tcp->resolve($connection); + $tls->reject(new \RuntimeException()); + unset($promise, $tcp, $tls); + + $this->assertEquals(0, gc_collect_cycles()); + } + private function throwRejection($promise) { $ex = null; From 8d396d663017abfaf6107b259ffe14991d435789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Aug 2018 13:30:45 +0200 Subject: [PATCH 3/4] Improve TLS server error messages when incoming connection fails --- src/SecureServer.php | 6 ++++++ tests/FunctionalSecureServerTest.php | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/SecureServer.php b/src/SecureServer.php index 302ae938..1c96262a 100644 --- a/src/SecureServer.php +++ b/src/SecureServer.php @@ -184,6 +184,12 @@ function ($conn) use ($that) { $that->emit('connection', array($conn)); }, function ($error) use ($that, $connection) { + $error = new \RuntimeException( + 'Connection from ' . $connection->getRemoteAddress() . ' failed during TLS handshake: ' . $error->getMessage(), + $error->getCode(), + $error + ); + $that->emit('error', array($error)); $connection->end(); } diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index ce32f366..9ae1c8c8 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -420,8 +420,10 @@ public function testEmitsErrorIfConnectionIsClosedBeforeHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshak $this->assertTrue($error instanceof \RuntimeException); - $this->assertEquals('Connection lost during TLS handshake', $error->getMessage()); + $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); + $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); } @@ -445,8 +447,10 @@ public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake() $error = Block\await($errorEvent, $loop, self::TIMEOUT); + // Connection from tcp://127.0.0.1:39528 failed during TLS handshake: Connection lost during TLS handshak $this->assertTrue($error instanceof \RuntimeException); - $this->assertEquals('Connection lost during TLS handshake', $error->getMessage()); + $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); + $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); } From 3abb49d0b09c8fffb5ec7e862cac07ee1617316c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 27 Sep 2018 17:23:33 +0200 Subject: [PATCH 4/4] Reduce number of references by discarding internal previous Exception --- src/SecureConnector.php | 3 +-- src/SecureServer.php | 3 +-- tests/FunctionalSecureServerTest.php | 2 ++ tests/SecureConnectorTest.php | 14 ++++++++------ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/SecureConnector.php b/src/SecureConnector.php index b6b5c223..ca8c838c 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -62,8 +62,7 @@ public function connect($uri) throw new \RuntimeException( 'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(), - 0, - $error + $error->getCode() ); }); }); diff --git a/src/SecureServer.php b/src/SecureServer.php index 1c96262a..59562c60 100644 --- a/src/SecureServer.php +++ b/src/SecureServer.php @@ -186,8 +186,7 @@ function ($conn) use ($that) { function ($error) use ($that, $connection) { $error = new \RuntimeException( 'Connection from ' . $connection->getRemoteAddress() . ' failed during TLS handshake: ' . $error->getMessage(), - $error->getCode(), - $error + $error->getCode() ); $that->emit('error', array($error)); diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index 9ae1c8c8..72a79faa 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -425,6 +425,7 @@ public function testEmitsErrorIfConnectionIsClosedBeforeHandshake() $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); + $this->assertNull($error->getPrevious()); } public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake() @@ -452,6 +453,7 @@ public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake() $this->assertStringStartsWith('Connection from tcp://', $error->getMessage()); $this->assertStringEndsWith('failed during TLS handshake: Connection lost during TLS handshake', $error->getMessage()); $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); + $this->assertNull($error->getPrevious()); } public function testEmitsNothingIfConnectionIsIdle() diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 56aff3dd..10cfdf37 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -107,17 +107,13 @@ public function testStreamEncryptionWillBeEnabledAfterConnecting() $promise = $this->connector->connect('example.com:80'); } - /** - * @expectedException RuntimeException - * @expectedExceptionMessage Connection to example.com:80 failed during TLS handshake: TLS error - */ public function testConnectionWillBeRejectedIfStreamEncryptionFailsAndClosesConnection() { $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('close'); $encryption = $this->getMockBuilder('React\Socket\StreamEncryption')->disableOriginalConstructor()->getMock(); - $encryption->expects($this->once())->method('enable')->willReturn(Promise\reject(new \RuntimeException('TLS error'))); + $encryption->expects($this->once())->method('enable')->willReturn(Promise\reject(new \RuntimeException('TLS error', 123))); $ref = new \ReflectionProperty($this->connector, 'streamEncryption'); $ref->setAccessible(true); @@ -128,7 +124,13 @@ public function testConnectionWillBeRejectedIfStreamEncryptionFailsAndClosesConn $promise = $this->connector->connect('example.com:80'); - $this->throwRejection($promise); + try { + $this->throwRejection($promise); + } catch (\RuntimeException $e) { + $this->assertEquals('Connection to example.com:80 failed during TLS handshake: TLS error', $e->getMessage()); + $this->assertEquals(123, $e->getCode()); + $this->assertNull($e->getPrevious()); + } } /** 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