Skip to content
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

Add Opportunistic TLS implementation #302

Open
wants to merge 1 commit into
base: 1.x
Choose a base branch
from
Open
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
115 changes: 111 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -23,6 +23,8 @@ handle multiple concurrent connections without blocking.
* [ConnectionInterface](#connectioninterface)
* [getRemoteAddress()](#getremoteaddress)
* [getLocalAddress()](#getlocaladdress)
* [OpportunisticTlsConnectionInterface](#opportunistictlsconnectioninterface)
* [enableEncryption()](#enableencryption)
* [Server usage](#server-usage)
* [ServerInterface](#serverinterface)
* [connection event](#connection-event)
@@ -193,6 +195,64 @@ If your system has multiple interfaces (e.g. a WAN and a LAN interface),
you can use this method to find out which interface was actually
used for this connection.

### OpportunisticTlsConnectionInterface

The `OpportunisticTlsConnectionInterface` extends the
[`ConnectionInterface`](#connectioninterface) and adds the ability of
enabling the TLS encryption on the connection when desired.

#### enableEncryption

When negotiated with the server when to start encrypting traffic using TLS, you
can enable it by calling `enableEncryption()`. This will either return a promise
that resolves with a `OpportunisticTlsConnectionInterface` connection or throw a
`RuntimeException` if the encryption failed. If successful, all traffic back and
forth will be encrypted. In the following example we ask the server if they want
to encrypt the connection, and when it responds with `yes` we enable the encryption:

```php
$connector = new React\Socket\Connector();
$connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
$connection->write('let\'s encrypt?');

return React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'yes') {
return $connection->enableEncryption();
}

return $stream;
});
})->then(function (React\Socket\ConnectionInterface $connection) {
$connection->write('Hello!');
});
```

The `enableEncryption` function resolves with itself. As such you can't see the data
encrypted when you hook into the events before enabling, as shown below:

```php
$connector = new React\Socket\Connector();
$connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
$connection->on('data', function ($data) {
echo 'Raw: ', $data, PHP_EOL;
});

return $connection->enableEncryption();
})->then(function (React\Socket\ConnectionInterface $connection) {
$connection->on('data', function ($data) {
echo 'TLS: ', $data, PHP_EOL;
});
});
```

When the other side sends `Hello World!` over the encrypted connection, the output
will be the following:

```
Raw: Hello World!
TLS: Hello World!
```

## Server usage

### ServerInterface
@@ -253,10 +313,10 @@ If the address can not be determined or is unknown at this time (such as
after the socket has been closed), it MAY return a `NULL` value instead.

Otherwise, it will return the full address (URI) as a string value, such
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`
`unix://example.sock` or `unix:///path/to/example.sock`.
Note that individual URI components are application specific and depend
on the underlying transport protocol.
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`,
`unix://example.sock`, `unix:///path/to/example.sock`, or
`opportunistic+tls://127.0.0.1:443`. Note that individual URI components
are application specific and depend on the underlying transport protocol.

If this is a TCP/IP based server and you only want the local port, you may
use something like this:
@@ -478,6 +538,22 @@ $socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', array(
));
```

To start a server with opportunistic TLS support use `opportunistic+tls://` as the scheme instead of `tls://`:

```php
$socket = new React\Socket\SocketServer('opportunistic+tls://127.0.0.1:8000', array(
'tls' => array(
'local_cert' => 'server.pem',
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
)
));
$server->on('connection', static function (OpportunisticTlsConnectionInterface $connection) use ($server) {
return $connection->enableEncryption();
});
```

See also the [examples](examples).

> Note that available [TLS context options](https://www.php.net/manual/en/context.ssl.php),
their defaults and effects of changing these may vary depending on your system
and/or PHP version.
@@ -697,6 +773,21 @@ here in order to use the [default loop](https://github.com/reactphp/event-loop#l
This value SHOULD NOT be given unless you're sure you want to explicitly use a
given event loop instance.

The `SecureServer` class supports opportunistic TLS by passing true in as a 4th
constructor parameter. This, when a client connects, emits a
[`OpportunisticTlsConnectionInterface`](#opportunistictlsconnectioninterface)
instead of the default [`ConnectionInterface`](#connectioninterface). It won't be
TLS encrypted from the start, but you can enable the TLS encryption on the connection
after negotiating with the client.

```php
$server = new React\Socket\TcpServer(8000);
$server = new React\Socket\SecureServer($server, null, array(
'local_cert' => 'server.pem',
'passphrase' => 'secret'
), true);
```

> Advanced usage: Despite allowing any `ServerInterface` as first parameter,
you SHOULD pass a `TcpServer` instance as first parameter, unless you
know what you're doing.
@@ -1389,6 +1480,22 @@ $secureConnector = new React\Socket\SecureConnector($dnsConnector, null, array(
));
```

The `SecureConnector` class supports opportunistic TLS by using
`opportunistic-tls://` as scheme instead of `tls://`. This, when connected,
returns a [`OpportunisticTlsConnectionInterface`](#opportunistictlsconnectioninterface)
instead of the default [`ConnectionInterface`](#connectioninterface). It won't be
TLS encrypted from the start, but you can enable the TLS encryption on the connection
after negotiating with the server.

```php
$secureConnector = new React\Socket\SecureConnector($dnsConnector);
$secureConnector->connect('opportunistic-tls://example.com:5432')->then(function (OpportunisticTlsConnectionInterface $connection) {
return $connection->enableEncryption();
})->then(function (OpportunisticTlsConnectionInterface $connection) {
$connection->write('Encrypted hi!');
});
```

> Advanced usage: Internally, the `SecureConnector` relies on setting up the
required *context options* on the underlying stream resource.
It should therefor be used with a `TcpConnector` somewhere in the connector
68 changes: 68 additions & 0 deletions examples/31-opportunistic-tls.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

// Opportunistic TLS example showing a basic negotiation before enabling the encryption. It starts out as an
// unencrypted TCP connection. After both parties agreed to encrypt the connection they both enable the encryption.
// After which any communication over the line is encrypted.
//
// This example is design to show both sides in one go, as such the server stops listening for new connection after
// the first, this makes sure the loop shuts down after the example connection has closed.
//
// $ php examples/31-opportunistic-tls.php

use React\EventLoop\Loop;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Socket\OpportunisticTlsConnectionInterface;
use React\Socket\SocketServer;

require __DIR__ . '/../vendor/autoload.php';

$server = new SocketServer('opportunistic+tls://127.0.0.1:0', array(
'tls' => array(
'local_cert' => __DIR__ . '/localhost.pem',
)
));
$server->on('connection', static function (OpportunisticTlsConnectionInterface $connection) use ($server) {
$server->close();

$connection->on('data', function ($data) {
echo 'From Client: ', $data, PHP_EOL;
});
React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'Let\'s encrypt?') {
$connection->write('yes');
return $connection->enableEncryption();
}

return $connection;
})->then(static function (ConnectionInterface $connection) {
$connection->write('Encryption enabled!');
})->done();
});

$client = new Connector(array(
'tls' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
),
));
$client->connect($server->getAddress())->then(static function (OpportunisticTlsConnectionInterface $connection) {
$connection->on('data', function ($data) {
echo 'From Server: ', $data, PHP_EOL;
});
$connection->write('Let\'s encrypt?');

return React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'yes') {
return $connection->enableEncryption();
}

return $connection;
});
})->then(function (ConnectionInterface $connection) {
$connection->write('Encryption enabled!');
Loop::addTimer(1, static function () use ($connection) {
$connection->end('Cool! Bye!');
});
})->done();
4 changes: 4 additions & 0 deletions src/Connector.php
Original file line number Diff line number Diff line change
@@ -75,6 +75,7 @@ public function __construct($context = array(), $loop = null)
'dns' => true,
'timeout' => true,
'happy_eyeballs' => true,
'opportunistic+tls' => true,
);

if ($context['timeout'] === true) {
@@ -150,6 +151,9 @@ public function __construct($context = array(), $loop = null)
}

$this->connectors['tls'] = $context['tls'];
if ($context['opportunistic+tls'] !== false) {
$this->connectors['opportunistic+tls'] = $this->connectors['tls'];
}
}

if ($context['unix'] !== false) {
109 changes: 109 additions & 0 deletions src/OpportunisticTlsConnection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace React\Socket;

use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;
use React\Stream\DuplexResourceStream;
use React\Stream\Util;
use React\Stream\WritableResourceStream;
use React\Stream\WritableStreamInterface;

/**
* The actual connection implementation for StartTlsConnectionInterface
*
* This class should only be used internally, see StartTlsConnectionInterface instead.
*
* @see OpportunisticTlsConnectionInterface
* @internal
*/
class OpportunisticTlsConnection extends EventEmitter implements OpportunisticTlsConnectionInterface
{
/** @var Connection */
private $connection;

/** @var StreamEncryption */
private $streamEncryption;

/** @var string */
private $uri;

public function __construct(Connection $connection, StreamEncryption $streamEncryption, $uri)
{
$this->connection = $connection;
$this->streamEncryption = $streamEncryption;
$this->uri = $uri;

Util::forwardEvents($connection, $this, array('data', 'end', 'error', 'close'));
}

public function getRemoteAddress()
{
return $this->connection->getRemoteAddress();
}

public function getLocalAddress()
{
return $this->connection->getLocalAddress();
}

public function isReadable()
{
return $this->connection->isReadable();
}

public function pause()
{
$this->connection->pause();
}

public function resume()
{
$this->connection->resume();
}

public function pipe(WritableStreamInterface $dest, array $options = array())
{
return $this->connection->pipe($dest, $options);
}

public function close()
{
$this->connection->close();
}

public function enableEncryption()
{
$that = $this;
$connection = $this->connection;
$uri = $this->uri;

return $this->streamEncryption->enable($connection)->then(function () use ($that) {
return $that;
}, function ($error) use ($connection, $uri) {
// establishing encryption failed => close invalid connection and return error
$connection->close();

throw new \RuntimeException(
'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(),
$error->getCode()
);
});
}

public function isWritable()
{
return $this->connection->isWritable();
}

public function write($data)
{
return $this->connection->write($data);
}

public function end($data = null)
{
$this->connection->end($data);
}
}
72 changes: 72 additions & 0 deletions src/OpportunisticTlsConnectionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace React\Socket;

use React\Promise\PromiseInterface;

/**
* The `OpportunisticTlsConnectionInterface` extends the
* [`ConnectionInterface`](#connectioninterface) and adds the ability of
* enabling the TLS encryption on the connection when desired.
*
* @see DuplexStreamInterface
* @see ServerInterface
* @see ConnectionInterface
*/
interface OpportunisticTlsConnectionInterface extends ConnectionInterface
{
/**
* When negotiated with the server when to start encrypting traffic using TLS, you
* can enable it by calling `enableEncryption()`. This will either return a promise
* that resolves with a `OpportunisticTlsConnectionInterface` connection or throw a
* `RuntimeException` if the encryption failed. If successful, all traffic back and
* forth will be encrypted. In the following example we ask the server if they want
* to encrypt the connection, and when it responds with `yes` we enable the encryption:
*
* ```php
* $connector = new React\Socket\Connector();
* $connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
* $connection->write('let\'s encrypt?');
*
* return React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
* if ($data === 'yes') {
* return $connection->enableEncryption();
* }
*
* return $stream;
* });
* })->then(function (React\Socket\ConnectionInterface $connection) {
* $connection->write('Hello!');
* });
* ```
*
* The `enableEncryption` function resolves with itself. As such you can't see the data
* encrypted when you hook into the events before enabling, as shown below:
*
* ```php
* $connector = new React\Socket\Connector();
* $connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
* $connection->on('data', function ($data) {
* echo 'Raw: ', $data, PHP_EOL;
* });
*
* return $connection->enableEncryption();
* })->then(function (React\Socket\ConnectionInterface $connection) {
* $connection->on('data', function ($data) {
* echo 'TLS: ', $data, PHP_EOL;
* });
* });
* ```
*
* When the other side sends `Hello World!` over the encrypted connection, the output
* will be the following:
*
* ```
* Raw: Hello World!
* TLS: Hello World!
* ```
*
* @return PromiseInterface<OpportunisticTlsConnectionInterface>
*/
public function enableEncryption();
}
11 changes: 8 additions & 3 deletions src/SecureConnector.php
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ public function connect($uri)
}

$parts = \parse_url($uri);
if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') {
if (!$parts || !isset($parts['scheme']) || ($parts['scheme'] !== 'tls' && $parts['scheme'] !== 'opportunistic+tls')) {
return Promise\reject(new \InvalidArgumentException(
'Given URI "' . $uri . '" is invalid (EINVAL)',
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22
@@ -42,11 +42,12 @@ public function connect($uri)

$context = $this->context;
$encryption = $this->streamEncryption;
$opportunisticTls = $parts['scheme'] === 'opportunistic+tls';
$connected = false;
/** @var \React\Promise\PromiseInterface $promise */
$promise = $this->connector->connect(
\str_replace('tls://', '', $uri)
)->then(function (ConnectionInterface $connection) use ($context, $encryption, $uri, &$promise, &$connected) {
\str_replace(array('opportunistic+tls://', 'tls://'), '', $uri)
)->then(function (ConnectionInterface $connection) use ($context, $encryption, $opportunisticTls, $uri, &$promise, &$connected) {
// (unencrypted) TCP/IP connection succeeded
$connected = true;

@@ -60,6 +61,10 @@ public function connect($uri)
\stream_context_set_option($connection->stream, 'ssl', $name, $value);
}

if ($opportunisticTls === true) {
return new OpportunisticTlsConnection($connection, $encryption, $uri);
}

// try to enable encryption
return $promise = $encryption->enable($connection)->then(null, function ($error) use ($connection, $uri) {
// establishing encryption failed => close invalid connection and return error
15 changes: 14 additions & 1 deletion src/SecureServer.php
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ final class SecureServer extends EventEmitter implements ServerInterface
private $tcp;
private $encryption;
private $context;
private $opportunisticTls = false;

/**
* Creates a secure TLS server and starts waiting for incoming connections
@@ -122,12 +123,14 @@ final class SecureServer extends EventEmitter implements ServerInterface
* @see TcpServer
* @link https://www.php.net/manual/en/context.ssl.php for TLS context options
*/
public function __construct(ServerInterface $tcp, LoopInterface $loop = null, array $context = array())
public function __construct(ServerInterface $tcp, LoopInterface $loop = null, array $context = array(), $opportunisticTls = false)
{
if (!\function_exists('stream_socket_enable_crypto')) {
throw new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); // @codeCoverageIgnore
}

$this->opportunisticTls = $opportunisticTls;

// default to empty passphrase to suppress blocking passphrase prompt
$context += array(
'passphrase' => ''
@@ -153,6 +156,10 @@ public function getAddress()
return null;
}

if ($this->opportunisticTls) {
$address = 'opportunistic+' . $address;
}

return \str_replace('tcp://' , 'tls://', $address);
}

@@ -188,6 +195,12 @@ public function handleConnection(ConnectionInterface $connection)
$remote = $connection->getRemoteAddress();
$that = $this;

if ($this->opportunisticTls === true) {
$connection = new OpportunisticTlsConnection($connection, $this->encryption, $remote);
$that->emit('connection', array($connection));
return ;
}

$this->encryption->enable($connection)->then(
function ($conn) use ($that) {
$that->emit('connection', array($conn));
7 changes: 4 additions & 3 deletions src/ServerInterface.php
Original file line number Diff line number Diff line change
@@ -60,9 +60,10 @@ interface ServerInterface extends EventEmitterInterface
* after the socket has been closed), it MAY return a `NULL` value instead.
*
* Otherwise, it will return the full address (URI) as a string value, such
* as `tcp://127.0.0.1:8080`, `tcp://[::1]:80` or `tls://127.0.0.1:443`.
* Note that individual URI components are application specific and depend
* on the underlying transport protocol.
* as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`,
* `unix://example.sock`, `unix:///path/to/example.sock`, or
* `opportunistic+tls://127.0.0.1:443`. Note that individual URI components
* are application specific and depend on the underlying transport protocol.
*
* If this is a TCP/IP based server and you only want the local port, you may
* use something like this:
5 changes: 4 additions & 1 deletion src/SocketServer.php
Original file line number Diff line number Diff line change
@@ -58,11 +58,14 @@ public function __construct($uri, array $context = array(), LoopInterface $loop
);
}

$server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);
$server = new TcpServer(str_replace(array('opportunistic+tls://', 'tls://'), '', $uri), $loop, $context['tcp']);

if ($scheme === 'tls') {
$server = new SecureServer($server, $loop, $context['tls']);
}
if ($scheme === 'opportunistic+tls') {
$server = new SecureServer($server, $loop, $context['tls'], true);
}
}

$this->server = $server;
128 changes: 128 additions & 0 deletions tests/FunctionalOpportunisticTLSTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace React\Tests\Socket;

use React\EventLoop\Factory;
use React\EventLoop\Loop;
use React\Promise\Stream;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Socket\OpportunisticTlsConnectionInterface;
use React\Socket\SocketServer;

class FunctionalOpportunisticTLSTest extends TestCase
{
const TIMEOUT = 2;

/**
* @before
*/
public function setUpSkipTest()
{
if (defined('HHVM_VERSION')) {
$this->markTestSkipped('Not supported on legacy HHVM');
}
}

public function testNegotiatedLSSSuccessful()
{
// let loop tick for reactphp/async v4 to clean up any remaining stream resources
// @link https://github.com/reactphp/async/pull/65 reported upstream // TODO remove me once merged
if (function_exists('React\Async\async')) {
\React\Async\await(\React\Promise\Timer\sleep(0));
Loop::run();
}

$expectCallableNever = $this->expectCallableNever();
$messagesExpected = array(
'client' => array(
'Let\'s encrypt?',
'Encryption enabled!',
'Cool! Bye!',
),
'server' => array(
'yes',
'Encryption enabled!',
),
);
$messages = array(
'client' => array(),
'server' => array(),
);
$server = new SocketServer('opportunistic+tls://127.0.0.1:0', array(
'tls' => array(
'local_cert' => dirname(__DIR__) . '/examples/localhost.pem',
)
));
$server->on('connection', function (OpportunisticTlsConnectionInterface $connection) use ($expectCallableNever, $server, &$messages) {
$server->close();

$connection->on('data', function ($data) use (&$messages) {
$messages['client'][] = $data;
});
Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'Let\'s encrypt?') {
$connection->write('yes');
return $connection->enableEncryption();
}

return $connection;
})->then(function (ConnectionInterface $connection) {
$connection->write('Encryption enabled!');
})->then(null, $expectCallableNever);
});

$client = new Connector(array(
'tls' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
),
));
$client->connect($server->getAddress())->then(function (OpportunisticTlsConnectionInterface $connection) use (&$messages) {
$connection->on('data', function ($data) use (&$messages) {
$messages['server'][] = $data;
});
$connection->write('Let\'s encrypt?');

return Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'yes') {
return $connection->enableEncryption();
}

return $connection;
});
})->then(function (ConnectionInterface $connection) {
$connection->write('Encryption enabled!');
Loop::addTimer(1, function () use ($connection) {
$connection->end('Cool! Bye!');
});
})->then(null, $expectCallableNever);

Loop::run();

self::assertSame($messagesExpected, $messages);
}

public function testNegotiatedTLSUnsuccessful()
{
$this->setExpectedException('RuntimeException');

$server = new SocketServer('opportunistic+tls://127.0.0.1:0', array(
'tls' => array(
'local_cert' => dirname(__DIR__) . '/examples/localhost.pem',
)
));
$server->on('connection', function (ConnectionInterface $connection) use ($server) {
$server->close();
$connection->write('Hi!');
$connection->enableEncryption();
});

$client = new Connector();
\React\Async\await($client->connect($server->getAddress())->then(function (OpportunisticTlsConnectionInterface $connection) use (&$messages) {
$connection->write('Hi!');
return $connection->enableEncryption();
}));
}
}
74 changes: 74 additions & 0 deletions tests/OpportunisticTlsConnectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace React\Tests\Socket;

use React\EventLoop\Loop;
use React\Socket\Connection;
use React\Socket\OpportunisticTlsConnection;
use React\Socket\StreamEncryption;

class OpportunisticTlsConnectionTest extends TestCase
{
public function testGetRemoteAddressWillForwardCallToUnderlyingConnection()
{
$underlyingConnection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$underlyingConnection->expects($this->once())->method('getRemoteAddress')->willReturn('[::1]:13');

$connection = new OpportunisticTlsConnection($underlyingConnection, new StreamEncryption(Loop::get(), false), '');
$this->assertSame('[::1]:13', $connection->getRemoteAddress());
}

public function testGetLocalAddressWillForwardCallToUnderlyingConnection()
{
$underlyingConnection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$underlyingConnection->expects($this->once())->method('getLocalAddress')->willReturn('[::1]:13');

$connection = new OpportunisticTlsConnection($underlyingConnection, new StreamEncryption(Loop::get(), false), '');
$this->assertSame('[::1]:13', $connection->getLocalAddress());
}

public function testPauseWillForwardCallToUnderlyingConnection()
{
$underlyingConnection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$underlyingConnection->expects($this->once())->method('pause');

$connection = new OpportunisticTlsConnection($underlyingConnection, new StreamEncryption(Loop::get(), false), '');
$connection->pause();
}

public function testResumeWillForwardCallToUnderlyingConnection()
{
$underlyingConnection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$underlyingConnection->expects($this->once())->method('resume');

$connection = new OpportunisticTlsConnection($underlyingConnection, new StreamEncryption(Loop::get(), false), '');
$connection->resume();
}

public function testPipeWillForwardCallToUnderlyingConnection()
{
$underlyingConnection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$underlyingConnection->expects($this->once())->method('pipe');

$connection = new OpportunisticTlsConnection($underlyingConnection, new StreamEncryption(Loop::get(), false), '');
$connection->pipe($underlyingConnection);
}

public function testCloseWillForwardCallToUnderlyingConnection()
{
$underlyingConnection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$underlyingConnection->expects($this->once())->method('close');

$connection = new OpportunisticTlsConnection($underlyingConnection, new StreamEncryption(Loop::get(), false), '');
$connection->close();
}

public function testIsWritableWillForwardCallToUnderlyingConnection()
{
$underlyingConnection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$underlyingConnection->expects($this->once())->method('isWritable')->willReturn(true);

$connection = new OpportunisticTlsConnection($underlyingConnection, new StreamEncryption(Loop::get(), false), '');
$this->assertTrue($connection->isWritable());
}
}