From 1c16a5aa346901f1e947fd370409080552d52209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 20 Oct 2016 13:16:49 +0200 Subject: [PATCH 1/4] Add SecureServer for secure TLS connections --- README.md | 51 +++++++++ composer.json | 3 +- examples/01-echo.php | 18 ++- examples/02-chat-server.php | 16 +++ examples/03-benchmark.php | 16 +++ examples/10-generate-self-signed.php | 31 ++++++ examples/localhost.pem | 49 +++++++++ src/SecureServer.php | 86 +++++++++++++++ src/Server.php | 8 +- src/StreamEncryption.php | 126 +++++++++++++++++++++ tests/FunctionalSecureServerTest.php | 157 +++++++++++++++++++++++++++ tests/SecureServerTest.php | 39 +++++++ 12 files changed, 597 insertions(+), 3 deletions(-) create mode 100644 examples/10-generate-self-signed.php create mode 100644 examples/localhost.pem create mode 100644 src/SecureServer.php create mode 100644 src/StreamEncryption.php create mode 100644 tests/FunctionalSecureServerTest.php create mode 100644 tests/SecureServerTest.php diff --git a/README.md b/README.md index 023b1bcf..d4b60db8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ and [`Stream`](https://github.com/reactphp/stream) components. * [Quickstart example](#quickstart-example) * [Usage](#usage) * [Server](#server) + * [SecureServer](#secureserver) * [ConnectionInterface](#connectioninterface) * [getRemoteAddress()](#getremoteaddress) * [Install](#install) @@ -77,10 +78,60 @@ instance implementing [`ConnectionInterface`](#connectioninterface): ```php $server->on('connection', function (ConnectionInterface $connection) { + echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); … }); ``` +### SecureServer + +The `SecureServer` class implements the `ServerInterface` and is responsible +for providing a secure TLS (formerly known as SSL) server. + +It does so by wrapping a [`Server`](#server) instance which waits for plaintext +TCP/IP connections and then performs a TLS handshake for each connection. +It thus requires valid [TLS context options](http://php.net/manual/en/context.ssl.php), +which in its most basic form may look something like this if you're using a +PEM encoded certificate file: + +```php +$server = new Server($loop); + +$server = new SecureServer($server, $loop, array( + 'local_cert' => 'server.pem' +)); + +$server->listen(8000); +``` + +> Note that the certificate file will not be loaded on instantiation but when an +incoming connection initializes its TLS context. +This implies that any invalid certificate file paths or contents will only cause +an `error` event at a later time. + +Whenever a client completes the TLS handshake, it will emit a `connection` event +with a connection instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (ConnectionInterface $connection) { + echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +Whenever a client fails to perform a successful TLS handshake, it will emit an +`error` event and then close the underlying TCP/IP connection: + +```php +$server->on('error', function (Exception $e) { + echo 'Error' . $e->getMessage() . PHP_EOL; +}); +``` + ### ConnectionInterface The `ConnectionInterface` is used to represent any incoming connection. diff --git a/composer.json b/composer.json index a32e94e8..75e5bf5e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "php": ">=5.3.0", "evenement/evenement": "~2.0|~1.0", "react/event-loop": "0.4.*|0.3.*", - "react/stream": "^0.4.2" + "react/stream": "^0.4.5", + "react/promise": "^2.0 || ^1.1" }, "require-dev": { "react/socket-client": "^0.5.1", diff --git a/examples/01-echo.php b/examples/01-echo.php index fea43402..a1955190 100644 --- a/examples/01-echo.php +++ b/examples/01-echo.php @@ -5,16 +5,30 @@ // // $ php examples/01-echo.php 8000 // $ telnet localhost 8000 +// +// You can also run a secure TLS echo server like this: +// +// $ php examples/01-echo.php 8000 examples/localhost.pem +// $ openssl s_client -connect localhost:8000 use React\EventLoop\Factory; use React\Socket\Server; use React\Socket\ConnectionInterface; +use React\Socket\SecureServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $server = new Server($loop); + +// secure TLS mode if certificate is given as second parameter +if (isset($argv[2])) { + $server = new SecureServer($server, $loop, array( + 'local_cert' => $argv[2] + )); +} + $server->listen(isset($argv[1]) ? $argv[1] : 0); $server->on('connection', function (ConnectionInterface $conn) use ($loop) { @@ -22,6 +36,8 @@ $conn->pipe($conn); }); -echo 'Listening on ' . $server->getPort() . PHP_EOL; +$server->on('error', 'printf'); + +echo 'bound to ' . $server->getPort() . PHP_EOL; $loop->run(); diff --git a/examples/02-chat-server.php b/examples/02-chat-server.php index b33881ec..52e0d0c2 100644 --- a/examples/02-chat-server.php +++ b/examples/02-chat-server.php @@ -5,16 +5,30 @@ // // $ php examples/02-chat-server.php 8000 // $ telnet localhost 8000 +// +// You can also run a secure TLS chat server like this: +// +// $ php examples/02-chat-server.php 8000 examples/localhost.pem +// $ openssl s_client -connect localhost:8000 use React\EventLoop\Factory; use React\Socket\Server; use React\Socket\ConnectionInterface; +use React\Socket\SecureServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $server = new Server($loop); + +// secure TLS mode if certificate is given as second parameter +if (isset($argv[2])) { + $server = new SecureServer($server, $loop, array( + 'local_cert' => $argv[2] + )); +} + $server->listen(isset($argv[1]) ? $argv[1] : 0, '0.0.0.0'); $clients = array(); @@ -44,6 +58,8 @@ }); }); +$server->on('error', 'printf'); + echo 'Listening on ' . $server->getPort() . PHP_EOL; $loop->run(); diff --git a/examples/03-benchmark.php b/examples/03-benchmark.php index e03e1197..6619944d 100644 --- a/examples/03-benchmark.php +++ b/examples/03-benchmark.php @@ -8,16 +8,32 @@ // $ telnet localhost 8000 // $ echo hello world | nc -v localhost 8000 // $ dd if=/dev/zero bs=1M count=1000 | nc -v localhost 8000 +// +// You can also run a secure TLS benchmarking server like this: +// +// $ php examples/03-benchmark.php 8000 examples/localhost.pem +// $ openssl s_client -connect localhost:8000 +// $ echo hello world | openssl s_client -connect localhost:8000 +// $ dd if=/dev/zero bs=1M count=1000 | openssl s_client -connect localhost:8000 use React\EventLoop\Factory; use React\Socket\Server; use React\Socket\ConnectionInterface; +use React\Socket\SecureServer; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $server = new Server($loop); + +// secure TLS mode if certificate is given as second parameter +if (isset($argv[2])) { + $server = new SecureServer($server, $loop, array( + 'local_cert' => $argv[2] + )); +} + $server->listen(isset($argv[1]) ? $argv[1] : 0); $server->on('connection', function (ConnectionInterface $conn) use ($loop) { diff --git a/examples/10-generate-self-signed.php b/examples/10-generate-self-signed.php new file mode 100644 index 00000000..00f93140 --- /dev/null +++ b/examples/10-generate-self-signed.php @@ -0,0 +1,31 @@ + secret.pem + +// certificate details (Distinguished Name) +// (OpenSSL applies defaults to missing fields) +$dn = array( + "commonName" => isset($argv[1]) ? $argv[1] : "localhost", +// "countryName" => "AU", +// "stateOrProvinceName" => "Some-State", +// "localityName" => "London", +// "organizationName" => "Internet Widgits Pty Ltd", +// "organizationalUnitName" => "R&D", +// "emailAddress" => "admin@example.com" +); + +// create certificate which is valid for ~10 years +$privkey = openssl_pkey_new(); +$cert = openssl_csr_new($dn, $privkey); +$cert = openssl_csr_sign($cert, null, $privkey, 3650); + +// export public and (optionally encrypted) private key in PEM format +openssl_x509_export($cert, $out); +echo $out; + +$passphrase = isset($argv[2]) ? $argv[2] : null; +openssl_pkey_export($privkey, $out, $passphrase); +echo $out; diff --git a/examples/localhost.pem b/examples/localhost.pem new file mode 100644 index 00000000..be692792 --- /dev/null +++ b/examples/localhost.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu +MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQ1OTA2WhcNMjYx +MjI4MTQ1OTA2WjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw +EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAGjUDBOMB0GA1UdDgQWBBQ2GRz3QsQzdXaTMnPVCKfpigA10DAf +BgNVHSMEGDAWgBQ2GRz3QsQzdXaTMnPVCKfpigA10DAMBgNVHRMEBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQA77iZ4KrpPY18Ezjt0mngYAuAxunKddXYdLZ2khywN +0uI/VzYnkFVtrsC7y2jLHSxlmE2/viPPGZDUplENV2acN6JNW+tlt7/bsrQHDQw3 +7VCF27EWiDxHsaghhLkqC+kcop5YR5c0oDQTdEWEKSbow2zayUXDYbRRs76SClTe +824Yul+Ts8Mka+AX2PXDg47iZ84fJRN/nKavcJUTJ2iS1uYw0GNnFMge/uwsfMR3 +V47qN0X5emky8fcq99FlMCbcy0gHAeSWAjClgr2dd2i0LDatUbj7YmdmFcskOgII +IwGfvuWR2yPevYGAE0QgFeLHniN3RW8zmpnX/XtrJ4a7 +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAECggEBAKiO/3FE1CMddkCLZVtUp8ShqJgRokx9WI5ecwFApAkV +ZHsjqDQQYRNmxhDUX/w0tOzLGyhde2xjJyZG29YviKsbHwu6zYwbeOzy/mkGOaK/ +g6DmmMmRs9Z6juifoQCu4GIFZ6il2adIL2vF7OeJh+eKudQj/7NFRSB7mXzNrQWK +tZY3eux5zXWmio7pgZrx1HFZQiiL9NVLwT9J7oBnaoO3fREiu5J2xBpljG9Cr0j1 +LLiVLhukWJYRlHDtGt1CzI9w8iKo44PCRzpKyxpbsOrQxeSyEWUYQRv9VHA59LC7 +tVAJTbnTX1BNHkGZkOkoOpoZLwBaM2XbbDtcOGCAZMECgYEA+mTURFQ85/pxawvk +9ndqZ+5He1u/bMLYIJDp0hdB/vgD+vw3gb2UyRwp0I6Wc6Si4FEEnbY7L0pzWsiR +43CpLs+cyLfnD9NycuIasxs5fKb/1s1nGTkRAp7x9x/ZTtEf8v4YTmmMXFHzdo7V +pv+czO89ppEDkxEtMf/b5SifhO8CgYEAwIDIUvXLduGhL+RPDwjc2SKdydXGV6om +OEdt/V8oS801Z7k8l3gHXFm7zL/MpHmh9cag+F9dHK42kw2RSjDGsBlXXiAO1Z0I +2A34OdPw/kow8fmIKWTMu3+28Kca+3RmUqeyaq0vazQ/bWMO9px+Ud3YfLo1Tn5I +li0MecAx8DECgYEAvsLceKYYtL83c09fg2oc1ctSCCgw4WJcGAtvJ9DyRZacKbXH +b/+H/+OF8879zmKqd+0hcCnqUzAMTCisBLPLIM+o6b45ufPkqKObpcJi/JWaKgLY +vf2c+Psw6o4IF6T5Cz4MNIjzF06UBknxecYZpoPJ20F1kLCwVvxPgfl99l8CgYAb +XfOcv67WTstgiJ+oroTfJamy+P5ClkDqvVTosW+EHz9ZaJ8xlXHOcj9do2LPey9I +Rp250azmF+pQS5x9JKQKgv/FtN8HBVUtigbhCb14GUoODICMCfWFLmnumoMefnTR +iV+3BLn6Dqp5vZxx+NuIffZ5/Or5JsDhALSGVomC8QKBgAi3Z/dNQrDHfkXMNn/L ++EAoLuAbFgLs76r9VGgNaRQ/q5gex2bZEGoBj4Sxvs95NUIcfD9wKT7FF8HdxARv +y3o6Bfc8Xp9So9SlFXrje+gkdEJ0rQR67d+XBuJZh86bXJHVrMwpoNL+ahLGdVSe +81oh1uCH1YPLM29hPyaohxL8 +-----END PRIVATE KEY----- diff --git a/src/SecureServer.php b/src/SecureServer.php new file mode 100644 index 00000000..ac35507b --- /dev/null +++ b/src/SecureServer.php @@ -0,0 +1,86 @@ + __DIR__ . '/localhost.pem' + * ); + * ``` + * + * @see Server + * @link http://php.net/manual/en/context.ssl.php for TLS context options + */ +class SecureServer extends EventEmitter implements ServerInterface +{ + private $tcp; + private $context; + private $loop; + private $encryption; + + public function __construct(Server $tcp, LoopInterface $loop, array $context) + { + $this->tcp = $tcp; + $this->context = $context; + $this->loop = $loop; + $this->encryption = new StreamEncryption($loop); + + $that = $this; + $this->tcp->on('connection', function ($connection) use ($that) { + $that->handleConnection($connection); + }); + $this->tcp->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); + } + + public function listen($port, $host = '127.0.0.1') + { + $this->tcp->listen($port, $host); + + foreach ($this->context as $name => $value) { + stream_context_set_option($this->tcp->master, 'ssl', $name, $value); + } + } + + public function getPort() + { + return $this->tcp->getPort(); + } + + public function shutdown() + { + return $this->tcp->shutdown(); + } + + /** @internal */ + public function handleConnection(ConnectionInterface $connection) + { + $that = $this; + + $this->encryption->enable($connection)->then( + function ($conn) use ($that) { + $that->emit('connection', array($conn)); + }, + function ($error) use ($that, $connection) { + $that->emit('error', array($error)); + $connection->end(); + } + ); + } +} diff --git a/src/Server.php b/src/Server.php index 76e2cf9b..dbc4055d 100644 --- a/src/Server.php +++ b/src/Server.php @@ -23,7 +23,13 @@ public function listen($port, $host = '127.0.0.1') $host = '[' . $host . ']'; } - $this->master = @stream_socket_server("tcp://$host:$port", $errno, $errstr); + $this->master = @stream_socket_server( + "tcp://$host:$port", + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + stream_context_create() + ); if (false === $this->master) { $message = "Could not bind to tcp://$host:$port: $errstr"; throw new ConnectionException($message, $errno); diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php new file mode 100644 index 00000000..fd2fc0d3 --- /dev/null +++ b/src/StreamEncryption.php @@ -0,0 +1,126 @@ +loop = $loop; + + // See https://bugs.php.net/bug.php?id=65137 + // https://bugs.php.net/bug.php?id=41631 + // https://github.com/reactphp/socket-client/issues/24 + // On versions affected by this bug we need to fread the stream until we + // get an empty string back because the buffer indicator could be wrong + if (version_compare(PHP_VERSION, '5.6.8', '<')) { + $this->wrapSecure = true; + } + + if (defined('STREAM_CRYPTO_METHOD_TLSv1_0_SERVER')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_0_SERVER; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_1_SERVER')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_1_SERVER; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_SERVER')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; + } + } + + public function enable(Stream $stream) + { + return $this->toggle($stream, true); + } + + public function disable(Stream $stream) + { + return $this->toggle($stream, false); + } + + public function toggle(Stream $stream, $toggle) + { + // pause actual stream instance to continue operation on raw stream socket + $stream->pause(); + + // TODO: add write() event to make sure we're not sending any excessive data + + $deferred = new Deferred(); + + // get actual stream socket from stream instance + $socket = $stream->stream; + + $that = $this; + $toggleCrypto = function () use ($socket, $deferred, $toggle, $that) { + $that->toggleCrypto($socket, $deferred, $toggle); + }; + + $this->loop->addReadStream($socket, $toggleCrypto); + + $wrap = $this->wrapSecure && $toggle; + + return $deferred->promise()->then(function () use ($stream, $wrap) { + if ($wrap) { + $stream->bufferSize = null; + } + + $stream->resume(); + + return $stream; + }, function($error) use ($stream) { + $stream->resume(); + throw $error; + }); + } + + public function toggleCrypto($socket, Deferred $deferred, $toggle) + { + set_error_handler(array($this, 'handleError')); + $result = stream_socket_enable_crypto($socket, $toggle, $this->method); + restore_error_handler(); + + if (true === $result) { + $this->loop->removeStream($socket); + + $deferred->resolve(); + } else if (false === $result) { + $this->loop->removeStream($socket); + + $deferred->reject(new UnexpectedValueException( + sprintf("Unable to complete SSL/TLS handshake: %s", $this->errstr), + $this->errno + )); + } else { + // need more data, will retry + } + } + + public function handleError($errno, $errstr) + { + $this->errstr = str_replace(array("\r", "\n"), ' ', $errstr); + $this->errno = $errno; + } +} diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php new file mode 100644 index 00000000..3bd9591d --- /dev/null +++ b/tests/FunctionalSecureServerTest.php @@ -0,0 +1,157 @@ +markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + } + + public function testEmitsConnectionForNewConnection() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsErrorForServerWithInvalidCertificate() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => 'invalid.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then(null, $this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsErrorForConnectionWithPeerVerification() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => true + )); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then(null, $this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsErrorIfConnectionIsCancelled() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + $promise->cancel(); + + $promise->then(null, $this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsNothingIfConnectionIsIdle() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableNever()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new TcpConnector($loop); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsErrorIfConnectionIsNotSecureHandshake() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new TcpConnector($loop); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then(function (Stream $stream) { + $stream->write("GET / HTTP/1.0\r\n\r\n"); + }); + + Block\sleep(0.1, $loop); + } +} diff --git a/tests/SecureServerTest.php b/tests/SecureServerTest.php new file mode 100644 index 00000000..9278483a --- /dev/null +++ b/tests/SecureServerTest.php @@ -0,0 +1,39 @@ +markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + } + + public function testGetPortWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->getMock(); + $tcp->expects($this->once())->method('getPort')->willReturn(1234); + + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $server = new SecureServer($tcp, $loop, array()); + + $this->assertEquals(1234, $server->getPort()); + } + + public function testShutdownWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\Server')->disableOriginalConstructor()->getMock(); + $tcp->expects($this->once())->method('shutdown'); + + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $server = new SecureServer($tcp, $loop, array()); + + $server->shutdown(); + } +} From b3e062130e2b238e89ebfc895fcdc97f56d59c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 31 Dec 2016 18:10:09 +0100 Subject: [PATCH 2/4] Documentation and tests for encrypted private keys --- README.md | 10 ++++ examples/localhost_swordfish.pem | 51 ++++++++++++++++++++ src/SecureServer.php | 15 ++++++ tests/FunctionalSecureServerTest.php | 71 ++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 examples/localhost_swordfish.pem diff --git a/README.md b/README.md index d4b60db8..47b1a719 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,16 @@ incoming connection initializes its TLS context. This implies that any invalid certificate file paths or contents will only cause an `error` event at a later time. +If your private key is encrypted with a passphrase, you have to specify it +like this: + +```php +$server = new SecureServer($server, $loop, array( + 'local_cert' => 'server.pem', + 'passphrase' => 'secret' +)); +``` + Whenever a client completes the TLS handshake, it will emit a `connection` event with a connection instance implementing [`ConnectionInterface`](#connectioninterface): diff --git a/examples/localhost_swordfish.pem b/examples/localhost_swordfish.pem new file mode 100644 index 00000000..7d1ee804 --- /dev/null +++ b/examples/localhost_swordfish.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu +MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQxMDQzWhcNMjYx +MjI4MTQxMDQzWjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw +EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRXt83SrKIHr/i +3lc8O8pz6NHE1DNHJa4xg2xalXWzCEV6m1qLd9VdaLT9cJD1afNmEMBgY6RblNL/ +paJWVoR9MOUeIoYl2PrhUCxsf7h6MRtezQQe3e+n+/0XunF0JUQIZuJqbxfRk5WT +XmYnphqOZKEcistAYvFBjzl/D+Cl/nYsreADc+t9l5Vni89oTWEuqIrsM4WUZqqB +VMAakd2nZJLWIrMxq9hbW1XNukOQfcmZVFTC6CUnLq8qzGbtfZYBuMBACnL1k/E/ +yPaAgR46l14VAcndDUJBtMeL2qYuNwvXQhg3KuBmpTUpH+yzxU+4T3lmv0xXmPqu +ySH3xvW3AgMBAAGjUDBOMB0GA1UdDgQWBBRu68WTI4pVeTB7wuG9QGI3Ie441TAf +BgNVHSMEGDAWgBRu68WTI4pVeTB7wuG9QGI3Ie441TAMBgNVHRMEBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQCc4pEjEHO47VRJkbHgC+c2gAVgxekkaA1czBA1uAvh +ILRda0NLlvyftbjaG0zZp2ABUCfRfksl/Pf/PzWLUMEuH/9kEW2rgP43z6YgiL6k +kBPlmAU607UjD726RPGkw8QPSXS/dWiNJ5CBpPWLpxC45pokqItYbY0ijQ5Piq09 +TchYlCX044oSRnPiP394PQ3HVdaGhJB2DnjDq3in5dVivFf8EdgzQSvp/wXy3WQs +uFSVonSnrZGY/4AgT3psGaQ6fqKb4SBoqtf5bFQvp1XNNRkuEJnS/0dygEya0c+c +aCe/1gXC2wDjx0/TekY5m1Nyw5SY6z7stOqL/ekwgejt +-----END CERTIFICATE----- +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIG7idPRLgiHkCAggA +MBQGCCqGSIb3DQMHBAg+MLPdepHWSwSCBMgVW9LseCjfTAmF9U1qRnKsq3kIwEnW +6aERBqs/mnmEhrXgZYgcvRRK7kD12TdHt/Nz46Ymu0h+Lrvuwtl1fHQUARTk/gFh +onLhc9kjMUhLRIR007vJe3HvWOb/v+SBSDB38OpUxUwJmBVBuSaYLWVuPR6J5kUj +xOgBS049lN3E9cfrHvb3bF/epIQrU0OgfyyxEvIi5n30y+tlRn3y68PY6Qd46t4Y +UN5VZUwvJBgoRy9TGxSkiSRjhxC2PWpLYq/HMzDbcRcFF5dVAIioUd/VZ7fdgBfA +uMW4SFfpFLDUX0aaYe+ZdA5tM0Bc0cOtG8Z0sc9JYDNNcvjmSiGCi646h8F0D3O6 +JKAQMMxQGWiyQeJ979LVjtq4lJESXA8VEKz9rV03y5xunmFCLy6dGt+6GJwXgabn +OH7nvEv4GqAOqKc6E9je4JM+AF/oUazrfPse1KEEtsPKarazjCB/SKYtHyDJaavD +GGjtiU9zWwGMOgIDyNmXe3ga7/TWoGOAg5YlTr6Hbq2Y/5ycgjAgPFjuXtvnoT0+ +mF5TnNfMAqTgQsE2gjhonK1pdlOen0lN5FtoUXp3CXU0dOq0J70GiX+1YA7VDn30 +n5WNAgfOXX3l3E95jGN370pHXyli5RUNW0NZVHV+22jlNWCVtQHUh+DVswQZg+i5 ++DqaIHz2jUetMo7gWtqGn/wwSopOs87VM1rcALhZL4EsJ+Zy81I/hA32RNnGbuol +NAiZh+0KrtTcc/fPunpd8vRtOwGphM11dKucozUufuiPG2inR3aEqt5yNx54ec/f +J6nryWRYiHEA/rCU9MSBM9cqKFtEmy9/8oxV41/SPxhXjHwDlABWTtFuJ3pf2sOF +ILSYYFwB0ZGvdjE5yAJFBr9efno/L9fafmGk7a3vmVgK2AmUC9VNB5XHw1GjF8OP +aQAXe4md9Bh0jk/D/iyp7e7IWNssul/7XejhabidWgFj6EXc9YxE59+FlhDqyMhn +V6houc+QeUXuwsAKgRJJhJtpv/QSZ5BI3esxHHUt3ayGnvhFElpAc0t7C/EiXKIv +DAFYP2jksBqijM8YtEgPWYzEP5buYxZnf/LK7FDocLsNcdF38UaKBbeF90e7bR8j +SHspG9aJWICu8Yawnh8zuy/vQv+h9gWyGodd2p9lQzlbRXrutbwfmPf7xP6nzT9i +9GcugJxTaZgkCfhhHxFk/nRHS2NAzagKVib1xkUlZJg2hX0fIFUdYteL1GGTvOx5 +m3mTOino4T19z9SEdZYb2OHYh29e/T74bJiLCYdXwevSYHxfZc8pYAf0jp4UnMT2 +f7B0ctX1iXuQ2uZVuxh+U1Mcu+v0gDla1jWh7AhcePSi4xBNUCak0kQip6r5e6Oi +r4MIyMRk/Pc5pzEKo8G6nk26rNvX3aRvECoVfmK7IVdsqZ6IXlt9kOmWx3IeKzrO +J5DxpzW+9oIRZJgPTkc4/XRb0tFmFQYTiChiQ1AJUEiCX0GpkFf7cq61aLGYtWyn +vL2lmQhljzjrDo15hKErvk7eBZW7GW/6j/m/PfRdcBI4ceuP9zWQXnDOd9zmaE4b +q3bJ+IbbyVZA2WwyzN7umCKWghsiPMAolxEnYM9JRf8BcqeqQiwVZlfO5KFuN6Ze +le4= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/src/SecureServer.php b/src/SecureServer.php index ac35507b..df30b03f 100644 --- a/src/SecureServer.php +++ b/src/SecureServer.php @@ -23,6 +23,16 @@ * ); * ``` * + * If your private key is encrypted with a passphrase, you have to specify it + * like this: + * + * ```php + * $context = array( + * 'local_cert' => 'server.pem', + * 'passphrase' => 'secret' + * ); + * ``` + * * @see Server * @link http://php.net/manual/en/context.ssl.php for TLS context options */ @@ -35,6 +45,11 @@ class SecureServer extends EventEmitter implements ServerInterface public function __construct(Server $tcp, LoopInterface $loop, array $context) { + // default to empty passphrase to surpress blocking passphrase prompt + $context += array( + 'passphrase' => '' + ); + $this->tcp = $tcp; $this->context = $context; $this->loop = $loop; diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index 3bd9591d..ab7d4e1e 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -27,6 +27,30 @@ public function testEmitsConnectionForNewConnection() $server = new SecureServer($server, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); + $server->on('error', 'var_dump'); + $server->on('connection', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsConnectionForNewConnectionWithEncryptedCertificate() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem', + 'passphrase' => 'swordfish' + )); $server->on('connection', $this->expectCallableOnce()); $server->listen(0); $port = $server->getPort(); @@ -64,6 +88,53 @@ public function testEmitsErrorForServerWithInvalidCertificate() Block\sleep(0.1, $loop); } + public function testEmitsErrorForServerWithEncryptedCertificateMissingPassphrase() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then(null, $this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsErrorForServerWithEncryptedCertificateWithInvalidPassphrase() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem', + 'passphrase' => 'nope' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $promise->then(null, $this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + public function testEmitsErrorForConnectionWithPeerVerification() { $loop = Factory::create(); From 61dcdd0f3ae5669047d3ea03867e16c4b4ab81cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 31 Dec 2016 18:47:18 +0100 Subject: [PATCH 3/4] Raise test timeouts to avoid sporadic test failures --- tests/FunctionalSecureServerTest.php | 37 +++++++++++----------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index ab7d4e1e..a5ed403e 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -12,6 +12,8 @@ class FunctionalSecureServerTest extends TestCase { + const TIMEOUT = 0.5; + public function setUp() { if (!function_exists('stream_socket_enable_crypto')) { @@ -27,7 +29,6 @@ public function testEmitsConnectionForNewConnection() $server = new SecureServer($server, $loop, array( 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); - $server->on('error', 'var_dump'); $server->on('connection', $this->expectCallableOnce()); $server->listen(0); $port = $server->getPort(); @@ -37,9 +38,7 @@ public function testEmitsConnectionForNewConnection() )); $promise = $connector->create('127.0.0.1', $port); - $promise->then($this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + Block\await($promise, $loop, self::TIMEOUT); } public function testEmitsConnectionForNewConnectionWithEncryptedCertificate() @@ -60,9 +59,7 @@ public function testEmitsConnectionForNewConnectionWithEncryptedCertificate() )); $promise = $connector->create('127.0.0.1', $port); - $promise->then($this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + Block\await($promise, $loop, self::TIMEOUT); } public function testEmitsErrorForServerWithInvalidCertificate() @@ -83,9 +80,8 @@ public function testEmitsErrorForServerWithInvalidCertificate() )); $promise = $connector->create('127.0.0.1', $port); - $promise->then(null, $this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); } public function testEmitsErrorForServerWithEncryptedCertificateMissingPassphrase() @@ -106,9 +102,8 @@ public function testEmitsErrorForServerWithEncryptedCertificateMissingPassphrase )); $promise = $connector->create('127.0.0.1', $port); - $promise->then(null, $this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); } public function testEmitsErrorForServerWithEncryptedCertificateWithInvalidPassphrase() @@ -130,9 +125,8 @@ public function testEmitsErrorForServerWithEncryptedCertificateWithInvalidPassph )); $promise = $connector->create('127.0.0.1', $port); - $promise->then(null, $this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); } public function testEmitsErrorForConnectionWithPeerVerification() @@ -154,8 +148,7 @@ public function testEmitsErrorForConnectionWithPeerVerification() $promise = $connector->create('127.0.0.1', $port); $promise->then(null, $this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + Block\sleep(self::TIMEOUT, $loop); } public function testEmitsErrorIfConnectionIsCancelled() @@ -178,8 +171,7 @@ public function testEmitsErrorIfConnectionIsCancelled() $promise->cancel(); $promise->then(null, $this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + Block\sleep(self::TIMEOUT, $loop); } public function testEmitsNothingIfConnectionIsIdle() @@ -199,8 +191,7 @@ public function testEmitsNothingIfConnectionIsIdle() $promise = $connector->create('127.0.0.1', $port); $promise->then($this->expectCallableOnce()); - - Block\sleep(0.1, $loop); + Block\sleep(self::TIMEOUT, $loop); } public function testEmitsErrorIfConnectionIsNotSecureHandshake() @@ -223,6 +214,6 @@ public function testEmitsErrorIfConnectionIsNotSecureHandshake() $stream->write("GET / HTTP/1.0\r\n\r\n"); }); - Block\sleep(0.1, $loop); + Block\sleep(self::TIMEOUT, $loop); } } From 4e002715076f5c685e995ecec9cba4d3aa32e616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 1 Jan 2017 19:58:17 +0100 Subject: [PATCH 4/4] Add functional tests for sending/receiving large chunks --- tests/FunctionalSecureServerTest.php | 165 +++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/tests/FunctionalSecureServerTest.php b/tests/FunctionalSecureServerTest.php index a5ed403e..72e19499 100644 --- a/tests/FunctionalSecureServerTest.php +++ b/tests/FunctionalSecureServerTest.php @@ -9,6 +9,7 @@ use React\Socket\SecureServer; use React\SocketClient\SecureConnector; use React\Stream\Stream; +use React\Socket\ConnectionInterface; class FunctionalSecureServerTest extends TestCase { @@ -41,6 +42,170 @@ public function testEmitsConnectionForNewConnection() Block\await($promise, $loop, self::TIMEOUT); } + public function testWritesDataToConnection() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $server->on('connection', function (ConnectionInterface $conn) { + $conn->write('foo'); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $local->on('data', $this->expectCallableOnceWith('foo')); + + Block\sleep(self::TIMEOUT, $loop); + } + + public function testWritesDataInMultipleChunksToConnection() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + + $server->on('connection', function (ConnectionInterface $conn) { + $conn->write(str_repeat('*', 400000)); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $received = 0; + $local->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + + Block\sleep(self::TIMEOUT, $loop); + + $this->assertEquals(400000, $received); + } + + public function testEmitsDataFromConnection() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $once = $this->expectCallableOnceWith('foo'); + $server->on('connection', function (ConnectionInterface $conn) use ($once) { + $conn->on('data', $once); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $local->write("foo"); + + Block\sleep(self::TIMEOUT, $loop); + } + + public function testEmitsDataInMultipleChunksFromConnection() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $received = 0; + $server->on('connection', function (ConnectionInterface $conn) use (&$received) { + $conn->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $local->write(str_repeat('*', 400000)); + + Block\sleep(self::TIMEOUT, $loop); + + $this->assertEquals(400000, $received); + } + + public function testPipesDataBackInMultipleChunksFromConnection() + { + $loop = Factory::create(); + + $server = new Server($loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + $server->listen(0); + $port = $server->getPort(); + + $server->on('connection', function (ConnectionInterface $conn) use (&$received) { + $conn->pipe($conn); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $received = 0; + $local->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + + $local->write(str_repeat('*', 400000)); + + Block\sleep(self::TIMEOUT, $loop); + + $this->assertEquals(400000, $received); + } + public function testEmitsConnectionForNewConnectionWithEncryptedCertificate() { $loop = Factory::create(); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy