Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 454673d

Browse files
committedDec 5, 2022
Add Opportunistic TLS implementation
1 parent 936546b commit 454673d

11 files changed

+528
-9
lines changed
 

‎.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jobs:
99
name: PHPUnit (PHP ${{ matrix.php }} on ${{ matrix.os }})
1010
runs-on: ${{ matrix.os }}
1111
strategy:
12+
fail-fast: false
1213
matrix:
1314
os:
1415
- ubuntu-22.04

‎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)
Please sign in to comment.