Skip to content

Commit 12d266a

Browse files
authored
Merge pull request #169 from clue-labs/tls-errors
Improve TLS error messages (Connection lost during TLS handshake)
2 parents a966cc0 + ab69a78 commit 12d266a

File tree

2 files changed

+149
-34
lines changed

2 files changed

+149
-34
lines changed

src/StreamEncryption.php

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ class StreamEncryption
1919
private $method;
2020
private $server;
2121

22-
private $errstr;
23-
private $errno;
24-
2522
public function __construct(LoopInterface $loop, $server = true)
2623
{
2724
$this->loop = $loop;
@@ -88,7 +85,7 @@ public function toggle(Connection $stream, $toggle)
8885

8986
// get crypto method from context options or use global setting from constructor
9087
$method = $this->method;
91-
$context = stream_context_get_options($socket);
88+
$context = \stream_context_get_options($socket);
9289
if (isset($context['ssl']['crypto_method'])) {
9390
$method = $context['ssl']['crypto_method'];
9491
}
@@ -122,25 +119,37 @@ public function toggle(Connection $stream, $toggle)
122119

123120
public function toggleCrypto($socket, Deferred $deferred, $toggle, $method)
124121
{
125-
set_error_handler(array($this, 'handleError'));
126-
$result = stream_socket_enable_crypto($socket, $toggle, $method);
127-
restore_error_handler();
122+
$error = null;
123+
\set_error_handler(function ($_, $errstr) use (&$error) {
124+
$error = \str_replace(array("\r", "\n"), ' ', $errstr);
125+
126+
// remove useless function name from error message
127+
if (($pos = \strpos($error, "): ")) !== false) {
128+
$error = \substr($error, $pos + 3);
129+
}
130+
});
131+
132+
$result = \stream_socket_enable_crypto($socket, $toggle, $method);
133+
134+
\restore_error_handler();
128135

129136
if (true === $result) {
130137
$deferred->resolve();
131138
} else if (false === $result) {
132-
$deferred->reject(new UnexpectedValueException(
133-
sprintf("Unable to complete SSL/TLS handshake: %s", $this->errstr),
134-
$this->errno
135-
));
139+
if (\feof($socket) || $error === null) {
140+
// EOF or failed without error => connection closed during handshake
141+
$deferred->reject(new UnexpectedValueException(
142+
'Connection lost during TLS handshake',
143+
\defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 0
144+
));
145+
} else {
146+
// handshake failed with error message
147+
$deferred->reject(new UnexpectedValueException(
148+
'Unable to complete TLS handshake: ' . $error
149+
));
150+
}
136151
} else {
137152
// need more data, will retry
138153
}
139154
}
140-
141-
public function handleError($errno, $errstr)
142-
{
143-
$this->errstr = str_replace(array("\r", "\n"), ' ', $errstr);
144-
$this->errno = $errno;
145-
}
146155
}

tests/FunctionalSecureServerTest.php

Lines changed: 123 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
namespace React\Tests\Socket;
44

5+
use Clue\React\Block;
6+
use Evenement\EventEmitterInterface;
57
use React\EventLoop\Factory;
6-
use React\Socket\SecureServer;
8+
use React\Promise\Promise;
79
use React\Socket\ConnectionInterface;
10+
use React\Socket\SecureConnector;
11+
use React\Socket\SecureServer;
812
use React\Socket\TcpServer;
913
use React\Socket\TcpConnector;
10-
use React\Socket\SecureConnector;
11-
use Clue\React\Block;
14+
use React\Socket\ServerInterface;
1215

