diff --git a/README.md b/README.md index 023b1bcf..47b1a719 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,70 @@ 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. + +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): + +```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/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 new file mode 100644 index 00000000..df30b03f --- /dev/null +++ b/src/SecureServer.php @@ -0,0 +1,101 @@ + __DIR__ . '/localhost.pem' + * ); + * ``` + * + * 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 + */ +class SecureServer extends EventEmitter implements ServerInterface +{ + private $tcp; + private $context; + private $loop; + private $encryption; + + 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; + $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..72e19499 --- /dev/null +++ b/tests/FunctionalSecureServerTest.php @@ -0,0 +1,384 @@ +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); + + 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(); + + $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(); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->create('127.0.0.1', $port); + + Block\await($promise, $loop, self::TIMEOUT); + } + + 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); + + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); + } + + 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); + + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); + } + + 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); + + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); + } + + 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(self::TIMEOUT, $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(self::TIMEOUT, $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(self::TIMEOUT, $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(self::TIMEOUT, $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(); + } +}
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: