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

Documentation and tests for exposing secure context options #69

Merged
merged 1 commit into from
Feb 8, 2017
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ $server = new SecureServer($server, $loop, array(
));
```

> Note that available [TLS context options](http://php.net/manual/en/context.ssl.php),
their defaults and effects of changing these may vary depending on your system
and/or PHP version.
Passing unknown context options has no effect.

Whenever a client completes the TLS handshake, it will emit a `connection` event
with a connection instance implementing [`ConnectionInterface`](#connectioninterface):

Expand All @@ -286,6 +291,19 @@ $server->on('error', function (Exception $e) {

See also the [`ServerInterface`](#serverinterface) for more details.

Note that the `SecureServer` class is a concrete implementation for TLS sockets.
If you want to typehint in your higher-level protocol implementation, you SHOULD
use the generic [`ServerInterface`](#serverinterface) instead.

> Advanced usage: Internally, the `SecureServer` has to set the required
context options on the underlying stream resources.
It should therefor be used with an unmodified `Server` instance as first
parameter so that it can allocate an empty context resource which this
class uses to set required TLS context options.
Failing to do so may result in some hard to trace race conditions,
because all stream resources will use a single, shared default context
resource otherwise.

### ConnectionInterface

The `ConnectionInterface` is used to represent any incoming connection.
Expand Down
103 changes: 89 additions & 14 deletions src/SecureServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,110 @@
use React\EventLoop\LoopInterface;
use React\Socket\Server;
use React\Socket\ConnectionInterface;
use React\Stream\Stream;

/**
* The `SecureServer` class implements the `ServerInterface` and is responsible
* for providing a secure TLS (formerly known as SSL) server.
*
* It does so by wrapping a `Server` instance which waits for plaintext
* TCP/IP connections and then performs a TLS handshake for each connection.
* It thus requires valid [TLS context options],
* which in its most basic form may look something like this if you're using a
* PEM encoded certificate file:
*
* ```php
* $server = new Server(8000, $loop);
* $server = new SecureServer($server, $loop, array(
* // tls context options here…
* ));
* ```
* $context = array(
* 'local_cert' => __DIR__ . '/localhost.pem'
* );
*
* Whenever a client completes the TLS handshake, it will emit a `connection` event
* with a connection instance implementing [`ConnectionInterface`](#connectioninterface):
*
* ```php
* $server->on('connection', function (ConnectionInterface $connection) {
* echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL;
*
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* If your private key is encrypted with a passphrase, you have to specify it
* like this:
* Whenever a client fails to perform a successful TLS handshake, it will emit an
* `error` event and then close the underlying TCP/IP connection:
*
* ```php
* $context = array(
* 'local_cert' => 'server.pem',
* 'passphrase' => 'secret'
* );
* $server->on('error', function (Exception $e) {
* echo 'Error' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* @see Server
* @link http://php.net/manual/en/context.ssl.php for TLS context options
* See also the `ServerInterface` for more details.
*
* Note that the `SecureServer` class is a concrete implementation for TLS sockets.
* If you want to typehint in your higher-level protocol implementation, you SHOULD
* use the generic `ServerInterface` instead.
*
* @see ServerInterface
* @see ConnectionInterface
*/
class SecureServer extends EventEmitter implements ServerInterface
{
private $tcp;
private $encryption;

/**
* Creates a secure TLS server and starts waiting for incoming connections
*
* It does so by wrapping a `Server` instance which waits for plaintext
* TCP/IP connections and then performs a TLS handshake for each connection.
* It thus requires valid [TLS context options],
* which in its most basic form may look something like this if you're using a
* PEM encoded certificate file:
*
* ```php
* $server = new Server(8000, $loop);
* $server = new SecureServer($server, $loop, array(
* 'local_cert' => 'server.pem'
* ));
* ```
*
* Note that the certificate file will not be loaded on instantiation but when an
* incoming connection initializes its TLS context.
* This implies that any invalid certificate file paths or contents will only cause
* an `error` event at a later time.
*
* If your private key is encrypted with a passphrase, you have to specify it
* like this:
*
* ```php
* $server = new Server(8000, $loop);
* $server = new SecureServer($server, $loop, array(
* 'local_cert' => 'server.pem',
* 'passphrase' => 'secret'
* ));
* ```
*
* Note that available [TLS context options],
* their defaults and effects of changing these may vary depending on your system
* and/or PHP version.
* Passing unknown context options has no effect.
*
* Advanced usage: Internally, the `SecureServer` has to set the required
* context options on the underlying stream resources.
* It should therefor be used with an unmodified `Server` instance as first
* parameter so that it can allocate an empty context resource which this
* class uses to set required TLS context options.
* Failing to do so may result in some hard to trace race conditions,
* because all stream resources will use a single, shared default context
* resource otherwise.
*
* @param Server $tcp
* @param LoopInterface $loop
* @param array $context
* @throws ConnectionException
* @see Server
* @link http://php.net/manual/en/context.ssl.php for TLS context options
*/
public function __construct(Server $tcp, LoopInterface $loop, array $context)
{
if (!is_resource($tcp->master)) {
Expand Down Expand Up @@ -81,6 +150,12 @@ public function close()
/** @internal */
public function handleConnection(ConnectionInterface $connection)
{
if (!$connection instanceof Stream) {
$this->emit('error', array(new \UnexpectedValueException('Connection event MUST emit an instance extending Stream in order to access underlying stream resource')));
$connection->end();
return;
}

$that = $this;

$this->encryption->enable($connection)->then(
Expand Down
31 changes: 31 additions & 0 deletions tests/SecureServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,35 @@ public function testCloseWillBePassedThroughToTcpServer()

$server->close();
}

public function testConnectionWillBeEndedWithErrorIfItIsNotAStream()
{
$tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->setMethods(null)->getMock();
$tcp->master = stream_socket_server('tcp://localhost:0');

$loop = $this->getMock('React\EventLoop\LoopInterface');

$connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$connection->expects($this->once())->method('end');

$server = new SecureServer($tcp, $loop, array());

$server->on('error', $this->expectCallableOnce());

$tcp->emit('connection', array($connection));
}

public function testSocketErrorWillBeForwarded()
{
$tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->setMethods(null)->getMock();
$tcp->master = stream_socket_server('tcp://localhost:0');

$loop = $this->getMock('React\EventLoop\LoopInterface');

$server = new SecureServer($tcp, $loop, array());

$server->on('error', $this->expectCallableOnce());

$tcp->emit('error', array(new \RuntimeException('test')));
}
}