8000 Merge pull request #169 from clue-labs/tls-errors · reactphp/socket@12d266a · GitHub
[go: up one dir, main page]

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, $ 10000 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)
10CA
0