8000 Improve TLS error messages (Connection lost during TLS handshake) by clue · Pull Request #169 · reactphp/socket · GitHub
[go: up one dir, main page]

Skip to content

Improve TLS error messages (Connection lost during TLS handshake) #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from 8000
Sep 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 26 additions & 17 deletions src/StreamEncryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ class StreamEncryption
private $method;
private $server;

private $errstr;
private $errno;

public function __construct(LoopInterface $loop, $server = true)
{
$this->loop = $loop;
Expand Down Expand Up @@ -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'];
}
Expand Down Expand Up @@ -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;
}
}
140 changes: 123 additions & 17 deletions tests/FunctionalSecureServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -415,7 +468,7 @@ public function testEmitsNothingIfConnectionIsIdle()
Block\sleep(self::TIMEOUT, $loop);
}

public function testEmitsErrorIfConnectionIsNotSecureHandshake()
public function testEmitsErrorIfConnectionIsHttpInsteadOfSecureHandshake()
{
$loop = Factory::create();

Expand All @@ -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()));
Expand All @@ -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()));
});
});
}
}
0