diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index ba5d4720..06a0936a 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -19,9 +19,6 @@ class StreamEncryption private $method; private $server; - private $errstr; - private $errno; - public function __construct(LoopInterface $loop, $server = true) { $this->loop = $loop; @@ -88,7 +85,7 @@ public function toggle(Connection $stream, $toggle) // get crypto method from context options or use global setting from constructor $method = $this->method; - $context = stream_context_get_options($socket); + $context = \stream_context_get_options($socket); if (isset($context['ssl']['crypto_method'])) { $method = $context['ssl']['crypto_method']; } @@ -122,25 +119,37 @@ public function toggle(Connection $stream, $toggle) public function toggleCrypto($socket, Deferred $deferred, $toggle, $method) { - set_error_handler(array($this, 'handleError')); - $result = stream_socket_enable_crypto($socket, $toggle, $method); - restore_error_handler(); + $error = null; + \set_error_handler(function ($_, $errstr) use (&$error) { + $error = \str_replace(array("\r", "\n"), ' ', $errstr); + + // remove useless function name from error message + if (($pos = \strpos($error, "): ")) !== false) { + $error = \substr($error, $pos + 3); + } + }); + + $result = \stream_socket_enable_crypto($socket, $toggle, $method); + + \restore_error_handler(); if (true === $result) { $deferred->resolve(); } else if (false === $result) { - $deferred->reject(new UnexpectedValueException( - sprintf("Unable to complete SSL/TLS handshake: %s", $this->errstr), - $this->errno - )); + if (\feof($socket) || $error === null) { + // EOF or failed without error => connection closed during handshake + $deferred->reject(new UnexpectedValueException( + 'Connection lost during TLS handshake', + \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 0 + )); + } else { + // handshake failed with error message + $deferred->reject(new UnexpectedValueException( + 'Unable to complete TLS handshake: ' . $error + )); + } } else { // need more data, will retry } } - - public function handleError($errno, $errstr) - { - $this->errstr = str_replace(array("\r", "\n"), ' ', $errstr); - $this->errno = $errno; - } } diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index 78a59d00..ce32f366 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -2,13 +2,16 @@ namespace React\Tests\Socket; +use Clue\React\Block; +use Evenement\EventEmitterInterface; use React\EventLoop\Factory; -use React\Socket\SecureServer; +use React\Promise\Promise; use React\Socket\ConnectionInterface; +use React\Socket\SecureConnector; +use React\Socket\SecureServer; use React\Socket\TcpServer; use React\Socket\TcpConnector; -use React\Socket\SecureConnector; -use Clue\React\Block; +use React\Socket\ServerInterface; class FunctionalSecureServerTest extends TestCase { @@ -86,7 +89,7 @@ public function testWritesDataInMultipleChunksToConnection() $promise = $connector->connect($server->getAddress()); $local = Block\await($promise, $loop, self::TIMEOUT); - /* @var $local React\Stream\Stream */ + /* @var $local ConnectionInterface */ $received = 0; $local->on('data', function ($chunk) use (&$received) { @@ -118,7 +121,7 @@ public function testWritesMoreDataInMultipleChunksToConnection() $promise = $connector->connect($server->getAddress()); $local = Block\await($promise, $loop, self::TIMEOUT); - /* @var $local React\Stream\Stream */ + /* @var $local ConnectionInterface */ $received = 0; $local->on('data', function ($chunk) use (&$received) { @@ -151,7 +154,7 @@ public function testEmitsDataFromConnection() $promise = $connector->connect($server->getAddress()); $local = Block\await($promise, $loop, self::TIMEOUT); - /* @var $local React\Stream\Stream */ + /* @var $local ConnectionInterface */ $local->write("foo"); @@ -181,7 +184,7 @@ public function testEmitsDataInMultipleChunksFromConnection() $promise = $connector->connect($server->getAddress()); $local = Block\await($promise, $loop, self::TIMEOUT); - /* @var $local React\Stream\Stream */ + /* @var $local ConnectionInterface */ $local->write(str_repeat('*', 400000)); @@ -210,7 +213,7 @@ public function testPipesDataBackInMultipleChunksFromConnection() $promise = $connector->connect($server->getAddress()); $local = Block\await($promise, $loop, self::TIMEOUT); - /* @var $local React\Stream\Stream */ + /* @var $local ConnectionInterface */ $received = 0; $local->on('data', function ($chunk) use (&$received) { @@ -361,15 +364,15 @@ public function testEmitsErrorForConnectionWithPeerVerification() 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); $server->on('connection', $this->expectCallableNever()); - $server->on('error', $this->expectCallableOnce()); + $errorEvent = $this->createPromiseForServerError($server); $connector = new SecureConnector(new TcpConnector($loop), $loop, array( 'verify_peer' => true )); $promise = $connector->connect($server->getAddress()); - $promise->then(null, $this->expectCallableOnce()); - Block\sleep(self::TIMEOUT, $loop); + + Block\await($errorEvent, $loop, self::TIMEOUT); } public function testEmitsErrorIfConnectionIsCancelled() @@ -385,16 +388,66 @@ public function testEmitsErrorIfConnectionIsCancelled() 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); $server->on('connection', $this->expectCallableNever()); - $server->on('error', $this->expectCallableOnce()); + $errorEvent = $this->createPromiseForServerError($server); $connector = new SecureConnector(new TcpConnector($loop), $loop, array( 'verify_peer' => false )); $promise = $connector->connect($server->getAddress()); $promise->cancel(); - $promise->then(null, $this->expectCallableOnce()); - Block\sleep(self::TIMEOUT, $loop); + + Block\await($errorEvent, $loop, self::TIMEOUT); + } + + public function testEmitsErrorIfConnectionIsClosedBeforeHandshake() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $errorEvent = $this->createPromiseForServerError($server); + + $connector = new TcpConnector($loop); + $promise = $connector->connect(str_replace('tls://', '', $server->getAddress())); + + $promise->then(function (ConnectionInterface $stream) { + $stream->close(); + }); + + $error = Block\await($errorEvent, $loop, self::TIMEOUT); + + $this->assertTrue($error instanceof \RuntimeException); + $this->assertEquals('Connection lost during TLS handshake', $error->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); + } + + public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $errorEvent = $this->createPromiseForServerError($server); + + $connector = new TcpConnector($loop); + $promise = $connector->connect(str_replace('tls://', '', $server->getAddress())); + + $promise->then(function (ConnectionInterface $stream) { + $stream->end("\x1e"); + }); + + $error = Block\await($errorEvent, $loop, self::TIMEOUT); + + $this->assertTrue($error instanceof \RuntimeException); + $this->assertEquals('Connection lost during TLS handshake', $error->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode()); } public function testEmitsNothingIfConnectionIsIdle() @@ -415,7 +468,7 @@ public function testEmitsNothingIfConnectionIsIdle() Block\sleep(self::TIMEOUT, $loop); } - public function testEmitsErrorIfConnectionIsNotSecureHandshake() + public function testEmitsErrorIfConnectionIsHttpInsteadOfSecureHandshake() { $loop = Factory::create(); @@ -424,7 +477,7 @@ public function testEmitsErrorIfConnectionIsNotSecureHandshake() 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); $server->on('connection', $this->expectCallableNever()); - $server->on('error', $this->expectCallableOnce()); + $errorEvent = $this->createPromiseForServerError($server); $connector = new TcpConnector($loop); $promise = $connector->connect(str_replace('tls://', '', $server->getAddress())); @@ -433,6 +486,59 @@ public function testEmitsErrorIfConnectionIsNotSecureHandshake() $stream->write("GET / HTTP/1.0\r\n\r\n"); }); - Block\sleep(self::TIMEOUT, $loop); + $error = Block\await($errorEvent, $loop, self::TIMEOUT); + + $this->assertTrue($error instanceof \RuntimeException); + + // OpenSSL error messages are version/platform specific + // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:SSL3_GET_RECORD:http request + // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:ssl3_get_record:wrong version number + // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:func(143):reason(267) + // Unable to complete TLS handshake: Failed setting RSA key + } + + public function testEmitsErrorIfConnectionIsUnknownProtocolInsteadOfSecureHandshake() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $errorEvent = $this->createPromiseForServerError($server); + + $connector = new TcpConnector($loop); + $promise = $connector->connect(str_replace('tls://', '', $server->getAddress())); + + $promise->then(function (ConnectionInterface $stream) { + $stream->write("Hello world!\n"); + }); + + $error = Block\await($errorEvent, $loop, self::TIMEOUT); + + $this->assertTrue($error instanceof \RuntimeException); + + // OpenSSL error messages are version/platform specific + // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:SSL3_GET_RECORD:unknown protocol + // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:ssl3_get_record:wrong version number + // Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:func(143):reason(267) + // Unable to complete TLS handshake: Failed setting RSA key + } + + private function createPromiseForServerError(ServerInterface $server) + { + return $this->createPromiseForEvent($server, 'error', function ($error) { + return $error; + }); + } + + private function createPromiseForEvent(EventEmitterInterface $emitter, $event, $fn) + { + return new Promise(function ($resolve) use ($emitter, $event, $fn) { + $emitter->on($event, function () use ($resolve, $fn) { + $resolve(call_user_func_array($fn, func_get_args())); + }); + }); } }