1316
class FunctionalSecureServerTest extends TestCase
1417
{
@@ -86,7 +89,7 @@ public function testWritesDataInMultipleChunksToConnection()
8689
$promise = $connector->connect($server->getAddress());
8790

8891
$local = Block\await($promise, $loop, self::TIMEOUT);
89-
/* @var $local React\Stream\Stream */
92+
/* @var $local ConnectionInterface */
9093

9194
$received = 0;
9295
$local->on('data', function ($chunk) use (&$received) {
@@ -118,7 +121,7 @@ public function testWritesMoreDataInMultipleChunksToConnection()
118121
$promise = $connector->connect($server->getAddress());
119122

120123
$local = Block\await($promise, $loop, self::TIMEOUT);
121-
/* @var $local React\Stream\Stream */
124+
/* @var $local ConnectionInterface */
122125

123126
$received = 0;
124127
$local->on('data', function ($chunk) use (&$received) {
@@ -151,7 +154,7 @@ public function testEmitsDataFromConnection()
151154
$promise = $connector->connect($server->getAddress());
152155

153156
$local = Block\await($promise, $loop, self::TIMEOUT);
154-
/* @var $local React\Stream\Stream */
157+
/* @var $local ConnectionInterface */
155158

156159
$local->write("foo");
157160

@@ -181,7 +184,7 @@ public function testEmitsDataInMultipleChunksFromConnection()
181184
$promise = $connector->connect($server->getAddress());
182185

183186
$local = Block\await($promise, $loop, self::TIMEOUT);
184-
/* @var $local React\Stream\Stream */
187+
/* @var $local ConnectionInterface */
185188

186189
$local->write(str_repeat('*', 400000));
187190

@@ -210,7 +213,7 @@ public function testPipesDataBackInMultipleChunksFromConnection()
210213
$promise = $connector->connect($server->getAddress());
211214

212215
$local = Block\await($promise, $loop, self::TIMEOUT);
213-
/* @var $local React\Stream\Stream */
216+
/* @var $local ConnectionInterface */
214217

215218
$received = 0;
216219
$local->on('data', function ($chunk) use (&$received) {
@@ -361,15 +364,15 @@ public function testEmitsErrorForConnectionWithPeerVerification()
361364
'local_cert' => __DIR__ . '/../examples/localhost.pem'
362365
));
363366
$server->on('connection', $this->expectCallableNever());
364-
$server->on('error', $this->expectCallableOnce());
367+
$errorEvent = $this->createPromiseForServerError($server);
365368

366369
$connector = new SecureConnector(new TcpConnector($loop), $loop, array(
367370
'verify_peer' => true
368371
));
369372
$promise = $connector->connect($server->getAddress());
370-
371373
$promise->then(null, $this->expectCallableOnce());
372-
Block\sleep(self::TIMEOUT, $loop);
374+
375+
Block\await($errorEvent, $loop, self::TIMEOUT);
373376
}
374377

375378
public function testEmitsErrorIfConnectionIsCancelled()
@@ -385,16 +388,66 @@ public function testEmitsErrorIfConnectionIsCancelled()
385388
'local_cert' => __DIR__ . '/../examples/localhost.pem'
386389
));
387390
$server->on('connection', $this->expectCallableNever());
388-
$server->on('error', $this->expectCallableOnce());
391+
$errorEvent = $this->createPromiseForServerError($server);
389392

390393
$connector = new SecureConnector(new TcpConnector($loop), $loop, array(
391394
'verify_peer' => false
392395
));
393396
$promise = $connector->connect($server->getAddress());
394397
$promise->cancel();
395-
396398
$promise->then(null, $this->expectCallableOnce());
397-
Block\sleep(self::TIMEOUT, $loop);
399+
400+
Block\await($errorEvent, $loop, self::TIMEOUT);
401+
}
402+
403+
public function testEmitsErrorIfConnectionIsClosedBeforeHandshake()
404+
{
405+
$loop = Factory::create();
406+
407+
$server = new TcpServer(0, $loop);
408+
$server = new SecureServer($server, $loop, array(
409+
'local_cert' => __DIR__ . '/../examples/localhost.pem'
410+
));
411+
$server->on('connection', $this->expectCallableNever());
412+
$errorEvent = $this->createPromiseForServerError($server);
413+
414+
$connector = new TcpConnector($loop);
415+
$promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
416+
417+
$promise->then(function (ConnectionInterface $stream) {
418+
$stream->close();
419+
});
420+
421+
$error = Block\await($errorEvent, $loop, self::TIMEOUT);
422+
423+
$this->assertTrue($error instanceof \RuntimeException);
424+
$this->assertEquals('Connection lost during TLS handshake', $error->getMessage());
425+
$this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode());
426+
}
427+
428+
public function testEmitsErrorIfConnectionIsClosedWithIncompleteHandshake()
429+
{
430+
$loop = Factory::create();
431+
432+
$server = new TcpServer(0, $loop);
433+
$server = new SecureServer($server, $loop, array(
434+
'local_cert' => __DIR__ . '/../examples/localhost.pem'
435+
));
436+
$server->on('connection', $this->expectCallableNever());
437+
$errorEvent = $this->createPromiseForServerError($server);
438+
439+
$connector = new TcpConnector($loop);
440+
$promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
441+
442+
$promise->then(function (ConnectionInterface $stream) {
443+
$stream->end("\x1e");
444+
});
445+
446+
$error = Block\await($errorEvent, $loop, self::TIMEOUT);
447+
448+
$this->assertTrue($error instanceof \RuntimeException);
449+
$this->assertEquals('Connection lost during TLS handshake', $error->getMessage());
450+
$this->assertEquals(defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 0, $error->getCode());
398451
}
399452

