Skip to content

Commit 1c16a5a

Browse files
committed
Add SecureServer for secure TLS connections
1 parent 8740211 commit 1c16a5a

12 files changed

+597
-3
lines changed

README.md

+51
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and [`Stream`](https://github.com/reactphp/stream) components.
1313
* [Quickstart example](#quickstart-example)
1414
* [Usage](#usage)
1515
* [Server](#server)
16+
* [SecureServer](#secureserver)
1617
* [ConnectionInterface](#connectioninterface)
1718
* [getRemoteAddress()](#getremoteaddress)
1819
* [Install](#install)
@@ -77,10 +78,60 @@ instance implementing [`ConnectionInterface`](#connectioninterface):
7778

7879
```php
7980
$server->on('connection', function (ConnectionInterface $connection) {
81+
echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
82+
83+
$connection->write('hello there!' . PHP_EOL);
8084
8185
});
8286
```
8387

88+
### SecureServer
89+
90+
The `SecureServer` class implements the `ServerInterface` and is responsible
91+
for providing a secure TLS (formerly known as SSL) server.
92+
93+
It does so by wrapping a [`Server`](#server) instance which waits for plaintext
94+
TCP/IP connections and then performs a TLS handshake for each connection.
95+
It thus requires valid [TLS context options](http://php.net/manual/en/context.ssl.php),
96+
which in its most basic form may look something like this if you're using a
97+
PEM encoded certificate file:
98+
99+
```php
100+
$server = new Server($loop);
101+
102+
$server = new SecureServer($server, $loop, array(
103+
'local_cert' => 'server.pem'
104+
));
105+
106+
$server->listen(8000);
107+
```
108+
109+
> Note that the certificate file will not be loaded on instantiation but when an
110+
incoming connection initializes its TLS context.
111+
This implies that any invalid certificate file paths or contents will only cause
112+
an `error` event at a later time.
113+
114+
Whenever a client completes the TLS handshake, it will emit a `connection` event
115+
with a connection instance implementing [`ConnectionInterface`](#connectioninterface):
116+
117+
```php
118+
$server->on('connection', function (ConnectionInterface $connection) {
119+
echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL;
120+
121+
$connection->write('hello there!' . PHP_EOL);
122+
123+
});
124+
```
125+
126+
Whenever a client fails to perform a successful TLS handshake, it will emit an
127+
`error` event and then close the underlying TCP/IP connection:
128+
129+
```php
130+
$server->on('error', function (Exception $e) {
131+
echo 'Error' . $e->getMessage() . PHP_EOL;
132+
});
133+
```
134+
84135
### ConnectionInterface
85136

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

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"php": ">=5.3.0",
88
"evenement/evenement": "~2.0|~1.0",
99
"react/event-loop": "0.4.*|0.3.*",
10-
"react/stream": "^0.4.2"
10+
"react/stream": "^0.4.5",
11+
"react/promise": "^2.0 || ^1.1"
1112
},
1213
"require-dev": {
1314
"react/socket-client": "^0.5.1",

examples/01-echo.php

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,39 @@
55
//
66
// $ php examples/01-echo.php 8000
77
// $ telnet localhost 8000
8+
//
9+
// You can also run a secure TLS echo server like this:
10+
//
11+
// $ php examples/01-echo.php 8000 examples/localhost.pem
12+
// $ openssl s_client -connect localhost:8000
813

914
use React\EventLoop\Factory;
1015
use React\Socket\Server;
1116
use React\Socket\ConnectionInterface;
17+
use React\Socket\SecureServer;
1218

1319
require __DIR__ . '/../vendor/autoload.php';
1420

1521
$loop = Factory::create();
1622

1723
$server = new Server($loop);
24+
25+
// secure TLS mode if certificate is given as second parameter
26+
if (isset($argv[2])) {
27+
$server = new SecureServer($server, $loop, array(
28+
'local_cert' => $argv[2]
29+
));
30+
}
31+
1832
$server->listen(isset($argv[1]) ? $argv[1] : 0);
1933

2034
$server->on('connection', function (ConnectionInterface $conn) use ($loop) {
2135
echo '[connected]' . PHP_EOL;
2236
$conn->pipe($conn);
2337
});
2438

25-
echo 'Listening on ' . $server->getPort() . PHP_EOL;
39+
$server->on('error', 'printf');
40+
41+
echo 'bound to ' . $server->getPort() . PHP_EOL;
2642

2743
$loop->run();

examples/02-chat-server.php

+16
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,30 @@
55
//
66
// $ php examples/02-chat-server.php 8000
77
// $ telnet localhost 8000
8+
//
9+
// You can also run a secure TLS chat server like this:
10+
//
11+
// $ php examples/02-chat-server.php 8000 examples/localhost.pem
12+
// $ openssl s_client -connect localhost:8000
813

914
use React\EventLoop\Factory;
1015
use React\Socket\Server;
1116
use React\Socket\ConnectionInterface;
17+
use React\Socket\SecureServer;
1218

1319
require __DIR__ . '/../vendor/autoload.php';
1420

1521
$loop = Factory::create();
1622

1723
$server = new Server($loop);
24+
25+
// secure TLS mode if certificate is given as second parameter
26+
if (isset($argv[2])) {
27+
$server = new SecureServer($server, $loop, array(
28+
'local_cert' => $argv[2]
29+
));
30+
}
31+
1832
$server->listen(isset($argv[1]) ? $argv[1] : 0, '0.0.0.0');
1933

2034
$clients = array();
@@ -44,6 +58,8 @@
4458
});
4559
});
4660

61+
$server->on('error', 'printf');
62+
4763
echo 'Listening on ' . $server->getPort() . PHP_EOL;
4864

4965
$loop->run();

examples/03-benchmark.php

+16
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,32 @@
88
// $ telnet localhost 8000
99
// $ echo hello world | nc -v localhost 8000
1010
// $ dd if=/dev/zero bs=1M count=1000 | nc -v localhost 8000
11+
//
12+
// You can also run a secure TLS benchmarking server like this:
13+
//
14+
// $ php examples/03-benchmark.php 8000 examples/localhost.pem
15+
// $ openssl s_client -connect localhost:8000
16+
// $ echo hello world | openssl s_client -connect localhost:8000
17+
// $ dd if=/dev/zero bs=1M count=1000 | openssl s_client -connect localhost:8000
1118

1219
use React\EventLoop\Factory;
1320
use React\Socket\Server;
1421
use React\Socket\ConnectionInterface;
22+
use React\Socket\SecureServer;
1523

1624
require __DIR__ . '/../vendor/autoload.php';
1725

1826
$loop = Factory::create();
1927

2028
$server = new Server($loop);
29+
30+
// secure TLS mode if certificate is given as second parameter
31+
if (isset($argv[2])) {
32+
$server = new SecureServer($server, $loop, array(
33+
'local_cert' => $argv[2]
34+
));
35+
}
36+
2137
$server->listen(isset($argv[1]) ? $argv[1] : 0);
2238

2339
$server->on('connection', function (ConnectionInterface $conn) use ($loop) {

examples/10-generate-self-signed.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
// A very simple helper script used to generate self-signed certificates.
4+
// Accepts the CN and an optional passphrase to encrypt the private key.
5+
//
6+
// $ php 10-generate-self-signed.php localhost swordfish > secret.pem
7+
8+
// certificate details (Distinguished Name)
9+
// (OpenSSL applies defaults to missing fields)
10+
$dn = array(
11+
"commonName" => isset($argv[1]) ? $argv[1] : "localhost",
12+
// "countryName" => "AU",
13+
// "stateOrProvinceName" => "Some-State",
14+
// "localityName" => "London",
15+
// "organizationName" => "Internet Widgits Pty Ltd",
16+
// "organizationalUnitName" => "R&D",
17+
// "emailAddress" => "[email protected]"
18+
);
19+
20+
// create certificate which is valid for ~10 years
21+
$privkey = openssl_pkey_new();
22+
$cert = openssl_csr_new($dn, $privkey);
23+
$cert = openssl_csr_sign($cert, null, $privkey, 3650);
24+
25+
// export public and (optionally encrypted) private key in PEM format
26+
openssl_x509_export($cert, $out);
27+
echo $out;
28+
29+
$passphrase = isset($argv[2]) ? $argv[2] : null;
30+
openssl_pkey_export($privkey, $out, $passphrase);
31+
echo $out;

examples/localhost.pem

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu
3+
MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK
4+
DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQ1OTA2WhcNMjYx
5+
MjI4MTQ1OTA2WjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw
6+
EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0
7+
eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8SZWNS+Ktg0Py
8+
W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN
9+
2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9
10+
zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0
11+
UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4
12+
wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY
13+
YCUE54G/AgMBAAGjUDBOMB0GA1UdDgQWBBQ2GRz3QsQzdXaTMnPVCKfpigA10DAf
14+
BgNVHSMEGDAWgBQ2GRz3QsQzdXaTMnPVCKfpigA10DAMBgNVHRMEBTADAQH/MA0G
15+
CSqGSIb3DQEBBQUAA4IBAQA77iZ4KrpPY18Ezjt0mngYAuAxunKddXYdLZ2khywN
16+
0uI/VzYnkFVtrsC7y2jLHSxlmE2/viPPGZDUplENV2acN6JNW+tlt7/bsrQHDQw3
17+
7VCF27EWiDxHsaghhLkqC+kcop5YR5c0oDQTdEWEKSbow2zayUXDYbRRs76SClTe
18+
824Yul+Ts8Mka+AX2PXDg47iZ84fJRN/nKavcJUTJ2iS1uYw0GNnFMge/uwsfMR3
19+
V47qN0X5emky8fcq99FlMCbcy0gHAeSWAjClgr2dd2i0LDatUbj7YmdmFcskOgII
20+
IwGfvuWR2yPevYGAE0QgFeLHniN3RW8zmpnX/XtrJ4a7
21+
-----END CERTIFICATE-----
22+
-----BEGIN PRIVATE KEY-----
23+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8SZWNS+Ktg0Py
24+
W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN
25+
2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9
26+
zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0
27+
UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4
28+
wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY
29+
YCUE54G/AgMBAAECggEBAKiO/3FE1CMddkCLZVtUp8ShqJgRokx9WI5ecwFApAkV
30+
ZHsjqDQQYRNmxhDUX/w0tOzLGyhde2xjJyZG29YviKsbHwu6zYwbeOzy/mkGOaK/
31+
g6DmmMmRs9Z6juifoQCu4GIFZ6il2adIL2vF7OeJh+eKudQj/7NFRSB7mXzNrQWK
32+
tZY3eux5zXWmio7pgZrx1HFZQiiL9NVLwT9J7oBnaoO3fREiu5J2xBpljG9Cr0j1
33+
LLiVLhukWJYRlHDtGt1CzI9w8iKo44PCRzpKyxpbsOrQxeSyEWUYQRv9VHA59LC7
34+
tVAJTbnTX1BNHkGZkOkoOpoZLwBaM2XbbDtcOGCAZMECgYEA+mTURFQ85/pxawvk
35+
9ndqZ+5He1u/bMLYIJDp0hdB/vgD+vw3gb2UyRwp0I6Wc6Si4FEEnbY7L0pzWsiR
36+
43CpLs+cyLfnD9NycuIasxs5fKb/1s1nGTkRAp7x9x/ZTtEf8v4YTmmMXFHzdo7V
37+
pv+czO89ppEDkxEtMf/b5SifhO8CgYEAwIDIUvXLduGhL+RPDwjc2SKdydXGV6om
38+
OEdt/V8oS801Z7k8l3gHXFm7zL/MpHmh9cag+F9dHK42kw2RSjDGsBlXXiAO1Z0I
39+
2A34OdPw/kow8fmIKWTMu3+28Kca+3RmUqeyaq0vazQ/bWMO9px+Ud3YfLo1Tn5I
40+
li0MecAx8DECgYEAvsLceKYYtL83c09fg2oc1ctSCCgw4WJcGAtvJ9DyRZacKbXH
41+
b/+H/+OF8879zmKqd+0hcCnqUzAMTCisBLPLIM+o6b45ufPkqKObpcJi/JWaKgLY
42+
vf2c+Psw6o4IF6T5Cz4MNIjzF06UBknxecYZpoPJ20F1kLCwVvxPgfl99l8CgYAb
43+
XfOcv67WTstgiJ+oroTfJamy+P5ClkDqvVTosW+EHz9ZaJ8xlXHOcj9do2LPey9I
44+
Rp250azmF+pQS5x9JKQKgv/FtN8HBVUtigbhCb14GUoODICMCfWFLmnumoMefnTR
45+
iV+3BLn6Dqp5vZxx+NuIffZ5/Or5JsDhALSGVomC8QKBgAi3Z/dNQrDHfkXMNn/L
46+
+EAoLuAbFgLs76r9VGgNaRQ/q5gex2bZEGoBj4Sxvs95NUIcfD9wKT7FF8HdxARv
47+
y3o6Bfc8Xp9So9SlFXrje+gkdEJ0rQR67d+XBuJZh86bXJHVrMwpoNL+ahLGdVSe
48+
81oh1uCH1YPLM29hPyaohxL8
49+
-----END PRIVATE KEY-----

src/SecureServer.php

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace React\Socket;
4+
5+
use Evenement\EventEmitter;
6+
use React\EventLoop\LoopInterface;
7+
use React\Socket\Server;
8+
use React\Socket\ConnectionInterface;
9+
10+
/**
11+
* The `SecureServer` class implements the `ServerInterface` and is responsible
12+
* for providing a secure TLS (formerly known as SSL) server.
13+
*
14+
* It does so by wrapping a `Server` instance which waits for plaintext
15+
* 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:
19+
*
20+
* ```
21+
* $context = array(
22+
* 'local_cert' => __DIR__ . '/localhost.pem'
23+
* );
24+
* ```
25+
*
26+
* @see Server
27+
* @link http://php.net/manual/en/context.ssl.php for TLS context options
28+
*/
29+
class SecureServer extends EventEmitter implements ServerInterface
30+
{
31+
private $tcp;
32+
private $context;
33+
private $loop;
34+
private $encryption;
35+
36+
public function __construct(Server $tcp, LoopInterface $loop, array $context)
37+
{
38+
$this->tcp = $tcp;
39+
$this->context = $context;
40+
$this->loop = $loop;
41+
$this->encryption = new StreamEncryption($loop);
42+
43+
$that = $this;
44+
$this->tcp->on('connection', function ($connection) use ($that) {
45+
$that->handleConnection($connection);
46+
});
47+
$this->tcp->on('error', function ($error) use ($that) {
48+
$that->emit('error', array($error));
49+
});
50+
}
51+
52+
public function listen($port, $host = '127.0.0.1')
53+
{
54+
$this->tcp->listen($port, $host);
55+
56+
foreach ($this->context as $name => $value) {
57+
stream_context_set_option($this->tcp->master, 'ssl', $name, $value);
58+
}
59+
}
60+
61+
public function getPort()
62+
{
63+
return $this->tcp->getPort();
64+
}
65+
66+
public function shutdown()
67+
{
68+
return $this->tcp->shutdown();
69+
}
70+
71+
/** @internal */
72+
public function handleConnection(ConnectionInterface $connection)
73+
{
74+
$that = $this;
75+
76+
$this->encryption->enable($connection)->then(
77+
function ($conn) use ($that) {
78+
$that->emit('connection', array($conn));
79+
},
80+
function ($error) use ($that, $connection) {
81+
$that->emit('error', array($error));
82+
$connection->end();
83+
}
84+
);
85+
}
86+
}

src/Server.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ public function listen($port, $host = '127.0.0.1')
2323
$host = '[' . $host . ']';
2424
}
2525

26-
$this->master = @stream_socket_server("tcp://$host:$port", $errno, $errstr);
26+
$this->master = @stream_socket_server(
27+
"tcp://$host:$port",
28+
$errno,
29+
$errstr,
30+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
31+
stream_context_create()
32+
);
2733
if (false === $this->master) {
2834
$message = "Could not bind to tcp://$host:$port: $errstr";
2935
throw new ConnectionException($message, $errno);

0 commit comments

Comments
 (0)