Skip to content

Do not expose $master property and accept any ServerInterface for SecureServer #70

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
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
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,19 @@ 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.
> Advanced usage: Despite allowing any `ServerInterface` as first parameter,
you SHOULD pass an unmodified `Server` instance as first parameter, unless you
know what you're doing.
Internally, the `SecureServer` has to set the required TLS context options on
the underlying stream resources.
These resources are not exposed through any of the interfaces defined in this
package, but only through the `React\Stream\Stream` class.
The unmodified `Server` class is guaranteed to emit connections that implement
the `ConnectionInterface` and also extend the `Stream` class in order to
expose these underlying resources.
If you use a custom `ServerInterface` and its `connection` event does not
meet this requirement, the `SecureServer` will emit an `error` event and
then close the underlying connection.

### ConnectionInterface

Expand Down
40 changes: 21 additions & 19 deletions src/SecureServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class SecureServer extends EventEmitter implements ServerInterface
{
private $tcp;
private $encryption;
private $context;

/**
* Creates a secure TLS server and starts waiting for incoming connections
Expand Down Expand Up @@ -94,39 +95,36 @@ class SecureServer extends EventEmitter implements ServerInterface
* 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.
* Advanced usage: Despite allowing any `ServerInterface` as first parameter,
* you SHOULD pass an unmodified `Server` instance as first parameter, unless you
* know what you're doing.
* Internally, the `SecureServer` has to set the required TLS context options on
* the underlying stream resources.
* These resources are not exposed through any of the interfaces defined in this
* package, but only through the `React\Stream\Stream` class.
* The unmodified `Server` class is guaranteed to emit connections that implement
* the `ConnectionInterface` and also extend the `Stream` class in order to
* expose these underlying resources.
* If you use a custom `ServerInterface` and its `connection` event does not
* meet this requirement, the `SecureServer` will emit an `error` event and
* then close the underlying connection.
*
* @param Server $tcp
* @param ServerInterface|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)
public function __construct(ServerInterface $tcp, LoopInterface $loop, array $context)
{
if (!is_resource($tcp->master)) {
throw new ConnectionException('TCP server already shut down');
}

// default to empty passphrase to surpress blocking passphrase prompt
$context += array(
'passphrase' => ''
);

foreach ($context as $name => $value) {
stream_context_set_option($tcp->master, 'ssl', $name, $value);
}

$this->tcp = $tcp;
$this->encryption = new StreamEncryption($loop);
$this->context = $context;

$that = $this;
$this->tcp->on('connection', function ($connection) use ($that) {
Expand Down Expand Up @@ -156,6 +154,10 @@ public function handleConnection(ConnectionInterface $connection)
return;
}

foreach ($this->context as $name => $value) {
stream_context_set_option($connection->stream, 'ssl', $name, $value);
}

$that = $this;

$this->encryption->enable($connection)->then(
Expand Down
2 changes: 1 addition & 1 deletion src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
*/
class Server extends EventEmitter implements ServerInterface
{
public $master;
private $master;
private $loop;

/**
Expand Down
13 changes: 0 additions & 13 deletions tests/FunctionalSecureServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -368,19 +368,6 @@ public function testEmitsErrorIfConnectionIsNotSecureHandshake()
Block\sleep(self::TIMEOUT, $loop);
}

/**
* @expectedException React\Socket\ConnectionException
*/
public function testFailsToCreateSecureServerOnClosedSocket()
{
$loop = Factory::create();

$server = new Server(0, $loop);
$server->close();

new SecureServer($server, $loop, array());
}

private function getPort(ServerInterface $server)
{
return parse_url($server->getAddress(), PHP_URL_PORT);
Expand Down
15 changes: 13 additions & 2 deletions tests/FunctionalServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public function testEmitsConnectionWithLocalIpv6()
$this->assertEquals($server->getAddress(), $local);
}

public function testAppliesContextOptionsToSocketStreamResource()
public function testEmitsConnectionWithInheritedContextOptions()
{
if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.13', '<')) {
// https://3v4l.org/hB4Tc
Expand All @@ -242,7 +242,18 @@ public function testAppliesContextOptionsToSocketStreamResource()
'backlog' => 4
));

$all = stream_context_get_options($server->master);
$all = null;
$server->on('connection', function (ConnectionInterface $conn) use (&$all) {
$all = stream_context_get_options($conn->stream);
});
$port = $this->getPort($server);

$connector = new TcpConnector($loop);
$promise = $connector->create('127.0.0.1', $port);

$promise->then($this->expectCallableOnce());

Block\sleep(0.1, $loop);

$this->assertEquals(array('socket' => array('backlog' => 4)), $all);
}
Expand Down
8 changes: 2 additions & 6 deletions tests/SecureServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ public function setUp()

public function testGetAddressWillBePassedThroughToTcpServer()
{
$tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->getMock();
$tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock();
$tcp->expects($this->once())->method('getAddress')->willReturn('127.0.0.1:1234');
$tcp->master = stream_socket_server('tcp://localhost:0');

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

Expand All @@ -28,9 +27,8 @@ public function testGetAddressWillBePassedThroughToTcpServer()

public function testCloseWillBePassedThroughToTcpServer()
{
$tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->getMock();
$tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock();
$tcp->expects($this->once())->method('close');
$tcp->master = stream_socket_server('tcp://localhost:0');

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

Expand All @@ -42,7 +40,6 @@ public function testCloseWillBePassedThroughToTcpServer()
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');

Expand All @@ -59,7 +56,6 @@ public function testConnectionWillBeEndedWithErrorIfItIsNotAStream()
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');

Expand Down