Skip to content

Commit ca73095

Browse files
authored
Merge pull request #69 from clue-labs/secure-connection
Documentation and tests for exposing secure context options
2 parents 535f323 + f4d7314 commit ca73095

File tree

3 files changed

+138
-14
lines changed

3 files changed

+138
-14
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ $server = new SecureServer($server, $loop, array(
263263
));
264264
```
265265

266+
> Note that available [TLS context options](http://php.net/manual/en/context.ssl.php),
267+
their defaults and effects of changing these may vary depending on your system
268+
and/or PHP version.
269+
Passing unknown context options has no effect.
270+
266271
Whenever a client completes the TLS handshake, it will emit a `connection` event
267272
with a connection instance implementing [`ConnectionInterface`](#connectioninterface):
268273

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

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

294+
Note that the `SecureServer` class is a concrete implementation for TLS sockets.
295+
If you want to typehint in your higher-level protocol implementation, you SHOULD
296+
use the generic [`ServerInterface`](#serverinterface) instead.
297+
298+
> Advanced usage: Internally, the `SecureServer` has to set the required
299+
context options on the underlying stream resources.
300+
It should therefor be used with an unmodified `Server` instance as first
301+
parameter so that it can allocate an empty context resource which this
302+
class uses to set required TLS context options.
303+
Failing to do so may result in some hard to trace race conditions,
304+
because all stream resources will use a single, shared default context
305+
resource otherwise.
306+
289307
### ConnectionInterface
290308

291309
The `ConnectionInterface` is used to represent any incoming connection.

src/SecureServer.php

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,110 @@
66
use React\EventLoop\LoopInterface;
77
use React\Socket\Server;
88
use React\Socket\ConnectionInterface;
9+
use React\Stream\Stream;
910

1011
/**
1112
* The `SecureServer` class implements the `ServerInterface` and is responsible
1213
* for providing a secure TLS (formerly known as SSL) server.
1314
*
1415
* It does so by wrapping a `Server` instance which waits for plaintext
1516
* TCP/IP connections and then performs a TLS handshake for each connection.
16-
* It thus requires valid [TLS context options],
17-
* which in its most basic form may look something like this if you're using a
18-
* PEM encoded certificate file:
1917
*
18+
* ```php
19+
* $server = new Server(8000, $loop);
20+
* $server = new SecureServer($server, $loop, array(
21+
* // tls context options here…
22+
* ));
2023
* ```
21-
* $context = array(
22-
* 'local_cert' => __DIR__ . '/localhost.pem'
23-
* );
24+
*
25+
* Whenever a client completes the TLS handshake, it will emit a `connection` event
26+
* with a connection instance implementing [`ConnectionInterface`](#connectioninterface):
27+
*
28+
* ```php
29+
* $server->on('connection', function (ConnectionInterface $connection) {
30+
* echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL;
31+
*
32+
* $connection->write('hello there!' . PHP_EOL);
33+
* …
34+
* });
2435
* ```
2536
*
26-
* If your private key is encrypted with a passphrase, you have to specify it
27-
* like this:
37+
* Whenever a client fails to perform a successful TLS handshake, it will emit an
38+
* `error` event and then close the underlying TCP/IP connection:
2839
*
2940
* ```php
30-
* $context = array(
31-
* 'local_cert' => 'server.pem',
32-
* 'passphrase' => 'secret'
33-
* );
41+
* $server->on('error', function (Exception $e) {
42+
* echo 'Error' . $e->getMessage() . PHP_EOL;
43+
* });
3444
* ```
3545
*
36-
* @see Server
37-
* @link http://php.net/manual/en/context.ssl.php for TLS context options
46+
* See also the `ServerInterface` for more details.
47+
*
48+
* Note that the `SecureServer` class is a concrete implementation for TLS sockets.
49+
* If you want to typehint in your higher-level protocol implementation, you SHOULD
50+
* use the generic `ServerInterface` instead.
51+
*
52+
* @see ServerInterface
53+
* @see ConnectionInterface
3854
*/
3955
class SecureServer extends EventEmitter implements ServerInterface
4056
{
4157
private $tcp;
4258
private $encryption;
4359

60+
/**
61+
* Creates a secure TLS server and starts waiting for incoming connections
62+
*
63+
* It does so by wrapping a `Server` instance which waits for plaintext
64+
* TCP/IP connections and then performs a TLS handshake for each connection.
65+
* It thus requires valid [TLS context options],
66+
* which in its most basic form may look something like this if you're using a
67+
* PEM encoded certificate file:
68+
*
69+
* ```php
70+
* $server = new Server(8000, $loop);
71+
* $server = new SecureServer($server, $loop, array(
72+
* 'local_cert' => 'server.pem'
73+
* ));
74+
* ```
75+
*
76+
* Note that the certificate file will not be loaded on instantiation but when an
77+
* incoming connection initializes its TLS context.
78+
* This implies that any invalid certificate file paths or contents will only cause
79+
* an `error` event at a later time.
80+
*
81+
* If your private key is encrypted with a passphrase, you have to specify it
82+
* like this:
83+
*
84+
* ```php
85+
* $server = new Server(8000, $loop);
86+
* $server = new SecureServer($server, $loop, array(
87+
* 'local_cert' => 'server.pem',
88+
* 'passphrase' => 'secret'
89+
* ));
90+
* ```
91+
*
92+
* Note that available [TLS context options],
93+
* their defaults and effects of changing these may vary depending on your system
94+
* and/or PHP version.
95+
* Passing unknown context options has no effect.
96+
*
97+
* Advanced usage: Internally, the `SecureServer` has to set the required
98+
* context options on the underlying stream resources.
99+
* It should therefor be used with an unmodified `Server` instance as first
100+
* parameter so that it can allocate an empty context resource which this
101+
* class uses to set required TLS context options.
102+
* Failing to do so may result in some hard to trace race conditions,
103+
* because all stream resources will use a single, shared default context
104+
* resource otherwise.
105+
*
106+
* @param Server $tcp
107+
* @param LoopInterface $loop
108+
* @param array $context
109+
* @throws ConnectionException
110+
* @see Server
111+
* @link http://php.net/manual/en/context.ssl.php for TLS context options
112+
*/
44113
public function __construct(Server $tcp, LoopInterface $loop, array $context)
45114
{
46115
if (!is_resource($tcp->master)) {
@@ -81,6 +150,12 @@ public function close()
81150
/** @internal */
82151
public function handleConnection(ConnectionInterface $connection)
83152
{
153+
if (!$connection instanceof Stream) {
154+
$this->emit('error', array(new \UnexpectedValueException('Connection event MUST emit an instance extending Stream in order to access underlying stream resource')));
155+
$connection->end();
156+
return;
157+
}
158+
84159
$that = $this;
85160

86161
$this->encryption->enable($connection)->then(

tests/SecureServerTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,35 @@ public function testCloseWillBePassedThroughToTcpServer()
3838

3939
$server->close();
4040
}
41+
42+
public function testConnectionWillBeEndedWithErrorIfItIsNotAStream()
43+
{
44+
$tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->setMethods(null)->getMock();
45+
$tcp->master = stream_socket_server('tcp://localhost:0');
46+
47+
$loop = $this->getMock('React\EventLoop\LoopInterface');
48+
49+
$connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
50+
$connection->expects($this->once())->method('end');
51+
52+
$server = new SecureServer($tcp, $loop, array());
53+
54+
$server->on('error', $this->expectCallableOnce());
55+
56+
$tcp->emit('connection', array($connection));
57+
}
58+
59+
public function testSocketErrorWillBeForwarded()
60+
{
61+
$tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->setMethods(null)->getMock();
62+
$tcp->master = stream_socket_server('tcp://localhost:0');
63+
64+
$loop = $this->getMock('React\EventLoop\LoopInterface');
65+
66+
$server = new SecureServer($tcp, $loop, array());
67+
68+
$server->on('error', $this->expectCallableOnce());
69+
70+
$tcp->emit('error', array(new \RuntimeException('test')));
71+
}
4172
}

0 commit comments

Comments
 (0)