400453
public function testEmitsNothingIfConnectionIsIdle()
@@ -415,7 +468,7 @@ public function testEmitsNothingIfConnectionIsIdle()
415468
Block\sleep(self::TIMEOUT, $loop);
416469
}
417470

418-
public function testEmitsErrorIfConnectionIsNotSecureHandshake()
471+
public function testEmitsErrorIfConnectionIsHttpInsteadOfSecureHandshake()
419472
{
420473
$loop = Factory::create();
421474

@@ -424,7 +477,7 @@ public function testEmitsErrorIfConnectionIsNotSecureHandshake()
424477
'local_cert' => __DIR__ . '/../examples/localhost.pem'
425478
));
426479
$server->on('connection', $this->expectCallableNever());
427-
$server->on('error', $this->expectCallableOnce());
480+
$errorEvent = $this->createPromiseForServerError($server);
428481

429482
$connector = new TcpConnector($loop);
430483
$promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
@@ -433,6 +486,59 @@ public function testEmitsErrorIfConnectionIsNotSecureHandshake()
433486
$stream->write("GET / HTTP/1.0\r\n\r\n");
434487
});
435488

436-
Block\sleep(self::TIMEOUT, $loop);
489+
$error = Block\await($errorEvent, $loop, self::TIMEOUT);
490+
491+
$this->assertTrue($error instanceof \RuntimeException);
492+
493+
// OpenSSL error messages are version/platform specific
494+
// Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:SSL3_GET_RECORD:http request
495+
// Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:ssl3_get_record:wrong version number
496+
// Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:func(143):reason(267)
497+
// Unable to complete TLS handshake: Failed setting RSA key
498+
}
499+
500+
public function testEmitsErrorIfConnectionIsUnknownProtocolInsteadOfSecureHandshake()
501+
{
502+
$loop = Factory::create();
503+
504+
$server = new TcpServer(0, $loop);
505+
$server = new SecureServer($server, $loop, array(
506+
'local_cert' => __DIR__ . '/../examples/localhost.pem'
507+
));
508+
$server->on('connection', $this->expectCallableNever());
509+
$errorEvent = $this->createPromiseForServerError($server);
510+
511+
$connector = new TcpConnector($loop);
512+
$promise = $connector->connect(str_replace('tls://', '', $server->getAddress()));
513+
514+
$promise->then(function (ConnectionInterface $stream) {
515+
$stream->write("Hello world!\n");
516+
});
517+
518+
$error = Block\await($errorEvent, $loop, self::TIMEOUT);
519+
520+
$this->assertTrue($error instanceof \RuntimeException);
521+
522+
// OpenSSL error messages are version/platform specific
523+
// Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:SSL3_GET_RECORD:unknown protocol
524+
// Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:ssl3_get_record:wrong version number
525+
// Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1408F10B:SSL routines:func(143):reason(267)
526+
// Unable to complete TLS handshake: Failed setting RSA key
527+
}
528+
529+
private function createPromiseForServerError(ServerInterface $server)
530+
{
531+
return $this->createPromiseForEvent($server, 'error', function ($error) {
532+
return $error;
533+
});
534+
}
535+
536+
private function createPromiseForEvent(EventEmitterInterface $emitter, $event, $fn)
537+
{
538+
return new Promise(function ($resolve) use ($emitter, $event, $fn) {
539+
$emitter->on($event, function () use ($resolve, $fn) {
540+
$resolve(call_user_func_array($fn, func_get_args()));
541+
});
542+
});
437543
}
438544
}

0 commit comments

Comments
 (0)
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