Skip to content

Commit 6306292

Browse files
committed
Add Opportunistic TLS implementation
This commit introduces the functionality required to build opportunistic TLS clients and servers with ReactPHP. It does so by introducing a prefix to `tls://`, namely `opportunistic`, to create `opportunistic+tls://example.com:5432` for example as the full URL. This will create an `OpportunisticTlsConnectionInterface` (instead of a `ConnectionInterface`) that extends the `ConnectionInterface` and exposes the `enableEncryption` method to enable TLS encryption at the desired moment. Inside this PR is an example of a server and client negotiating when to enable TLS and enable it when ready. Depends on: reactphp/async#65 Opportunistic Security described in RFC7435: https://www.rfc-editor.org/rfc/rfc7435 External PR using the proposed changes in this commit: voryx/PgAsync#52
1 parent 936546b commit 6306292

10 files changed

+524
-9
lines changed

README.md

+107-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ handle multiple concurrent connections without blocking.
2323
* [ConnectionInterface](#connectioninterface)
2424
* [getRemoteAddress()](#getremoteaddress)
2525
* [getLocalAddress()](#getlocaladdress)
26+
* [OpportunisticTlsConnectionInterface](#opportunistictlsconnectioninterface)
27+
* [enableEncryption()](#enableencryption)
2628
* [Server usage](#server-usage)
2729
* [ServerInterface](#serverinterface)
2830
* [connection event](#connection-event)
@@ -193,6 +195,62 @@ If your system has multiple interfaces (e.g. a WAN and a LAN interface),
193195
you can use this method to find out which interface was actually
194196
used for this connection.
195197

198+
### OpportunisticTlsConnectionInterface
199+
200+
The `OpportunisticTlsConnectionInterface` extends the [`ConnectionInterface`](#connectioninterface) and adds the ability of
201+
enabling the TLS encryption on the connection when desired.
202+
203+
#### enableEncryption
204+
205+
When negotiated with the server when to start encrypting traffic using TLS you enable it by calling
206+
`enableEncryption()` which returns a promise that resolve with a `OpportunisticTlsConnectionInterface` connection but now all
207+
traffic back and forth will be encrypted.
208+
209+
In the following example we ask the server if they want to encrypt the connection, and when it responds with `yes` we
210+
enable the encryption:
211+
212+
```php
213+
$connector = new React\Socket\Connector();
214+
$connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
215+
$connection->write('let\'s encrypt?');
216+
217+
return React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
218+
if ($data === 'yes') {
219+
return $connection->enableEncryption();
220+
}
221+
222+
return $stream;
223+
});
224+
})->then(function (React\Socket\ConnectionInterface $connection) {
225+
$connection->write('Hello!');
226+
});
227+
```
228+
229+
The `enableEncryption` function resolves with itself. As such you can't see the data encrypted when you hook into the
230+
events before enabling, as shown below:
231+
232+
```php
233+
$connector = new React\Socket\Connector();
234+
$connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
235+
$connection->on('data', function ($data) {
236+
echo 'Raw: ', $data, PHP_EOL;
237+
});
238+
239+
return $connection->enableEncryption();
240+
})->then(function (React\Socket\ConnectionInterface $connection) {
241+
$connection->on('data', function ($data) {
242+
echo 'TLS: ', $data, PHP_EOL;
243+
});
244+
});
245+
```
246+
247+
When the other side send `Hello World!` over the encrypted connection, the output will be the following:
248+
249+
```
250+
Raw: Hello World!
251+
TLS: Hello World!
252+
```
253+
196254
## Server usage
197255

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

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

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

539+
To start a server with opportunistic TLS support use `opportunistic+tls://` as the scheme instead of `tls://`:
540+
541+
```php
542+
$socket = new React\Socket\SocketServer('opportunistic+tls://127.0.0.1:8000', array(
543+
'tls' => array(
544+
'local_cert' => 'server.pem',
545+
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
546+
)
547+
));
548+
$server->on('connection', static function (OpportunisticTlsConnectionInterface $connection) use ($server) {
549+
return $connection->enableEncryption();
550+
});
551+
```
552+
553+
See also the [examples](examples).
554+
481555
> Note that available [TLS context options](https://www.php.net/manual/en/context.ssl.php),
482556
their defaults and effects of changing these may vary depending on your system
483557
and/or PHP version.
@@ -697,6 +771,21 @@ here in order to use the [default loop](https://github.com/reactphp/event-loop#l
697771
This value SHOULD NOT be given unless you're sure you want to explicitly use a
698772
given event loop instance.
699773

774+
Opportunistic TLS is supported by the secure server by passing true in as 4th constructor
775+
parameter. This, when a client connects, emits a
776+
[`OpportunisticTlsConnectionInterface`](#opportunistictlsconnectioninterface) instead
777+
of the default [`ConnectionInterface`](#connectioninterface) and won't be TLS encrypted
778+
from the start so you can enable the TLS encryption on the connection after negotiating
779+
with the client.
780+
781+
```php
782+
$server = new React\Socket\TcpServer(8000);
783+
$server = new React\Socket\SecureServer($server, null, array(
784+
'local_cert' => 'server.pem',
785+
'passphrase' => 'secret'
786+
), true);
787+
```
788+
700789
> Advanced usage: Despite allowing any `ServerInterface` as first parameter,
701790
you SHOULD pass a `TcpServer` instance as first parameter, unless you
702791
know what you're doing.
@@ -1389,6 +1478,20 @@ $secureConnector = new React\Socket\SecureConnector($dnsConnector, null, array(
13891478
));
13901479
```
13911480

1481+
Opportunistic TLS is supported by the secure connector by using `opportunistic-tls://` as scheme instead of `tls://`. This, when
1482+
connected, returns a [`OpportunisticTlsConnectionInterface`](#opportunistictlsconnectioninterface) instead of the default
1483+
[`ConnectionInterface`](#connectioninterface) and won't be TLS encrypted from the start so you can enable the TLS
1484+
encryption on the connection after negotiating with the server.
1485+
1486+
```php
1487+
$secureConnector = new React\Socket\SecureConnector($dnsConnector);
1488+
$secureConnector->connect('opportunistic-tls://example.com:5432')->then(function (OpportunisticTlsConnectionInterface $connection) {
1489+
return $connection->enableEncryption();
1490+
})->then(function (OpportunisticTlsConnectionInterface $connection) {
1491+
$connection->write('Encrypted hi!');
1492+
});
1493+
```
1494+
13921495
> Advanced usage: Internally, the `SecureConnector` relies on setting up the
13931496
required *context options* on the underlying stream resource.
13941497
It should therefor be used with a `TcpConnector` somewhere in the connector

examples/31-opportunistic-tls.php

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
// Opportunistic TLS example showing a basic negotiation before enabling the encryption. It starts out as an
4+
// unencrypted TCP connection. After both parties agreed to encrypt the connection they both enable the encryption.
5+
// After which any communication over the line is encrypted.
6+
//
7+
// This example is design to show both sides in one go, as such the server stops listening for new connection after
8+
// the first, this makes sure the loop shuts down after the example connection has closed.
9+
//
10+
// $ php examples/31-opportunistic-tls.php
11+
12+
use React\EventLoop\Loop;
13+
use React\Socket\ConnectionInterface;
14+
use React\Socket\Connector;
15+
use React\Socket\OpportunisticTlsConnectionInterface;
16+
use React\Socket\SocketServer;
17+
18+
require __DIR__ . '/../vendor/autoload.php';
19+
20+
$server = new SocketServer('opportunistic+tls://127.0.0.1:0', array(
21+
'tls' => array(
22+
'local_cert' => __DIR__ . '/localhost.pem',
23+
)
24+
));
25+
$server->on('connection', static function (OpportunisticTlsConnectionInterface $connection) use ($server) {
26+
$server->close();
27+
28+
$connection->on('data', function ($data) {
29+
echo 'From Client: ', $data, PHP_EOL;
30+
});
31+
React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
32+
if ($data === 'Let\'s encrypt?') {
33+
$connection->write('yes');
34+
return $connection->enableEncryption();
35+
}
36+
37+
return $connection;
38+
})->then(static function (ConnectionInterface $connection) {
39+
$connection->write('Encryption enabled!');
40+
})->done();
41+
});
42+
43+
$client = new Connector(array(
44+
'tls' => array(
45+
'verify_peer' => false,
46+
'verify_peer_name' => false,
47+
'allow_self_signed' => true,
48+
),
49+
));
50+
$client->connect($server->getAddress())->then(static function (OpportunisticTlsConnectionInterface $connection) {
51+
$connection->on('data', function ($data) {
52+
echo 'From Server: ', $data, PHP_EOL;
53+
});
54+
$connection->write('Let\'s encrypt?');
55+
56+
return React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
57+
if ($data === 'yes') {
58+
return $connection->enableEncryption();
59+
}
60+
61+
return $connection;
62+
});
63+
})->then(function (ConnectionInterface $connection) {
64+
$connection->write('Encryption enabled!');
65+
Loop::addTimer(1, static function () use ($connection) {
66+
$connection->end('Cool! Bye!');
67+
});
68+
})->done();

src/Connector.php

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public function __construct($context = array(), $loop = null)
7575
'dns' => true,
7676
'timeout' => true,
7777
'happy_eyeballs' => true,
78+
'opportunistic+tls' => true,
7879
);
7980

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

152153
$this->connectors['tls'] = $context['tls'];
154+
if ($context['opportunistic+tls'] !== false) {
155+
$this->connectors['opportunistic+tls'] = $this->connectors['tls'];
156+
}
153157
}
154158

155159
if ($context['unix'] !== false) {

src/OpportunisticTlsConnection.php

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace React\Socket;
4+
5+
use Evenement\EventEmitter;
6+
use React\EventLoop\LoopInterface;
7+
use React\Promise\PromiseInterface;
8+
use React\Stream\DuplexResourceStream;
9+
use React\Stream\Util;
10+
use React\Stream\WritableResourceStream;
11+
use React\Stream\WritableStreamInterface;
12+
13+
/**
14+
* The actual connection implementation for StartTlsConnectionInterface
15+
*
16+
* This class should only be used internally, see StartTlsConnectionInterface instead.
17+
*
18+
* @see OpportunisticTlsConnectionInterface
19+
* @internal
20+
*/
21+
class OpportunisticTlsConnection extends EventEmitter implements OpportunisticTlsConnectionInterface
22+
{
23+
/** @var Connection */
24+
private $connection;
25+
26+
/** @var StreamEncryption */
27+
private $streamEncryption;
28+
29+
/** @var string */
30+
private $uri;
31+
32+
public function __construct(Connection $connection, StreamEncryption $streamEncryption, $uri)
33+
{
34+
$this->connection = $connection;
35+
$this->streamEncryption = $streamEncryption;
36+
$this->uri = $uri;
37+
38+
Util::forwardEvents($connection, $this, array('data', 'end', 'error', 'close'));
39+
}
40+
41+
public function getRemoteAddress()
42+
{
43+
return $this->connection->getRemoteAddress();
44+
}
45+
46+
public function getLocalAddress()
47+
{
48+
return $this->connection->getLocalAddress();
49+
}
50+
51+
public function isReadable()
52+
{
53+
return $this->connection->isReadable();
54+
}
55+
56+
public function pause()
57+
{
58+
$this->connection->pause();
59+
}
60+
61+
public function resume()
62+
{
63+
$this->connection->resume();
64+
}
65+
66+
public function pipe(WritableStreamInterface $dest, array $options = array())
67+
{
68+
return $this->connection->pipe($dest, $options);
69+
}
70+
71+
public function close()
72+
{
73+
$this->connection->close();
74+
}
75+
76+
public function enableEncryption()
77+
{
78+
$that = $this;
79+
$connection = $this->connection;
80+
$uri = $this->uri;
81+
82+
return $this->streamEncryption->enable($connection)->then(function () use ($that) {
83+
return $that;
84+
}, function ($error) use ($connection, $uri) {
85+
// establishing encryption failed => close invalid connection and return error
86+
$connection->close();
87+
88+
throw new \RuntimeException(
89+
'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(),
90+
$error->getCode()
91+
);
92+
});
93+
}
94+
95+
public function isWritable()
96+
{
97+
return $this->connection->isWritable();
98+
}
99+
100+
public function write($data)
101+
{
102+
return $this->connection->write($data);
103+
}
104+
105+
public function end($data = null)
106+
{
107+
$this->connection->end($data);
108+
}
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace React\Socket;
4+
5+
use React\Promise\PromiseInterface;
6+
7+
/**
8+
* @see DuplexStreamInterface
9+
* @see ServerInterface
10+
* @see ConnectionInterface
11+
*/
12+
interface OpportunisticTlsConnectionInterface extends ConnectionInterface
13+
{
14+
/**
15+
* @return PromiseInterface<OpportunisticTlsConnectionInterface>
16+
*/
17+
public function enableEncryption();
18+
}

0 commit comments

Comments
 (0)