From 64034cf193136ca244a1e55796b0ce087263dca8 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Tue, 15 Jan 2013 12:26:14 +0100 Subject: [PATCH 001/146] Move ConnectionManager* to new SocketClient component --- ConnectionException.php | 7 +++ ConnectionManager.php | 88 ++++++++++++++++++++++++++++++++++ ConnectionManagerInterface.php | 8 ++++ SecureConnectionManager.php | 46 ++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 ConnectionException.php create mode 100644 ConnectionManager.php create mode 100644 ConnectionManagerInterface.php create mode 100644 SecureConnectionManager.php diff --git a/ConnectionException.php b/ConnectionException.php new file mode 100644 index 00000000..d2878eab --- /dev/null +++ b/ConnectionException.php @@ -0,0 +1,7 @@ +loop = $loop; + $this->resolver = $resolver; + } + + public function getConnection($host, $port) + { + $that = $this; + + return $this + ->resolveHostname($host) + ->then(function ($address) use ($port, $that) { + return $that->getConnectionForAddress($address, $port); + }); + } + + public function getConnectionForAddress($address, $port) + { + $url = $this->getSocketUrl($address, $port); + + $socket = stream_socket_client($url, $errno, $errstr, ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + + if (!$socket) { + return new RejectedPromise(new \RuntimeException( + sprintf("connection to %s:%d failed: %s", $address, $port, $errstr), + $errno + )); + } + + stream_set_blocking($socket, 0); + + // wait for connection + + return $this + ->waitForStreamOnce($socket) + ->then(array($this, 'handleConnectedSocket')); + } + + protected function waitForStreamOnce($stream) + { + $deferred = new Deferred(); + + $loop = $this->loop; + + $this->loop->addWriteStream($stream, function ($stream) use ($loop, $deferred) { + $loop->removeWriteStream($stream); + $deferred->resolve($stream); + }); + + return $deferred->promise(); + } + + public function handleConnectedSocket($socket) + { + return new Stream($socket, $this->loop); + } + + protected function getSocketUrl($host, $port) + { + return sprintf('tcp://%s:%s', $host, $port); + } + + protected function resolveHostname($host) + { + if (false !== filter_var($host, FILTER_VALIDATE_IP)) { + return new FulfilledPromise($host); + } + + return $this->resolver->resolve($host); + } +} diff --git a/ConnectionManagerInterface.php b/ConnectionManagerInterface.php new file mode 100644 index 00000000..1d694b32 --- /dev/null +++ b/ConnectionManagerInterface.php @@ -0,0 +1,8 @@ +enableCrypto($socket, $deferred); + }; + + $this->loop->addWriteStream($socket, $enableCrypto); + $this->loop->addReadStream($socket, $enableCrypto); + $enableCrypto(); + + return $deferred->promise(); + } + + public function enableCrypto($socket, ResolverInterface $resolver) + { + $result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + + if (true === $result) { + $this->loop->removeWriteStream($socket); + $this->loop->removeReadStream($socket); + + $resolver->resolve(new Stream($socket, $this->loop)); + } else if (false === $result) { + $this->loop->removeWriteStream($socket); + $this->loop->removeReadStream($socket); + + $resolver->reject(); + } else { + // need more data, will retry + } + } +} From 730f9335c6c6bae16b73d4fca9d7ef1dd41a7869 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Tue, 15 Jan 2013 12:33:10 +0100 Subject: [PATCH 002/146] Handle connection failure in ConnectionManager (thanks @nrk) --- ConnectionManager.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ConnectionManager.php b/ConnectionManager.php index 3bba4ee0..c84421fb 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -69,6 +69,11 @@ protected function waitForStreamOnce($stream) public function handleConnectedSocket($socket) { + if (false === stream_socket_get_name($socket, true)) { + $e = new ConnectionException('Connection refused'); + return new RejectedPromise($e); + } + return new Stream($socket, $this->loop); } From fc4d27dd8599101ee5090350264a5ab211da1add Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Tue, 15 Jan 2013 12:42:41 +0100 Subject: [PATCH 003/146] Fix ConnectionException namespace --- ConnectionException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ConnectionException.php b/ConnectionException.php index d2878eab..b5f9f47b 100644 --- a/ConnectionException.php +++ b/ConnectionException.php @@ -1,6 +1,6 @@ Date: Tue, 15 Jan 2013 20:42:19 +0100 Subject: [PATCH 004/146] Move error handling to inner promise --- ConnectionManager.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ConnectionManager.php b/ConnectionManager.php index c84421fb..7894f30b 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -61,7 +61,14 @@ protected function waitForStreamOnce($stream) $this->loop->addWriteStream($stream, function ($stream) use ($loop, $deferred) { $loop->removeWriteStream($stream); - $deferred->resolve($stream); + + // The following one is a terrible hack but it looks like this is the only way to + // detect connection refused errors with PHP's stream sockets. Blame PHP as usual. + if (false === stream_socket_get_name($stream, true)) { + $deferred->reject(new ConnectionException('Connection refused')); + } else { + $deferred->resolve($stream); + } }); return $deferred->promise(); @@ -69,11 +76,6 @@ protected function waitForStreamOnce($stream) public function handleConnectedSocket($socket) { - if (false === stream_socket_get_name($socket, true)) { - $e = new ConnectionException('Connection refused'); - return new RejectedPromise($e); - } - return new Stream($socket, $this->loop); } From 83b721038de4b71f2c8d6356f677e56d1ffe8a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Jan 2013 20:48:17 +0100 Subject: [PATCH 005/146] Do not pretend a timeout is supported --- ConnectionManager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ConnectionManager.php b/ConnectionManager.php index 7894f30b..592ca194 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -18,6 +18,7 @@ public function __construct(LoopInterface $loop, Resolver $resolver) { $this->loop = $loop; $this->resolver = $resolver; + /* todo: $this->timeout = ini_get("default_socket_timeout") */ } public function getConnection($host, $port) @@ -35,7 +36,7 @@ public function getConnectionForAddress($address, $port) { $url = $this->getSocketUrl($address, $port); - $socket = stream_socket_client($url, $errno, $errstr, ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if (!$socket) { return new RejectedPromise(new \RuntimeException( From b3b764a2066b0056faf74333d748cf133ff510e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Jan 2013 20:50:30 +0100 Subject: [PATCH 006/146] Split to provide a SecureConnectionManager decorator --- SecureConnectionManager.php | 53 ++++++++------------ StreamEncryption.php | 98 +++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 33 deletions(-) create mode 100644 StreamEncryption.php diff --git a/SecureConnectionManager.php b/SecureConnectionManager.php index 3498ee6d..8283a30f 100644 --- a/SecureConnectionManager.php +++ b/SecureConnectionManager.php @@ -2,45 +2,32 @@ namespace React\SocketClient; +use React\EventLoop\LoopInterface; use React\Stream\Stream; -use React\Promise\Deferred; -use React\Promise\ResolverInterface; -class SecureConnectionManager extends ConnectionManager +class SecureConnectionManager implements ConnectionManagerInterface { - public function handleConnectedSocket($socket) - { - $that = $this; - - $deferred = new Deferred(); - - $enableCrypto = function () use ($that, $socket, $deferred) { - $that->enableCrypto($socket, $deferred); - }; - - $this->loop->addWriteStream($socket, $enableCrypto); - $this->loop->addReadStream($socket, $enableCrypto); - $enableCrypto(); + protected $connectionManager; + protected $loop; + protected $streamEncryption; - return $deferred->promise(); + public function __construct(ConnectionManagerInterface $connectionManager, LoopInterface $loop) + { + $this->connectionManager = $connectionManager; + $this->loop = $loop; + $this->streamEncryption = new StreamEncryption($loop); } - public function enableCrypto($socket, ResolverInterface $resolver) + public function getConnection($host, $port) { - $result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); - - if (true === $result) { - $this->loop->removeWriteStream($socket); - $this->loop->removeReadStream($socket); - - $resolver->resolve(new Stream($socket, $this->loop)); - } else if (false === $result) { - $this->loop->removeWriteStream($socket); - $this->loop->removeReadStream($socket); - - $resolver->reject(); - } else { - // need more data, will retry - } + $streamEncryption = $this->streamEncryption; + return $this->connectionManager->getConnection($host, $port)->then(function (Stream $stream) use ($streamEncryption) { + // (unencrypted) connection succeeded => try to enable encryption + return $streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { + // establishing encryption failed => close invalid connection and return error + $stream->close(); + throw $error; + }); + }); } } diff --git a/StreamEncryption.php b/StreamEncryption.php new file mode 100644 index 00000000..bb93004c --- /dev/null +++ b/StreamEncryption.php @@ -0,0 +1,98 @@ +loop = $loop; + } + + public function setMethod($method) + { +// if (!in_array($method, array(), true)) { +// throw new InvalidArgumentException('Invalid encryption method given'); +// } + $this->method = $method; + } + + 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 ($that, $socket, $deferred, $toggle) { + $that->toggleCrypto($socket, $deferred, $toggle); + }; + + $this->loop->addWriteStream($socket, $toggleCrypto); + $this->loop->addReadStream($socket, $toggleCrypto); + $toggleCrypto(); + + return $deferred->then(function () use ($stream) { + $stream->resume(); + return $stream; + }, function($error) use ($stream) { + $stream->resume(); + throw $error; + }); + } + + + + public function toggleCrypto($socket, ResolverInterface $resolver, $toggle) + { + $error = 'unknown error'; + set_error_handler(function ($errno, $errstr) use (&$error) { + $error = str_replace(array("\r","\n"),' ',$errstr); + }); + + $result = stream_socket_enable_crypto($socket, $toggle, $this->method); + + restore_error_handler(); + + if (true === $result) { + $this->loop->removeWriteStream($socket); + $this->loop->removeReadStream($socket); + + $resolver->resolve(); + } else if (false === $result) { + $this->loop->removeWriteStream($socket); + $this->loop->removeReadStream($socket); + + $resolver->reject(new UnexpectedValueException('Unable to initiate SSL/TLS handshake: "'.$error.'"')); + } else { + // need more data, will retry + } + } +} From 30d52b4c19c8a5f76ceb6e8f83ec480160885633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Jan 2013 20:55:24 +0100 Subject: [PATCH 007/146] Import initial sketch of README and composer.json --- README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ composer.json | 20 ++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 README.md create mode 100644 composer.json diff --git a/README.md b/README.md new file mode 100644 index 00000000..a40c7bf3 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +connection-manager +================== + +Async ConnectionManager to open TCP/IP and SSL/TLS based connections in a non-blocking fashion + +## Introduction + +Think of this library as an async (non-blocking) version of [`fsockopen()`](http://php.net/manual/en/function.fsockopen.php) +or [`stream_socket_client()`](http://php.net/manual/en/function.stream-socket-client.php). + +Before you can actually transmit and receive data to/from a remote server, you have to establish a connection +to the remote end. Establishing this connection through the internet/network takes some time as it requires +several steps in order to complete: + +1. Resolve remote target hostname via DNS (+cache) +2. Complete TCP handshake (2 roundtrips) with remote target IP:port +3. Optionally enable SSL/TLS on the new resulting connection + +This project is built on top of [reactphp](https://github.com/reactphp/react). + +## Usage + +In order to use this project, you'll need the following react boilerplate code to initialize the main loop and select +your DNS server if you have not already set it up anyway. + +```php + +$loop = React\EventLoop\Factory::create(); + +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); +``` + +### Async TCP/IP connections + +The `React\SocketClient\ConnectionManager` provides a single promise-based `getConnection($host, $ip)` method +which resolves as soon as the connection succeeds or fails. + +```php + +$connectionManager = new React\SocketClient\ConnectionManager($loop, $dns); + +$connectionManager->getConnection('www.google.com', 80)->then(function (React\Stream\Stream $stream) { + $stream->write('...'); + $stream->close(); +}); +``` + +### Async SSL/TLS connections + +The `React\SocketClient\SecureConnectionManager` class decorates a given `React\SocketClient\ConnectionManager` instance +by enabling SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. It provides the same +promise-based `getConnection($host, $ip)` method which resolves with a `Stream` instance that can be used just like +any non-encrypted stream. + +```php + +$secureConnectionManager = new React\SocketClient\SecureConnectionManager($connectionManager, $loop); + +$secureConnectionManager->getConnection('www.google.com', 443)->then(function (React\Stream\Stream $stream) { + $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); + ... +}); +``` + +### Async UDP connections + +[UDP](http://en.wikipedia.org/wiki/User_Datagram_Protocol) is a simple connectionless and message-based protocol and thus has no concept such as a "connection" - +you can simply send / receive datagrams and as such nothing can block. + +## License + +MIT + diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..003e6eea --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "react/socket-client", + "type": "library", + "license": "MIT", + "require": { + "php": ">=5.3.3", + "react/dns": "0.2.*", + "react/event-loop": "0.2.*", + "react/promise": "1.*", + }, + "autoload": { + "psr-0": { "React\\SocketClient": "" } + }, + "target-dir": "React/SocketClient", + "extra": { + "branch-alias": { + "dev-master": "0.2-dev" + } + } +} \ No newline at end of file From 4dfcdbdd75e7a4ff99ea99d069e4acbefb1f96b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Jan 2013 21:52:21 +0100 Subject: [PATCH 008/146] Remove useless setter for the time being --- StreamEncryption.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/StreamEncryption.php b/StreamEncryption.php index bb93004c..a76394d3 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -20,14 +20,6 @@ public function __construct(LoopInterface $loop) $this->loop = $loop; } - public function setMethod($method) - { -// if (!in_array($method, array(), true)) { -// throw new InvalidArgumentException('Invalid encryption method given'); -// } - $this->method = $method; - } - public function enable(Stream $stream) { return $this->toggle($stream, true); From e4fa8421a8fdd761e613e3f6022b7cc56074c5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Jan 2013 21:53:27 +0100 Subject: [PATCH 009/146] Fix whitespace --- StreamEncryption.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/StreamEncryption.php b/StreamEncryption.php index a76394d3..4145e95e 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -60,13 +60,11 @@ public function toggle(Stream $stream, $toggle) }); } - - public function toggleCrypto($socket, ResolverInterface $resolver, $toggle) { $error = 'unknown error'; set_error_handler(function ($errno, $errstr) use (&$error) { - $error = str_replace(array("\r","\n"),' ',$errstr); + $error = str_replace(array("\r", "\n"), ' ', $errstr); }); $result = stream_socket_enable_crypto($socket, $toggle, $this->method); From c56cb75c08848a6bee0511fdb71177a4ac3d7b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Jan 2013 21:56:30 +0100 Subject: [PATCH 010/146] Remove leftovers of timeout handling --- ConnectionManager.php | 1 - 1 file changed, 1 deletion(-) diff --git a/ConnectionManager.php b/ConnectionManager.php index 592ca194..c9f14a88 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -18,7 +18,6 @@ public function __construct(LoopInterface $loop, Resolver $resolver) { $this->loop = $loop; $this->resolver = $resolver; - /* todo: $this->timeout = ini_get("default_socket_timeout") */ } public function getConnection($host, $port) From faaac4b0c55780f8ad7bf6802db94eb1dee6f05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 19 Jan 2013 00:39:46 +0100 Subject: [PATCH 011/146] Improve error handling --- StreamEncryption.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/StreamEncryption.php b/StreamEncryption.php index 4145e95e..9d24608a 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -7,7 +7,6 @@ use React\Stream\Stream; use React\EventLoop\LoopInterface; use \UnexpectedValueException; -use \InvalidArgumentException; // this class is considered internal and its API should not be relied upon outside of React\SocketClient class StreamEncryption @@ -15,6 +14,9 @@ class StreamEncryption protected $loop; protected $method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + protected $errstr; + protected $errno; + public function __construct(LoopInterface $loop) { $this->loop = $loop; @@ -62,13 +64,8 @@ public function toggle(Stream $stream, $toggle) public function toggleCrypto($socket, ResolverInterface $resolver, $toggle) { - $error = 'unknown error'; - set_error_handler(function ($errno, $errstr) use (&$error) { - $error = str_replace(array("\r", "\n"), ' ', $errstr); - }); - + set_error_handler(array($this, 'handleError')); $result = stream_socket_enable_crypto($socket, $toggle, $this->method); - restore_error_handler(); if (true === $result) { @@ -80,9 +77,18 @@ public function toggleCrypto($socket, ResolverInterface $resolver, $toggle) $this->loop->removeWriteStream($socket); $this->loop->removeReadStream($socket); - $resolver->reject(new UnexpectedValueException('Unable to initiate SSL/TLS handshake: "'.$error.'"')); + $resolver->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; + } } From fa6c34119d3b8c55f9a00fb8984f2826711bd14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 19 Jan 2013 00:54:40 +0100 Subject: [PATCH 012/146] Move error handling to separate chained promise --- ConnectionManager.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ConnectionManager.php b/ConnectionManager.php index c9f14a88..38a6df26 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -50,6 +50,7 @@ public function getConnectionForAddress($address, $port) return $this ->waitForStreamOnce($socket) + ->then(array($this, 'checkConnectedSocket')) ->then(array($this, 'handleConnectedSocket')); } @@ -62,18 +63,22 @@ protected function waitForStreamOnce($stream) $this->loop->addWriteStream($stream, function ($stream) use ($loop, $deferred) { $loop->removeWriteStream($stream); - // The following one is a terrible hack but it looks like this is the only way to - // detect connection refused errors with PHP's stream sockets. Blame PHP as usual. - if (false === stream_socket_get_name($stream, true)) { - $deferred->reject(new ConnectionException('Connection refused')); - } else { - $deferred->resolve($stream); - } + $deferred->resolve($stream); }); return $deferred->promise(); } + public function checkConnectedSocket($socket) + { + // The following hack looks like the only way to + // detect connection refused errors with PHP's stream sockets. + if (false === stream_socket_get_name($socket, true)) { + return When::reject(new ConnectionException('Connection refused')); + } + return When::resolve($socket); + } + public function handleConnectedSocket($socket) { return new Stream($socket, $this->loop); From ee7518569cd70ff4fb5967ee0df80c2091442e6e Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:04:09 +0100 Subject: [PATCH 013/146] Update composer.json for v0.3 --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 003e6eea..964fa3ce 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "license": "MIT", "require": { "php": ">=5.3.3", - "react/dns": "0.2.*", - "react/event-loop": "0.2.*", + "react/dns": "0.3.*", + "react/event-loop": "0.3.*", "react/promise": "1.*", }, "autoload": { @@ -14,7 +14,7 @@ "target-dir": "React/SocketClient", "extra": { "branch-alias": { - "dev-master": "0.2-dev" + "dev-master": "0.3-dev" } } } \ No newline at end of file From 2e259da028e26784c92eae26bacad1e4fccdea95 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:04:25 +0100 Subject: [PATCH 014/146] Trailing newline for composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 964fa3ce..7d3452a5 100644 --- a/composer.json +++ b/composer.json @@ -17,4 +17,4 @@ "dev-master": "0.3-dev" } } -} \ No newline at end of file +} From d0200b5360d1725abe76d60e99b75a18f1c62606 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:05:08 +0100 Subject: [PATCH 015/146] Wrap README --- README.md | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a40c7bf3..282f5ebc 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ connection-manager ================== -Async ConnectionManager to open TCP/IP and SSL/TLS based connections in a non-blocking fashion +Async ConnectionManager to open TCP/IP and SSL/TLS based connections in a non- +blocking fashion ## Introduction -Think of this library as an async (non-blocking) version of [`fsockopen()`](http://php.net/manual/en/function.fsockopen.php) -or [`stream_socket_client()`](http://php.net/manual/en/function.stream-socket-client.php). +Think of this library as an async (non-blocking) version of +[`fsockopen()`](http://php.net/manual/en/function.fsockopen.php) or +[`stream_socket_client()`](http://php.net/manual/en/function.stream-socket- +client.php). -Before you can actually transmit and receive data to/from a remote server, you have to establish a connection -to the remote end. Establishing this connection through the internet/network takes some time as it requires -several steps in order to complete: +Before you can actually transmit and receive data to/from a remote server, you +have to establish a connection to the remote end. Establishing this connection +through the internet/network takes some time as it requires several steps in +order to complete: 1. Resolve remote target hostname via DNS (+cache) 2. Complete TCP handshake (2 roundtrips) with remote target IP:port @@ -20,8 +24,9 @@ This project is built on top of [reactphp](https://github.com/reactphp/react). ## Usage -In order to use this project, you'll need the following react boilerplate code to initialize the main loop and select -your DNS server if you have not already set it up anyway. +In order to use this project, you'll need the following react boilerplate code +to initialize the main loop and select your DNS server if you have not already +set it up anyway. ```php @@ -33,8 +38,9 @@ $dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); ### Async TCP/IP connections -The `React\SocketClient\ConnectionManager` provides a single promise-based `getConnection($host, $ip)` method -which resolves as soon as the connection succeeds or fails. +The `React\SocketClient\ConnectionManager` provides a single promise-based +`getConnection($host, $ip)` method which resolves as soon as the connection +succeeds or fails. ```php @@ -48,10 +54,11 @@ $connectionManager->getConnection('www.google.com', 80)->then(function (React\St ### Async SSL/TLS connections -The `React\SocketClient\SecureConnectionManager` class decorates a given `React\SocketClient\ConnectionManager` instance -by enabling SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. It provides the same -promise-based `getConnection($host, $ip)` method which resolves with a `Stream` instance that can be used just like -any non-encrypted stream. +The `React\SocketClient\SecureConnectionManager` class decorates a given +`React\SocketClient\ConnectionManager` instance by enabling SSL/TLS encryption +as soon as the raw TCP/IP connection succeeds. It provides the same promise- +based `getConnection($host, $ip)` method which resolves with a `Stream` +instance that can be used just like any non-encrypted stream. ```php @@ -65,10 +72,11 @@ $secureConnectionManager->getConnection('www.google.com', 443)->then(function (R ### Async UDP connections -[UDP](http://en.wikipedia.org/wiki/User_Datagram_Protocol) is a simple connectionless and message-based protocol and thus has no concept such as a "connection" - -you can simply send / receive datagrams and as such nothing can block. +[UDP](http://en.wikipedia.org/wiki/User_Datagram_Protocol) is a simple +connectionless and message-based protocol and thus has no concept such as a +"connection" - you can simply send / receive datagrams and as such nothing can +block. ## License MIT - From 010795e1ba295c6a3f8327380498f5ef93bf53f9 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:05:54 +0100 Subject: [PATCH 016/146] Remove UDP and LICENSE sections from README --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 282f5ebc..56d332c4 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,3 @@ $secureConnectionManager->getConnection('www.google.com', 443)->then(function (R ... }); ``` - -### Async UDP connections - -[UDP](http://en.wikipedia.org/wiki/User_Datagram_Protocol) is a simple -connectionless and message-based protocol and thus has no concept such as a -"connection" - you can simply send / receive datagrams and as such nothing can -block. - -## License - -MIT From f1d85f1715bc3e984553cd0472e725f976aecd45 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:11:08 +0100 Subject: [PATCH 017/146] Make composer.json consistent with the other ones --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7d3452a5..8b95e065 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "react/socket-client", - "type": "library", + "description": "Async ConnectionManager to open TCP/IP and SSL/TLS based connections.", + "keywords": ["socket"], "license": "MIT", "require": { "php": ">=5.3.3", From 37ba732b462d4dccb64bb3282405f5eab92dcf44 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:11:16 +0100 Subject: [PATCH 018/146] Simplify README --- README.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 56d332c4..9b99cc8b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ -connection-manager -================== +# SocketClient Component -Async ConnectionManager to open TCP/IP and SSL/TLS based connections in a non- -blocking fashion +Async ConnectionManager to open TCP/IP and SSL/TLS based connections. ## Introduction -Think of this library as an async (non-blocking) version of +Think of this library as an async version of [`fsockopen()`](http://php.net/manual/en/function.fsockopen.php) or [`stream_socket_client()`](http://php.net/manual/en/function.stream-socket- client.php). @@ -20,8 +18,6 @@ order to complete: 2. Complete TCP handshake (2 roundtrips) with remote target IP:port 3. Optionally enable SSL/TLS on the new resulting connection -This project is built on top of [reactphp](https://github.com/reactphp/react). - ## Usage In order to use this project, you'll need the following react boilerplate code @@ -29,7 +25,6 @@ to initialize the main loop and select your DNS server if you have not already set it up anyway. ```php - $loop = React\EventLoop\Factory::create(); $dnsResolverFactory = new React\Dns\Resolver\Factory(); @@ -43,7 +38,6 @@ The `React\SocketClient\ConnectionManager` provides a single promise-based succeeds or fails. ```php - $connectionManager = new React\SocketClient\ConnectionManager($loop, $dns); $connectionManager->getConnection('www.google.com', 80)->then(function (React\Stream\Stream $stream) { @@ -54,14 +48,13 @@ $connectionManager->getConnection('www.google.com', 80)->then(function (React\St ### Async SSL/TLS connections -The `React\SocketClient\SecureConnectionManager` class decorates a given -`React\SocketClient\ConnectionManager` instance by enabling SSL/TLS encryption -as soon as the raw TCP/IP connection succeeds. It provides the same promise- -based `getConnection($host, $ip)` method which resolves with a `Stream` -instance that can be used just like any non-encrypted stream. +The `SecureConnectionManager` class decorates a given `ConnectionManager` +instance by enabling SSL/TLS encryption as soon as the raw TCP/IP connection +succeeds. It provides the same promise- based `getConnection($host, $ip)` +method which resolves with a `Stream` instance that can be used just like any +non-encrypted stream. ```php - $secureConnectionManager = new React\SocketClient\SecureConnectionManager($connectionManager, $loop); $secureConnectionManager->getConnection('www.google.com', 443)->then(function (React\Stream\Stream $stream) { From ea1ed9e0806bcf01ab8c52c9617bfd99c714659c Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:47:50 +0100 Subject: [PATCH 019/146] Add newline to ConnectionManager --- ConnectionManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/ConnectionManager.php b/ConnectionManager.php index 38a6df26..f355b1a0 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -76,6 +76,7 @@ public function checkConnectedSocket($socket) if (false === stream_socket_get_name($socket, true)) { return When::reject(new ConnectionException('Connection refused')); } + return When::resolve($socket); } From ff0122e2f7240fb092f5dd69378c365329fff275 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:50:42 +0100 Subject: [PATCH 020/146] Wrap StreamEncryption comment --- StreamEncryption.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/StreamEncryption.php b/StreamEncryption.php index 9d24608a..25568c13 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -8,7 +8,10 @@ use React\EventLoop\LoopInterface; use \UnexpectedValueException; -// this class is considered internal and its API should not be relied upon outside of React\SocketClient +/** + * This class is considered internal and its API should not be relied upon + * outside of SocketClient + */ class StreamEncryption { protected $loop; From abfe8435ebdf43262ab828e0f3fe80e73c672e78 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:54:11 +0100 Subject: [PATCH 021/146] Add missing import of Promise\\When to ConnectionManager --- ConnectionManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/ConnectionManager.php b/ConnectionManager.php index f355b1a0..737992e7 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -5,6 +5,7 @@ use React\EventLoop\LoopInterface; use React\Dns\Resolver\Resolver; use React\Stream\Stream; +use React\Promise\When; use React\Promise\Deferred; use React\Promise\FulfilledPromise; use React\Promise\RejectedPromise; From bdd080ae31545e00ff79fa7f2ef77be110656e15 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 19:54:31 +0100 Subject: [PATCH 022/146] Tighten visibility of SocketClient properties --- ConnectionManager.php | 4 ++-- SecureConnectionManager.php | 6 +++--- StreamEncryption.php | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ConnectionManager.php b/ConnectionManager.php index 737992e7..34e1598c 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -12,8 +12,8 @@ class ConnectionManager implements ConnectionManagerInterface { - protected $loop; - protected $resolver; + private $loop; + private $resolver; public function __construct(LoopInterface $loop, Resolver $resolver) { diff --git a/SecureConnectionManager.php b/SecureConnectionManager.php index 8283a30f..99c6fbdb 100644 --- a/SecureConnectionManager.php +++ b/SecureConnectionManager.php @@ -7,9 +7,9 @@ class SecureConnectionManager implements ConnectionManagerInterface { - protected $connectionManager; - protected $loop; - protected $streamEncryption; + private $connectionManager; + private $loop; + private $streamEncryption; public function __construct(ConnectionManagerInterface $connectionManager, LoopInterface $loop) { diff --git a/StreamEncryption.php b/StreamEncryption.php index 25568c13..a3e26c72 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -14,11 +14,11 @@ */ class StreamEncryption { - protected $loop; - protected $method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + private $loop; + private $method = STREAM_CRYPTO_METHOD_TLS_CLIENT; - protected $errstr; - protected $errno; + private $errstr; + private $errno; public function __construct(LoopInterface $loop) { From 16bf0dd60c77815587ebbecf2c2d1748c4c6c814 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 20 Jan 2013 20:14:33 +0100 Subject: [PATCH 023/146] Make promise constraint consistent by using tilde --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8b95e065..6b6a1a1c 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.3", "react/dns": "0.3.*", "react/event-loop": "0.3.*", - "react/promise": "1.*", + "react/promise": "~1.0", }, "autoload": { "psr-0": { "React\\SocketClient": "" } From 0117b8758fbda40b78f27f0907aece3178c1cb8a Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Mon, 21 Jan 2013 22:03:01 +0100 Subject: [PATCH 024/146] Use When instead of FulfilledPromise and RejectedPromise --- ConnectionManager.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ConnectionManager.php b/ConnectionManager.php index 34e1598c..fbadc2cc 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -7,8 +7,6 @@ use React\Stream\Stream; use React\Promise\When; use React\Promise\Deferred; -use React\Promise\FulfilledPromise; -use React\Promise\RejectedPromise; class ConnectionManager implements ConnectionManagerInterface { @@ -39,7 +37,7 @@ public function getConnectionForAddress($address, $port) $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if (!$socket) { - return new RejectedPromise(new \RuntimeException( + return When::reject(new \RuntimeException( sprintf("connection to %s:%d failed: %s", $address, $port, $errstr), $errno )); @@ -94,7 +92,7 @@ protected function getSocketUrl($host, $port) protected function resolveHostname($host) { if (false !== filter_var($host, FILTER_VALIDATE_IP)) { - return new FulfilledPromise($host); + return When::resolve($host); } return $this->resolver->resolve($host); From 73dec56121a4fd733565416e7a85d68ac77b2d67 Mon Sep 17 00:00:00 2001 From: Robin van der Vleuten Date: Mon, 28 Jan 2013 14:53:18 +0100 Subject: [PATCH 025/146] Make the transport method of the socket variable. --- ConnectionManager.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ConnectionManager.php b/ConnectionManager.php index fbadc2cc..015d44ed 100644 --- a/ConnectionManager.php +++ b/ConnectionManager.php @@ -19,20 +19,20 @@ public function __construct(LoopInterface $loop, Resolver $resolver) $this->resolver = $resolver; } - public function getConnection($host, $port) + public function getConnection($host, $port, $transport = 'tcp') { $that = $this; return $this ->resolveHostname($host) - ->then(function ($address) use ($port, $that) { - return $that->getConnectionForAddress($address, $port); + ->then(function ($address) use ($port, $transport, $that) { + return $that->getConnectionForAddress($address, $port, $transport); }); } - public function getConnectionForAddress($address, $port) + public function getConnectionForAddress($address, $port, $transport = 'tcp') { - $url = $this->getSocketUrl($address, $port); + $url = $this->getSocketUrl($address, $port, $transport); $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); @@ -84,9 +84,9 @@ public function handleConnectedSocket($socket) return new Stream($socket, $this->loop); } - protected function getSocketUrl($host, $port) + protected function getSocketUrl($host, $port, $transport = 'tcp') { - return sprintf('tcp://%s:%s', $host, $port); + return sprintf('%s://%s:%s', $transport, $host, $port); } protected function resolveHostname($host) From 9f75e7238d49a08b981178cccd68e0c065368cb3 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Mon, 28 Jan 2013 21:13:30 +0100 Subject: [PATCH 026/146] Rename ConnectionManager to Connector --- ConnectionManager.php => Connector.php | 2 +- ...gerInterface.php => ConnectorInterface.php | 2 +- README.md | 21 +++++++++---------- ...nnectionManager.php => SecureConnector.php | 10 ++++----- composer.json | 2 +- 5 files changed, 18 insertions(+), 19 deletions(-) rename ConnectionManager.php => Connector.php (97%) rename ConnectionManagerInterface.php => ConnectorInterface.php (71%) rename SecureConnectionManager.php => SecureConnector.php (65%) diff --git a/ConnectionManager.php b/Connector.php similarity index 97% rename from ConnectionManager.php rename to Connector.php index fbadc2cc..23a50594 100644 --- a/ConnectionManager.php +++ b/Connector.php @@ -8,7 +8,7 @@ use React\Promise\When; use React\Promise\Deferred; -class ConnectionManager implements ConnectionManagerInterface +class Connector implements ConnectorInterface { private $loop; private $resolver; diff --git a/ConnectionManagerInterface.php b/ConnectorInterface.php similarity index 71% rename from ConnectionManagerInterface.php rename to ConnectorInterface.php index 1d694b32..f5490f00 100644 --- a/ConnectionManagerInterface.php +++ b/ConnectorInterface.php @@ -2,7 +2,7 @@ namespace React\SocketClient; -interface ConnectionManagerInterface +interface ConnectorInterface { public function getConnection($host, $port); } diff --git a/README.md b/README.md index 9b99cc8b..66da08cd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SocketClient Component -Async ConnectionManager to open TCP/IP and SSL/TLS based connections. +Async Connector to open TCP/IP and SSL/TLS based connections. ## Introduction @@ -33,14 +33,14 @@ $dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); ### Async TCP/IP connections -The `React\SocketClient\ConnectionManager` provides a single promise-based +The `React\SocketClient\Connector` provides a single promise-based `getConnection($host, $ip)` method which resolves as soon as the connection succeeds or fails. ```php -$connectionManager = new React\SocketClient\ConnectionManager($loop, $dns); +$connector = new React\SocketClient\Connector($loop, $dns); -$connectionManager->getConnection('www.google.com', 80)->then(function (React\Stream\Stream $stream) { +$connector->getConnection('www.google.com', 80)->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->close(); }); @@ -48,16 +48,15 @@ $connectionManager->getConnection('www.google.com', 80)->then(function (React\St ### Async SSL/TLS connections -The `SecureConnectionManager` class decorates a given `ConnectionManager` -instance by enabling SSL/TLS encryption as soon as the raw TCP/IP connection -succeeds. It provides the same promise- based `getConnection($host, $ip)` -method which resolves with a `Stream` instance that can be used just like any -non-encrypted stream. +The `SecureConnector` class decorates a given `Connector` instance by enabling +SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. It provides +the same promise- based `getConnection($host, $ip)` method which resolves with +a `Stream` instance that can be used just like any non-encrypted stream. ```php -$secureConnectionManager = new React\SocketClient\SecureConnectionManager($connectionManager, $loop); +$secureConnector = new React\SocketClient\SecureConnector($connector, $loop); -$secureConnectionManager->getConnection('www.google.com', 443)->then(function (React\Stream\Stream $stream) { +$secureConnector->getConnection('www.google.com', 443)->then(function (React\Stream\Stream $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); diff --git a/SecureConnectionManager.php b/SecureConnector.php similarity index 65% rename from SecureConnectionManager.php rename to SecureConnector.php index 99c6fbdb..a73189bf 100644 --- a/SecureConnectionManager.php +++ b/SecureConnector.php @@ -5,15 +5,15 @@ use React\EventLoop\LoopInterface; use React\Stream\Stream; -class SecureConnectionManager implements ConnectionManagerInterface +class SecureConnector implements ConnectorInterface { - private $connectionManager; + private $connector; private $loop; private $streamEncryption; - public function __construct(ConnectionManagerInterface $connectionManager, LoopInterface $loop) + public function __construct(ConnectorInterface $connector, LoopInterface $loop) { - $this->connectionManager = $connectionManager; + $this->connector = $connector; $this->loop = $loop; $this->streamEncryption = new StreamEncryption($loop); } @@ -21,7 +21,7 @@ public function __construct(ConnectionManagerInterface $connectionManager, LoopI public function getConnection($host, $port) { $streamEncryption = $this->streamEncryption; - return $this->connectionManager->getConnection($host, $port)->then(function (Stream $stream) use ($streamEncryption) { + return $this->connector->getConnection($host, $port)->then(function (Stream $stream) use ($streamEncryption) { // (unencrypted) connection succeeded => try to enable encryption return $streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { // establishing encryption failed => close invalid connection and return error diff --git a/composer.json b/composer.json index 6b6a1a1c..fd580da5 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "react/socket-client", - "description": "Async ConnectionManager to open TCP/IP and SSL/TLS based connections.", + "description": "Async connector to open TCP/IP and SSL/TLS based connections.", "keywords": ["socket"], "license": "MIT", "require": { From 3cdd19e4b7f554fad8ed2ffc8151ae9671817fbb Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Mon, 28 Jan 2013 21:18:22 +0100 Subject: [PATCH 027/146] Rename Connector::getConnection to createTcp --- Connector.php | 6 +++--- ConnectorInterface.php | 2 +- README.md | 8 ++++---- SecureConnector.php | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Connector.php b/Connector.php index 23a50594..283a8e5f 100644 --- a/Connector.php +++ b/Connector.php @@ -19,18 +19,18 @@ public function __construct(LoopInterface $loop, Resolver $resolver) $this->resolver = $resolver; } - public function getConnection($host, $port) + public function createTcp($host, $port) { $that = $this; return $this ->resolveHostname($host) ->then(function ($address) use ($port, $that) { - return $that->getConnectionForAddress($address, $port); + return $that->createTcpForAddress($address, $port); }); } - public function getConnectionForAddress($address, $port) + public function createTcpForAddress($address, $port) { $url = $this->getSocketUrl($address, $port); diff --git a/ConnectorInterface.php b/ConnectorInterface.php index f5490f00..51f1dc0a 100644 --- a/ConnectorInterface.php +++ b/ConnectorInterface.php @@ -4,5 +4,5 @@ interface ConnectorInterface { - public function getConnection($host, $port); + public function createTcp($host, $port); } diff --git a/README.md b/README.md index 66da08cd..0facb9db 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ $dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); ### Async TCP/IP connections The `React\SocketClient\Connector` provides a single promise-based -`getConnection($host, $ip)` method which resolves as soon as the connection +`createTcp($host, $ip)` method which resolves as soon as the connection succeeds or fails. ```php $connector = new React\SocketClient\Connector($loop, $dns); -$connector->getConnection('www.google.com', 80)->then(function (React\Stream\Stream $stream) { +$connector->createTcp('www.google.com', 80)->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->close(); }); @@ -50,13 +50,13 @@ $connector->getConnection('www.google.com', 80)->then(function (React\Stream\Str The `SecureConnector` class decorates a given `Connector` instance by enabling SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. It provides -the same promise- based `getConnection($host, $ip)` method which resolves with +the same promise- based `createTcp($host, $ip)` method which resolves with a `Stream` instance that can be used just like any non-encrypted stream. ```php $secureConnector = new React\SocketClient\SecureConnector($connector, $loop); -$secureConnector->getConnection('www.google.com', 443)->then(function (React\Stream\Stream $stream) { +$secureConnector->createTcp('www.google.com', 443)->then(function (React\Stream\Stream $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); diff --git a/SecureConnector.php b/SecureConnector.php index a73189bf..98679bca 100644 --- a/SecureConnector.php +++ b/SecureConnector.php @@ -18,10 +18,10 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop) $this->streamEncryption = new StreamEncryption($loop); } - public function getConnection($host, $port) + public function createTcp($host, $port) { $streamEncryption = $this->streamEncryption; - return $this->connector->getConnection($host, $port)->then(function (Stream $stream) use ($streamEncryption) { + return $this->connector->createTcp($host, $port)->then(function (Stream $stream) use ($streamEncryption) { // (unencrypted) connection succeeded => try to enable encryption return $streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { // establishing encryption failed => close invalid connection and return error From 12b54c4ed17eaa678e6aa5913bef08eaa5359d82 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Thu, 31 Jan 2013 01:06:52 +0100 Subject: [PATCH 028/146] [socket-client] Remove leading backslash for use statement --- StreamEncryption.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StreamEncryption.php b/StreamEncryption.php index a3e26c72..6f479d8f 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -6,7 +6,7 @@ use React\Promise\Deferred; use React\Stream\Stream; use React\EventLoop\LoopInterface; -use \UnexpectedValueException; +use UnexpectedValueException; /** * This class is considered internal and its API should not be relied upon From c7488bf9f5ced2b9604af0651d47cc9428659864 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 14 Apr 2013 06:50:22 +0200 Subject: [PATCH 029/146] Fix socket-client composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fd580da5..0aff9343 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.3", "react/dns": "0.3.*", "react/event-loop": "0.3.*", - "react/promise": "~1.0", + "react/promise": "~1.0" }, "autoload": { "psr-0": { "React\\SocketClient": "" } From cc7ca1347164074f99d474b60b5fc56046a2f19c Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 14 Apr 2013 22:37:25 +0200 Subject: [PATCH 030/146] [SocketClient] Remove ConnectorInterface::createUdp It does not make sense to represent UDP as a stream. Streams have end semantics, UDP does not. --- Connector.php | 19 ++++--------------- ConnectorInterface.php | 1 - SecureConnector.php | 5 ----- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/Connector.php b/Connector.php index 90aab71d..f114e721 100644 --- a/Connector.php +++ b/Connector.php @@ -30,20 +30,9 @@ public function createTcp($host, $port) }); } - public function createUdp($host, $port) + public function createSocketForAddress($address, $port) { - $that = $this; - - return $this - ->resolveHostname($host) - ->then(function ($address) use ($port, $that) { - return $that->createSocketForAddress($address, $port, 'udp'); - }); - } - - public function createSocketForAddress($address, $port, $transport = 'tcp') - { - $url = $this->getSocketUrl($address, $port, $transport); + $url = $this->getSocketUrl($address, $port); $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); @@ -95,9 +84,9 @@ public function handleConnectedSocket($socket) return new Stream($socket, $this->loop); } - protected function getSocketUrl($host, $port, $transport) + protected function getSocketUrl($host, $port) { - return sprintf('%s://%s:%s', $transport, $host, $port); + return sprintf('tcp://%s:%s', $host, $port); } protected function resolveHostname($host) diff --git a/ConnectorInterface.php b/ConnectorInterface.php index 27d6d979..51f1dc0a 100644 --- a/ConnectorInterface.php +++ b/ConnectorInterface.php @@ -5,5 +5,4 @@ interface ConnectorInterface { public function createTcp($host, $port); - public function createUdp($host, $port); } diff --git a/SecureConnector.php b/SecureConnector.php index 9d5ac454..ed57de07 100644 --- a/SecureConnector.php +++ b/SecureConnector.php @@ -31,9 +31,4 @@ public function createTcp($host, $port) }); }); } - - public function createUdp($host, $port) - { - return When::reject(new \RuntimeException('Secured UDP connection is not supported.')); - } } From a6e852bb1f6c3bb4931459d1e35476c49f09508d Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 14 Apr 2013 22:45:17 +0200 Subject: [PATCH 031/146] [SocketClient] Rename Connector::createTcp to Connector::create --- Connector.php | 2 +- ConnectorInterface.php | 2 +- README.md | 8 ++++---- SecureConnector.php | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Connector.php b/Connector.php index f114e721..8aec0408 100644 --- a/Connector.php +++ b/Connector.php @@ -19,7 +19,7 @@ public function __construct(LoopInterface $loop, Resolver $resolver) $this->resolver = $resolver; } - public function createTcp($host, $port) + public function create($host, $port) { $that = $this; diff --git a/ConnectorInterface.php b/ConnectorInterface.php index 51f1dc0a..b40b3a1b 100644 --- a/ConnectorInterface.php +++ b/ConnectorInterface.php @@ -4,5 +4,5 @@ interface ConnectorInterface { - public function createTcp($host, $port); + public function create($host, $port); } diff --git a/README.md b/README.md index 0facb9db..be1eecfc 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ $dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); ### Async TCP/IP connections The `React\SocketClient\Connector` provides a single promise-based -`createTcp($host, $ip)` method which resolves as soon as the connection +`create($host, $ip)` method which resolves as soon as the connection succeeds or fails. ```php $connector = new React\SocketClient\Connector($loop, $dns); -$connector->createTcp('www.google.com', 80)->then(function (React\Stream\Stream $stream) { +$connector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->close(); }); @@ -50,13 +50,13 @@ $connector->createTcp('www.google.com', 80)->then(function (React\Stream\Stream The `SecureConnector` class decorates a given `Connector` instance by enabling SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. It provides -the same promise- based `createTcp($host, $ip)` method which resolves with +the same promise- based `create($host, $ip)` method which resolves with a `Stream` instance that can be used just like any non-encrypted stream. ```php $secureConnector = new React\SocketClient\SecureConnector($connector, $loop); -$secureConnector->createTcp('www.google.com', 443)->then(function (React\Stream\Stream $stream) { +$secureConnector->create('www.google.com', 443)->then(function (React\Stream\Stream $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); diff --git a/SecureConnector.php b/SecureConnector.php index ed57de07..f2fded12 100644 --- a/SecureConnector.php +++ b/SecureConnector.php @@ -19,10 +19,10 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop) $this->streamEncryption = new StreamEncryption($loop); } - public function createTcp($host, $port) + public function create($host, $port) { $streamEncryption = $this->streamEncryption; - return $this->connector->createTcp($host, $port)->then(function (Stream $stream) use ($streamEncryption) { + return $this->connector->create($host, $port)->then(function (Stream $stream) use ($streamEncryption) { // (unencrypted) connection succeeded => try to enable encryption return $streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { // establishing encryption failed => close invalid connection and return error From 06d0c732b04e2fb2f4d7332edcf0ef34f82f7a54 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 14 Apr 2013 17:14:39 -0400 Subject: [PATCH 032/146] CS --- SecureConnector.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/SecureConnector.php b/SecureConnector.php index f2fded12..a0673acb 100644 --- a/SecureConnector.php +++ b/SecureConnector.php @@ -9,13 +9,11 @@ class SecureConnector implements ConnectorInterface { private $connector; - private $loop; private $streamEncryption; public function __construct(ConnectorInterface $connector, LoopInterface $loop) { $this->connector = $connector; - $this->loop = $loop; $this->streamEncryption = new StreamEncryption($loop); } From 0bbbbace502a0c8742e32827bea6ed2b807f4931 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 14 Apr 2013 03:42:29 +0200 Subject: [PATCH 033/146] Clean up annoying 5.3 $that = $this --- Connector.php | 6 ++---- StreamEncryption.php | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Connector.php b/Connector.php index 8aec0408..fdfa823c 100644 --- a/Connector.php +++ b/Connector.php @@ -21,12 +21,10 @@ public function __construct(LoopInterface $loop, Resolver $resolver) public function create($host, $port) { - $that = $this; - return $this ->resolveHostname($host) - ->then(function ($address) use ($port, $that) { - return $that->createSocketForAddress($address, $port); + ->then(function ($address) use ($port) { + return $this->createSocketForAddress($address, $port); }); } diff --git a/StreamEncryption.php b/StreamEncryption.php index 6f479d8f..11b708fd 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -47,9 +47,8 @@ public function toggle(Stream $stream, $toggle) // get actual stream socket from stream instance $socket = $stream->stream; - $that = $this; - $toggleCrypto = function () use ($that, $socket, $deferred, $toggle) { - $that->toggleCrypto($socket, $deferred, $toggle); + $toggleCrypto = function () use ($socket, $deferred, $toggle) { + $this->toggleCrypto($socket, $deferred, $toggle); }; $this->loop->addWriteStream($socket, $toggleCrypto); From 23c9417672361c400354c5117f5b1b9e30d3c47c Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 14 Apr 2013 23:09:16 +0200 Subject: [PATCH 034/146] Remove $that craziness from SecureConnector --- SecureConnector.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SecureConnector.php b/SecureConnector.php index a0673acb..51f867f9 100644 --- a/SecureConnector.php +++ b/SecureConnector.php @@ -19,10 +19,9 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop) public function create($host, $port) { - $streamEncryption = $this->streamEncryption; - return $this->connector->create($host, $port)->then(function (Stream $stream) use ($streamEncryption) { + return $this->connector->create($host, $port)->then(function (Stream $stream) { // (unencrypted) connection succeeded => try to enable encryption - return $streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { + return $this->streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { // establishing encryption failed => close invalid connection and return error $stream->close(); throw $error; From 87935a0223362c36cd30cf215cbec33377d31ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 20 Apr 2013 16:55:59 +0200 Subject: [PATCH 035/146] Support connecting to IPv6 addresses --- Connector.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Connector.php b/Connector.php index 8aec0408..102ac6d2 100644 --- a/Connector.php +++ b/Connector.php @@ -86,6 +86,10 @@ public function handleConnectedSocket($socket) protected function getSocketUrl($host, $port) { + if (strpos($host, ':') !== false) { + // enclose IPv6 addresses in square brackets before appending port + $host = '[' . $host . ']'; + } return sprintf('tcp://%s:%s', $host, $port); } From d18db3482ceb0f50a27280f2502d7fafd6522927 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Tue, 10 Dec 2013 19:45:50 +0100 Subject: [PATCH 036/146] Update to React/Promise 2.0 --- Connector.php | 10 +++++----- SecureConnector.php | 1 - StreamEncryption.php | 9 ++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Connector.php b/Connector.php index 863c5e3c..7dd5b105 100644 --- a/Connector.php +++ b/Connector.php @@ -5,7 +5,7 @@ use React\EventLoop\LoopInterface; use React\Dns\Resolver\Resolver; use React\Stream\Stream; -use React\Promise\When; +use React\Promise; use React\Promise\Deferred; class Connector implements ConnectorInterface @@ -35,7 +35,7 @@ public function createSocketForAddress($address, $port) $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if (!$socket) { - return When::reject(new \RuntimeException( + return Promise\reject(new \RuntimeException( sprintf("connection to %s:%d failed: %s", $address, $port, $errstr), $errno )); @@ -71,10 +71,10 @@ public function checkConnectedSocket($socket) // The following hack looks like the only way to // detect connection refused errors with PHP's stream sockets. if (false === stream_socket_get_name($socket, true)) { - return When::reject(new ConnectionException('Connection refused')); + return Promise\reject(new ConnectionException('Connection refused')); } - return When::resolve($socket); + return Promise\resolve($socket); } public function handleConnectedSocket($socket) @@ -94,7 +94,7 @@ protected function getSocketUrl($host, $port) protected function resolveHostname($host) { if (false !== filter_var($host, FILTER_VALIDATE_IP)) { - return When::resolve($host); + return Promise\resolve($host); } return $this->resolver->resolve($host); diff --git a/SecureConnector.php b/SecureConnector.php index 51f867f9..fed2da28 100644 --- a/SecureConnector.php +++ b/SecureConnector.php @@ -4,7 +4,6 @@ use React\EventLoop\LoopInterface; use React\Stream\Stream; -use React\Promise\When; class SecureConnector implements ConnectorInterface { diff --git a/StreamEncryption.php b/StreamEncryption.php index 11b708fd..84b2d289 100644 --- a/StreamEncryption.php +++ b/StreamEncryption.php @@ -2,7 +2,6 @@ namespace React\SocketClient; -use React\Promise\ResolverInterface; use React\Promise\Deferred; use React\Stream\Stream; use React\EventLoop\LoopInterface; @@ -55,7 +54,7 @@ public function toggle(Stream $stream, $toggle) $this->loop->addReadStream($socket, $toggleCrypto); $toggleCrypto(); - return $deferred->then(function () use ($stream) { + return $deferred->promise()->then(function () use ($stream) { $stream->resume(); return $stream; }, function($error) use ($stream) { @@ -64,7 +63,7 @@ public function toggle(Stream $stream, $toggle) }); } - public function toggleCrypto($socket, ResolverInterface $resolver, $toggle) + public function toggleCrypto($socket, Deferred $deferred, $toggle) { set_error_handler(array($this, 'handleError')); $result = stream_socket_enable_crypto($socket, $toggle, $this->method); @@ -74,12 +73,12 @@ public function toggleCrypto($socket, ResolverInterface $resolver, $toggle) $this->loop->removeWriteStream($socket); $this->loop->removeReadStream($socket); - $resolver->resolve(); + $deferred->resolve(); } else if (false === $result) { $this->loop->removeWriteStream($socket); $this->loop->removeReadStream($socket); - $resolver->reject(new UnexpectedValueException( + $deferred->reject(new UnexpectedValueException( sprintf("Unable to complete SSL/TLS handshake: %s", $this->errstr), $this->errno )); From bfecaa89e6861f3a23a15e7140e65e19977684d1 Mon Sep 17 00:00:00 2001 From: Elliot Anderson Date: Wed, 18 Dec 2013 17:14:19 +1000 Subject: [PATCH 037/146] Fixed broken link for stream_socket_client documentation and updated both to the new website URL scheme --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be1eecfc..02799914 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,8 @@ Async Connector to open TCP/IP and SSL/TLS based connections. ## Introduction Think of this library as an async version of -[`fsockopen()`](http://php.net/manual/en/function.fsockopen.php) or -[`stream_socket_client()`](http://php.net/manual/en/function.stream-socket- -client.php). +[`fsockopen()`](http://www.php.net/function.fsockopen) or +[`stream_socket_client()`](http://php.net/function.stream-socket-client). Before you can actually transmit and receive data to/from a remote server, you have to establish a connection to the remote end. Establishing this connection From 6ec8c3aefc018acec763fa6e950e1baa947231fc Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 1 Feb 2014 16:15:43 -0500 Subject: [PATCH 038/146] Update child repos to PSR-4 for git subs-plit --- composer.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 0aff9343..07b3f5eb 100644 --- a/composer.json +++ b/composer.json @@ -4,18 +4,17 @@ "keywords": ["socket"], "license": "MIT", "require": { - "php": ">=5.3.3", - "react/dns": "0.3.*", - "react/event-loop": "0.3.*", - "react/promise": "~1.0" + "php": ">=5.4.0", + "react/dns": "0.4.*", + "react/event-loop": "0.4.*", + "react/promise": "~2.0" }, "autoload": { - "psr-0": { "React\\SocketClient": "" } + "psr-4": { "React\\SocketClient\\": "" } }, - "target-dir": "React/SocketClient", "extra": { "branch-alias": { - "dev-master": "0.3-dev" + "dev-master": "0.4-dev" } } } From 5ee205c0c4eef3553ee3d7bfb6907e620f70bc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 7 May 2014 16:33:59 +0200 Subject: [PATCH 039/146] Move tests to each component --- tests/ConnectorTest.php | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 tests/ConnectorTest.php diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php new file mode 100644 index 00000000..168c7439 --- /dev/null +++ b/tests/ConnectorTest.php @@ -0,0 +1,102 @@ +createResolverMock(); + + $connector = new Connector($loop, $dns); + $connector->create('127.0.0.1', 9999) + ->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $loop->run(); + } + + /** @test */ + public function connectionToTcpServerShouldSucceed() + { + $capturedStream = null; + + $loop = new StreamSelectLoop(); + + $server = new Server($loop); + $server->on('connection', $this->expectCallableOnce()); + $server->on('connection', function () use ($server, $loop) { + $server->shutdown(); + }); + $server->listen(9999); + + $dns = $this->createResolverMock(); + + $connector = new Connector($loop, $dns); + $connector->create('127.0.0.1', 9999) + ->then(function ($stream) use (&$capturedStream) { + $capturedStream = $stream; + $stream->end(); + }); + + $loop->run(); + + $this->assertInstanceOf('React\Stream\Stream', $capturedStream); + } + + /** @test */ + public function connectionToEmptyIp6PortShouldFail() + { + $loop = new StreamSelectLoop(); + + $dns = $this->createResolverMock(); + + $connector = new Connector($loop, $dns); + $connector + ->create('::1', 9999) + ->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $loop->run(); + } + + /** @test */ + public function connectionToIp6TcpServerShouldSucceed() + { + $capturedStream = null; + + $loop = new StreamSelectLoop(); + + $server = new Server($loop); + $server->on('connection', $this->expectCallableOnce()); + $server->on('connection', array($server, 'shutdown')); + $server->listen(9999, '::1'); + + $dns = $this->createResolverMock(); + + $connector = new Connector($loop, $dns); + $connector + ->create('::1', 9999) + ->then(function ($stream) use (&$capturedStream) { + $capturedStream = $stream; + $stream->end(); + }); + + $loop->run(); + + $this->assertInstanceOf('React\Stream\Stream', $capturedStream); + } + + private function createResolverMock() + { + return $this->getMockBuilder('React\Dns\Resolver\Resolver') + ->disableOriginalConstructor() + ->getMock(); + } +} From d3476750f109b8866003e2399f5b25359d56b700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 7 May 2014 17:35:03 +0200 Subject: [PATCH 040/146] Make components' tests run on their own and from main repo. Each component has dedicated test config and bootstrap. Duplication of parts of the skeleton is not ideal, but helps to reduce dependencies between each test suite. Also, this eases the future subtree split. --- phpunit.xml.dist | 25 +++++++++++++++++++++++++ tests/CallableStub.php | 10 ++++++++++ tests/ConnectorTest.php | 1 - tests/TestCase.php | 41 +++++++++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 7 +++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 phpunit.xml.dist create mode 100644 tests/CallableStub.php create mode 100644 tests/TestCase.php create mode 100644 tests/bootstrap.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..cba6d4dd --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + ./tests/ + + + + + + ./src/ + + + diff --git a/tests/CallableStub.php b/tests/CallableStub.php new file mode 100644 index 00000000..181a426b --- /dev/null +++ b/tests/CallableStub.php @@ -0,0 +1,10 @@ +createCallableMock(); + $mock + ->expects($this->exactly($amount)) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + protected function createCallableMock() + { + return $this->getMock('React\Tests\SocketClient\CallableStub'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..965fc438 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,7 @@ +addPsr4('React\\Tests\\SocketClient\\', __DIR__); From 52ff3bf0b8d8a42bd4b86c18ec4a89daf8791a71 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 25 May 2014 11:29:10 -0400 Subject: [PATCH 041/146] Update repo to work as a standalone component --- .gitignore | 2 ++ .travis.yml | 14 ++++++++++++++ README.md | 2 ++ composer.json | 4 +++- .../ConnectionException.php | 0 Connector.php => src/Connector.php | 0 .../ConnectorInterface.php | 0 SecureConnector.php => src/SecureConnector.php | 0 StreamEncryption.php => src/StreamEncryption.php | 0 9 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .travis.yml rename ConnectionException.php => src/ConnectionException.php (100%) rename Connector.php => src/Connector.php (100%) rename ConnectorInterface.php => src/ConnectorInterface.php (100%) rename SecureConnector.php => src/SecureConnector.php (100%) rename StreamEncryption.php => src/StreamEncryption.php (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..987e2a25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..525cdc6a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm + +matrix: + allow_failures: + - php: hhvm + +before_script: + - composer install --dev --prefer-source diff --git a/README.md b/README.md index 02799914..feec9c19 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # SocketClient Component +[![Build Status](https://secure.travis-ci.org/reactphp/socket-client.png?branch=master)](http://travis-ci.org/reactphp/socket-client) + Async Connector to open TCP/IP and SSL/TLS based connections. ## Introduction diff --git a/composer.json b/composer.json index 07b3f5eb..a82e187f 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,9 @@ "react/promise": "~2.0" }, "autoload": { - "psr-4": { "React\\SocketClient\\": "" } + "psr-4": { + "React\\SocketClient\\": "src" + } }, "extra": { "branch-alias": { diff --git a/ConnectionException.php b/src/ConnectionException.php similarity index 100% rename from ConnectionException.php rename to src/ConnectionException.php diff --git a/Connector.php b/src/Connector.php similarity index 100% rename from Connector.php rename to src/Connector.php diff --git a/ConnectorInterface.php b/src/ConnectorInterface.php similarity index 100% rename from ConnectorInterface.php rename to src/ConnectorInterface.php diff --git a/SecureConnector.php b/src/SecureConnector.php similarity index 100% rename from SecureConnector.php rename to src/SecureConnector.php diff --git a/StreamEncryption.php b/src/StreamEncryption.php similarity index 100% rename from StreamEncryption.php rename to src/StreamEncryption.php From 970cf1822fbc713a4f4b3b5ef39f5e788edd19ac Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 25 May 2014 12:25:30 -0400 Subject: [PATCH 042/146] Adjusted parent test bootstrap loader path --- tests/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 965fc438..c322debf 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,6 +2,6 @@ $loader = @include __DIR__ . '/../vendor/autoload.php'; if (!$loader) { - $loader = require __DIR__ . '/../../../vendor/autoload.php'; + $loader = require __DIR__ . '/../../../../vendor/autoload.php'; } $loader->addPsr4('React\\Tests\\SocketClient\\', __DIR__); From a813a91e2b13a938768c4f894c91fa8295f5b449 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 1 Jun 2014 08:59:28 -0400 Subject: [PATCH 043/146] Added license file from react --- LICENSE | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a808108c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Igor Wiedler, Chris Boden + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. From cd0ddfb637465ba90c4258763c24e0f82eba009b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 7 Jun 2014 01:56:26 +0200 Subject: [PATCH 044/146] Add CHANGELOG Source: https://github.com/reactphp/react/blob/a6de34d61f68adebd3cc3b855268a5f1475749b8/CHANGELOG.md --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3bdba8c3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 + +## 0.3.1 (2013-04-21) + +* Feature: [SocketClient] Support connecting to IPv6 addresses (@clue) + +## 0.3.0 (2013-04-14) + +* Feature: [SocketClient] New SocketClient component extracted from HttpClient (@clue) From 00b65327b043f8497d3c308c38117f41e47826e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 9 Jun 2014 03:16:59 +0200 Subject: [PATCH 045/146] Add bumped versions to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdba8c3..e574ae5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks * BC break: Update to React/Promise 2.0 * Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 +* Bump React dependencies to v0.4 ## 0.3.1 (2013-04-21) From 7c1c7012759effc4371fc50083b33682cec1c9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 9 Jun 2014 23:54:12 +0200 Subject: [PATCH 046/146] Explicitly depend on react/stream --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index a82e187f..467cbef3 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "php": ">=5.4.0", "react/dns": "0.4.*", "react/event-loop": "0.4.*", + "react/stream": "0.4.*", "react/promise": "~2.0" }, "autoload": { From 20a9d24713349736d572767831feb3802b7c4377 Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Tue, 17 Jun 2014 08:32:29 +0300 Subject: [PATCH 047/146] Show test coverage directly after running test --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 525cdc6a..8fc9fb52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,3 +12,6 @@ matrix: before_script: - composer install --dev --prefer-source + +script: + - phpunit --coverage-text From a91bb2f986c8e07674708aba75629c227db8c7b3 Mon Sep 17 00:00:00 2001 From: Chris Wright Date: Fri, 1 Aug 2014 00:37:11 +0100 Subject: [PATCH 048/146] Ensure SNI details are always set on socket creation In PHP versions <5.6, SNI is not handled correctly unless the context options are set at the time of socket creation. This causes Apache to return 400 responses to HTTPS requests, as the hostname specified in the Host: header does not match the name indicated by SNI. More info on the Apache-specific effect of this can be found here: https://bugzilla.redhat.com/show_bug.cgi?id=1098711#c7 This patch addresses the problem by always setting the SNI_server_name for every socket at creation time. Unfortunately the nature of the problem combined with the manner in which connectors work means that this is done to every socket regardless of whether it will be used for SSL or not, but this does not have any adverse effects except the microscopic performance hit of creating a stream context where it is not needed. --- src/Connector.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Connector.php b/src/Connector.php index 7dd5b105..3d1b4d86 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -23,16 +23,24 @@ public function create($host, $port) { return $this ->resolveHostname($host) - ->then(function ($address) use ($port) { - return $this->createSocketForAddress($address, $port); + ->then(function ($address) use ($port, $host) { + return $this->createSocketForAddress($address, $port, $host); }); } - public function createSocketForAddress($address, $port) + public function createSocketForAddress($address, $port, $hostName = null) { $url = $this->getSocketUrl($address, $port); - $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + $contextOpts = array(); + if ($hostName !== null) { + $contextOpts['ssl']['SNI_enabled'] = true; + $contextOpts['ssl']['SNI_server_name'] = $hostName; + } + + $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT; + $context = stream_context_create($contextOpts); + $socket = stream_socket_client($url, $errno, $errstr, 0, $flags, $context); if (!$socket) { return Promise\reject(new \RuntimeException( From 6a813e0ffed5f707e6fc7c6176765e9a2cc222a1 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 23 Aug 2014 09:11:30 -0400 Subject: [PATCH 049/146] Only toggle the stream crypto handshake once --- src/StreamEncryption.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 84b2d289..2dfe40c3 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -50,7 +50,6 @@ public function toggle(Stream $stream, $toggle) $this->toggleCrypto($socket, $deferred, $toggle); }; - $this->loop->addWriteStream($socket, $toggleCrypto); $this->loop->addReadStream($socket, $toggleCrypto); $toggleCrypto(); @@ -70,12 +69,10 @@ public function toggleCrypto($socket, Deferred $deferred, $toggle) restore_error_handler(); if (true === $result) { - $this->loop->removeWriteStream($socket); $this->loop->removeReadStream($socket); $deferred->resolve(); } else if (false === $result) { - $this->loop->removeWriteStream($socket); $this->loop->removeReadStream($socket); $deferred->reject(new UnexpectedValueException( From 72eea35099ed801ad87cd23c3184fb6c24ea93e5 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 23 Aug 2014 10:52:27 -0400 Subject: [PATCH 050/146] SecureStream to address SSL buffering problem --- src/SecureStream.php | 92 ++++++++++++++++++++++++++++++++++++++++ src/StreamEncryption.php | 7 ++- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/SecureStream.php diff --git a/src/SecureStream.php b/src/SecureStream.php new file mode 100644 index 00000000..53842bdd --- /dev/null +++ b/src/SecureStream.php @@ -0,0 +1,92 @@ +stream = $stream; + $this->loop = $loop; + + $stream->on('error', function($error) { + $this->emit('error', [$error, $this]); + }); + $stream->on('end', function() { + $this->emit('end', [$this]); + }); + $stream->on('close', function() { + $this->emit('close', [$this]); + }); + $stream->on('drain', function() { + $this->emit('drain', [$this]); + }); + + $stream->pause(); + + $this->resume(); + } + + public function handleData($stream) + { + $data = stream_get_contents($stream); + + $this->emit('data', [$data, $this]); + + if (!is_resource($stream) || feof($stream)) { + $this->end(); + } + } + + public function pause() + { + $this->loop->removeReadStream($this->stream->stream); + } + + public function resume() + { + if ($this->isReadable()) { + $this->loop->addReadStream($this->stream->stream, [$this, 'handleData']); + } + } + + public function isReadable() + { + return $this->stream->isReadable(); + } + + public function isWritable() + { + return $this->stream->isWritable(); + } + + public function write($data) + { + return $this->stream->write($data); + } + + public function close() + { + return $this->stream->close(); + } + + public function end($data = null) + { + return $this->stream->end($data); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return $this->stream->pipe($dest, $options); + } +} \ No newline at end of file diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 84b2d289..f6a339fc 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -54,8 +54,13 @@ public function toggle(Stream $stream, $toggle) $this->loop->addReadStream($socket, $toggleCrypto); $toggleCrypto(); - return $deferred->promise()->then(function () use ($stream) { + return $deferred->promise()->then(function () use ($stream, $toggle) { + if ($toggle) { + return new SecureStream($stream, $this->loop); + } + $stream->resume(); + return $stream; }, function($error) use ($stream) { $stream->resume(); From ac87c2d36f4864f54be770158d6dd190ad911283 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 23 Aug 2014 11:24:05 -0400 Subject: [PATCH 051/146] Fixed wrong emit when piping data through Secure --- src/SecureStream.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SecureStream.php b/src/SecureStream.php index 53842bdd..729527a4 100644 --- a/src/SecureStream.php +++ b/src/SecureStream.php @@ -7,6 +7,7 @@ use React\Stream\DuplexStreamInterface; use React\Stream\WritableStreamInterface; use React\Stream\Stream; +use React\Stream\Util; class SecureStream implements DuplexStreamInterface { @@ -87,6 +88,8 @@ public function end($data = null) public function pipe(WritableStreamInterface $dest, array $options = array()) { - return $this->stream->pipe($dest, $options); + Util::pipe($this, $dest, $options); + + return $dest; } } \ No newline at end of file From 44ab73cffe85d499ee2eb100e9d5be05c7ef6c85 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 23 Aug 2014 11:35:20 -0400 Subject: [PATCH 052/146] Unwrap SecureStream, better decorating API --- src/SecureStream.php | 25 ++++++++++++++----------- src/StreamEncryption.php | 4 ++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/SecureStream.php b/src/SecureStream.php index 729527a4..c185331c 100644 --- a/src/SecureStream.php +++ b/src/SecureStream.php @@ -12,13 +12,16 @@ class SecureStream implements DuplexStreamInterface { use EventEmitterTrait; - + + public $stream; + + public $decorating; protected $loop; - protected $stream; public function __construct(Stream $stream, LoopInterface $loop) { - $this->stream = $stream; - $this->loop = $loop; + $this->stream = $stream->stream; + $this->decorating = $stream; + $this->loop = $loop; $stream->on('error', function($error) { $this->emit('error', [$error, $this]); @@ -51,39 +54,39 @@ public function handleData($stream) public function pause() { - $this->loop->removeReadStream($this->stream->stream); + $this->loop->removeReadStream($this->decorating->stream); } public function resume() { if ($this->isReadable()) { - $this->loop->addReadStream($this->stream->stream, [$this, 'handleData']); + $this->loop->addReadStream($this->decorating->stream, [$this, 'handleData']); } } public function isReadable() { - return $this->stream->isReadable(); + return $this->decorating->isReadable(); } public function isWritable() { - return $this->stream->isWritable(); + return $this->decorating->isWritable(); } public function write($data) { - return $this->stream->write($data); + return $this->decorating->write($data); } public function close() { - return $this->stream->close(); + return $this->decorating->close(); } public function end($data = null) { - return $this->stream->end($data); + return $this->decorating->end($data); } public function pipe(WritableStreamInterface $dest, array $options = array()) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index f6a339fc..79c71b87 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -36,6 +36,10 @@ public function disable(Stream $stream) public function toggle(Stream $stream, $toggle) { + if (__NAMESPACE__ . '\SecureStream' === get_class($stream)) { + $stream = $stream->decorating; + } + // pause actual stream instance to continue operation on raw stream socket $stream->pause(); From aa071b84e68f217e71a7fae6514d6f63e7077e64 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 31 Aug 2014 08:28:17 -0400 Subject: [PATCH 053/146] Only do file_get_contents on PHP versions needed --- src/StreamEncryption.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 79c71b87..de0eba01 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -18,10 +18,23 @@ class StreamEncryption private $errstr; private $errno; + + private $wrapSecure = false; public function __construct(LoopInterface $loop) { $this->loop = $loop; + + // See https://bugs.php.net/bug.php?id=65137 + // 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 ( + PHP_VERSION_ID < 50433 + || (PHP_VERSION_ID >= 50000 && PHP_VERSION_ID < 50517) + || PHP_VERSION_ID === 50600 + ) { + $this->wrapSecure = true; + } } public function enable(Stream $stream) @@ -59,7 +72,7 @@ public function toggle(Stream $stream, $toggle) $toggleCrypto(); return $deferred->promise()->then(function () use ($stream, $toggle) { - if ($toggle) { + if ($toggle && $this->wrapSecure) { return new SecureStream($stream, $this->loop); } From 08ff406ad01b82177359bd5d4814f77ee8566ace Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 1 Sep 2014 20:51:47 -0400 Subject: [PATCH 054/146] Fix version check to 5.5 --- src/StreamEncryption.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index de0eba01..d001130f 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -30,7 +30,7 @@ public function __construct(LoopInterface $loop) // get an empty string back because the buffer indicator could be wrong if ( PHP_VERSION_ID < 50433 - || (PHP_VERSION_ID >= 50000 && PHP_VERSION_ID < 50517) + || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50517) || PHP_VERSION_ID === 50600 ) { $this->wrapSecure = true; From 6ebaa4e0a79bc14432bfb4c6e7080324bb19ddba Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 21 Sep 2014 22:44:29 +0200 Subject: [PATCH 055/146] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e574ae5a..ae902ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.4.1 (2014-xx-xx) + +* Bugfix: Only toggle the stream crypto handshake once (@DaveRandom and @rdlowrey) +* Bugfix: Workaround for ext-openssl bug (@DaveRandom) + ## 0.4.0 (2014-02-02) * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks From 5b4ccbb9071f81280f20971cccd2abe980d027ca Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 16 Oct 2014 18:23:10 -0400 Subject: [PATCH 056/146] Changelog for release --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae902ebf..d16d2a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## 0.4.1 (2014-xx-xx) +## 0.4.2 (2014-10-16) * Bugfix: Only toggle the stream crypto handshake once (@DaveRandom and @rdlowrey) -* Bugfix: Workaround for ext-openssl bug (@DaveRandom) +* Bugfix: Workaround for ext-openssl buffering bug (@DaveRandom) +* Bugfix: SNI fix for PHP < 5.6 (@DaveRandom) -## 0.4.0 (2014-02-02) +## 0.4.(0/1) (2014-02-02) * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks * BC break: Update to React/Promise 2.0 From 80eeb4717f1df78a72e1dc8c854b343f1707fd47 Mon Sep 17 00:00:00 2001 From: e3betht Date: Thu, 18 Dec 2014 12:05:38 -0600 Subject: [PATCH 057/146] Adding Code Climate badge to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index feec9c19..e6358fdf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SocketClient Component -[![Build Status](https://secure.travis-ci.org/reactphp/socket-client.png?branch=master)](http://travis-ci.org/reactphp/socket-client) +[![Build Status](https://secure.travis-ci.org/reactphp/socket-client.png?branch=master)](http://travis-ci.org/reactphp/socket-client) [![Code Climate](https://codeclimate.com/github/reactphp/socket-client/badges/gpa.svg)](https://codeclimate.com/github/reactphp/socket-client) Async Connector to open TCP/IP and SSL/TLS based connections. From e81abac5bc58eb7cc6a3e3875e0503bc3b99153a Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 2 Feb 2015 16:19:37 +0100 Subject: [PATCH 058/146] Added peer_name to ssl context options --- src/Connector.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Connector.php b/src/Connector.php index 3d1b4d86..d132a74c 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -36,6 +36,7 @@ public function createSocketForAddress($address, $port, $hostName = null) if ($hostName !== null) { $contextOpts['ssl']['SNI_enabled'] = true; $contextOpts['ssl']['SNI_server_name'] = $hostName; + $contextOpts['ssl']['peer_name'] = $hostName; } $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT; From 6f92680bb862f8efe0c2a7f51a60eae807c6a826 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 3 Feb 2015 18:03:55 -0500 Subject: [PATCH 059/146] "Fix" minor BC break :-) --- README.md | 4 ++-- src/SecureStream.php | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index feec9c19..2bb80efd 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ succeeds or fails. ```php $connector = new React\SocketClient\Connector($loop, $dns); -$connector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) { +$connector->create('www.google.com', 80)->then(function (React\Stream\DuplexStreamInterface $stream) { $stream->write('...'); $stream->close(); }); @@ -57,7 +57,7 @@ a `Stream` instance that can be used just like any non-encrypted stream. ```php $secureConnector = new React\SocketClient\SecureConnector($connector, $loop); -$secureConnector->create('www.google.com', 443)->then(function (React\Stream\Stream $stream) { +$secureConnector->create('www.google.com', 443)->then(function (React\Stream\DuplexStreamInterface $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); diff --git a/src/SecureStream.php b/src/SecureStream.php index c185331c..ce6e1da5 100644 --- a/src/SecureStream.php +++ b/src/SecureStream.php @@ -9,12 +9,12 @@ use React\Stream\Stream; use React\Stream\Util; -class SecureStream implements DuplexStreamInterface +class SecureStream extends Stream implements DuplexStreamInterface { - use EventEmitterTrait; - +// use EventEmitterTrait; + public $stream; - + public $decorating; protected $loop; @@ -35,12 +35,12 @@ public function __construct(Stream $stream, LoopInterface $loop) { $stream->on('drain', function() { $this->emit('drain', [$this]); }); - + $stream->pause(); - + $this->resume(); } - + public function handleData($stream) { $data = stream_get_contents($stream); @@ -51,7 +51,7 @@ public function handleData($stream) $this->end(); } } - + public function pause() { $this->loop->removeReadStream($this->decorating->stream); From b302dfc13d71fab81a3eb0e267fd75370e3b7e65 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 3 Feb 2015 18:12:01 -0500 Subject: [PATCH 060/146] Always Stream if SSL refs #24 --- src/StreamEncryption.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 3a676c49..2a463596 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -18,23 +18,19 @@ class StreamEncryption private $errstr; private $errno; - + private $wrapSecure = false; public function __construct(LoopInterface $loop) { $this->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 ( - PHP_VERSION_ID < 50433 - || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50517) - || PHP_VERSION_ID === 50600 - ) { - $this->wrapSecure = true; - } + $this->wrapSecure = true; } public function enable(Stream $stream) @@ -50,7 +46,7 @@ public function disable(Stream $stream) public function toggle(Stream $stream, $toggle) { if (__NAMESPACE__ . '\SecureStream' === get_class($stream)) { - $stream = $stream->decorating; + $stream = $stream->decorating; } // pause actual stream instance to continue operation on raw stream socket @@ -74,9 +70,9 @@ public function toggle(Stream $stream, $toggle) if ($toggle && $this->wrapSecure) { return new SecureStream($stream, $this->loop); } - + $stream->resume(); - + return $stream; }, function($error) use ($stream) { $stream->resume(); From 14116e4cbd0a13d0d1bb4963373da038649e67b5 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 11 Mar 2015 14:47:25 -0400 Subject: [PATCH 061/146] Undo API change in docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2bb80efd..feec9c19 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ succeeds or fails. ```php $connector = new React\SocketClient\Connector($loop, $dns); -$connector->create('www.google.com', 80)->then(function (React\Stream\DuplexStreamInterface $stream) { +$connector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->close(); }); @@ -57,7 +57,7 @@ a `Stream` instance that can be used just like any non-encrypted stream. ```php $secureConnector = new React\SocketClient\SecureConnector($connector, $loop); -$secureConnector->create('www.google.com', 443)->then(function (React\Stream\DuplexStreamInterface $stream) { +$secureConnector->create('www.google.com', 443)->then(function (React\Stream\Stream $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); From 25686f574e0bd9348cb2d488f243a4463ad9a963 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 20 Mar 2015 11:08:17 -0400 Subject: [PATCH 062/146] v0.4.3 changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d16d2a32..67752396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.4.3 (2015-03-20) + +* Bugfix: Set peer name to hostname to correct security concern in PHP 5.6 (@WyyriHaximus) +* Bugfix: Always wrap secure to pull buffer due to regression in PHP +* Bugfix: SecureStream extends Stream to match documentation preventing BC (@clue) + ## 0.4.2 (2014-10-16) * Bugfix: Only toggle the stream crypto handshake once (@DaveRandom and @rdlowrey) From 0406b34fa3ec8f825c8e8722ebeda85f4b05f75f Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 21 Sep 2014 16:53:20 +0200 Subject: [PATCH 063/146] Integration test that performs a HTTP request against google.com --- tests/IntegrationTest.php | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/IntegrationTest.php diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php new file mode 100644 index 00000000..511f3779 --- /dev/null +++ b/tests/IntegrationTest.php @@ -0,0 +1,72 @@ +create('8.8.8.8', $loop); + + $connected = false; + $response = null; + + $connector = new Connector($loop, $dns); + $connector->create('google.com', 80) + ->then(function ($conn) use (&$connected) { + $connected = true; + $conn->write("GET / HTTP/1.0\r\n\r\n"); + return BufferedSink::createPromise($conn); + }) + ->then(function ($data) use (&$response) { + $response = $data; + }); + + $loop->run(); + + $this->assertTrue($connected); + $this->assertContains('HTTP/1.0 302 Found', $response); + } + + /** @test */ + public function gettingEncryptedStuffFromGoogleShouldWork() + { + $loop = new StreamSelectLoop(); + + $factory = new Factory(); + $dns = $factory->create('8.8.8.8', $loop); + + $connected = false; + $response = null; + + $secureConnector = new SecureConnector( + new Connector($loop, $dns), + $loop + ); + $secureConnector->create('google.com', 443) + ->then(function ($conn) use (&$connected) { + $connected = true; + $conn->write("GET / HTTP/1.0\r\n\r\n"); + return BufferedSink::createPromise($conn); + }) + ->then(function ($data) use (&$response) { + $response = $data; + }); + + $loop->run(); + + $this->assertTrue($connected); + $this->assertContains('HTTP/1.0 302 Found', $response); + } +} From 7cc7043f94cf5779d333e2bed5432af5c6c8cb96 Mon Sep 17 00:00:00 2001 From: Igor Wiedler Date: Sun, 21 Sep 2014 17:10:58 +0200 Subject: [PATCH 064/146] just check for beginning of HTTP line, since google is strange --- tests/IntegrationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 511f3779..889a8cb7 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -36,7 +36,7 @@ public function gettingStuffFromGoogleShouldWork() $loop->run(); $this->assertTrue($connected); - $this->assertContains('HTTP/1.0 302 Found', $response); + $this->assertRegExp('#^HTTP/1\.0#', $response); } /** @test */ @@ -67,6 +67,6 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $loop->run(); $this->assertTrue($connected); - $this->assertContains('HTTP/1.0 302 Found', $response); + $this->assertRegExp('#^HTTP/1\.0#', $response); } } From 7f713ace56a57c9ca15b5084e498e13719795a69 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 2 Apr 2015 22:13:15 +0200 Subject: [PATCH 065/146] Explicitly set supported TLS version for PHP5.6+ --- src/StreamEncryption.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 2a463596..23effa97 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -31,6 +31,10 @@ public function __construct(LoopInterface $loop) // 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 $this->wrapSecure = true; + + if (PHP_VERSION_ID >= 50600) { + $this->method = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + } } public function enable(Stream $stream) From cf29c6c9e12e36148360f8876eb862f0065a2f1c Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 12 Apr 2015 13:27:07 +0200 Subject: [PATCH 066/146] Using defined instead of a version check as suggested by @cboden --- src/StreamEncryption.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 23effa97..f7de6f5d 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -32,8 +32,14 @@ public function __construct(LoopInterface $loop) // get an empty string back because the buffer indicator could be wrong $this->wrapSecure = true; - if (PHP_VERSION_ID >= 50600) { - $this->method = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + if (defined('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; } } From a12dc4ecd25d755a3b49f007ba09032268e637b2 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 15 Apr 2015 21:09:37 +0200 Subject: [PATCH 067/146] Test against PHP7 --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8fc9fb52..c30e9349 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,16 @@ php: - 5.4 - 5.5 - 5.6 + - 7 - hhvm + - hhvm-nightly matrix: allow_failures: + - php: 7 - php: hhvm + - php: hhvm-nightly + fast_finish: true before_script: - composer install --dev --prefer-source From 6d765b08e36ea9e8f8b0765e07f51612e857621d Mon Sep 17 00:00:00 2001 From: Alex Mace Date: Thu, 7 May 2015 17:00:07 +0100 Subject: [PATCH 068/146] 65137 was fixed in PHP 5.6.8, so this code now fails on that version --- src/StreamEncryption.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index f7de6f5d..38918bb2 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -30,7 +30,9 @@ public function __construct(LoopInterface $loop) // 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 - $this->wrapSecure = true; + if (version_compare(PHP_VERSION, '5.6.8', '<')) { + $this->wrapSecure = true; + } if (defined('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT')) { $this->method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT; From 98b609589281b2cc717c04e87b1efd9a626001b5 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 13 May 2015 10:09:13 -0400 Subject: [PATCH 069/146] spaces > tabs --- src/StreamEncryption.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 38918bb2..f3613dc3 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -30,9 +30,9 @@ public function __construct(LoopInterface $loop) // 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 (version_compare(PHP_VERSION, '5.6.8', '<')) { + $this->wrapSecure = true; + } if (defined('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT')) { $this->method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT; From fa736cfad83dfff9b277db2e41a1176750ac2817 Mon Sep 17 00:00:00 2001 From: ThijsFeryn Date: Tue, 2 Jun 2015 15:43:27 +0200 Subject: [PATCH 070/146] Adding $loop->run(); to the documentation --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e6358fdf..b5aa5ba0 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ $connector->create('www.google.com', 80)->then(function (React\Stream\Stream $st $stream->write('...'); $stream->close(); }); + +$loop->run(); ``` ### Async SSL/TLS connections @@ -61,4 +63,6 @@ $secureConnector->create('www.google.com', 443)->then(function (React\Stream\Str $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); + +$loop->run(); ``` From 2536462fb140082cea149d6f0afa922bb8defd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 15 Jul 2015 03:14:16 +0200 Subject: [PATCH 071/146] Add support for Unix domain sockets (UDS) --- README.md | 15 ++++++++++++ src/UnixConnector.php | 36 +++++++++++++++++++++++++++++ tests/UnixConnectorTest.php | 46 +++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/UnixConnector.php create mode 100644 tests/UnixConnectorTest.php diff --git a/README.md b/README.md index b5aa5ba0..ac06dbf0 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,18 @@ $secureConnector->create('www.google.com', 443)->then(function (React\Stream\Str $loop->run(); ``` + +### Unix domain sockets + +Similarly, the `UnixConnector` class can be used to connect to Unix domain socket (UDS) +paths like this: + +```php +$connector = new React\SocketClient\UnixConnector($loop); + +$connector->create('/tmp/demo.sock')->then(function (React\Stream\Stream $stream) { + $stream->write("HELLO\n"); +}); + +$loop->run(); +``` diff --git a/src/UnixConnector.php b/src/UnixConnector.php new file mode 100644 index 00000000..e12e7efa --- /dev/null +++ b/src/UnixConnector.php @@ -0,0 +1,36 @@ +loop = $loop; + } + + public function create($path, $unusedPort = 0) + { + $resource = @stream_socket_client('unix://' . $path, $errno, $errstr, 1.0); + + if (!$resource) { + return Promise\reject(new RuntimeException('Unable to connect to unix domain socket "' . $path . '": ' . $errstr, $errno)); + } + + return Promise\resolve(new Stream($resource, $this->loop)); + } +} diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php new file mode 100644 index 00000000..4070aede --- /dev/null +++ b/tests/UnixConnectorTest.php @@ -0,0 +1,46 @@ +loop = $this->getMock('React\EventLoop\LoopInterface'); + $this->connector = new UnixConnector($this->loop); + } + + public function testInvalid() + { + $promise = $this->connector->create('google.com', 80); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testValid() + { + // random unix domain socket path + $path = sys_get_temp_dir() . '/test' . uniqid() . '.sock'; + + // temporarily create unix domain socket server to connect to + $server = stream_socket_server('unix://' . $path, $errno, $errstr); + + // skip test if we can not create a test server (Windows etc.) + if (!$server) { + $this->markTestSkipped('Unable to create socket "' . $path . '": ' . $errstr . '(' . $errno .')'); + return; + } + + // tests succeeds if we get notified of successful connection + $promise = $this->connector->create($path, 0); + $promise->then($this->expectCallableOnce()); + + // clean up server + fclose($server); + unlink($path); + } +} From 78d8d3df1013adae837fdc148c1c6a69c1d9ed27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 6 Sep 2015 15:19:35 +0200 Subject: [PATCH 072/146] Move SSL/TLS context options to SecureConnector * TLS endpoints do not have to match connection endpoints (proxy setup) * More SOLID design, better separation of concerns --- src/Connector.php | 16 ++++------------ src/SecureConnector.php | 12 ++++++++++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Connector.php b/src/Connector.php index d132a74c..eed8d91b 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -23,25 +23,17 @@ public function create($host, $port) { return $this ->resolveHostname($host) - ->then(function ($address) use ($port, $host) { - return $this->createSocketForAddress($address, $port, $host); + ->then(function ($address) use ($port) { + return $this->createSocketForAddress($address, $port); }); } - public function createSocketForAddress($address, $port, $hostName = null) + public function createSocketForAddress($address, $port) { $url = $this->getSocketUrl($address, $port); - $contextOpts = array(); - if ($hostName !== null) { - $contextOpts['ssl']['SNI_enabled'] = true; - $contextOpts['ssl']['SNI_server_name'] = $hostName; - $contextOpts['ssl']['peer_name'] = $hostName; - } - $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT; - $context = stream_context_create($contextOpts); - $socket = stream_socket_client($url, $errno, $errstr, 0, $flags, $context); + $socket = stream_socket_client($url, $errno, $errstr, 0, $flags); if (!$socket) { return Promise\reject(new \RuntimeException( diff --git a/src/SecureConnector.php b/src/SecureConnector.php index fed2da28..f80c11b6 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -18,8 +18,16 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop) public function create($host, $port) { - return $this->connector->create($host, $port)->then(function (Stream $stream) { - // (unencrypted) connection succeeded => try to enable encryption + return $this->connector->create($host, $port)->then(function (Stream $stream) use ($host) { + // (unencrypted) TCP/IP connection succeeded + + // set required SSL/TLS context options + $resource = $stream->stream; + stream_context_set_option($resource, 'ssl', 'SNI_enabled', true); + stream_context_set_option($resource, 'ssl', 'SNI_server_name', $host); + stream_context_set_option($resource, 'ssl', 'peer_name', $host); + + // try to enable encryption return $this->streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { // establishing encryption failed => close invalid connection and return error $stream->close(); From 3ca814d7e03e2a5bffeaa5773423107957a8aece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 24 Sep 2015 00:40:04 +0200 Subject: [PATCH 073/146] Prepare v0.4.4 release --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67752396..1bb8ef5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ # Changelog +## 0.4.4 (2015-09-23) + +* Feature: Add support for Unix domain sockets (UDS) (#41 by @clue) +* Bugfix: Explicitly set supported TLS versions for PHP 5.6+ (#31 by @WyriHaximus) +* Bugfix: Ignore SSL non-draining buffer workaround for PHP 5.6.8+ (#33 by @alexmace) + ## 0.4.3 (2015-03-20) -* Bugfix: Set peer name to hostname to correct security concern in PHP 5.6 (@WyyriHaximus) +* Bugfix: Set peer name to hostname to correct security concern in PHP 5.6 (@WyriHaximus) * Bugfix: Always wrap secure to pull buffer due to regression in PHP * Bugfix: SecureStream extends Stream to match documentation preventing BC (@clue) From 1a4b1c671f35cbb760fcc29dfe2bcdc4592fbc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 15 Jul 2015 03:11:26 +0200 Subject: [PATCH 074/146] Split Connector into TcpConnector and DnsConnector --- README.md | 53 ++++++++++++++----- src/DnsConnector.php | 41 ++++++++++++++ src/{Connector.php => TcpConnector.php} | 29 ++-------- tests/IntegrationTest.php | 10 +++- ...ConnectorTest.php => TcpConnectorTest.php} | 27 +++------- 5 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 src/DnsConnector.php rename src/{Connector.php => TcpConnector.php} (71%) rename tests/{ConnectorTest.php => TcpConnectorTest.php} (75%) diff --git a/README.md b/README.md index ac06dbf0..79484ddf 100644 --- a/README.md +++ b/README.md @@ -22,28 +22,52 @@ order to complete: ## Usage In order to use this project, you'll need the following react boilerplate code -to initialize the main loop and select your DNS server if you have not already -set it up anyway. +to initialize the main loop. ```php $loop = React\EventLoop\Factory::create(); - -$dnsResolverFactory = new React\Dns\Resolver\Factory(); -$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); ``` ### Async TCP/IP connections -The `React\SocketClient\Connector` provides a single promise-based -`create($host, $ip)` method which resolves as soon as the connection +The `React\SocketClient\TcpConnector` provides a single promise-based +`create($ip, $port)` method which resolves as soon as the connection succeeds or fails. ```php -$connector = new React\SocketClient\Connector($loop, $dns); +$tcpConnector = new React\SocketClient\TcpConnector($loop); + +$tcpConnector->create('127.0.0.1', 80)->then(function (React\Stream\Stream $stream) { + $stream->write('...'); + $stream->end(); +}); + +$loop->run(); +``` + +Note that this class only allows you to connect to IP/port combinations. +If you want to connect to hostname/port combinations, see also the following chapter. + +### DNS resolution + +The `DnsConnector` class decorates a given `TcpConnector` instance by first +looking up the given domain name and then establishing the underlying TCP/IP +connection to the resolved IP address. + +It provides the same promise-based `create($host, $port)` method which resolves with +a `Stream` instance that can be used just like above. + +Make sure to set up your DNS resolver and underlying TCP connector like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); -$connector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) { +$dnsConnector = new React\SocketClient\DnsConnector($tcpConnector, $dns); + +$dnsConnector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) { $stream->write('...'); - $stream->close(); + $stream->end(); }); $loop->run(); @@ -52,12 +76,13 @@ $loop->run(); ### Async SSL/TLS connections The `SecureConnector` class decorates a given `Connector` instance by enabling -SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. It provides -the same promise- based `create($host, $ip)` method which resolves with -a `Stream` instance that can be used just like any non-encrypted stream. +SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. + +It provides the same promise- based `create($host, $port)` method which resolves with +a `Stream` instance that can be used just like any non-encrypted stream: ```php -$secureConnector = new React\SocketClient\SecureConnector($connector, $loop); +$secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop); $secureConnector->create('www.google.com', 443)->then(function (React\Stream\Stream $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); diff --git a/src/DnsConnector.php b/src/DnsConnector.php new file mode 100644 index 00000000..a151b26e --- /dev/null +++ b/src/DnsConnector.php @@ -0,0 +1,41 @@ +connector = $connector; + $this->resolver = $resolver; + } + + public function create($host, $port) + { + $connector = $this->connector; + + return $this + ->resolveHostname($host) + ->then(function ($address) use ($connector, $port) { + return $connector->create($address, $port); + }); + } + + protected function resolveHostname($host) + { + if (false !== filter_var($host, FILTER_VALIDATE_IP)) { + return Promise\resolve($host); + } + + return $this->resolver->resolve($host); + } +} diff --git a/src/Connector.php b/src/TcpConnector.php similarity index 71% rename from src/Connector.php rename to src/TcpConnector.php index eed8d91b..7d76d83b 100644 --- a/src/Connector.php +++ b/src/TcpConnector.php @@ -8,32 +8,20 @@ use React\Promise; use React\Promise\Deferred; -class Connector implements ConnectorInterface +class TcpConnector implements ConnectorInterface { private $loop; - private $resolver; - public function __construct(LoopInterface $loop, Resolver $resolver) + public function __construct(LoopInterface $loop) { $this->loop = $loop; - $this->resolver = $resolver; } - public function create($host, $port) - { - return $this - ->resolveHostname($host) - ->then(function ($address) use ($port) { - return $this->createSocketForAddress($address, $port); - }); - } - - public function createSocketForAddress($address, $port) + public function create($address, $port) { $url = $this->getSocketUrl($address, $port); - $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT; - $socket = stream_socket_client($url, $errno, $errstr, 0, $flags); + $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if (!$socket) { return Promise\reject(new \RuntimeException( @@ -91,13 +79,4 @@ protected function getSocketUrl($host, $port) } return sprintf('tcp://%s:%s', $host, $port); } - - protected function resolveHostname($host) - { - if (false !== filter_var($host, FILTER_VALIDATE_IP)) { - return Promise\resolve($host); - } - - return $this->resolver->resolve($host); - } } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 889a8cb7..f58385a4 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -8,6 +8,8 @@ use React\SocketClient\Connector; use React\SocketClient\SecureConnector; use React\Stream\BufferedSink; +use React\SocketClient\TcpConnector; +use React\SocketClient\DnsConnector; class IntegrationTest extends TestCase { @@ -16,13 +18,15 @@ public function gettingStuffFromGoogleShouldWork() { $loop = new StreamSelectLoop(); + $connector = new TcpConnector($loop); + $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); + $connector = new DnsConnector($connector, $dns); $connected = false; $response = null; - $connector = new Connector($loop, $dns); $connector->create('google.com', 80) ->then(function ($conn) use (&$connected) { $connected = true; @@ -44,6 +48,8 @@ public function gettingEncryptedStuffFromGoogleShouldWork() { $loop = new StreamSelectLoop(); + $connector = new TcpConnector($loop); + $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); @@ -51,7 +57,7 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $response = null; $secureConnector = new SecureConnector( - new Connector($loop, $dns), + new DnsConnector($connector, $dns), $loop ); $secureConnector->create('google.com', 443) diff --git a/tests/ConnectorTest.php b/tests/TcpConnectorTest.php similarity index 75% rename from tests/ConnectorTest.php rename to tests/TcpConnectorTest.php index a34b7aaa..73366032 100644 --- a/tests/ConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -4,18 +4,16 @@ use React\EventLoop\StreamSelectLoop; use React\Socket\Server; -use React\SocketClient\Connector; +use React\SocketClient\TcpConnector; -class ConnectorTest extends TestCase +class TcpConnectorTest extends TestCase { /** @test */ public function connectionToEmptyPortShouldFail() { $loop = new StreamSelectLoop(); - $dns = $this->createResolverMock(); - - $connector = new Connector($loop, $dns); + $connector = new TcpConnector($loop); $connector->create('127.0.0.1', 9999) ->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -36,9 +34,7 @@ public function connectionToTcpServerShouldSucceed() }); $server->listen(9999); - $dns = $this->createResolverMock(); - - $connector = new Connector($loop, $dns); + $connector = new TcpConnector($loop); $connector->create('127.0.0.1', 9999) ->then(function ($stream) use (&$capturedStream) { $capturedStream = $stream; @@ -55,9 +51,7 @@ public function connectionToEmptyIp6PortShouldFail() { $loop = new StreamSelectLoop(); - $dns = $this->createResolverMock(); - - $connector = new Connector($loop, $dns); + $connector = new TcpConnector($loop); $connector ->create('::1', 9999) ->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -77,9 +71,7 @@ public function connectionToIp6TcpServerShouldSucceed() $server->on('connection', array($server, 'shutdown')); $server->listen(9999, '::1'); - $dns = $this->createResolverMock(); - - $connector = new Connector($loop, $dns); + $connector = new TcpConnector($loop); $connector ->create('::1', 9999) ->then(function ($stream) use (&$capturedStream) { @@ -91,11 +83,4 @@ public function connectionToIp6TcpServerShouldSucceed() $this->assertInstanceOf('React\Stream\Stream', $capturedStream); } - - private function createResolverMock() - { - return $this->getMockBuilder('React\Dns\Resolver\Resolver') - ->disableOriginalConstructor() - ->getMock(); - } } From 9fe3407dea00774ed134129cabf22c95c22302bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Sep 2015 00:56:51 +0200 Subject: [PATCH 075/146] Reject hostnames for TcpConnector and improve test coverage --- src/TcpConnector.php | 18 +++++++++------ tests/DnsConnectorTest.php | 45 ++++++++++++++++++++++++++++++++++++++ tests/TcpConnectorTest.php | 12 ++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 tests/DnsConnectorTest.php diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 7d76d83b..58129a2d 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -17,15 +17,19 @@ public function __construct(LoopInterface $loop) $this->loop = $loop; } - public function create($address, $port) + public function create($ip, $port) { - $url = $this->getSocketUrl($address, $port); + if (false === filter_var($ip, FILTER_VALIDATE_IP)) { + return Promise\reject(new \InvalidArgumentException('Given parameter "' . $ip . '" is not a valid IP')); + } + + $url = $this->getSocketUrl($ip, $port); $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if (!$socket) { return Promise\reject(new \RuntimeException( - sprintf("connection to %s:%d failed: %s", $address, $port, $errstr), + sprintf("connection to %s:%d failed: %s", $ip, $port, $errstr), $errno )); } @@ -71,12 +75,12 @@ public function handleConnectedSocket($socket) return new Stream($socket, $this->loop); } - protected function getSocketUrl($host, $port) + protected function getSocketUrl($ip, $port) { - if (strpos($host, ':') !== false) { + if (strpos($ip, ':') !== false) { // enclose IPv6 addresses in square brackets before appending port - $host = '[' . $host . ']'; + $ip = '[' . $ip . ']'; } - return sprintf('tcp://%s:%s', $host, $port); + return sprintf('tcp://%s:%s', $ip, $port); } } diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php new file mode 100644 index 00000000..f8ab96ee --- /dev/null +++ b/tests/DnsConnectorTest.php @@ -0,0 +1,45 @@ +tcp = $this->getMock('React\SocketClient\ConnectorInterface'); + $this->resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); + + $this->connector = new DnsConnector($this->tcp, $this->resolver); + } + + public function testPassByResolverIfGivenIp() + { + $this->resolver->expects($this->never())->method('resolve'); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('127.0.0.1'), $this->equalTo(80)); + + $this->connector->create('127.0.0.1', 80); + } + + public function testPassThroughResolverIfGivenHost() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80)); + + $this->connector->create('google.com', 80); + } + + public function testSkipConnectionIfDnsFails() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->never())->method('create'); + + $this->connector->create('example.invalid', 80); + } +} diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 73366032..b949c60a 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -83,4 +83,16 @@ public function connectionToIp6TcpServerShouldSucceed() $this->assertInstanceOf('React\Stream\Stream', $capturedStream); } + + /** @test */ + public function connectionToHostnameShouldFailImmediately() + { + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $connector = new TcpConnector($loop); + $connector->create('www.google.com', 80)->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } } From b46865449b1f8c682ab9ac99e94620abf0b7a6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Sep 2015 21:05:01 +0200 Subject: [PATCH 076/146] Add legacy Connector as BC layer --- README.md | 10 ++++++++++ src/Connector.php | 24 ++++++++++++++++++++++++ tests/IntegrationTest.php | 10 ++-------- 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 src/Connector.php diff --git a/README.md b/README.md index 79484ddf..aa54e34d 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,16 @@ $dnsConnector->create('www.google.com', 80)->then(function (React\Stream\Stream $loop->run(); ``` +The legacy `Connector` class can be used for backwards-compatiblity reasons. +It works very much like the newer `DnsConnector` but instead has to be +set up like this: + +```php +$connector = new React\SocketClient\Connector($loop, $dns); + +$connector->create('www.google.com', 80)->then($callback); +``` + ### Async SSL/TLS connections The `SecureConnector` class decorates a given `Connector` instance by enabling diff --git a/src/Connector.php b/src/Connector.php new file mode 100644 index 00000000..6cf991c0 --- /dev/null +++ b/src/Connector.php @@ -0,0 +1,24 @@ +connector = new DnsConnector(new TcpConnector($loop), $resolver); + } + + public function create($host, $port) + { + return $this->connector->create($host, $port); + } +} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index f58385a4..a2183b40 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -8,8 +8,6 @@ use React\SocketClient\Connector; use React\SocketClient\SecureConnector; use React\Stream\BufferedSink; -use React\SocketClient\TcpConnector; -use React\SocketClient\DnsConnector; class IntegrationTest extends TestCase { @@ -18,11 +16,9 @@ public function gettingStuffFromGoogleShouldWork() { $loop = new StreamSelectLoop(); - $connector = new TcpConnector($loop); - $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); - $connector = new DnsConnector($connector, $dns); + $connector = new Connector($loop, $dns); $connected = false; $response = null; @@ -48,8 +44,6 @@ public function gettingEncryptedStuffFromGoogleShouldWork() { $loop = new StreamSelectLoop(); - $connector = new TcpConnector($loop); - $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); @@ -57,7 +51,7 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $response = null; $secureConnector = new SecureConnector( - new DnsConnector($connector, $dns), + new Connector($loop, $dns), $loop ); $secureConnector->create('google.com', 443) From 051638960b57bf0b243e25b14de8c7f5fb5a130c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 19 Nov 2015 00:10:39 +0100 Subject: [PATCH 077/146] Mark internals as such in order to avoid a future BC break --- src/DnsConnector.php | 2 +- src/TcpConnector.php | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/DnsConnector.php b/src/DnsConnector.php index a151b26e..66119954 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -30,7 +30,7 @@ public function create($host, $port) }); } - protected function resolveHostname($host) + private function resolveHostname($host) { if (false !== filter_var($host, FILTER_VALIDATE_IP)) { return Promise\resolve($host); diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 58129a2d..9526a625 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -44,7 +44,7 @@ public function create($ip, $port) ->then(array($this, 'handleConnectedSocket')); } - protected function waitForStreamOnce($stream) + private function waitForStreamOnce($stream) { $deferred = new Deferred(); @@ -59,6 +59,7 @@ protected function waitForStreamOnce($stream) return $deferred->promise(); } + /** @internal */ public function checkConnectedSocket($socket) { // The following hack looks like the only way to @@ -70,12 +71,13 @@ public function checkConnectedSocket($socket) return Promise\resolve($socket); } + /** @internal */ public function handleConnectedSocket($socket) { return new Stream($socket, $this->loop); } - protected function getSocketUrl($ip, $port) + private function getSocketUrl($ip, $port) { if (strpos($ip, ':') !== false) { // enclose IPv6 addresses in square brackets before appending port From 0f07289276d769d1ca94101246fa936b228733c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 21 Nov 2015 00:54:38 +0100 Subject: [PATCH 078/146] Improve exception error message --- src/TcpConnector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 9526a625..ddd5bd31 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -29,7 +29,7 @@ public function create($ip, $port) if (!$socket) { return Promise\reject(new \RuntimeException( - sprintf("connection to %s:%d failed: %s", $ip, $port, $errstr), + sprintf("Connection to %s:%d failed: %s", $ip, $port, $errstr), $errno )); } From 7b4cfdcb93dc31344076c850975f09add4edb191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 23 Sep 2015 00:59:45 +0200 Subject: [PATCH 079/146] Test creating invalid socket address and improve test coverage --- src/TcpConnector.php | 4 ++-- tests/TcpConnectorTest.php | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/TcpConnector.php b/src/TcpConnector.php index ddd5bd31..3fb84756 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -25,9 +25,9 @@ public function create($ip, $port) $url = $this->getSocketUrl($ip, $port); - $socket = stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + $socket = @stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); - if (!$socket) { + if (false === $socket) { return Promise\reject(new \RuntimeException( sprintf("Connection to %s:%d failed: %s", $ip, $port, $errstr), $errno diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index b949c60a..172102ee 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -95,4 +95,16 @@ public function connectionToHostnameShouldFailImmediately() $this->expectCallableOnce() ); } + + /** @test */ + public function connectionToInvalidAddressShouldFailImmediately() + { + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $connector = new TcpConnector($loop); + $connector->create('255.255.255.255', 12345678)->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } } From a9499e72d6afa881b4f791899070ce89f4c1209f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 13 Oct 2015 12:42:14 +0200 Subject: [PATCH 080/146] Close stream resource if connection fails --- src/TcpConnector.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/TcpConnector.php b/src/TcpConnector.php index ddd5bd31..62d4b5f4 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -65,6 +65,8 @@ public function checkConnectedSocket($socket) // The following hack looks like the only way to // detect connection refused errors with PHP's stream sockets. if (false === stream_socket_get_name($socket, true)) { + fclose($socket); + return Promise\reject(new ConnectionException('Connection refused')); } From 818fe1c0d97b264cb780f201902707bb7aec8613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 22 Nov 2015 20:24:26 +0100 Subject: [PATCH 081/146] First class support for PHP7 and HHVM --- .travis.yml | 12 +++--------- src/SecureConnector.php | 5 +++++ tests/IntegrationTest.php | 4 ++++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index c30e9349..acb32626 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,17 +6,11 @@ php: - 5.6 - 7 - hhvm - - hhvm-nightly -matrix: - allow_failures: - - php: 7 - - php: hhvm - - php: hhvm-nightly - fast_finish: true +sudo: false -before_script: - - composer install --dev --prefer-source +install: + - composer install --prefer-source --no-interaction script: - phpunit --coverage-text diff --git a/src/SecureConnector.php b/src/SecureConnector.php index f80c11b6..7790a07f 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -4,6 +4,7 @@ use React\EventLoop\LoopInterface; use React\Stream\Stream; +use React\Promise; class SecureConnector implements ConnectorInterface { @@ -18,6 +19,10 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop) public function create($host, $port) { + if (!function_exists('stream_socket_enable_crypto')) { + return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); + } + return $this->connector->create($host, $port)->then(function (Stream $stream) use ($host) { // (unencrypted) TCP/IP connection succeeded diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index a2183b40..f8ca5d78 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -42,6 +42,10 @@ public function gettingStuffFromGoogleShouldWork() /** @test */ public function gettingEncryptedStuffFromGoogleShouldWork() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $loop = new StreamSelectLoop(); $factory = new Factory(); From 7052fe2026790274aa9f11387075f0860e4583f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 20 Nov 2015 02:18:01 +0100 Subject: [PATCH 082/146] Add socket and SSL/TLS context options to connectors --- README.md | 21 ++++++++++++++++++++ composer.json | 3 +++ src/SecureConnector.php | 17 ++++++++++------ src/TcpConnector.php | 13 ++++++++++-- tests/IntegrationTest.php | 42 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aa54e34d..699f6b95 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,16 @@ $tcpConnector->create('127.0.0.1', 80)->then(function (React\Stream\Stream $stre $loop->run(); ``` +You can optionally pass additional +[socket context options](http://php.net/manual/en/context.socket.php) +to the constructor like this: + +```php +$tcpConnector = new React\SocketClient\TcpConnector($loop, array( + 'bindto' => '192.168.0.1:0' +)); +``` + Note that this class only allows you to connect to IP/port combinations. If you want to connect to hostname/port combinations, see also the following chapter. @@ -102,6 +112,17 @@ $secureConnector->create('www.google.com', 443)->then(function (React\Stream\Str $loop->run(); ``` +You can optionally pass additional +[SSL context options](http://php.net/manual/en/context.ssl.php) +to the constructor like this: + +```php +$secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop, array( + 'verify_peer' => false, + 'verify_peer_name' => false +)); +``` + ### Unix domain sockets Similarly, the `UnixConnector` class can be used to connect to Unix domain socket (UDS) diff --git a/composer.json b/composer.json index 467cbef3..8e5edf41 100644 --- a/composer.json +++ b/composer.json @@ -19,5 +19,8 @@ "branch-alias": { "dev-master": "0.4-dev" } + }, + "require-dev": { + "clue/block-react": "~1.0" } } diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 7790a07f..47257f52 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -10,11 +10,13 @@ class SecureConnector implements ConnectorInterface { private $connector; private $streamEncryption; + private $context; - public function __construct(ConnectorInterface $connector, LoopInterface $loop) + public function __construct(ConnectorInterface $connector, LoopInterface $loop, array $context = array()) { $this->connector = $connector; $this->streamEncryption = new StreamEncryption($loop); + $this->context = $context; } public function create($host, $port) @@ -23,14 +25,17 @@ public function create($host, $port) return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); } - return $this->connector->create($host, $port)->then(function (Stream $stream) use ($host) { + $context = $this->context + array( + 'SNI_enabled' => true, + 'SNI_server_name' => $host, + 'peer_name' => $host + ); + + return $this->connector->create($host, $port)->then(function (Stream $stream) use ($context) { // (unencrypted) TCP/IP connection succeeded // set required SSL/TLS context options - $resource = $stream->stream; - stream_context_set_option($resource, 'ssl', 'SNI_enabled', true); - stream_context_set_option($resource, 'ssl', 'SNI_server_name', $host); - stream_context_set_option($resource, 'ssl', 'peer_name', $host); + stream_context_set_option($stream->stream, array('ssl' => $context)); // try to enable encryption return $this->streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { diff --git a/src/TcpConnector.php b/src/TcpConnector.php index ec112353..70a283c0 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -11,10 +11,12 @@ class TcpConnector implements ConnectorInterface { private $loop; + private $context; - public function __construct(LoopInterface $loop) + public function __construct(LoopInterface $loop, array $context = array()) { $this->loop = $loop; + $this->context = $context; } public function create($ip, $port) @@ -25,7 +27,14 @@ public function create($ip, $port) $url = $this->getSocketUrl($ip, $port); - $socket = @stream_socket_client($url, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + $socket = @stream_socket_client( + $url, + $errno, + $errstr, + 0, + STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT, + stream_context_create(array('socket' => $this->context)) + ); if (false === $socket) { return Promise\reject(new \RuntimeException( diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index f8ca5d78..319c9c23 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -8,6 +8,7 @@ use React\SocketClient\Connector; use React\SocketClient\SecureConnector; use React\Stream\BufferedSink; +use Clue\React\Block; class IntegrationTest extends TestCase { @@ -73,4 +74,45 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $this->assertTrue($connected); $this->assertRegExp('#^HTTP/1\.0#', $response); } + + /** @test */ + public function testSelfSignedRejectsIfVerificationIsEnabled() + { + $loop = new StreamSelectLoop(); + + $factory = new Factory(); + $dns = $factory->create('8.8.8.8', $loop); + + + $secureConnector = new SecureConnector( + new Connector($loop, $dns), + $loop, + array( + 'verify_peer' => true + ) + ); + + $this->setExpectedException('RuntimeException'); + Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop); + } + + /** @test */ + public function testSelfSignedResolvesIfVerificationIsDisabled() + { + $loop = new StreamSelectLoop(); + + $factory = new Factory(); + $dns = $factory->create('8.8.8.8', $loop); + + $secureConnector = new SecureConnector( + new Connector($loop, $dns), + $loop, + array( + 'verify_peer' => false + ) + ); + + $conn = Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop); + $conn->close(); + } } From e3d41bccb3791dad44ac407650e3b8e77f94a551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 23 Feb 2016 01:39:58 +0100 Subject: [PATCH 083/146] Work around HHVM not being able to set context options as an array --- src/SecureConnector.php | 4 +++- tests/IntegrationTest.php | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 47257f52..4ed38009 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -35,7 +35,9 @@ public function create($host, $port) // (unencrypted) TCP/IP connection succeeded // set required SSL/TLS context options - stream_context_set_option($stream->stream, array('ssl' => $context)); + foreach ($context as $name => $value) { + stream_context_set_option($stream->stream, 'ssl', $name, $value); + } // try to enable encryption return $this->streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 319c9c23..f39b59e8 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -78,6 +78,10 @@ public function gettingEncryptedStuffFromGoogleShouldWork() /** @test */ public function testSelfSignedRejectsIfVerificationIsEnabled() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $loop = new StreamSelectLoop(); $factory = new Factory(); @@ -99,6 +103,10 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() /** @test */ public function testSelfSignedResolvesIfVerificationIsDisabled() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $loop = new StreamSelectLoop(); $factory = new Factory(); From 35524f63c2d697c22d489858d3171da993288a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 14 Dec 2015 13:14:39 +0100 Subject: [PATCH 084/146] PHP 5.6+ uses new SSL/TLS context options --- src/SecureConnector.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 4ed38009..34176975 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -27,10 +27,17 @@ public function create($host, $port) $context = $this->context + array( 'SNI_enabled' => true, - 'SNI_server_name' => $host, 'peer_name' => $host ); + // legacy PHP < 5.6 ignores peer_name and requires legacy context options instead + if (PHP_VERSION_ID < 50600) { + $context += array( + 'SNI_server_name' => $host, + 'CN_match' => $host + ); + } + return $this->connector->create($host, $port)->then(function (Stream $stream) use ($context) { // (unencrypted) TCP/IP connection succeeded From 1279cb3d806830c5f22aa7314e26945ac7d00a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 13 Oct 2015 14:33:02 +0200 Subject: [PATCH 085/146] Compatiblity with legacy versions --- .travis.yml | 1 + composer.json | 10 +++++----- src/SecureConnector.php | 5 +++-- src/SecureStream.php | 24 ++++++++++++------------ src/StreamEncryption.php | 14 +++++++++----- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index acb32626..57ab0985 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: php php: + - 5.3 - 5.4 - 5.5 - 5.6 diff --git a/composer.json b/composer.json index 8e5edf41..1e24ceaf 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,11 @@ "keywords": ["socket"], "license": "MIT", "require": { - "php": ">=5.4.0", - "react/dns": "0.4.*", - "react/event-loop": "0.4.*", - "react/stream": "0.4.*", - "react/promise": "~2.0" + "php": ">=5.3.0", + "react/dns": "0.4.*|0.3.*", + "react/event-loop": "0.4.*|0.3.*", + "react/stream": "0.4.*|0.3.*", + "react/promise": "~2.0|~1.1" }, "autoload": { "psr-4": { diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 34176975..1cdfc838 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -38,7 +38,8 @@ public function create($host, $port) ); } - return $this->connector->create($host, $port)->then(function (Stream $stream) use ($context) { + $encryption = $this->streamEncryption; + return $this->connector->create($host, $port)->then(function (Stream $stream) use ($context, $encryption) { // (unencrypted) TCP/IP connection succeeded // set required SSL/TLS context options @@ -47,7 +48,7 @@ public function create($host, $port) } // try to enable encryption - return $this->streamEncryption->enable($stream)->then(null, function ($error) use ($stream) { + return $encryption->enable($stream)->then(null, function ($error) use ($stream) { // establishing encryption failed => close invalid connection and return error $stream->close(); throw $error; diff --git a/src/SecureStream.php b/src/SecureStream.php index ce6e1da5..5aa879c6 100644 --- a/src/SecureStream.php +++ b/src/SecureStream.php @@ -4,12 +4,11 @@ use Evenement\EventEmitterTrait; use React\EventLoop\LoopInterface; -use React\Stream\DuplexStreamInterface; use React\Stream\WritableStreamInterface; use React\Stream\Stream; use React\Stream\Util; -class SecureStream extends Stream implements DuplexStreamInterface +class SecureStream extends Stream { // use EventEmitterTrait; @@ -22,18 +21,19 @@ public function __construct(Stream $stream, LoopInterface $loop) { $this->stream = $stream->stream; $this->decorating = $stream; $this->loop = $loop; + $that = $this; - $stream->on('error', function($error) { - $this->emit('error', [$error, $this]); + $stream->on('error', function($error) use ($that) { + $that->emit('error', array($error, $that)); }); - $stream->on('end', function() { - $this->emit('end', [$this]); + $stream->on('end', function() use ($that) { + $that->emit('end', array($that)); }); - $stream->on('close', function() { - $this->emit('close', [$this]); + $stream->on('close', function() use ($that) { + $that->emit('close', array($that)); }); - $stream->on('drain', function() { - $this->emit('drain', [$this]); + $stream->on('drain', function() use ($that) { + $that->emit('drain', array($that)); }); $stream->pause(); @@ -45,7 +45,7 @@ public function handleData($stream) { $data = stream_get_contents($stream); - $this->emit('data', [$data, $this]); + $this->emit('data', array($data, $this)); if (!is_resource($stream) || feof($stream)) { $this->end(); @@ -60,7 +60,7 @@ public function pause() public function resume() { if ($this->isReadable()) { - $this->loop->addReadStream($this->decorating->stream, [$this, 'handleData']); + $this->loop->addReadStream($this->decorating->stream, array($this, 'handleData')); } } diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index f3613dc3..e15d3693 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -71,16 +71,20 @@ public function toggle(Stream $stream, $toggle) // get actual stream socket from stream instance $socket = $stream->stream; - $toggleCrypto = function () use ($socket, $deferred, $toggle) { - $this->toggleCrypto($socket, $deferred, $toggle); + $that = $this; + $toggleCrypto = function () use ($socket, $deferred, $toggle, $that) { + $that->toggleCrypto($socket, $deferred, $toggle); }; $this->loop->addReadStream($socket, $toggleCrypto); $toggleCrypto(); - return $deferred->promise()->then(function () use ($stream, $toggle) { - if ($toggle && $this->wrapSecure) { - return new SecureStream($stream, $this->loop); + $wrap = $this->wrapSecure && $toggle; + $loop = $this->loop; + + return $deferred->promise()->then(function () use ($stream, $wrap, $loop) { + if ($wrap) { + return new SecureStream($stream, $loop); } $stream->resume(); From 36126a0bcf273056fd15e4c9d386515b4a1b8e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 22 Nov 2015 19:12:56 +0100 Subject: [PATCH 086/146] Simplify tests by blocking --- tests/IntegrationTest.php | 46 +++++++++++--------------------------- tests/TcpConnectorTest.php | 28 ++++++++--------------- 2 files changed, 22 insertions(+), 52 deletions(-) diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index f39b59e8..9e16281b 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -21,22 +21,12 @@ public function gettingStuffFromGoogleShouldWork() $dns = $factory->create('8.8.8.8', $loop); $connector = new Connector($loop, $dns); - $connected = false; - $response = null; - - $connector->create('google.com', 80) - ->then(function ($conn) use (&$connected) { - $connected = true; - $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); - }) - ->then(function ($data) use (&$response) { - $response = $data; - }); - - $loop->run(); - - $this->assertTrue($connected); + $conn = Block\await($connector->create('google.com', 80), $loop); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = Block\await(BufferedSink::createPromise($conn), $loop); + $this->assertRegExp('#^HTTP/1\.0#', $response); } @@ -52,26 +42,17 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); - $connected = false; - $response = null; - $secureConnector = new SecureConnector( new Connector($loop, $dns), $loop ); - $secureConnector->create('google.com', 443) - ->then(function ($conn) use (&$connected) { - $connected = true; - $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); - }) - ->then(function ($data) use (&$response) { - $response = $data; - }); - - $loop->run(); - - $this->assertTrue($connected); + + $conn = Block\await($secureConnector->create('google.com', 443), $loop); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = Block\await(BufferedSink::createPromise($conn), $loop); + $this->assertRegExp('#^HTTP/1\.0#', $response); } @@ -87,7 +68,6 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); - $secureConnector = new SecureConnector( new Connector($loop, $dns), $loop, diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 172102ee..2fd700e0 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -5,6 +5,7 @@ use React\EventLoop\StreamSelectLoop; use React\Socket\Server; use React\SocketClient\TcpConnector; +use Clue\React\Block; class TcpConnectorTest extends TestCase { @@ -23,8 +24,6 @@ public function connectionToEmptyPortShouldFail() /** @test */ public function connectionToTcpServerShouldSucceed() { - $capturedStream = null; - $loop = new StreamSelectLoop(); $server = new Server($loop); @@ -35,15 +34,12 @@ public function connectionToTcpServerShouldSucceed() $server->listen(9999); $connector = new TcpConnector($loop); - $connector->create('127.0.0.1', 9999) - ->then(function ($stream) use (&$capturedStream) { - $capturedStream = $stream; - $stream->end(); - }); - $loop->run(); + $stream = Block\await($connector->create('127.0.0.1', 9999), $loop); - $this->assertInstanceOf('React\Stream\Stream', $capturedStream); + $this->assertInstanceOf('React\Stream\Stream', $stream); + + $stream->close(); } /** @test */ @@ -62,8 +58,6 @@ public function connectionToEmptyIp6PortShouldFail() /** @test */ public function connectionToIp6TcpServerShouldSucceed() { - $capturedStream = null; - $loop = new StreamSelectLoop(); $server = new Server($loop); @@ -72,16 +66,12 @@ public function connectionToIp6TcpServerShouldSucceed() $server->listen(9999, '::1'); $connector = new TcpConnector($loop); - $connector - ->create('::1', 9999) - ->then(function ($stream) use (&$capturedStream) { - $capturedStream = $stream; - $stream->end(); - }); - $loop->run(); + $stream = Block\await($connector->create('::1', 9999), $loop); + + $this->assertInstanceOf('React\Stream\Stream', $stream); - $this->assertInstanceOf('React\Stream\Stream', $capturedStream); + $stream->close(); } /** @test */ From a825a7c2d1cccf8c2ab52a9c57d356c29b1e0fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 31 Aug 2014 16:28:49 +0200 Subject: [PATCH 087/146] Add SSL client example --- examples/ssl.php | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 examples/ssl.php diff --git a/examples/ssl.php b/examples/ssl.php new file mode 100644 index 00000000..0236dd52 --- /dev/null +++ b/examples/ssl.php @@ -0,0 +1,53 @@ +on('connection', function (Stream $stream) { + echo 'connected' . PHP_EOL; + + // $stream->pipe($stream); + $stream->on('data', function ($data) use ($stream) { + echo 'server received: ' . $data . PHP_EOL; + $stream->write($data); + }); +}); +$server->listen(6000); + +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$resolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); + +$connector = new Connector($loop, $resolver); +$secureConnector = new SecureConnector($connector, $loop); + +$promise = $secureConnector->create('127.0.0.1', 6001); +//$promise = $connector->create('127.0.0.1', 6000); + +$promise->then( + function (Stream $client) use ($loop) { + $loop->addReadStream(STDIN, function ($fd) use ($client) { + echo 'client send: '; + $m = rtrim(fread($fd, 8192)); + echo $m . PHP_EOL; + $client->write($m); + }); + + //$stdin = new Stream(STDIN, $loop); + //$stdin->pipe($client); + $client->on('data', function ($data) { + echo 'client received: ' . $data . PHP_EOL; + }); + + // send a 10k message once to fill buffer + $client->write(str_repeat('1234567890', 10000)); + }, + 'var_dump' +); + +$loop->run(); From 8022ba50758550e3538bbe4f1f0de785c31616b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 6 Sep 2014 18:02:25 +0200 Subject: [PATCH 088/146] Move example client into a test case --- examples/ssl.php | 53 ------------------------- tests/SecureConnectorTest.php | 74 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 53 deletions(-) delete mode 100644 examples/ssl.php create mode 100644 tests/SecureConnectorTest.php diff --git a/examples/ssl.php b/examples/ssl.php deleted file mode 100644 index 0236dd52..00000000 --- a/examples/ssl.php +++ /dev/null @@ -1,53 +0,0 @@ -on('connection', function (Stream $stream) { - echo 'connected' . PHP_EOL; - - // $stream->pipe($stream); - $stream->on('data', function ($data) use ($stream) { - echo 'server received: ' . $data . PHP_EOL; - $stream->write($data); - }); -}); -$server->listen(6000); - -$dnsResolverFactory = new React\Dns\Resolver\Factory(); -$resolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); - -$connector = new Connector($loop, $resolver); -$secureConnector = new SecureConnector($connector, $loop); - -$promise = $secureConnector->create('127.0.0.1', 6001); -//$promise = $connector->create('127.0.0.1', 6000); - -$promise->then( - function (Stream $client) use ($loop) { - $loop->addReadStream(STDIN, function ($fd) use ($client) { - echo 'client send: '; - $m = rtrim(fread($fd, 8192)); - echo $m . PHP_EOL; - $client->write($m); - }); - - //$stdin = new Stream(STDIN, $loop); - //$stdin->pipe($client); - $client->on('data', function ($data) { - echo 'client received: ' . $data . PHP_EOL; - }); - - // send a 10k message once to fill buffer - $client->write(str_repeat('1234567890', 10000)); - }, - 'var_dump' -); - -$loop->run(); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php new file mode 100644 index 00000000..12873b9d --- /dev/null +++ b/tests/SecureConnectorTest.php @@ -0,0 +1,74 @@ +on('connection', function (Stream $stream) use (&$receivedServer, &$connected) { + $connected++; + + // $stream->pipe($stream); + $stream->on('data', function ($data) use ($stream, &$receivedServer) { + $receivedServer .= $data; + $stream->write($data); + }); + }); + $server->listen(6000); + + $dnsResolverFactory = new DnsFactory(); + $resolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); + + $connector = new Connector($loop, $resolver); + $secureConnector = new SecureConnector($connector, $loop); + + $promise = $secureConnector->create('127.0.0.1', 6001); + //$promise = $connector->create('127.0.0.1', 6000); + $client = Block\await($promise, $loop); + /* @var $client Stream */ + + while (!$connected) { + $loop->tick(); + } + + $client->on('data', function ($data) use (&$receivedClient) { + $receivedClient .= $data; + }); + + $this->assertEquals('', $receivedServer); + $this->assertEquals('', $receivedClient); + + $echo = function ($str) use (&$receivedClient, &$receivedClient, $loop, $client) { + $receivedClient = $receivedServer = ''; + $client->write($str); + while ($receivedClient !== $str) { + $loop->tick(); + } + }; + + $echo('hello'); + + $echo('world'); + + // send a 10k message once to fill buffer (failing!) + //$echo(str_repeat('1234567890', 10000)); + + $echo('again'); + } +} From bab3fd1ab516a86d398004174de42c899e4760e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Sep 2014 12:19:05 +0200 Subject: [PATCH 089/146] Test SecureConnector by installing stunnel on Travis --- .travis.yml | 5 +++++ tests/SecureConnectorTest.php | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 57ab0985..612cfe83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,11 @@ sudo: false install: - composer install --prefer-source --no-interaction + - sudo apt-get install -y openssl stunnel + - openssl genrsa 1024 > stunnel.key + - openssl req -batch -subj '/CN=127.0.0.1' -new -x509 -nodes -sha1 -days 3650 -key stunnel.key > stunnel.cert + - cat stunnel.key stunnel.cert > stunnel.pem + - stunnel -f -p stunnel.pem -d 6001 -r 6000 & script: - phpunit --coverage-text diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 12873b9d..18dffd5a 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -36,6 +36,19 @@ public function testA() $resolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); $connector = new Connector($loop, $resolver); + + // verify server is listening by creating an unencrypted connection once + $promise = $connector->create('127.0.0.1', 6001); + try { + $client = Block\await($promise, $loop); + /* @var $client Stream */ + $client->close(); + } catch (\Exception $e) { + $this->markTestSkipped('stunnel not reachable?'); + } + + $this->assertEquals(0, $connected); + $secureConnector = new SecureConnector($connector, $loop); $promise = $secureConnector->create('127.0.0.1', 6001); @@ -67,7 +80,7 @@ public function testA() $echo('world'); // send a 10k message once to fill buffer (failing!) - //$echo(str_repeat('1234567890', 10000)); + $echo(str_repeat('1234567890', 10000)); $echo('again'); } From 2f49704bc3d29e62ff20446f10952f4c2bd6638e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 9 Sep 2014 16:26:25 +0200 Subject: [PATCH 090/146] Disable peer verification for SecureConnector tests This allows us to test with self-signed certificates across all PHP versions --- tests/SecureConnectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 18dffd5a..5f75e475 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -49,7 +49,7 @@ public function testA() $this->assertEquals(0, $connected); - $secureConnector = new SecureConnector($connector, $loop); + $secureConnector = new SecureConnector($connector, $loop, array('verify_peer' => false)); $promise = $secureConnector->create('127.0.0.1', 6001); //$promise = $connector->create('127.0.0.1', 6000); From a072b05dc0552e59ca6c2afadc20b652b8574aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 9 Mar 2016 00:38:26 +0100 Subject: [PATCH 091/146] Rely on environment variables to run TLS integration test --- .travis.yml | 8 +++- README.md | 21 +++++++++ ...ctorTest.php => SecureIntegrationTest.php} | 45 ++++++++----------- 3 files changed, 46 insertions(+), 28 deletions(-) rename tests/{SecureConnectorTest.php => SecureIntegrationTest.php} (58%) diff --git a/.travis.yml b/.travis.yml index 612cfe83..87f54236 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,13 +10,17 @@ php: sudo: false +env: + - TEST_SECURE=6001 + - TEST_PLAIN=6000 + install: - composer install --prefer-source --no-interaction - sudo apt-get install -y openssl stunnel - openssl genrsa 1024 > stunnel.key - openssl req -batch -subj '/CN=127.0.0.1' -new -x509 -nodes -sha1 -days 3650 -key stunnel.key > stunnel.cert - - cat stunnel.key stunnel.cert > stunnel.pem - - stunnel -f -p stunnel.pem -d 6001 -r 6000 & + - cat stunnel.cert stunnel.key > stunnel.pem + - stunnel -f -p stunnel.pem -d $TEST_SECURE -r $TEST_PLAIN 2>/dev/null & script: - phpunit --coverage-text diff --git a/README.md b/README.md index 699f6b95..a3bf9cfa 100644 --- a/README.md +++ b/README.md @@ -137,3 +137,24 @@ $connector->create('/tmp/demo.sock')->then(function (React\Stream\Stream $stream $loop->run(); ``` + +## Tests + +To run the test suite, you need PHPUnit. Go to the project root and run: + +```bash +$ phpunit +``` + +The test suite also contains some optional integration tests which operate on a +TCP/IP socket server and an optional TLS/SSL proxy in front of it. +The underlying TCP/IP socket server will be started automatically, whereas the +TLS/SSL proxy has to be started and enabled like this: + +```bash +$ stunnel -f -p stunnel.pem -d 6001 -r 6000 & +$ TEST_SECURE=6001 TEST_PLAIN=6000 phpunit +``` + +See also the [Travis configuration](.travis.yml) for details on how to set up +the required certificate file (`stunnel.pem`) if you're unsure. diff --git a/tests/SecureConnectorTest.php b/tests/SecureIntegrationTest.php similarity index 58% rename from tests/SecureConnectorTest.php rename to tests/SecureIntegrationTest.php index 5f75e475..766a937d 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureIntegrationTest.php @@ -4,14 +4,26 @@ use React\EventLoop\Factory as LoopFactory; use React\Socket\Server; -use React\Dns\Resolver\Factory as DnsFactory; -use React\SocketClient\Connector; +use React\SocketClient\TcpConnector; use React\SocketClient\SecureConnector; use React\Stream\Stream; use Clue\React\Block; -class SecureConnectorTest extends TestCase +class SecureIntegrationTest extends TestCase { + private $portSecure; + private $portPlain; + + public function setUp() + { + $this->portSecure = getenv('TEST_SECURE'); + $this->portPlain = getenv('TEST_PLAIN'); + + if ($this->portSecure === false || $this->portPlain === false) { + $this->markTestSkipped('Needs TEST_SECURE=X and TEST_PLAIN=Y environment variables to run, see README.md'); + } + } + public function testA() { $loop = LoopFactory::create(); @@ -30,30 +42,11 @@ public function testA() $stream->write($data); }); }); - $server->listen(6000); - - $dnsResolverFactory = new DnsFactory(); - $resolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); - - $connector = new Connector($loop, $resolver); - - // verify server is listening by creating an unencrypted connection once - $promise = $connector->create('127.0.0.1', 6001); - try { - $client = Block\await($promise, $loop); - /* @var $client Stream */ - $client->close(); - } catch (\Exception $e) { - $this->markTestSkipped('stunnel not reachable?'); - } - - $this->assertEquals(0, $connected); + $server->listen($this->portPlain); - $secureConnector = new SecureConnector($connector, $loop, array('verify_peer' => false)); + $connector = new SecureConnector(new TcpConnector($loop), $loop, array('verify_peer' => false)); - $promise = $secureConnector->create('127.0.0.1', 6001); - //$promise = $connector->create('127.0.0.1', 6000); - $client = Block\await($promise, $loop); + $client = Block\await($connector->create('127.0.0.1', $this->portSecure), $loop); /* @var $client Stream */ while (!$connected) { @@ -79,7 +72,7 @@ public function testA() $echo('world'); - // send a 10k message once to fill buffer (failing!) + // send a 10k message once to fill buffer $echo(str_repeat('1234567890', 10000)); $echo('again'); From e5bf902d82526e67acccd146b1655a4b5ad91f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 9 Mar 2016 23:41:05 +0100 Subject: [PATCH 092/146] Break up into smaller, independent test cases --- tests/SecureIntegrationTest.php | 141 ++++++++++++++++++++++++-------- tests/TestCase.php | 11 +++ 2 files changed, 117 insertions(+), 35 deletions(-) diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index 766a937d..90eba33d 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -8,12 +8,20 @@ use React\SocketClient\SecureConnector; use React\Stream\Stream; use Clue\React\Block; +use React\Promise\Promise; +use Evenement\EventEmitterInterface; +use React\Promise\Deferred; +use React\Stream\BufferedSink; class SecureIntegrationTest extends TestCase { private $portSecure; private $portPlain; + private $loop; + private $server; + private $connector; + public function setUp() { $this->portSecure = getenv('TEST_SECURE'); @@ -22,59 +30,122 @@ public function setUp() if ($this->portSecure === false || $this->portPlain === false) { $this->markTestSkipped('Needs TEST_SECURE=X and TEST_PLAIN=Y environment variables to run, see README.md'); } + + $this->loop = LoopFactory::create(); + $this->server = new Server($this->loop); + $this->server->listen($this->portPlain); + $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false));; } - public function testA() + public function tearDown() { - $loop = LoopFactory::create(); + if ($this->server !== null) { + $this->server->shutdown(); + $this->server = null; + } + } - $receivedServer = ''; - $receivedClient = ''; - $connected = 0; + public function testConnectToServer() + { + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ - $server = new Server($loop); - $server->on('connection', function (Stream $stream) use (&$receivedServer, &$connected) { - $connected++; + $client->close(); + } - // $stream->pipe($stream); - $stream->on('data', function ($data) use ($stream, &$receivedServer) { - $receivedServer .= $data; - $stream->write($data); - }); + public function testConnectToServerEmitsConnection() + { + $promiseServer = $this->createPromiseForEvent($this->server, 'connection', $this->expectCallableOnce()); + + $promiseClient = $this->connector->create('127.0.0.1', $this->portSecure); + + list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop); + /* @var $client Stream */ + + $client->close(); + } + + public function testSendSmallDataToServerReceivesOneChunk() + { + // server expects one connection which emits one data event + $receiveOnce = $this->expectCallableOnceWith('hello'); + $this->server->on('connection', function (Stream $peer) use ($receiveOnce) { + $peer->on('data', $receiveOnce); }); - $server->listen($this->portPlain); - $connector = new SecureConnector(new TcpConnector($loop), $loop, array('verify_peer' => false)); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ - $client = Block\await($connector->create('127.0.0.1', $this->portSecure), $loop); + $client->write('hello'); + + Block\sleep(0.01, $this->loop); + + $client->close(); + } + + public function testSendDataWithEndToServerReceivesAllData() + { + $disconnected = new Deferred(); + $this->server->on('connection', function (Stream $peer) use ($disconnected) { + $received = ''; + $peer->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + $peer->on('close', function () use (&$received, $disconnected) { + $disconnected->resolve($received); + }); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); /* @var $client Stream */ - while (!$connected) { - $loop->tick(); - } + $data = str_repeat('a', 200000); + $client->end($data); + + // await server to report connection "close" event + $received = Block\await($disconnected->promise(), $this->loop); - $client->on('data', function ($data) use (&$receivedClient) { - $receivedClient .= $data; + $this->assertEquals($data, $received); + } + + public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() + { + $this->server->on('connection', function (Stream $peer) { + $peer->write('hello'); }); - $this->assertEquals('', $receivedServer); - $this->assertEquals('', $receivedClient); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ - $echo = function ($str) use (&$receivedClient, &$receivedClient, $loop, $client) { - $receivedClient = $receivedServer = ''; - $client->write($str); - while ($receivedClient !== $str) { - $loop->tick(); - } - }; + $client->on('data', $this->expectCallableOnceWith('hello')); - $echo('hello'); + Block\sleep(0.01, $this->loop); - $echo('world'); + $client->close(); + } - // send a 10k message once to fill buffer - $echo(str_repeat('1234567890', 10000)); + public function testConnectToServerWhichSendsDataWithEndReceivesAllData() + { + $data = str_repeat('b', 100000); + $this->server->on('connection', function (Stream $peer) use ($data) { + $peer->end($data); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + // await data from client until it closes + $received = Block\await(BufferedSink::createPromise($client), $this->loop); - $echo('again'); + $this->assertEquals($data, $received); + } + + private function createPromiseForEvent(EventEmitterInterface $emitter, $event, $fn) + { + return new Promise(function ($resolve) use ($emitter, $event, $fn) { + $emitter->on($event, function () use ($resolve, $fn) { + $resolve(call_user_func_array($fn, func_get_args())); + }); + }); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6926ec82..bc3fc8bb 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,6 +24,17 @@ protected function expectCallableOnce() return $mock; } + protected function expectCallableOnceWith($value) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->equalTo($value)); + + return $mock; + } + protected function expectCallableNever() { $mock = $this->createCallableMock(); From 80a7d04ab564f0bd7f60994eac8c7502e6775da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 12 Mar 2016 13:10:48 +0100 Subject: [PATCH 093/146] Install stud from source in Travis' containers (replace stunnel) --- .travis.yml | 34 ++++++++++++++++++++++++++++++---- README.md | 7 ++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87f54236..82c4c1d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,16 +11,42 @@ php: sudo: false env: - - TEST_SECURE=6001 - - TEST_PLAIN=6000 + - TEST_SECURE=6001 TEST_PLAIN=6000 + +# install required system packages, see 'install' below for details +# Travis' containers require this, otherwise use this: +# sudo apt-get install openssl build-essential libev-dev libssl-dev +addons: + apt: + packages: + - openssl + - build-essential + - libev-dev + - libssl-dev install: + # install this library plus its dependencies - composer install --prefer-source --no-interaction - - sudo apt-get install -y openssl stunnel + + # we need openssl and either stunnel or stud + # unfortunately these are not available in Travis' containers + # sudo apt-get install -y openssl stud + # sudo apt-get install -y openssl stunnel4 + + # instead, let's install stud from source + # build dependencies are already installed, see 'addons.apt.packages' above + # sudo apt-get install openssl build-essential libev-dev libssl-dev + - git clone https://github.com/bumptech/stud.git + - (cd stud && make) + + # create self-signed certificate - openssl genrsa 1024 > stunnel.key - openssl req -batch -subj '/CN=127.0.0.1' -new -x509 -nodes -sha1 -days 3650 -key stunnel.key > stunnel.cert - cat stunnel.cert stunnel.key > stunnel.pem - - stunnel -f -p stunnel.pem -d $TEST_SECURE -r $TEST_PLAIN 2>/dev/null & + + # start TLS/SSL terminating proxy + # stunnel -f -p stunnel.pem -d $TEST_SECURE -r $TEST_PLAIN 2>/dev/null & + - ./stud/stud --daemon -f 127.0.0.1,$TEST_SECURE -b 127.0.0.1,$TEST_PLAIN stunnel.pem script: - phpunit --coverage-text diff --git a/README.md b/README.md index a3bf9cfa..2cc73989 100644 --- a/README.md +++ b/README.md @@ -147,9 +147,9 @@ $ phpunit ``` The test suite also contains some optional integration tests which operate on a -TCP/IP socket server and an optional TLS/SSL proxy in front of it. +TCP/IP socket server and an optional TLS/SSL terminating proxy in front of it. The underlying TCP/IP socket server will be started automatically, whereas the -TLS/SSL proxy has to be started and enabled like this: +TLS/SSL terminating proxy has to be started and enabled like this: ```bash $ stunnel -f -p stunnel.pem -d 6001 -r 6000 & @@ -157,4 +157,5 @@ $ TEST_SECURE=6001 TEST_PLAIN=6000 phpunit ``` See also the [Travis configuration](.travis.yml) for details on how to set up -the required certificate file (`stunnel.pem`) if you're unsure. +the TLS/SSL terminating proxy and the required certificate file (`stunnel.pem`) +if you're unsure. From 72535fc590eaa37466a066a0e2ab3c01d5395239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 12 Mar 2016 13:30:08 +0100 Subject: [PATCH 094/146] More reliable tests by awaiting events instead of sleeping --- tests/SecureIntegrationTest.php | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index 90eba33d..90a98d80 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -24,6 +24,10 @@ class SecureIntegrationTest extends TestCase public function setUp() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on HHVM'); + } + $this->portSecure = getenv('TEST_SECURE'); $this->portPlain = getenv('TEST_PLAIN'); @@ -34,7 +38,7 @@ public function setUp() $this->loop = LoopFactory::create(); $this->server = new Server($this->loop); $this->server->listen($this->portPlain); - $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false));; + $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false)); } public function tearDown() @@ -68,9 +72,11 @@ public function testConnectToServerEmitsConnection() public function testSendSmallDataToServerReceivesOneChunk() { // server expects one connection which emits one data event - $receiveOnce = $this->expectCallableOnceWith('hello'); - $this->server->on('connection', function (Stream $peer) use ($receiveOnce) { - $peer->on('data', $receiveOnce); + $received = new Deferred(); + $this->server->on('connection', function (Stream $peer) use ($received) { + $peer->on('data', function ($chunk) use ($received) { + $received->resolve($chunk); + }); }); $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); @@ -78,9 +84,12 @@ public function testSendSmallDataToServerReceivesOneChunk() $client->write('hello'); - Block\sleep(0.01, $this->loop); + // await server to report one "data" event + $data = Block\await($received->promise(), $this->loop); $client->close(); + + $this->assertEquals('hello', $data); } public function testSendDataWithEndToServerReceivesAllData() @@ -117,9 +126,9 @@ public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); /* @var $client Stream */ - $client->on('data', $this->expectCallableOnceWith('hello')); - - Block\sleep(0.01, $this->loop); + // await client to report one "data" event + $receive = $this->createPromiseForEvent($client, 'data', $this->expectCallableOnceWith('hello')); + Block\await($receive, $this->loop); $client->close(); } From f15683a6e700be7268731a0c51846ff794a9122c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 12 Mar 2016 14:49:44 +0100 Subject: [PATCH 095/146] Add tests to exhibit SSL/TLS buffering issues Test receiving larger buffers without ending the stream --- tests/SecureIntegrationTest.php | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index 90a98d80..9a9e600d 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -117,6 +117,29 @@ public function testSendDataWithEndToServerReceivesAllData() $this->assertEquals($data, $received); } + public function testSendDataWithoutEndingToServerReceivesAllData() + { + $received = ''; + $this->server->on('connection', function (Stream $peer) use (&$received) { + $peer->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + $data = str_repeat('d', 200000); + $client->write($data); + + // buffer incoming data for 0.1s (should be plenty of time) + Block\sleep(0.1, $this->loop); + + $client->close(); + + $this->assertEquals($data, $received); + } + public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() { $this->server->on('connection', function (Stream $peer) { @@ -149,6 +172,28 @@ public function testConnectToServerWhichSendsDataWithEndReceivesAllData() $this->assertEquals($data, $received); } + public function testConnectToServerWhichSendsDataWithoutEndingReceivesAllData() + { + $data = str_repeat('c', 100000); + $this->server->on('connection', function (Stream $peer) use ($data) { + $peer->write($data); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + // buffer incoming data for 0.1s (should be plenty of time) + $received = ''; + $client->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + Block\sleep(0.1, $this->loop); + + $client->close(); + + $this->assertEquals($data, $received); + } + private function createPromiseForEvent(EventEmitterInterface $emitter, $event, $fn) { return new Promise(function ($resolve) use ($emitter, $event, $fn) { From 4ed32f2ce1e732586783c7ebd10b7d75386f2588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 19 Mar 2016 14:11:48 +0100 Subject: [PATCH 096/146] Prepare v0.5.0 release --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 19 +++++++++++++++++++ composer.json | 5 ----- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bb8ef5a..8b08cc39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## 0.5.0 (2016-03-19) + +* Feature / BC break: Support Connector without DNS + (#46 by @clue) + + BC break: The `Connector` class now serves as a BC layer only. + The `TcpConnector` and `DnsConnector` classes replace its functionality. + If you're merely *using* this class, then you're *recommended* to upgrade as + per the below snippet – existing code will still work unchanged. + If you're `extend`ing the `Connector` (generally not recommended), then you + may have to rework your class hierarchy. + + ```php +// old (still supported, but marked deprecated) +$connector = new Connector($loop, $resolver); + +// new equivalent +$connector = new DnsConnector(new TcpConnector($loop), $resolver); + +// new feature: supports connecting to IP addresses only +$connector = new TcpConnector($loop); +``` + +* Feature: Add socket and SSL/TLS context options to connectors + (#52 by @clue) + +* Fix: PHP 5.6+ uses new SSL/TLS context options + (#61 by @clue) + +* Fix: Move SSL/TLS context options to SecureConnector + (#43 by @clue) + +* Fix: Fix error reporting for invalid addresses + (#47 by @clue) + +* Fix: Close stream resource if connection fails + (#48 by @clue) + +* First class support for PHP 5.3 through PHP 7 and HHVM + (#53, #54 by @clue) + +* Add integration tests for SSL/TLS sockets + (#62 by @clue) + ## 0.4.4 (2015-09-23) * Feature: Add support for Unix domain sockets (UDS) (#41 by @clue) diff --git a/README.md b/README.md index 2cc73989..34bd1d46 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,25 @@ $connector->create('/tmp/demo.sock')->then(function (React\Stream\Stream $stream $loop->run(); ``` +## Install + +The recommended way to install this library is [through Composer](http://getcomposer.org). +[New to Composer?](http://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: + +```bash +$ composer require react/socket-client:^0.5 +``` + +If you care a lot about BC, you may also want to look into supporting legacy versions: + +```bash +$ composer require "react/socket-client:^0.5||^0.4||^0.3" +``` + +More details and upgrade guides can be found in the [CHANGELOG](CHANGELOG.md). + ## Tests To run the test suite, you need PHPUnit. Go to the project root and run: diff --git a/composer.json b/composer.json index 1e24ceaf..ff58dc5a 100644 --- a/composer.json +++ b/composer.json @@ -15,11 +15,6 @@ "React\\SocketClient\\": "src" } }, - "extra": { - "branch-alias": { - "dev-master": "0.4-dev" - } - }, "require-dev": { "clue/block-react": "~1.0" } From 67662fec703f19b8da908df7ebb8acd6f6c4a465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 6 Sep 2015 16:14:56 +0200 Subject: [PATCH 097/146] Support Promise cancellation for TcpConnector --- README.md | 12 ++++++++++++ composer.json | 2 +- src/TcpConnector.php | 15 +++++++++------ tests/IntegrationTest.php | 17 +++++++++++++++++ tests/TcpConnectorTest.php | 13 +++++++++++++ 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 34bd1d46..cc81ac79 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,18 @@ $tcpConnector->create('127.0.0.1', 80)->then(function (React\Stream\Stream $stre $loop->run(); ``` +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $tcpConnector->create($host, $port); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will close the underlying socket +resource, thus cancelling the pending TCP/IP connection, and reject the +resulting promise. + You can optionally pass additional [socket context options](http://php.net/manual/en/context.socket.php) to the constructor like this: diff --git a/composer.json b/composer.json index ff58dc5a..f9bc7b46 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "react/dns": "0.4.*|0.3.*", "react/event-loop": "0.4.*|0.3.*", "react/stream": "0.4.*|0.3.*", - "react/promise": "~2.0|~1.1" + "react/promise": "^2.1 || ^1.2" }, "autoload": { "psr-4": { diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 70a283c0..fa64ed8c 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -55,17 +55,20 @@ public function create($ip, $port) private function waitForStreamOnce($stream) { - $deferred = new Deferred(); - $loop = $this->loop; - $this->loop->addWriteStream($stream, function ($stream) use ($loop, $deferred) { + return new Promise\Promise(function ($resolve) use ($loop, $stream) { + $loop->addWriteStream($stream, function ($stream) use ($loop, $resolve) { + $loop->removeWriteStream($stream); + + $resolve($stream); + }); + }, function () use ($loop, $stream) { $loop->removeWriteStream($stream); + fclose($stream); - $deferred->resolve($stream); + throw new \RuntimeException('Cancelled while waiting for TCP/IP connection to be established'); }); - - return $deferred->promise(); } /** @internal */ diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 9e16281b..581d77a5 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -7,6 +7,7 @@ use React\Socket\Server; use React\SocketClient\Connector; use React\SocketClient\SecureConnector; +use React\SocketClient\TcpConnector; use React\Stream\BufferedSink; use Clue\React\Block; @@ -103,4 +104,20 @@ public function testSelfSignedResolvesIfVerificationIsDisabled() $conn = Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop); $conn->close(); } + + public function testCancelPendingConnection() + { + $loop = new StreamSelectLoop(); + + $connector = new TcpConnector($loop); + $pending = $connector->create('8.8.8.8', 80); + + $loop->addTimer(0.001, function () use ($pending) { + $pending->cancel(); + }); + + $pending->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $loop->run(); + } } diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 2fd700e0..073133d5 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -97,4 +97,17 @@ public function connectionToInvalidAddressShouldFailImmediately() $this->expectCallableOnce() ); } + + /** @test */ + public function cancellingConnectionShouldRejectPromise() + { + $loop = new StreamSelectLoop(); + $connector = new TcpConnector($loop); + + $promise = $connector->create('127.0.0.1', 9999); + $promise->cancel(); + + $this->setExpectedException('RuntimeException', 'Cancelled'); + Block\await($promise, $loop); + } } From 69620345ba715d59d4ae99f139c17fe90231471c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 15 Jul 2015 03:12:41 +0200 Subject: [PATCH 098/146] Add TimeoutConnector decorator --- README.md | 14 ++++++ composer.json | 3 +- src/TimeoutConnector.php | 26 ++++++++++ tests/TimeoutConnectorTest.php | 86 ++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/TimeoutConnector.php create mode 100644 tests/TimeoutConnectorTest.php diff --git a/README.md b/README.md index 34bd1d46..53dc2585 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,20 @@ $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop, )); ``` +### Connection timeouts + +The `TimeoutConnector` class decorates any given `Connector` instance. +It provides the same `create()` method, but will automatically reject the +underlying connection attempt if it takes too long. + +```php +$timeoutConnector = new React\SocketClient\TimeoutConnector($connector, 3.0, $loop); + +$timeoutConnector->create('google.com', 80)->then(function (React\Stream\Stream $stream) { + // connection succeeded within 3.0 seconds +}); +``` + ### Unix domain sockets Similarly, the `UnixConnector` class can be used to connect to Unix domain socket (UDS) diff --git a/composer.json b/composer.json index ff58dc5a..4ab2c9c8 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "react/dns": "0.4.*|0.3.*", "react/event-loop": "0.4.*|0.3.*", "react/stream": "0.4.*|0.3.*", - "react/promise": "~2.0|~1.1" + "react/promise": "~2.0|~1.1", + "react/promise-timer": "~1.0" }, "autoload": { "psr-4": { diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php new file mode 100644 index 00000000..a79a9e88 --- /dev/null +++ b/src/TimeoutConnector.php @@ -0,0 +1,26 @@ +connector = $connector; + $this->timeout = $timeout; + $this->loop = $loop; + } + + public function create($host, $port) + { + return Timer\timeout($this->connector->create($host, $port), $this->timeout, $this->loop); + } +} diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php new file mode 100644 index 00000000..6aa0fec1 --- /dev/null +++ b/tests/TimeoutConnectorTest.php @@ -0,0 +1,86 @@ +getMock('React\SocketClient\ConnectorInterface'); + $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 0.01, $loop); + + $timeout->create('google.com', 80)->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + + $loop->run(); + } + + public function testRejectsWhenConnectorRejects() + { + $promise = Promise\reject(); + + $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 5.0, $loop); + + $timeout->create('google.com', 80)->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + + $loop->run(); + } + + public function testResolvesWhenConnectorResolves() + { + $promise = Promise\resolve(); + + $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 5.0, $loop); + + $timeout->create('google.com', 80)->then( + $this->expectCallableOnce(), + $this->expectCallableNever() + ); + + $loop->run(); + } + + public function testRejectsAndCancelsPendingPromiseOnTimeout() + { + $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); + + $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 0.01, $loop); + + $timeout->create('google.com', 80)->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + + $loop->run(); + } +} From 324250951397220aca07699032c45ac6e09e1241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Nov 2016 21:43:54 +0100 Subject: [PATCH 099/146] Support Promise cancellation for TimeoutConnector --- README.md | 11 +++++++++ src/TimeoutConnector.php | 26 ++++++++++++++++++++- tests/TimeoutConnectorTest.php | 41 +++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 53dc2585..8a6e9986 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,17 @@ $timeoutConnector->create('google.com', 80)->then(function (React\Stream\Stream }); ``` +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $timeoutConnector->create($host, $port); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying connection +attempt, abort the timer and reject the resulting promise. + ### Unix domain sockets Similarly, the `UnixConnector` class can be used to connect to Unix domain socket (UDS) diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index a79a9e88..c4cfd5e4 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -5,6 +5,9 @@ use React\SocketClient\ConnectorInterface; use React\EventLoop\LoopInterface; use React\Promise\Timer; +use React\Stream\Stream; +use React\Promise\Promise; +use React\Promise\CancellablePromiseInterface; class TimeoutConnector implements ConnectorInterface { @@ -21,6 +24,27 @@ public function __construct(ConnectorInterface $connector, $timeout, LoopInterfa public function create($host, $port) { - return Timer\timeout($this->connector->create($host, $port), $this->timeout, $this->loop); + $promise = $this->connector->create($host, $port); + + return Timer\timeout(new Promise( + function ($resolve, $reject) use ($promise) { + // resolve/reject with result of TCP/IP connection + $promise->then($resolve, $reject); + }, + function ($_, $reject) use ($promise) { + // cancellation should reject connection attempt + $reject(new \RuntimeException('Connection attempt cancelled during connection')); + + // forefully close TCP/IP connection if it completes despite cancellation + $promise->then(function (Stream $stream) { + $stream->close(); + }); + + // (try to) cancel pending TCP/IP connection + if ($promise instanceof CancellablePromiseInterface) { + $promise->cancel(); + } + } + ), $this->timeout, $this->loop); } } diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index 6aa0fec1..2cb09698 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -29,7 +29,7 @@ public function testRejectsOnTimeout() public function testRejectsWhenConnectorRejects() { - $promise = Promise\reject(); + $promise = Promise\reject(new \RuntimeException()); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); @@ -83,4 +83,43 @@ public function testRejectsAndCancelsPendingPromiseOnTimeout() $loop->run(); } + + public function testCancelsPendingPromiseOnCancel() + { + $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); + + $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 0.01, $loop); + + $out = $timeout->create('google.com', 80); + $out->cancel(); + + $out->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() + { + $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock(); + $stream->expects($this->once())->method('close'); + + $promise = new Promise\Promise(function () { }, function ($resolve) use ($stream) { + $resolve($stream); + }); + + $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 0.01, $loop); + + $out = $timeout->create('google.com', 80); + $out->cancel(); + + $out->then($this->expectCallableNever(), $this->expectCallableOnce()); + } } From d6254e62ee4903ebfea45cf48885ddc1faba3a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 13 Oct 2015 18:29:50 +0200 Subject: [PATCH 100/146] Support Promise cancellation for DnsConnector --- README.md | 11 ++++++++ src/DnsConnector.php | 54 +++++++++++++++++++++++++++++++++----- tests/DnsConnectorTest.php | 46 ++++++++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cc81ac79..0a06d099 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,17 @@ $dnsConnector->create('www.google.com', 80)->then(function (React\Stream\Stream $loop->run(); ``` +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $dnsConnector->create($host, $port); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying DNS lookup +and/or the underlying TCP/IP connection and reject the resulting promise. + The legacy `Connector` class can be used for backwards-compatiblity reasons. It works very much like the newer `DnsConnector` but instead has to be set up like this: diff --git a/src/DnsConnector.php b/src/DnsConnector.php index 66119954..44c2179b 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -2,11 +2,10 @@ namespace React\SocketClient; -use React\EventLoop\LoopInterface; use React\Dns\Resolver\Resolver; use React\Stream\Stream; use React\Promise; -use React\Promise\Deferred; +use React\Promise\CancellablePromiseInterface; class DnsConnector implements ConnectorInterface { @@ -21,12 +20,12 @@ public function __construct(ConnectorInterface $connector, Resolver $resolver) public function create($host, $port) { - $connector = $this->connector; + $that = $this; return $this ->resolveHostname($host) - ->then(function ($address) use ($connector, $port) { - return $connector->create($address, $port); + ->then(function ($ip) use ($that, $port) { + return $that->connect($ip, $port); }); } @@ -36,6 +35,49 @@ private function resolveHostname($host) return Promise\resolve($host); } - return $this->resolver->resolve($host); + $promise = $this->resolver->resolve($host); + + return new Promise\Promise( + function ($resolve, $reject) use ($promise) { + // resolve/reject with result of DNS lookup + $promise->then($resolve, $reject); + }, + function ($_, $reject) use ($promise) { + // cancellation should reject connection attempt + $reject(new \RuntimeException('Connection attempt cancelled during DNS lookup')); + + // (try to) cancel pending DNS lookup + if ($promise instanceof CancellablePromiseInterface) { + $promise->cancel(); + } + } + ); + } + + /** @internal */ + public function connect($ip, $port) + { + $promise = $this->connector->create($ip, $port); + + return new Promise\Promise( + function ($resolve, $reject) use ($promise) { + // resolve/reject with result of TCP/IP connection + $promise->then($resolve, $reject); + }, + function ($_, $reject) use ($promise) { + // cancellation should reject connection attempt + $reject(new \RuntimeException('Connection attempt cancelled during TCP/IP connection')); + + // forefully close TCP/IP connection if it completes despite cancellation + $promise->then(function (Stream $stream) { + $stream->close(); + }); + + // (try to) cancel pending TCP/IP connection + if ($promise instanceof CancellablePromiseInterface) { + $promise->cancel(); + } + } + ); } } diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index f8ab96ee..f588dd64 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -22,7 +22,7 @@ public function setUp() public function testPassByResolverIfGivenIp() { $this->resolver->expects($this->never())->method('resolve'); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('127.0.0.1'), $this->equalTo(80)); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('127.0.0.1'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); $this->connector->create('127.0.0.1', 80); } @@ -30,7 +30,7 @@ public function testPassByResolverIfGivenIp() public function testPassThroughResolverIfGivenHost() { $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80)); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); $this->connector->create('google.com', 80); } @@ -42,4 +42,46 @@ public function testSkipConnectionIfDnsFails() $this->connector->create('example.invalid', 80); } + + public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() + { + $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue($pending)); + $this->tcp->expects($this->never())->method('resolve'); + + $promise = $this->connector->create('example.com', 80); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testCancelDuringTcpConnectionCancelsTcpConnection() + { + $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); + + $promise = $this->connector->create('example.com', 80); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() + { + $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock(); + $stream->expects($this->once())->method('close'); + + $pending = new Promise\Promise(function () { }, function ($resolve) use ($stream) { + $resolve($stream); + }); + + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); + + $promise = $this->connector->create('example.com', 80); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } } From c47dbbbd9cddba5c97af3722034863fbac5db6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 17 Oct 2016 18:01:24 +0200 Subject: [PATCH 101/146] Documentation for atomic operation of UnixConnector --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0a06d099..bcf2d907 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,10 @@ $connector->create('/tmp/demo.sock')->then(function (React\Stream\Stream $stream $loop->run(); ``` +Connecting to Unix domain sockets is an atomic operation, i.e. its promise will +settle (either resolve or reject) immediately. +As such, calling `cancel()` on the resulting promise has no effect. + ## Install The recommended way to install this library is [through Composer](http://getcomposer.org). From edc3e46a910d16d523a4d7db6f181bfef46c93a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 18 Oct 2016 16:14:56 +0200 Subject: [PATCH 102/146] Support Promise cancellation for SecureConnector --- README.md | 11 +++++++ src/SecureConnector.php | 29 +++++++++++++++++- src/StreamEncryption.php | 16 +++++----- tests/SecureConnectorTest.php | 58 +++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 tests/SecureConnectorTest.php diff --git a/README.md b/README.md index bcf2d907..08b7bbac 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,17 @@ $secureConnector->create('www.google.com', 443)->then(function (React\Stream\Str $loop->run(); ``` +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $secureConnector->create($host, $port); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying TCP/IP +connection and/or the SSL/TLS negonation and reject the resulting promise. + You can optionally pass additional [SSL context options](http://php.net/manual/en/context.ssl.php) to the constructor like this: diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 1cdfc838..b4ac22b5 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -5,6 +5,7 @@ use React\EventLoop\LoopInterface; use React\Stream\Stream; use React\Promise; +use React\Promise\CancellablePromiseInterface; class SecureConnector implements ConnectorInterface { @@ -39,7 +40,7 @@ public function create($host, $port) } $encryption = $this->streamEncryption; - return $this->connector->create($host, $port)->then(function (Stream $stream) use ($context, $encryption) { + return $this->connect($host, $port)->then(function (Stream $stream) use ($context, $encryption) { // (unencrypted) TCP/IP connection succeeded // set required SSL/TLS context options @@ -55,4 +56,30 @@ public function create($host, $port) }); }); } + + private function connect($host, $port) + { + $promise = $this->connector->create($host, $port); + + return new Promise\Promise( + function ($resolve, $reject) use ($promise) { + // resolve/reject with result of TCP/IP connection + $promise->then($resolve, $reject); + }, + function ($_, $reject) use ($promise) { + // cancellation should reject connection attempt + $reject(new \RuntimeException('Connection attempt cancelled during TCP/IP connection')); + + // forefully close TCP/IP connection if it completes despite cancellation + $promise->then(function (Stream $stream) { + $stream->close(); + }); + + // (try to) cancel pending TCP/IP connection + if ($promise instanceof CancellablePromiseInterface) { + $promise->cancel(); + } + } + ); + } } diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index e15d3693..2a76dd0c 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -66,7 +66,10 @@ public function toggle(Stream $stream, $toggle) // TODO: add write() event to make sure we're not sending any excessive data - $deferred = new Deferred(); + $deferred = new Deferred(function ($_, $reject) use ($toggle) { + // cancelling this leaves this stream in an inconsistent state… + $reject(new \RuntimeException('Cancelled toggling encryption ' . $toggle ? 'on' : 'off')); + }); // get actual stream socket from stream instance $socket = $stream->stream; @@ -82,7 +85,9 @@ public function toggle(Stream $stream, $toggle) $wrap = $this->wrapSecure && $toggle; $loop = $this->loop; - return $deferred->promise()->then(function () use ($stream, $wrap, $loop) { + return $deferred->promise()->then(function () use ($stream, $socket, $wrap, $loop) { + $loop->removeReadStream($socket); + if ($wrap) { return new SecureStream($stream, $loop); } @@ -90,7 +95,8 @@ public function toggle(Stream $stream, $toggle) $stream->resume(); return $stream; - }, function($error) use ($stream) { + }, function($error) use ($stream, $socket, $loop) { + $loop->removeReadStream($socket); $stream->resume(); throw $error; }); @@ -103,12 +109,8 @@ public function toggleCrypto($socket, Deferred $deferred, $toggle) restore_error_handler(); if (true === $result) { - $this->loop->removeReadStream($socket); - $deferred->resolve(); } else if (false === $result) { - $this->loop->removeReadStream($socket); - $deferred->reject(new UnexpectedValueException( sprintf("Unable to complete SSL/TLS handshake: %s", $this->errstr), $this->errno diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php new file mode 100644 index 00000000..8ad045c5 --- /dev/null +++ b/tests/SecureConnectorTest.php @@ -0,0 +1,58 @@ +loop = $this->getMock('React\EventLoop\LoopInterface'); + $this->tcp = $this->getMock('React\SocketClient\ConnectorInterface'); + $this->connector = new SecureConnector($this->tcp, $this->loop); + } + + public function testConnectionWillWaitForTcpConnection() + { + $pending = new Promise\Promise(function () { }); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + + $promise = $this->connector->create('example.com', 80); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testCancelDuringTcpConnectionCancelsTcpConnection() + { + $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + + $promise = $this->connector->create('example.com', 80); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() + { + $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock(); + $stream->expects($this->once())->method('close'); + + $pending = new Promise\Promise(function () { }, function ($resolve) use ($stream) { + $resolve($stream); + }); + + $this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + + $promise = $this->connector->create('example.com', 80); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } +} From 64b7e2e7a5bb1359f63a545a147d646ac7abc61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Nov 2016 22:48:07 +0100 Subject: [PATCH 103/146] Skip TLS tests on outdated HHVM (Travis) --- tests/IntegrationTest.php | 12 ++++++------ tests/SecureConnectorTest.php | 4 ++++ tests/SecureIntegrationTest.php | 4 ++-- tests/TcpConnectorTest.php | 5 ++++- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 581d77a5..87d9d0a4 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -34,8 +34,8 @@ public function gettingStuffFromGoogleShouldWork() /** @test */ public function gettingEncryptedStuffFromGoogleShouldWork() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); } $loop = new StreamSelectLoop(); @@ -60,8 +60,8 @@ public function gettingEncryptedStuffFromGoogleShouldWork() /** @test */ public function testSelfSignedRejectsIfVerificationIsEnabled() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); } $loop = new StreamSelectLoop(); @@ -84,8 +84,8 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() /** @test */ public function testSelfSignedResolvesIfVerificationIsDisabled() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); } $loop = new StreamSelectLoop(); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 8ad045c5..9b2a6c9c 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -13,6 +13,10 @@ class SecureConnectorTest extends TestCase public function setUp() { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + $this->loop = $this->getMock('React\EventLoop\LoopInterface'); $this->tcp = $this->getMock('React\SocketClient\ConnectorInterface'); $this->connector = new SecureConnector($this->tcp, $this->loop); diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index 9a9e600d..aea710e9 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -24,8 +24,8 @@ class SecureIntegrationTest extends TestCase public function setUp() { - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('Not supported on HHVM'); + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); } $this->portSecure = getenv('TEST_SECURE'); diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 073133d5..c48a096d 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -104,7 +104,10 @@ public function cancellingConnectionShouldRejectPromise() $loop = new StreamSelectLoop(); $connector = new TcpConnector($loop); - $promise = $connector->create('127.0.0.1', 9999); + $server = new Server($loop); + $server->listen(0); + + $promise = $connector->create('127.0.0.1', $server->getPort()); $promise->cancel(); $this->setExpectedException('RuntimeException', 'Cancelled'); From 3cb406a14008c092207d95a63ebe7739f74843bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 20 Nov 2016 01:11:24 +0100 Subject: [PATCH 104/146] Prepare v0.5.1 release --- CHANGELOG.md | 21 +++++++++++++++++++++ README.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b08cc39..3af4068f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.5.1 (2016-11-20) + +* Feature: Support Promise cancellation for all connectors + (#71 by @clue) + + ```php + $promise = $connector->create($host, $port); + + $promise->cancel(); + ``` + +* Feature: Add TimeoutConnector decorator + (#51 by @clue) + + ```php + $timeout = new TimeoutConnector($connector, 3.0, $loop); + $timeout->create($host, $port)->then(function(Stream $stream) { + // connection resolved within 3.0s + }); + ``` + ## 0.5.0 (2016-03-19) * Feature / BC break: Support Connector without DNS diff --git a/README.md b/README.md index 92066c4c..f5e5edf0 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/socket-client:^0.5 +$ composer require react/socket-client:^0.5.1 ``` If you care a lot about BC, you may also want to look into supporting legacy versions: From 4d31e9d5b24e1c7cb2567124dae91e03c3a5bd87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Nov 2016 23:48:45 +0100 Subject: [PATCH 105/146] Replace SecureStream with unlimited read buffer from react/stream v0.4.5 --- composer.json | 2 +- src/SecureStream.php | 98 ---------------------------------------- src/StreamEncryption.php | 6 +-- 3 files changed, 2 insertions(+), 104 deletions(-) delete mode 100644 src/SecureStream.php diff --git a/composer.json b/composer.json index 0385653c..398ccc7e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.0", "react/dns": "0.4.*|0.3.*", "react/event-loop": "0.4.*|0.3.*", - "react/stream": "0.4.*|0.3.*", + "react/stream": "^0.4.5", "react/promise": "^2.1 || ^1.2", "react/promise-timer": "~1.0" }, diff --git a/src/SecureStream.php b/src/SecureStream.php deleted file mode 100644 index 5aa879c6..00000000 --- a/src/SecureStream.php +++ /dev/null @@ -1,98 +0,0 @@ -stream = $stream->stream; - $this->decorating = $stream; - $this->loop = $loop; - $that = $this; - - $stream->on('error', function($error) use ($that) { - $that->emit('error', array($error, $that)); - }); - $stream->on('end', function() use ($that) { - $that->emit('end', array($that)); - }); - $stream->on('close', function() use ($that) { - $that->emit('close', array($that)); - }); - $stream->on('drain', function() use ($that) { - $that->emit('drain', array($that)); - }); - - $stream->pause(); - - $this->resume(); - } - - public function handleData($stream) - { - $data = stream_get_contents($stream); - - $this->emit('data', array($data, $this)); - - if (!is_resource($stream) || feof($stream)) { - $this->end(); - } - } - - public function pause() - { - $this->loop->removeReadStream($this->decorating->stream); - } - - public function resume() - { - if ($this->isReadable()) { - $this->loop->addReadStream($this->decorating->stream, array($this, 'handleData')); - } - } - - public function isReadable() - { - return $this->decorating->isReadable(); - } - - public function isWritable() - { - return $this->decorating->isWritable(); - } - - public function write($data) - { - return $this->decorating->write($data); - } - - public function close() - { - return $this->decorating->close(); - } - - public function end($data = null) - { - return $this->decorating->end($data); - } - - public function pipe(WritableStreamInterface $dest, array $options = array()) - { - Util::pipe($this, $dest, $options); - - return $dest; - } -} \ No newline at end of file diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 2a76dd0c..e6a37330 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -57,10 +57,6 @@ public function disable(Stream $stream) public function toggle(Stream $stream, $toggle) { - if (__NAMESPACE__ . '\SecureStream' === get_class($stream)) { - $stream = $stream->decorating; - } - // pause actual stream instance to continue operation on raw stream socket $stream->pause(); @@ -89,7 +85,7 @@ public function toggle(Stream $stream, $toggle) $loop->removeReadStream($socket); if ($wrap) { - return new SecureStream($stream, $loop); + $stream->bufferSize = null; } $stream->resume(); From 12a7eaf863b8d650fa984641eb29416ccffb775c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 16 Nov 2016 00:20:23 +0100 Subject: [PATCH 106/146] Add timeouts for all integration tests --- composer.json | 2 +- tests/IntegrationTest.php | 10 ++++++---- tests/SecureIntegrationTest.php | 24 +++++++++++++----------- tests/TcpConnectorTest.php | 6 ++++-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 398ccc7e..bdd9d502 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,6 @@ } }, "require-dev": { - "clue/block-react": "~1.0" + "clue/block-react": "^1.1" } } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 87d9d0a4..0568dece 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -13,6 +13,8 @@ class IntegrationTest extends TestCase { + const TIMEOUT = 5.0; + /** @test */ public function gettingStuffFromGoogleShouldWork() { @@ -26,7 +28,7 @@ public function gettingStuffFromGoogleShouldWork() $conn->write("GET / HTTP/1.0\r\n\r\n"); - $response = Block\await(BufferedSink::createPromise($conn), $loop); + $response = Block\await(BufferedSink::createPromise($conn), $loop, self::TIMEOUT); $this->assertRegExp('#^HTTP/1\.0#', $response); } @@ -52,7 +54,7 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $conn->write("GET / HTTP/1.0\r\n\r\n"); - $response = Block\await(BufferedSink::createPromise($conn), $loop); + $response = Block\await(BufferedSink::createPromise($conn), $loop, self::TIMEOUT); $this->assertRegExp('#^HTTP/1\.0#', $response); } @@ -78,7 +80,7 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() ); $this->setExpectedException('RuntimeException'); - Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop); + Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop, self::TIMEOUT); } /** @test */ @@ -101,7 +103,7 @@ public function testSelfSignedResolvesIfVerificationIsDisabled() ) ); - $conn = Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop); + $conn = Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop, self::TIMEOUT); $conn->close(); } diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index aea710e9..ef85dad3 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -15,6 +15,8 @@ class SecureIntegrationTest extends TestCase { + const TIMEOUT = 0.5; + private $portSecure; private $portPlain; @@ -51,7 +53,7 @@ public function tearDown() public function testConnectToServer() { - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->close(); @@ -63,7 +65,7 @@ public function testConnectToServerEmitsConnection() $promiseClient = $this->connector->create('127.0.0.1', $this->portSecure); - list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop); + list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->close(); @@ -79,13 +81,13 @@ public function testSendSmallDataToServerReceivesOneChunk() }); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->write('hello'); // await server to report one "data" event - $data = Block\await($received->promise(), $this->loop); + $data = Block\await($received->promise(), $this->loop, self::TIMEOUT); $client->close(); @@ -105,14 +107,14 @@ public function testSendDataWithEndToServerReceivesAllData() }); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('a', 200000); $client->end($data); // await server to report connection "close" event - $received = Block\await($disconnected->promise(), $this->loop); + $received = Block\await($disconnected->promise(), $this->loop, self::TIMEOUT); $this->assertEquals($data, $received); } @@ -126,7 +128,7 @@ public function testSendDataWithoutEndingToServerReceivesAllData() }); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('d', 200000); @@ -146,12 +148,12 @@ public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() $peer->write('hello'); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await client to report one "data" event $receive = $this->createPromiseForEvent($client, 'data', $this->expectCallableOnceWith('hello')); - Block\await($receive, $this->loop); + Block\await($receive, $this->loop, self::TIMEOUT); $client->close(); } @@ -163,11 +165,11 @@ public function testConnectToServerWhichSendsDataWithEndReceivesAllData() $peer->end($data); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await data from client until it closes - $received = Block\await(BufferedSink::createPromise($client), $this->loop); + $received = Block\await(BufferedSink::createPromise($client), $this->loop, self::TIMEOUT); $this->assertEquals($data, $received); } diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index c48a096d..964297df 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -9,6 +9,8 @@ class TcpConnectorTest extends TestCase { + const TIMEOUT = 0.1; + /** @test */ public function connectionToEmptyPortShouldFail() { @@ -35,7 +37,7 @@ public function connectionToTcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->create('127.0.0.1', 9999), $loop); + $stream = Block\await($connector->create('127.0.0.1', 9999), $loop, self::TIMEOUT); $this->assertInstanceOf('React\Stream\Stream', $stream); @@ -67,7 +69,7 @@ public function connectionToIp6TcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->create('::1', 9999), $loop); + $stream = Block\await($connector->create('::1', 9999), $loop, self::TIMEOUT); $this->assertInstanceOf('React\Stream\Stream', $stream); From 43993f3bc3684315a934a1d5862111c75babcd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 6 Dec 2016 11:47:33 +0100 Subject: [PATCH 107/146] Documentation for using SecureConnector WRT default context The SecureConnector assumes a unique context resource for each stream resource. Failing to allocate one during stream creation may lead to some hard to trace race conditions. See #73 for possible issues. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index f5e5edf0..1272e56b 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,14 @@ $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop, )); ``` +> Advanced usage: Internally, the `SecureConnector` has to set the required +*context options* on the underlying stream resource. +It should therefor be used with a `TcpConnector` somewhere in the connector +stack so that it can allocate an empty *context* resource for each stream +resource. +Failing to do so may result in some hard to trace race conditions, because all +stream resources will use a single, shared *default context* resource otherwise. + ### Connection timeouts The `TimeoutConnector` class decorates any given `Connector` instance. From 0251f7e7cd3c3036ed07710f9ba85f39d1992c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 3 Dec 2016 07:52:29 +0100 Subject: [PATCH 108/146] Add examples --- README.md | 6 +++++ examples/01-http.php | 29 ++++++++++++++++++++++ examples/02-https.php | 31 +++++++++++++++++++++++ examples/03-netcat.php | 56 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 examples/01-http.php create mode 100644 examples/02-https.php create mode 100644 examples/03-netcat.php diff --git a/README.md b/README.md index f5e5edf0..c9cd19f7 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ $tcpConnector->create('127.0.0.1', 80)->then(function (React\Stream\Stream $stre $loop->run(); ``` +See also the [first example](examples). + Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php @@ -95,6 +97,8 @@ $dnsConnector->create('www.google.com', 80)->then(function (React\Stream\Stream $loop->run(); ``` +See also the [first example](examples). + Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php @@ -135,6 +139,8 @@ $secureConnector->create('www.google.com', 443)->then(function (React\Stream\Str $loop->run(); ``` +See also the [second example](examples). + Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php diff --git a/examples/01-http.php b/examples/01-http.php new file mode 100644 index 00000000..01a5eedc --- /dev/null +++ b/examples/01-http.php @@ -0,0 +1,29 @@ +create('8.8.8.8', $loop); + +$tcp = new TcpConnector($loop); +$dns = new DnsConnector($tcp, $resolver); + +$dns->create('www.google.com', 80)->then(function (Stream $stream) { + $stream->on('data', function ($data) { + echo $data; + }); + $stream->on('close', function () { + echo '[CLOSED]' . PHP_EOL; + }); + + $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); +}, 'printf'); + +$loop->run(); diff --git a/examples/02-https.php b/examples/02-https.php new file mode 100644 index 00000000..c7a01a98 --- /dev/null +++ b/examples/02-https.php @@ -0,0 +1,31 @@ +create('8.8.8.8', $loop); + +$tcp = new TcpConnector($loop); +$dns = new DnsConnector($tcp, $resolver); +$tls = new SecureConnector($dns, $loop); + +$tls->create('www.google.com', 443)->then(function (Stream $stream) { + $stream->on('data', function ($data) { + echo $data; + }); + $stream->on('close', function () { + echo '[CLOSED]' . PHP_EOL; + }); + + $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); +}, 'printf'); + +$loop->run(); diff --git a/examples/03-netcat.php b/examples/03-netcat.php new file mode 100644 index 00000000..d96e0dee --- /dev/null +++ b/examples/03-netcat.php @@ -0,0 +1,56 @@ + ' . PHP_EOL); + exit(1); +} + +$loop = Factory::create(); + +$factory = new \React\Dns\Resolver\Factory(); +$resolver = $factory->create('8.8.8.8', $loop); + +$tcp = new TcpConnector($loop); +$dns = new DnsConnector($tcp, $resolver); + +$stdin = new Stream(STDIN, $loop); +$stdin->pause(); +$stdout = new Stream(STDOUT, $loop); +$stdout->pause(); +$stderr = new Stream(STDERR, $loop); +$stderr->pause(); + +$stderr->write('Connecting' . PHP_EOL); + +$dns->create($argv[1], $argv[2])->then(function (Stream $stream) use ($stdin, $stdout, $stderr) { + // pipe everything from STDIN into connection + $stdin->resume(); + $stdin->pipe($stream); + + // pipe everything from connection to STDOUT + $stream->pipe($stdout); + + // report errors to STDERR + $stream->on('error', function ($error) use ($stderr) { + $stderr->write('Stream ERROR: ' . $error . PHP_EOL); + }); + + // report closing and stop reading from input + $stream->on('close', function () use ($stderr, $stdin) { + $stderr->write('[CLOSED]' . PHP_EOL); + $stdin->close(); + }); + + $stderr->write('Connected' . PHP_EOL); +}, function ($error) use ($stderr) { + $stderr->write('Connection ERROR: ' . $error . PHP_EOL); +}); + +$loop->run(); From 16ef9c831d38525f368964f8621a15ba4382b81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 4 Dec 2016 09:04:41 +0100 Subject: [PATCH 109/146] Add TimeoutConnector to examples --- README.md | 2 ++ examples/01-http.php | 4 ++++ examples/02-https.php | 4 ++++ examples/03-netcat.php | 4 ++++ 4 files changed, 14 insertions(+) diff --git a/README.md b/README.md index c9cd19f7..42157091 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,8 @@ $timeoutConnector->create('google.com', 80)->then(function (React\Stream\Stream }); ``` +See also any of the [examples](examples). + Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php diff --git a/examples/01-http.php b/examples/01-http.php index 01a5eedc..6a2f9311 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -4,6 +4,7 @@ use React\SocketClient\TcpConnector; use React\SocketClient\DnsConnector; use React\Stream\Stream; +use React\SocketClient\TimeoutConnector; require __DIR__ . '/../vendor/autoload.php'; @@ -15,6 +16,9 @@ $tcp = new TcpConnector($loop); $dns = new DnsConnector($tcp, $resolver); +// time out connection attempt in 3.0s +$dns = new TimeoutConnector($dns, 3.0, $loop); + $dns->create('www.google.com', 80)->then(function (Stream $stream) { $stream->on('data', function ($data) { echo $data; diff --git a/examples/02-https.php b/examples/02-https.php index c7a01a98..c70ddcd7 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -5,6 +5,7 @@ use React\SocketClient\DnsConnector; use React\SocketClient\SecureConnector; use React\Stream\Stream; +use React\SocketClient\TimeoutConnector; require __DIR__ . '/../vendor/autoload.php'; @@ -17,6 +18,9 @@ $dns = new DnsConnector($tcp, $resolver); $tls = new SecureConnector($dns, $loop); +// time out connection attempt in 3.0s +$tls = new TimeoutConnector($tls, 3.0, $loop); + $tls->create('www.google.com', 443)->then(function (Stream $stream) { $stream->on('data', function ($data) { echo $data; diff --git a/examples/03-netcat.php b/examples/03-netcat.php index d96e0dee..8ef34ad6 100644 --- a/examples/03-netcat.php +++ b/examples/03-netcat.php @@ -4,6 +4,7 @@ use React\SocketClient\TcpConnector; use React\SocketClient\DnsConnector; use React\Stream\Stream; +use React\SocketClient\TimeoutConnector; require __DIR__ . '/../vendor/autoload.php'; @@ -20,6 +21,9 @@ $tcp = new TcpConnector($loop); $dns = new DnsConnector($tcp, $resolver); +// time out connection attempt in 3.0s +$dns = new TimeoutConnector($dns, 3.0, $loop); + $stdin = new Stream(STDIN, $loop); $stdin->pause(); $stdout = new Stream(STDOUT, $loop); From fecbf59622ec541fdd883a4cda8acf60a2469311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 23 Nov 2015 02:37:58 +0100 Subject: [PATCH 110/146] Rename create() to connect() --- README.md | 30 +++++++++++++++--------------- src/Connector.php | 4 ++-- src/ConnectorInterface.php | 2 +- src/DnsConnector.php | 8 ++++---- src/SecureConnector.php | 8 ++++---- src/TcpConnector.php | 2 +- src/TimeoutConnector.php | 4 ++-- src/UnixConnector.php | 2 +- tests/DnsConnectorTest.php | 22 +++++++++++----------- tests/IntegrationTest.php | 10 +++++----- tests/SecureConnectorTest.php | 12 ++++++------ tests/SecureIntegrationTest.php | 16 ++++++++-------- tests/TcpConnectorTest.php | 14 +++++++------- tests/TimeoutConnectorTest.php | 24 ++++++++++++------------ tests/UnixConnectorTest.php | 4 ++-- 15 files changed, 81 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 204cac8f..32da5b0c 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ $loop = React\EventLoop\Factory::create(); ### Async TCP/IP connections The `React\SocketClient\TcpConnector` provides a single promise-based -`create($ip, $port)` method which resolves as soon as the connection +`connect($ip, $port)` method which resolves as soon as the connection succeeds or fails. ```php $tcpConnector = new React\SocketClient\TcpConnector($loop); -$tcpConnector->create('127.0.0.1', 80)->then(function (React\Stream\Stream $stream) { +$tcpConnector->connect('127.0.0.1', 80)->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->end(); }); @@ -50,7 +50,7 @@ See also the [first example](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $tcpConnector->create($host, $port); +$promise = $tcpConnector->connect($host, $port); $promise->cancel(); ``` @@ -78,18 +78,18 @@ The `DnsConnector` class decorates a given `TcpConnector` instance by first looking up the given domain name and then establishing the underlying TCP/IP connection to the resolved IP address. -It provides the same promise-based `create($host, $port)` method which resolves with +It provides the same promise-based `connect($host, $port)` method which resolves with a `Stream` instance that can be used just like above. Make sure to set up your DNS resolver and underlying TCP connector like this: ```php $dnsResolverFactory = new React\Dns\Resolver\Factory(); -$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); +$dns = $dnsResolverFactory->connectCached('8.8.8.8', $loop); $dnsConnector = new React\SocketClient\DnsConnector($tcpConnector, $dns); -$dnsConnector->create('www.google.com', 80)->then(function (React\Stream\Stream $stream) { +$dnsConnector->connect('www.google.com', 80)->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->end(); }); @@ -102,7 +102,7 @@ See also the [first example](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $dnsConnector->create($host, $port); +$promise = $dnsConnector->connect($host, $port); $promise->cancel(); ``` @@ -117,7 +117,7 @@ set up like this: ```php $connector = new React\SocketClient\Connector($loop, $dns); -$connector->create('www.google.com', 80)->then($callback); +$connector->connect('www.google.com', 80)->then($callback); ``` ### Async SSL/TLS connections @@ -125,13 +125,13 @@ $connector->create('www.google.com', 80)->then($callback); The `SecureConnector` class decorates a given `Connector` instance by enabling SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. -It provides the same promise- based `create($host, $port)` method which resolves with +It provides the same promise- based `connect($host, $port)` method which resolves with a `Stream` instance that can be used just like any non-encrypted stream: ```php $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop); -$secureConnector->create('www.google.com', 443)->then(function (React\Stream\Stream $stream) { +$secureConnector->connect('www.google.com', 443)->then(function (React\Stream\Stream $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); @@ -144,7 +144,7 @@ See also the [second example](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $secureConnector->create($host, $port); +$promise = $secureConnector->connect($host, $port); $promise->cancel(); ``` @@ -174,13 +174,13 @@ stream resources will use a single, shared *default context* resource otherwise. ### Connection timeouts The `TimeoutConnector` class decorates any given `Connector` instance. -It provides the same `create()` method, but will automatically reject the +It provides the same `connect()` method, but will automatically reject the underlying connection attempt if it takes too long. ```php $timeoutConnector = new React\SocketClient\TimeoutConnector($connector, 3.0, $loop); -$timeoutConnector->create('google.com', 80)->then(function (React\Stream\Stream $stream) { +$timeoutConnector->connect('google.com', 80)->then(function (React\Stream\Stream $stream) { // connection succeeded within 3.0 seconds }); ``` @@ -190,7 +190,7 @@ See also any of the [examples](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $timeoutConnector->create($host, $port); +$promise = $timeoutConnector->connect($host, $port); $promise->cancel(); ``` @@ -206,7 +206,7 @@ paths like this: ```php $connector = new React\SocketClient\UnixConnector($loop); -$connector->create('/tmp/demo.sock')->then(function (React\Stream\Stream $stream) { +$connector->connect('/tmp/demo.sock')->then(function (React\Stream\Stream $stream) { $stream->write("HELLO\n"); }); diff --git a/src/Connector.php b/src/Connector.php index 6cf991c0..d50c2229 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -17,8 +17,8 @@ public function __construct(LoopInterface $loop, Resolver $resolver) $this->connector = new DnsConnector(new TcpConnector($loop), $resolver); } - public function create($host, $port) + public function connect($host, $port) { - return $this->connector->create($host, $port); + return $this->connector->connect($host, $port); } } diff --git a/src/ConnectorInterface.php b/src/ConnectorInterface.php index b40b3a1b..6c6d8ad1 100644 --- a/src/ConnectorInterface.php +++ b/src/ConnectorInterface.php @@ -4,5 +4,5 @@ interface ConnectorInterface { - public function create($host, $port); + public function connect($host, $port); } diff --git a/src/DnsConnector.php b/src/DnsConnector.php index 44c2179b..d6979ec3 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -18,14 +18,14 @@ public function __construct(ConnectorInterface $connector, Resolver $resolver) $this->resolver = $resolver; } - public function create($host, $port) + public function connect($host, $port) { $that = $this; return $this ->resolveHostname($host) ->then(function ($ip) use ($that, $port) { - return $that->connect($ip, $port); + return $that->connectTcp($ip, $port); }); } @@ -55,9 +55,9 @@ function ($_, $reject) use ($promise) { } /** @internal */ - public function connect($ip, $port) + public function connectTcp($ip, $port) { - $promise = $this->connector->create($ip, $port); + $promise = $this->connector->connect($ip, $port); return new Promise\Promise( function ($resolve, $reject) use ($promise) { diff --git a/src/SecureConnector.php b/src/SecureConnector.php index b4ac22b5..5644b4b8 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -20,7 +20,7 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop, $this->context = $context; } - public function create($host, $port) + public function connect($host, $port) { if (!function_exists('stream_socket_enable_crypto')) { return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); @@ -40,7 +40,7 @@ public function create($host, $port) } $encryption = $this->streamEncryption; - return $this->connect($host, $port)->then(function (Stream $stream) use ($context, $encryption) { + return $this->connectTcp($host, $port)->then(function (Stream $stream) use ($context, $encryption) { // (unencrypted) TCP/IP connection succeeded // set required SSL/TLS context options @@ -57,9 +57,9 @@ public function create($host, $port) }); } - private function connect($host, $port) + private function connectTcp($host, $port) { - $promise = $this->connector->create($host, $port); + $promise = $this->connector->connect($host, $port); return new Promise\Promise( function ($resolve, $reject) use ($promise) { diff --git a/src/TcpConnector.php b/src/TcpConnector.php index fa64ed8c..809f3334 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -19,7 +19,7 @@ public function __construct(LoopInterface $loop, array $context = array()) $this->context = $context; } - public function create($ip, $port) + public function connect($ip, $port) { if (false === filter_var($ip, FILTER_VALIDATE_IP)) { return Promise\reject(new \InvalidArgumentException('Given parameter "' . $ip . '" is not a valid IP')); diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index c4cfd5e4..c9576556 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -22,9 +22,9 @@ public function __construct(ConnectorInterface $connector, $timeout, LoopInterfa $this->loop = $loop; } - public function create($host, $port) + public function connect($host, $port) { - $promise = $this->connector->create($host, $port); + $promise = $this->connector->connect($host, $port); return Timer\timeout(new Promise( function ($resolve, $reject) use ($promise) { diff --git a/src/UnixConnector.php b/src/UnixConnector.php index e12e7efa..b2351982 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -23,7 +23,7 @@ public function __construct(LoopInterface $loop) $this->loop = $loop; } - public function create($path, $unusedPort = 0) + public function connect($path, $unusedPort = 0) { $resource = @stream_socket_client('unix://' . $path, $errno, $errstr, 1.0); diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index f588dd64..b7bf1502 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -22,25 +22,25 @@ public function setUp() public function testPassByResolverIfGivenIp() { $this->resolver->expects($this->never())->method('resolve'); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('127.0.0.1'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('127.0.0.1'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); - $this->connector->create('127.0.0.1', 80); + $this->connector->connect('127.0.0.1', 80); } public function testPassThroughResolverIfGivenHost() { $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); - $this->connector->create('google.com', 80); + $this->connector->connect('google.com', 80); } public function testSkipConnectionIfDnsFails() { $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->will($this->returnValue(Promise\reject())); - $this->tcp->expects($this->never())->method('create'); + $this->tcp->expects($this->never())->method('connect'); - $this->connector->create('example.invalid', 80); + $this->connector->connect('example.invalid', 80); } public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() @@ -49,7 +49,7 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue($pending)); $this->tcp->expects($this->never())->method('resolve'); - $promise = $this->connector->create('example.com', 80); + $promise = $this->connector->connect('example.com', 80); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -59,9 +59,9 @@ public function testCancelDuringTcpConnectionCancelsTcpConnection() { $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); - $promise = $this->connector->create('example.com', 80); + $promise = $this->connector->connect('example.com', 80); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -77,9 +77,9 @@ public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() }); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); - $promise = $this->connector->create('example.com', 80); + $promise = $this->connector->connect('example.com', 80); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 0568dece..5795a47d 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -24,7 +24,7 @@ public function gettingStuffFromGoogleShouldWork() $dns = $factory->create('8.8.8.8', $loop); $connector = new Connector($loop, $dns); - $conn = Block\await($connector->create('google.com', 80), $loop); + $conn = Block\await($connector->connect('google.com', 80), $loop); $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -50,7 +50,7 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $loop ); - $conn = Block\await($secureConnector->create('google.com', 443), $loop); + $conn = Block\await($secureConnector->connect('google.com', 443), $loop); $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -80,7 +80,7 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() ); $this->setExpectedException('RuntimeException'); - Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop, self::TIMEOUT); + Block\await($secureConnector->connect('self-signed.badssl.com', 443), $loop, self::TIMEOUT); } /** @test */ @@ -103,7 +103,7 @@ public function testSelfSignedResolvesIfVerificationIsDisabled() ) ); - $conn = Block\await($secureConnector->create('self-signed.badssl.com', 443), $loop, self::TIMEOUT); + $conn = Block\await($secureConnector->connect('self-signed.badssl.com', 443), $loop, self::TIMEOUT); $conn->close(); } @@ -112,7 +112,7 @@ public function testCancelPendingConnection() $loop = new StreamSelectLoop(); $connector = new TcpConnector($loop); - $pending = $connector->create('8.8.8.8', 80); + $pending = $connector->connect('8.8.8.8', 80); $loop->addTimer(0.001, function () use ($pending) { $pending->cancel(); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 9b2a6c9c..e81936f8 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -25,9 +25,9 @@ public function setUp() public function testConnectionWillWaitForTcpConnection() { $pending = new Promise\Promise(function () { }); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); - $promise = $this->connector->create('example.com', 80); + $promise = $this->connector->connect('example.com', 80); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); } @@ -35,9 +35,9 @@ public function testConnectionWillWaitForTcpConnection() public function testCancelDuringTcpConnectionCancelsTcpConnection() { $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); - $promise = $this->connector->create('example.com', 80); + $promise = $this->connector->connect('example.com', 80); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -52,9 +52,9 @@ public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() $resolve($stream); }); - $this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); - $promise = $this->connector->create('example.com', 80); + $promise = $this->connector->connect('example.com', 80); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index ef85dad3..87d23087 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -53,7 +53,7 @@ public function tearDown() public function testConnectToServer() { - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->close(); @@ -63,7 +63,7 @@ public function testConnectToServerEmitsConnection() { $promiseServer = $this->createPromiseForEvent($this->server, 'connection', $this->expectCallableOnce()); - $promiseClient = $this->connector->create('127.0.0.1', $this->portSecure); + $promiseClient = $this->connector->connect('127.0.0.1', $this->portSecure); list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop, self::TIMEOUT); /* @var $client Stream */ @@ -81,7 +81,7 @@ public function testSendSmallDataToServerReceivesOneChunk() }); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->write('hello'); @@ -107,7 +107,7 @@ public function testSendDataWithEndToServerReceivesAllData() }); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('a', 200000); @@ -128,7 +128,7 @@ public function testSendDataWithoutEndingToServerReceivesAllData() }); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('d', 200000); @@ -148,7 +148,7 @@ public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() $peer->write('hello'); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await client to report one "data" event @@ -165,7 +165,7 @@ public function testConnectToServerWhichSendsDataWithEndReceivesAllData() $peer->end($data); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await data from client until it closes @@ -181,7 +181,7 @@ public function testConnectToServerWhichSendsDataWithoutEndingReceivesAllData() $peer->write($data); }); - $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop); /* @var $client Stream */ // buffer incoming data for 0.1s (should be plenty of time) diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 964297df..72770a33 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -17,7 +17,7 @@ public function connectionToEmptyPortShouldFail() $loop = new StreamSelectLoop(); $connector = new TcpConnector($loop); - $connector->create('127.0.0.1', 9999) + $connector->connect('127.0.0.1', 9999) ->then($this->expectCallableNever(), $this->expectCallableOnce()); $loop->run(); @@ -37,7 +37,7 @@ public function connectionToTcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->create('127.0.0.1', 9999), $loop, self::TIMEOUT); + $stream = Block\await($connector->connect('127.0.0.1', 9999), $loop, self::TIMEOUT); $this->assertInstanceOf('React\Stream\Stream', $stream); @@ -51,7 +51,7 @@ public function connectionToEmptyIp6PortShouldFail() $connector = new TcpConnector($loop); $connector - ->create('::1', 9999) + ->connect('::1', 9999) ->then($this->expectCallableNever(), $this->expectCallableOnce()); $loop->run(); @@ -69,7 +69,7 @@ public function connectionToIp6TcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->create('::1', 9999), $loop, self::TIMEOUT); + $stream = Block\await($connector->connect('::1', 9999), $loop, self::TIMEOUT); $this->assertInstanceOf('React\Stream\Stream', $stream); @@ -82,7 +82,7 @@ public function connectionToHostnameShouldFailImmediately() $loop = $this->getMock('React\EventLoop\LoopInterface'); $connector = new TcpConnector($loop); - $connector->create('www.google.com', 80)->then( + $connector->connect('www.google.com', 80)->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -94,7 +94,7 @@ public function connectionToInvalidAddressShouldFailImmediately() $loop = $this->getMock('React\EventLoop\LoopInterface'); $connector = new TcpConnector($loop); - $connector->create('255.255.255.255', 12345678)->then( + $connector->connect('255.255.255.255', 12345678)->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -109,7 +109,7 @@ public function cancellingConnectionShouldRejectPromise() $server = new Server($loop); $server->listen(0); - $promise = $connector->create('127.0.0.1', $server->getPort()); + $promise = $connector->connect('127.0.0.1', $server->getPort()); $promise->cancel(); $this->setExpectedException('RuntimeException', 'Cancelled'); diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index 2cb09698..59486cd7 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -13,13 +13,13 @@ public function testRejectsOnTimeout() $promise = new Promise\Promise(function () { }); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $timeout->create('google.com', 80)->then( + $timeout->connect('google.com', 80)->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -32,13 +32,13 @@ public function testRejectsWhenConnectorRejects() $promise = Promise\reject(new \RuntimeException()); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 5.0, $loop); - $timeout->create('google.com', 80)->then( + $timeout->connect('google.com', 80)->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -51,13 +51,13 @@ public function testResolvesWhenConnectorResolves() $promise = Promise\resolve(); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 5.0, $loop); - $timeout->create('google.com', 80)->then( + $timeout->connect('google.com', 80)->then( $this->expectCallableOnce(), $this->expectCallableNever() ); @@ -70,13 +70,13 @@ public function testRejectsAndCancelsPendingPromiseOnTimeout() $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $timeout->create('google.com', 80)->then( + $timeout->connect('google.com', 80)->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -89,13 +89,13 @@ public function testCancelsPendingPromiseOnCancel() $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $out = $timeout->create('google.com', 80); + $out = $timeout->connect('google.com', 80); $out->cancel(); $out->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -111,13 +111,13 @@ public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() }); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('create')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $out = $timeout->create('google.com', 80); + $out = $timeout->connect('google.com', 80); $out->cancel(); $out->then($this->expectCallableNever(), $this->expectCallableOnce()); diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index 4070aede..574afa4e 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -17,7 +17,7 @@ public function setUp() public function testInvalid() { - $promise = $this->connector->create('google.com', 80); + $promise = $this->connector->connect('google.com', 80); $promise->then(null, $this->expectCallableOnce()); } @@ -36,7 +36,7 @@ public function testValid() } // tests succeeds if we get notified of successful connection - $promise = $this->connector->create($path, 0); + $promise = $this->connector->connect($path, 0); $promise->then($this->expectCallableOnce()); // clean up server From 89ae4f6031964e5f797d5d1edbc8b23ad559eb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 25 Nov 2015 21:33:07 +0100 Subject: [PATCH 111/146] Use string URIs instead of host and port --- README.md | 26 +++++++++++++------------- src/Connector.php | 4 ++-- src/ConnectorInterface.php | 9 ++++++++- src/DnsConnector.php | 24 +++++++++++++++++++----- src/SecureConnector.php | 10 ++++++---- src/TcpConnector.php | 25 ++++++++++--------------- src/TimeoutConnector.php | 4 ++-- src/UnixConnector.php | 2 +- tests/DnsConnectorTest.php | 20 ++++++++++---------- tests/IntegrationTest.php | 10 +++++----- tests/SecureConnectorTest.php | 12 ++++++------ tests/SecureIntegrationTest.php | 16 ++++++++-------- tests/TcpConnectorTest.php | 14 +++++++------- tests/TimeoutConnectorTest.php | 24 ++++++++++++------------ tests/UnixConnectorTest.php | 4 ++-- 15 files changed, 111 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 32da5b0c..7f4ceb28 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ $loop = React\EventLoop\Factory::create(); ### Async TCP/IP connections The `React\SocketClient\TcpConnector` provides a single promise-based -`connect($ip, $port)` method which resolves as soon as the connection +`connect($uri)` method which resolves as soon as the connection succeeds or fails. ```php $tcpConnector = new React\SocketClient\TcpConnector($loop); -$tcpConnector->connect('127.0.0.1', 80)->then(function (React\Stream\Stream $stream) { +$tcpConnector->connect('127.0.0.1:80')->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->end(); }); @@ -50,7 +50,7 @@ See also the [first example](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $tcpConnector->connect($host, $port); +$promise = $tcpConnector->connect('127.0.0.1:80'); $promise->cancel(); ``` @@ -78,7 +78,7 @@ The `DnsConnector` class decorates a given `TcpConnector` instance by first looking up the given domain name and then establishing the underlying TCP/IP connection to the resolved IP address. -It provides the same promise-based `connect($host, $port)` method which resolves with +It provides the same promise-based `connect($uri)` method which resolves with a `Stream` instance that can be used just like above. Make sure to set up your DNS resolver and underlying TCP connector like this: @@ -89,7 +89,7 @@ $dns = $dnsResolverFactory->connectCached('8.8.8.8', $loop); $dnsConnector = new React\SocketClient\DnsConnector($tcpConnector, $dns); -$dnsConnector->connect('www.google.com', 80)->then(function (React\Stream\Stream $stream) { +$dnsConnector->connect('www.google.com:80')->then(function (React\Stream\Stream $stream) { $stream->write('...'); $stream->end(); }); @@ -102,7 +102,7 @@ See also the [first example](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $dnsConnector->connect($host, $port); +$promise = $dnsConnector->connect('www.google.com:80'); $promise->cancel(); ``` @@ -117,7 +117,7 @@ set up like this: ```php $connector = new React\SocketClient\Connector($loop, $dns); -$connector->connect('www.google.com', 80)->then($callback); +$connector->connect('www.google.com:80')->then($callback); ``` ### Async SSL/TLS connections @@ -125,13 +125,13 @@ $connector->connect('www.google.com', 80)->then($callback); The `SecureConnector` class decorates a given `Connector` instance by enabling SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. -It provides the same promise- based `connect($host, $port)` method which resolves with +It provides the same promise- based `connect($uri)` method which resolves with a `Stream` instance that can be used just like any non-encrypted stream: ```php $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop); -$secureConnector->connect('www.google.com', 443)->then(function (React\Stream\Stream $stream) { +$secureConnector->connect('www.google.com:443')->then(function (React\Stream\Stream $stream) { $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); @@ -144,7 +144,7 @@ See also the [second example](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $secureConnector->connect($host, $port); +$promise = $secureConnector->connect('www.google.com:443'); $promise->cancel(); ``` @@ -174,13 +174,13 @@ stream resources will use a single, shared *default context* resource otherwise. ### Connection timeouts The `TimeoutConnector` class decorates any given `Connector` instance. -It provides the same `connect()` method, but will automatically reject the +It provides the same `connect($uri)` method, but will automatically reject the underlying connection attempt if it takes too long. ```php $timeoutConnector = new React\SocketClient\TimeoutConnector($connector, 3.0, $loop); -$timeoutConnector->connect('google.com', 80)->then(function (React\Stream\Stream $stream) { +$timeoutConnector->connect('google.com:80')->then(function (React\Stream\Stream $stream) { // connection succeeded within 3.0 seconds }); ``` @@ -190,7 +190,7 @@ See also any of the [examples](examples). Pending connection attempts can be cancelled by cancelling its pending promise like so: ```php -$promise = $timeoutConnector->connect($host, $port); +$promise = $timeoutConnector->connect('google.com:80'); $promise->cancel(); ``` diff --git a/src/Connector.php b/src/Connector.php index d50c2229..79d5f098 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -17,8 +17,8 @@ public function __construct(LoopInterface $loop, Resolver $resolver) $this->connector = new DnsConnector(new TcpConnector($loop), $resolver); } - public function connect($host, $port) + public function connect($uri) { - return $this->connector->connect($host, $port); + return $this->connector->connect($uri); } } diff --git a/src/ConnectorInterface.php b/src/ConnectorInterface.php index 6c6d8ad1..00360862 100644 --- a/src/ConnectorInterface.php +++ b/src/ConnectorInterface.php @@ -4,5 +4,12 @@ interface ConnectorInterface { - public function connect($host, $port); + /** + * + * + * @param string $uri + * @return Promise Returns a Promise<\React\Stream\Stream, \Exception>, i.e. + * it either resolves with a Stream instance or rejects with an Exception. + */ + public function connect($uri); } diff --git a/src/DnsConnector.php b/src/DnsConnector.php index d6979ec3..9680e50d 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -18,14 +18,28 @@ public function __construct(ConnectorInterface $connector, Resolver $resolver) $this->resolver = $resolver; } - public function connect($host, $port) + public function connect($uri) { $that = $this; + $parts = parse_url('tcp://' . $uri); + if (!$parts || !isset($parts['host'], $parts['port'])) { + return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + } + + $host = trim($parts['host'], '[]'); + return $this ->resolveHostname($host) - ->then(function ($ip) use ($that, $port) { - return $that->connectTcp($ip, $port); + ->then(function ($ip) use ($that, $parts) { + if (strpos($ip, ':') !== false) { + // enclose IPv6 addresses in square brackets before appending port + $ip = '[' . $ip . ']'; + } + + return $that->connectTcp( + $ip . ':' . $parts['port'] + ); }); } @@ -55,9 +69,9 @@ function ($_, $reject) use ($promise) { } /** @internal */ - public function connectTcp($ip, $port) + public function connectTcp($uri) { - $promise = $this->connector->connect($ip, $port); + $promise = $this->connector->connect($uri); return new Promise\Promise( function ($resolve, $reject) use ($promise) { diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 5644b4b8..c0cb351f 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -20,12 +20,14 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop, $this->context = $context; } - public function connect($host, $port) + public function connect($uri) { if (!function_exists('stream_socket_enable_crypto')) { return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); } + $host = trim(parse_url('tcp://' . $uri, PHP_URL_HOST), '[]'); + $context = $this->context + array( 'SNI_enabled' => true, 'peer_name' => $host @@ -40,7 +42,7 @@ public function connect($host, $port) } $encryption = $this->streamEncryption; - return $this->connectTcp($host, $port)->then(function (Stream $stream) use ($context, $encryption) { + return $this->connectTcp($uri)->then(function (Stream $stream) use ($context, $encryption) { // (unencrypted) TCP/IP connection succeeded // set required SSL/TLS context options @@ -57,9 +59,9 @@ public function connect($host, $port) }); } - private function connectTcp($host, $port) + private function connectTcp($uri) { - $promise = $this->connector->connect($host, $port); + $promise = $this->connector->connect($uri); return new Promise\Promise( function ($resolve, $reject) use ($promise) { diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 809f3334..3bf5ca4f 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -19,16 +19,20 @@ public function __construct(LoopInterface $loop, array $context = array()) $this->context = $context; } - public function connect($ip, $port) + public function connect($uri) { - if (false === filter_var($ip, FILTER_VALIDATE_IP)) { - return Promise\reject(new \InvalidArgumentException('Given parameter "' . $ip . '" is not a valid IP')); + $parts = parse_url('tcp://' . $uri); + if (!$parts || !isset($parts['host'], $parts['port'])) { + return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); } + $ip = trim($parts['host'], '[]'); - $url = $this->getSocketUrl($ip, $port); + if (false === filter_var($ip, FILTER_VALIDATE_IP)) { + return Promise\reject(new \InvalidArgumentException('Given URI "' . $ip . '" does not contain a valid host IP')); + } $socket = @stream_socket_client( - $url, + $uri, $errno, $errstr, 0, @@ -38,7 +42,7 @@ public function connect($ip, $port) if (false === $socket) { return Promise\reject(new \RuntimeException( - sprintf("Connection to %s:%d failed: %s", $ip, $port, $errstr), + sprintf("Connection to %s failed: %s", $uri, $errstr), $errno )); } @@ -90,13 +94,4 @@ public function handleConnectedSocket($socket) { return new Stream($socket, $this->loop); } - - private function getSocketUrl($ip, $port) - { - if (strpos($ip, ':') !== false) { - // enclose IPv6 addresses in square brackets before appending port - $ip = '[' . $ip . ']'; - } - return sprintf('tcp://%s:%s', $ip, $port); - } } diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index c9576556..8088f83a 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -22,9 +22,9 @@ public function __construct(ConnectorInterface $connector, $timeout, LoopInterfa $this->loop = $loop; } - public function connect($host, $port) + public function connect($uri) { - $promise = $this->connector->connect($host, $port); + $promise = $this->connector->connect($uri); return Timer\timeout(new Promise( function ($resolve, $reject) use ($promise) { diff --git a/src/UnixConnector.php b/src/UnixConnector.php index b2351982..74ea3ae8 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -23,7 +23,7 @@ public function __construct(LoopInterface $loop) $this->loop = $loop; } - public function connect($path, $unusedPort = 0) + public function connect($path) { $resource = @stream_socket_client('unix://' . $path, $errno, $errstr, 1.0); diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index b7bf1502..0ac542cf 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -22,17 +22,17 @@ public function setUp() public function testPassByResolverIfGivenIp() { $this->resolver->expects($this->never())->method('resolve'); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('127.0.0.1'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('127.0.0.1:80'))->will($this->returnValue(Promise\reject())); - $this->connector->connect('127.0.0.1', 80); + $this->connector->connect('127.0.0.1:80'); } public function testPassThroughResolverIfGivenHost() { $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->will($this->returnValue(Promise\reject())); - $this->connector->connect('google.com', 80); + $this->connector->connect('google.com:80'); } public function testSkipConnectionIfDnsFails() @@ -40,7 +40,7 @@ public function testSkipConnectionIfDnsFails() $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->will($this->returnValue(Promise\reject())); $this->tcp->expects($this->never())->method('connect'); - $this->connector->connect('example.invalid', 80); + $this->connector->connect('example.invalid:80'); } public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() @@ -49,7 +49,7 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue($pending)); $this->tcp->expects($this->never())->method('resolve'); - $promise = $this->connector->connect('example.com', 80); + $promise = $this->connector->connect('example.com:80'); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -59,9 +59,9 @@ public function testCancelDuringTcpConnectionCancelsTcpConnection() { $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->will($this->returnValue($pending)); - $promise = $this->connector->connect('example.com', 80); + $promise = $this->connector->connect('example.com:80'); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -77,9 +77,9 @@ public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() }); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->will($this->returnValue($pending)); - $promise = $this->connector->connect('example.com', 80); + $promise = $this->connector->connect('example.com:80'); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 5795a47d..4c64973e 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -24,7 +24,7 @@ public function gettingStuffFromGoogleShouldWork() $dns = $factory->create('8.8.8.8', $loop); $connector = new Connector($loop, $dns); - $conn = Block\await($connector->connect('google.com', 80), $loop); + $conn = Block\await($connector->connect('google.com:80'), $loop); $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -50,7 +50,7 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $loop ); - $conn = Block\await($secureConnector->connect('google.com', 443), $loop); + $conn = Block\await($secureConnector->connect('google.com:443'), $loop); $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -80,7 +80,7 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() ); $this->setExpectedException('RuntimeException'); - Block\await($secureConnector->connect('self-signed.badssl.com', 443), $loop, self::TIMEOUT); + Block\await($secureConnector->connect('self-signed.badssl.com:443'), $loop, self::TIMEOUT); } /** @test */ @@ -103,7 +103,7 @@ public function testSelfSignedResolvesIfVerificationIsDisabled() ) ); - $conn = Block\await($secureConnector->connect('self-signed.badssl.com', 443), $loop, self::TIMEOUT); + $conn = Block\await($secureConnector->connect('self-signed.badssl.com:443'), $loop, self::TIMEOUT); $conn->close(); } @@ -112,7 +112,7 @@ public function testCancelPendingConnection() $loop = new StreamSelectLoop(); $connector = new TcpConnector($loop); - $pending = $connector->connect('8.8.8.8', 80); + $pending = $connector->connect('8.8.8.8:80'); $loop->addTimer(0.001, function () use ($pending) { $pending->cancel(); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index e81936f8..0d3346ed 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -25,9 +25,9 @@ public function setUp() public function testConnectionWillWaitForTcpConnection() { $pending = new Promise\Promise(function () { }); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); - $promise = $this->connector->connect('example.com', 80); + $promise = $this->connector->connect('example.com:80'); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); } @@ -35,9 +35,9 @@ public function testConnectionWillWaitForTcpConnection() public function testCancelDuringTcpConnectionCancelsTcpConnection() { $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); - $promise = $this->connector->connect('example.com', 80); + $promise = $this->connector->connect('example.com:80'); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -52,9 +52,9 @@ public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() $resolve($stream); }); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); - $promise = $this->connector->connect('example.com', 80); + $promise = $this->connector->connect('example.com:80'); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index 87d23087..279083d5 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -53,7 +53,7 @@ public function tearDown() public function testConnectToServer() { - $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->close(); @@ -63,7 +63,7 @@ public function testConnectToServerEmitsConnection() { $promiseServer = $this->createPromiseForEvent($this->server, 'connection', $this->expectCallableOnce()); - $promiseClient = $this->connector->connect('127.0.0.1', $this->portSecure); + $promiseClient = $this->connector->connect('127.0.0.1:' . $this->portSecure); list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop, self::TIMEOUT); /* @var $client Stream */ @@ -81,7 +81,7 @@ public function testSendSmallDataToServerReceivesOneChunk() }); }); - $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->write('hello'); @@ -107,7 +107,7 @@ public function testSendDataWithEndToServerReceivesAllData() }); }); - $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('a', 200000); @@ -128,7 +128,7 @@ public function testSendDataWithoutEndingToServerReceivesAllData() }); }); - $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('d', 200000); @@ -148,7 +148,7 @@ public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() $peer->write('hello'); }); - $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await client to report one "data" event @@ -165,7 +165,7 @@ public function testConnectToServerWhichSendsDataWithEndReceivesAllData() $peer->end($data); }); - $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await data from client until it closes @@ -181,7 +181,7 @@ public function testConnectToServerWhichSendsDataWithoutEndingReceivesAllData() $peer->write($data); }); - $client = Block\await($this->connector->connect('127.0.0.1', $this->portSecure), $this->loop); + $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop); /* @var $client Stream */ // buffer incoming data for 0.1s (should be plenty of time) diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 72770a33..5be71534 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -17,7 +17,7 @@ public function connectionToEmptyPortShouldFail() $loop = new StreamSelectLoop(); $connector = new TcpConnector($loop); - $connector->connect('127.0.0.1', 9999) + $connector->connect('127.0.0.1:9999') ->then($this->expectCallableNever(), $this->expectCallableOnce()); $loop->run(); @@ -37,7 +37,7 @@ public function connectionToTcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->connect('127.0.0.1', 9999), $loop, self::TIMEOUT); + $stream = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); $this->assertInstanceOf('React\Stream\Stream', $stream); @@ -51,7 +51,7 @@ public function connectionToEmptyIp6PortShouldFail() $connector = new TcpConnector($loop); $connector - ->connect('::1', 9999) + ->connect('[::1]:9999') ->then($this->expectCallableNever(), $this->expectCallableOnce()); $loop->run(); @@ -69,7 +69,7 @@ public function connectionToIp6TcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->connect('::1', 9999), $loop, self::TIMEOUT); + $stream = Block\await($connector->connect('[::1]:9999'), $loop, self::TIMEOUT); $this->assertInstanceOf('React\Stream\Stream', $stream); @@ -82,7 +82,7 @@ public function connectionToHostnameShouldFailImmediately() $loop = $this->getMock('React\EventLoop\LoopInterface'); $connector = new TcpConnector($loop); - $connector->connect('www.google.com', 80)->then( + $connector->connect('www.google.com:80')->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -94,7 +94,7 @@ public function connectionToInvalidAddressShouldFailImmediately() $loop = $this->getMock('React\EventLoop\LoopInterface'); $connector = new TcpConnector($loop); - $connector->connect('255.255.255.255', 12345678)->then( + $connector->connect('255.255.255.255:12345678')->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -109,7 +109,7 @@ public function cancellingConnectionShouldRejectPromise() $server = new Server($loop); $server->listen(0); - $promise = $connector->connect('127.0.0.1', $server->getPort()); + $promise = $connector->connect('127.0.0.1:' . $server->getPort()); $promise->cancel(); $this->setExpectedException('RuntimeException', 'Cancelled'); diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index 59486cd7..d00f5018 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -13,13 +13,13 @@ public function testRejectsOnTimeout() $promise = new Promise\Promise(function () { }); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $timeout->connect('google.com', 80)->then( + $timeout->connect('google.com:80')->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -32,13 +32,13 @@ public function testRejectsWhenConnectorRejects() $promise = Promise\reject(new \RuntimeException()); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 5.0, $loop); - $timeout->connect('google.com', 80)->then( + $timeout->connect('google.com:80')->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -51,13 +51,13 @@ public function testResolvesWhenConnectorResolves() $promise = Promise\resolve(); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 5.0, $loop); - $timeout->connect('google.com', 80)->then( + $timeout->connect('google.com:80')->then( $this->expectCallableOnce(), $this->expectCallableNever() ); @@ -70,13 +70,13 @@ public function testRejectsAndCancelsPendingPromiseOnTimeout() $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $timeout->connect('google.com', 80)->then( + $timeout->connect('google.com:80')->then( $this->expectCallableNever(), $this->expectCallableOnce() ); @@ -89,13 +89,13 @@ public function testCancelsPendingPromiseOnCancel() $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $out = $timeout->connect('google.com', 80); + $out = $timeout->connect('google.com:80'); $out->cancel(); $out->then($this->expectCallableNever(), $this->expectCallableOnce()); @@ -111,13 +111,13 @@ public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() }); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('connect')->with('google.com', 80)->will($this->returnValue($promise)); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); $timeout = new TimeoutConnector($connector, 0.01, $loop); - $out = $timeout->connect('google.com', 80); + $out = $timeout->connect('google.com:80'); $out->cancel(); $out->then($this->expectCallableNever(), $this->expectCallableOnce()); diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index 574afa4e..af80c4af 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -17,7 +17,7 @@ public function setUp() public function testInvalid() { - $promise = $this->connector->connect('google.com', 80); + $promise = $this->connector->connect('google.com:80'); $promise->then(null, $this->expectCallableOnce()); } @@ -36,7 +36,7 @@ public function testValid() } // tests succeeds if we get notified of successful connection - $promise = $this->connector->connect($path, 0); + $promise = $this->connector->connect($path); $promise->then($this->expectCallableOnce()); // clean up server From 5414f9847a1aa1927fd454070111c357d38f8111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 29 Nov 2016 11:05:42 +0100 Subject: [PATCH 112/146] Preserve all components from URI when passing through --- src/DnsConnector.php | 46 +++++++++++++++++++++++++++++------ src/SecureConnector.php | 12 ++++++++- src/TcpConnector.php | 10 +++++--- src/UnixConnector.php | 8 +++++- tests/DnsConnectorTest.php | 26 ++++++++++++++++++++ tests/SecureConnectorTest.php | 17 +++++++++++++ tests/TcpConnectorTest.php | 28 ++++++++++++++++++++- tests/UnixConnectorTest.php | 6 +++++ 8 files changed, 140 insertions(+), 13 deletions(-) diff --git a/src/DnsConnector.php b/src/DnsConnector.php index 9680e50d..21a4623b 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -20,26 +20,58 @@ public function __construct(ConnectorInterface $connector, Resolver $resolver) public function connect($uri) { - $that = $this; + if (strpos($uri, '://') === false) { + $parts = parse_url('tcp://' . $uri); + unset($parts['scheme']); + } else { + $parts = parse_url($uri); + } - $parts = parse_url('tcp://' . $uri); - if (!$parts || !isset($parts['host'], $parts['port'])) { + if (!$parts || !isset($parts['host'])) { return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); } + $that = $this; $host = trim($parts['host'], '[]'); return $this ->resolveHostname($host) ->then(function ($ip) use ($that, $parts) { + $uri = ''; + + // prepend original scheme if known + if (isset($parts['scheme'])) { + $uri .= $parts['scheme'] . '://'; + } + if (strpos($ip, ':') !== false) { // enclose IPv6 addresses in square brackets before appending port - $ip = '[' . $ip . ']'; + $uri .= '[' . $ip . ']'; + } else { + $uri .= $ip; + } + + // append original port if known + if (isset($parts['port'])) { + $uri .= ':' . $parts['port']; + } + + // append orignal path if known + if (isset($parts['path'])) { + $uri .= $parts['path']; + } + + // append original query if known + if (isset($parts['query'])) { + $uri .= '?' . $parts['query']; + } + + // append original fragment if known + if (isset($parts['fragment'])) { + $uri .= '#' . $parts['fragment']; } - return $that->connectTcp( - $ip . ':' . $parts['port'] - ); + return $that->connectTcp($uri); }); } diff --git a/src/SecureConnector.php b/src/SecureConnector.php index c0cb351f..bf06064d 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -26,7 +26,17 @@ public function connect($uri) return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); } - $host = trim(parse_url('tcp://' . $uri, PHP_URL_HOST), '[]'); + if (strpos($uri, '://') === false) { + $uri = 'tls://' . $uri; + } + + $parts = parse_url($uri); + if (!$parts || !isset($parts['host']) || $parts['scheme'] !== 'tls') { + return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + } + + $uri = str_replace('tls://', '', $uri); + $host = trim($parts['host'], '[]'); $context = $this->context + array( 'SNI_enabled' => true, diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 3bf5ca4f..d13f02ee 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -21,12 +21,16 @@ public function __construct(LoopInterface $loop, array $context = array()) public function connect($uri) { - $parts = parse_url('tcp://' . $uri); - if (!$parts || !isset($parts['host'], $parts['port'])) { + if (strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + } + + $parts = parse_url($uri); + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); } - $ip = trim($parts['host'], '[]'); + $ip = trim($parts['host'], '[]'); if (false === filter_var($ip, FILTER_VALIDATE_IP)) { return Promise\reject(new \InvalidArgumentException('Given URI "' . $ip . '" does not contain a valid host IP')); } diff --git a/src/UnixConnector.php b/src/UnixConnector.php index 74ea3ae8..44d225a0 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -25,7 +25,13 @@ public function __construct(LoopInterface $loop) public function connect($path) { - $resource = @stream_socket_client('unix://' . $path, $errno, $errstr, 1.0); + if (strpos($path, '://') === false) { + $path = 'unix://' . $path; + } elseif (substr($path, 0, 7) !== 'unix://') { + return Promise\reject(new \InvalidArgumentException('Given URI "' . $path . '" is invalid')); + } + + $resource = @stream_socket_client($path, $errno, $errstr, 1.0); if (!$resource) { return Promise\reject(new RuntimeException('Unable to connect to unix domain socket "' . $path . '": ' . $errstr, $errno)); diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index 0ac542cf..737ed9a8 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -35,6 +35,32 @@ public function testPassThroughResolverIfGivenHost() $this->connector->connect('google.com:80'); } + public function testPassThroughResolverIfGivenHostWhichResolvesToIpv6() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('::1'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('[::1]:80'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('google.com:80'); + } + + public function testPassByResolverIfGivenCompleteUri() + { + $this->resolver->expects($this->never())->method('resolve'); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://127.0.0.1:80/path?query#fragment'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('scheme://127.0.0.1:80/path?query#fragment'); + } + + public function testRejectsImmediatelyIfUriIsInvalid() + { + $this->resolver->expects($this->never())->method('resolve'); + $this->tcp->expects($this->never())->method('connect'); + + $promise = $this->connector->connect('////'); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + public function testSkipConnectionIfDnsFails() { $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->will($this->returnValue(Promise\reject())); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 0d3346ed..1756f431 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -32,6 +32,23 @@ public function testConnectionWillWaitForTcpConnection() $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); } + public function testConnectionWithCompleteUriWillBePassedThroughExpectForScheme() + { + $pending = new Promise\Promise(function () { }); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80/path?query#fragment'))->will($this->returnValue($pending)); + + $this->connector->connect('tls://example.com:80/path?query#fragment'); + } + + public function testConnectionToInvalidSchemeWillReject() + { + $this->tcp->expects($this->never())->method('connect'); + + $promise = $this->connector->connect('tcp://example.com:80'); + + $promise->then(null, $this->expectCallableOnce()); + } + public function testCancelDuringTcpConnectionCancelsTcpConnection() { $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 5be71534..01d54a39 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -89,7 +89,7 @@ public function connectionToHostnameShouldFailImmediately() } /** @test */ - public function connectionToInvalidAddressShouldFailImmediately() + public function connectionToInvalidPortShouldFailImmediately() { $loop = $this->getMock('React\EventLoop\LoopInterface'); @@ -100,6 +100,32 @@ public function connectionToInvalidAddressShouldFailImmediately() ); } + /** @test */ + public function connectionToInvalidSchemeShouldFailImmediately() + { + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $connector = new TcpConnector($loop); + $connector->connect('tls://google.com:443')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } + + /** @test */ + public function connectionWithInvalidContextShouldFailImmediately() + { + $this->markTestIncomplete(); + + $loop = $this->getMock('React\EventLoop\LoopInterface'); + + $connector = new TcpConnector($loop, array('bindto' => 'invalid.invalid:123456')); + $connector->connect('127.0.0.1:80')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } + /** @test */ public function cancellingConnectionShouldRejectPromise() { diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index af80c4af..9ade7204 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -21,6 +21,12 @@ public function testInvalid() $promise->then(null, $this->expectCallableOnce()); } + public function testInvalidScheme() + { + $promise = $this->connector->connect('tcp://google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + public function testValid() { // random unix domain socket path From 6c5560be7d19992f2eb385ef442889ea73e80ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 20 Dec 2016 00:23:16 +0100 Subject: [PATCH 113/146] Prepare v0.5.2 release --- CHANGELOG.md | 8 ++++++++ README.md | 10 ++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af4068f..4961c2b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.2 (2016-12-19) + +* Feature: Replace `SecureStream` with unlimited read buffer from react/stream v0.4.5 + (#72 by @clue) + +* Feature: Add examples + (#75 by @clue) + ## 0.5.1 (2016-11-20) * Feature: Support Promise cancellation for all connectors diff --git a/README.md b/README.md index 204cac8f..4ee7e0d9 100644 --- a/README.md +++ b/README.md @@ -225,16 +225,10 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/socket-client:^0.5.1 +$ composer require react/socket-client:^0.5.2 ``` -If you care a lot about BC, you may also want to look into supporting legacy versions: - -```bash -$ composer require "react/socket-client:^0.5||^0.4||^0.3" -``` - -More details and upgrade guides can be found in the [CHANGELOG](CHANGELOG.md). +More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). ## Tests From 867a137cda829ced85cba2cb5f632098978724cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 23 Dec 2016 14:29:06 +0100 Subject: [PATCH 114/146] Skip IPv6 tests if not supported by the system --- tests/TcpConnectorTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 964297df..f23479c3 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -65,7 +65,12 @@ public function connectionToIp6TcpServerShouldSucceed() $server = new Server($loop); $server->on('connection', $this->expectCallableOnce()); $server->on('connection', array($server, 'shutdown')); - $server->listen(9999, '::1'); + + try { + $server->listen(9999, '::1'); + } catch (\Exception $e) { + $this->markTestSkipped('Unable to start IPv6 server socket (IPv6 not supported on this system?)'); + } $connector = new TcpConnector($loop); From cf56ed9541386256b119d0b1f1a5482a5f858b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 21 Dec 2016 12:04:27 +0100 Subject: [PATCH 115/146] Documentation for ConnectorInterface --- README.md | 46 ++++++++++++++++++++++++++++++++++++++ src/Connector.php | 7 ++++++ src/ConnectorInterface.php | 30 +++++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/README.md b/README.md index 4ee7e0d9..472d06ac 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,52 @@ to initialize the main loop. $loop = React\EventLoop\Factory::create(); ``` +### ConnectorInterface + +The `ConnectorInterface` is responsible for providing an interface for +establishing streaming connections, such as a normal TCP/IP connection. + +This is the main interface defined in this package and it is used throughout +React's vast ecosystem. + +Most higher-level components (such as HTTP, database or other networking +service clients) accept an instance implementing this interface to create their +TCP/IP connection to the underlying networking service. +This is usually done via dependency injection, so it's fairly simple to actually +swap this implementation against any other implementation of this interface. + +The interface only offers a single method: + +#### create() + +The `create(string $host, int $port): PromiseInterface` method +can be used to establish a streaming connection. +It returns a [Promise](https://github.com/reactphp/promise) which either +fulfills with a [Stream](https://github.com/reactphp/stream) or +rejects with an `Exception`: + +```php +$connector->create('google.com', 443)->then( + function (Stream $stream) { + // connection successfully established + }, + function (Exception $error) { + // failed to connect due to $error + } +); +``` + +The returned Promise SHOULD be implemented in such a way that it can be +cancelled when it is still pending. Cancelling a pending promise SHOULD +reject its value with an `Exception`. It SHOULD clean up any underlying +resources and references as applicable: + +```php +$promise = $connector->create($host, $port); + +$promise->cancel(); +``` + ### Async TCP/IP connections The `React\SocketClient\TcpConnector` provides a single promise-based diff --git a/src/Connector.php b/src/Connector.php index 6cf991c0..dfcd6cb1 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -6,7 +6,14 @@ use React\Dns\Resolver\Resolver; /** + * Legacy Connector + * + * This class is not to be confused with the ConnectorInterface and should not + * be used as a typehint. + * * @deprecated Exists for BC only, consider using the newer DnsConnector instead + * @see DnsConnector for the newer replacement + * @see ConnectorInterface for the base interface */ class Connector implements ConnectorInterface { diff --git a/src/ConnectorInterface.php b/src/ConnectorInterface.php index b40b3a1b..9eff4408 100644 --- a/src/ConnectorInterface.php +++ b/src/ConnectorInterface.php @@ -2,7 +2,37 @@ namespace React\SocketClient; +/** + * The `ConnectorInterface` is responsible for providing an interface for + * establishing streaming connections, such as a normal TCP/IP connection. + * + * This is the main interface defined in this package and it is used throughout + * React's vast ecosystem. + * + * Most higher-level components (such as HTTP, database or other networking + * service clients) accept an instance implementing this interface to create their + * TCP/IP connection to the underlying networking service. + * This is usually done via dependency injection, so it's fairly simple to actually + * swap this implementation against any other implementation of this interface. + * + * The interface only offers a single `create()` method. + */ interface ConnectorInterface { + /** + * Creates a Promise which resolves with a stream once the connection to the given remote address succeeds + * + * The Promise resolves with a `React\Stream\Stream` instance on success or + * rejects with an `Exception` if the connection is not successful. + * + * The returned Promise SHOULD be implemented in such a way that it can be + * cancelled when it is still pending. Cancelling a pending promise SHOULD + * reject its value with an Exception. It SHOULD clean up any underlying + * resources and references as applicable. + * + * @param string $host + * @param int $port + * @return React\Promise\PromiseInterface resolves with a Stream on success or rejects with an Exception on error + */ public function create($host, $port); } From aafd20dfb5fc1b9cac7a56c8a57927002da7ec79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 24 Dec 2016 00:40:26 +0100 Subject: [PATCH 116/146] Documentation for how all connectors implement ConnectorInterface --- README.md | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 472d06ac..be902168 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,9 @@ $promise->cancel(); ### Async TCP/IP connections -The `React\SocketClient\TcpConnector` provides a single promise-based -`create($ip, $port)` method which resolves as soon as the connection -succeeds or fails. +The `React\SocketClient\TcpConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create plaintext +TCP/IP connections to any IP-port-combination: ```php $tcpConnector = new React\SocketClient\TcpConnector($loop); @@ -115,17 +115,18 @@ $tcpConnector = new React\SocketClient\TcpConnector($loop, array( )); ``` -Note that this class only allows you to connect to IP/port combinations. -If you want to connect to hostname/port combinations, see also the following chapter. +Note that this class only allows you to connect to IP-port-combinations. +If you want to connect to hostname-port-combinations, see also the following chapter. ### DNS resolution -The `DnsConnector` class decorates a given `TcpConnector` instance by first -looking up the given domain name and then establishing the underlying TCP/IP -connection to the resolved IP address. +The `DnsConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create plaintext +TCP/IP connections to any hostname-port-combination. -It provides the same promise-based `create($host, $port)` method which resolves with -a `Stream` instance that can be used just like above. +It does so by decorating a given `TcpConnector` instance so that it first +looks up the given domain name via DNS (if applicable) and then establishes the +underlying TCP/IP connection to the resolved target IP address. Make sure to set up your DNS resolver and underlying TCP connector like this: @@ -168,11 +169,13 @@ $connector->create('www.google.com', 80)->then($callback); ### Async SSL/TLS connections -The `SecureConnector` class decorates a given `Connector` instance by enabling -SSL/TLS encryption as soon as the raw TCP/IP connection succeeds. +The `SecureConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create secure +TLS (formerly known as SSL) connections to any hostname-port-combination. -It provides the same promise- based `create($host, $port)` method which resolves with -a `Stream` instance that can be used just like any non-encrypted stream: +It does so by decorating a given `DnsConnector` instance so that it first +creates a plaintext TCP/IP connection and then enables TLS encryption on this +stream. ```php $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop); @@ -219,8 +222,12 @@ stream resources will use a single, shared *default context* resource otherwise. ### Connection timeouts -The `TimeoutConnector` class decorates any given `Connector` instance. -It provides the same `create()` method, but will automatically reject the +The `TimeoutConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to add timeout +handling to any existing connector instance. + +It does so by decorating any given [`ConnectorInterface`](#connectorinterface) +instance and starting a timer that will automatically reject and abort any underlying connection attempt if it takes too long. ```php @@ -246,8 +253,9 @@ attempt, abort the timer and reject the resulting promise. ### Unix domain sockets -Similarly, the `UnixConnector` class can be used to connect to Unix domain socket (UDS) -paths like this: +The `UnixConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to connect to +Unix domain socket (UDS) paths like this: ```php $connector = new React\SocketClient\UnixConnector($loop); From 5570838d436f056695e9a5eaf01dd5f80386dd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 24 Dec 2016 12:19:15 +0100 Subject: [PATCH 117/146] Prepare v0.5.3 release --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4961c2b1..fc9570ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.3 (2016-12-24) + +* Fix: Skip IPv6 tests if not supported by the system + (#76 by @clue) + +* Documentation for `ConnectorInterface` + (#77 by @clue) + ## 0.5.2 (2016-12-19) * Feature: Replace `SecureStream` with unlimited read buffer from react/stream v0.4.5 diff --git a/README.md b/README.md index be902168..fe01844b 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/socket-client:^0.5.2 +$ composer require react/socket-client:^0.5.3 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From c73098ba1060826362660d4aa06be8be47f5cde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 8 Jan 2017 12:58:22 +0100 Subject: [PATCH 118/146] Simplify test suite by relying on React's secure TLS server --- .travis.yml | 37 +------------------------ README.md | 14 ---------- composer.json | 3 +- tests/SecureIntegrationTest.php | 18 +++++------- tests/localhost.pem | 49 +++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 62 deletions(-) create mode 100644 tests/localhost.pem diff --git a/.travis.yml b/.travis.yml index 82c4c1d1..3464291c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,43 +10,8 @@ php: sudo: false -env: - - TEST_SECURE=6001 TEST_PLAIN=6000 - -# install required system packages, see 'install' below for details -# Travis' containers require this, otherwise use this: -# sudo apt-get install openssl build-essential libev-dev libssl-dev -addons: - apt: - packages: - - openssl - - build-essential - - libev-dev - - libssl-dev - install: - # install this library plus its dependencies - - composer install --prefer-source --no-interaction - - # we need openssl and either stunnel or stud - # unfortunately these are not available in Travis' containers - # sudo apt-get install -y openssl stud - # sudo apt-get install -y openssl stunnel4 - - # instead, let's install stud from source - # build dependencies are already installed, see 'addons.apt.packages' above - # sudo apt-get install openssl build-essential libev-dev libssl-dev - - git clone https://github.com/bumptech/stud.git - - (cd stud && make) - - # create self-signed certificate - - openssl genrsa 1024 > stunnel.key - - openssl req -batch -subj '/CN=127.0.0.1' -new -x509 -nodes -sha1 -days 3650 -key stunnel.key > stunnel.cert - - cat stunnel.cert stunnel.key > stunnel.pem - - # start TLS/SSL terminating proxy - # stunnel -f -p stunnel.pem -d $TEST_SECURE -r $TEST_PLAIN 2>/dev/null & - - ./stud/stud --daemon -f 127.0.0.1,$TEST_SECURE -b 127.0.0.1,$TEST_PLAIN stunnel.pem + - composer install --no-interaction script: - phpunit --coverage-text diff --git a/README.md b/README.md index fe01844b..07d09641 100644 --- a/README.md +++ b/README.md @@ -291,17 +291,3 @@ To run the test suite, you need PHPUnit. Go to the project root and run: ```bash $ phpunit ``` - -The test suite also contains some optional integration tests which operate on a -TCP/IP socket server and an optional TLS/SSL terminating proxy in front of it. -The underlying TCP/IP socket server will be started automatically, whereas the -TLS/SSL terminating proxy has to be started and enabled like this: - -```bash -$ stunnel -f -p stunnel.pem -d 6001 -r 6000 & -$ TEST_SECURE=6001 TEST_PLAIN=6000 phpunit -``` - -See also the [Travis configuration](.travis.yml) for details on how to set up -the TLS/SSL terminating proxy and the required certificate file (`stunnel.pem`) -if you're unsure. diff --git a/composer.json b/composer.json index bdd9d502..a77d17b6 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ } }, "require-dev": { - "clue/block-react": "^1.1" + "clue/block-react": "^1.1", + "react/socket": "^0.4.5" } } diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index ef85dad3..e12f22e7 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -4,6 +4,7 @@ use React\EventLoop\Factory as LoopFactory; use React\Socket\Server; +use React\Socket\SecureServer; use React\SocketClient\TcpConnector; use React\SocketClient\SecureConnector; use React\Stream\Stream; @@ -17,12 +18,10 @@ class SecureIntegrationTest extends TestCase { const TIMEOUT = 0.5; - private $portSecure; - private $portPlain; - private $loop; private $server; private $connector; + private $portSecure; public function setUp() { @@ -30,16 +29,13 @@ public function setUp() $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); } - $this->portSecure = getenv('TEST_SECURE'); - $this->portPlain = getenv('TEST_PLAIN'); - - if ($this->portSecure === false || $this->portPlain === false) { - $this->markTestSkipped('Needs TEST_SECURE=X and TEST_PLAIN=Y environment variables to run, see README.md'); - } - $this->loop = LoopFactory::create(); $this->server = new Server($this->loop); - $this->server->listen($this->portPlain); + $this->server = new SecureServer($this->server, $this->loop, array( + 'local_cert' => __DIR__ . '/localhost.pem' + )); + $this->server->listen(0); + $this->portSecure = $this->server->getPort(); $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false)); } diff --git a/tests/localhost.pem b/tests/localhost.pem new file mode 100644 index 00000000..be692792 --- /dev/null +++ b/tests/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----- From cb51eab0f18fae88a1ed2dc6d1321aa49add9a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 9 Jan 2017 15:47:38 +0100 Subject: [PATCH 119/146] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 51568770..5f85f5fa 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Async Connector to open TCP/IP and SSL/TLS based connections. +> The master branch contains the code for the upcoming 0.6 release. +For the code of the current stable 0.5.x release, checkout the +[0.5 branch](https://github.com/reactphp/socket-client/tree/0.5). + ## Introduction Think of this library as an async version of From bdc03ce74b8a458c2397de7919ee91a012bd9004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 2 Dec 2016 17:51:35 +0100 Subject: [PATCH 120/146] Require cancellation support --- README.md | 4 ++-- src/ConnectorInterface.php | 4 ++-- src/DnsConnector.php | 32 +++----------------------------- src/SecureConnector.php | 28 +--------------------------- src/TimeoutConnector.php | 23 +---------------------- tests/DnsConnectorTest.php | 22 ++-------------------- tests/SecureConnectorTest.php | 19 +------------------ tests/TimeoutConnectorTest.php | 24 +----------------------- 8 files changed, 13 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 5f85f5fa..cfe35e7f 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ $connector->connect('google.com:443')->then( ); ``` -The returned Promise SHOULD be implemented in such a way that it can be -cancelled when it is still pending. Cancelling a pending promise SHOULD +The returned Promise MUST be implemented in such a way that it can be +cancelled when it is still pending. Cancelling a pending promise MUST reject its value with an `Exception`. It SHOULD clean up any underlying resources and references as applicable: diff --git a/src/ConnectorInterface.php b/src/ConnectorInterface.php index 6471cfdc..46b0182d 100644 --- a/src/ConnectorInterface.php +++ b/src/ConnectorInterface.php @@ -25,8 +25,8 @@ interface ConnectorInterface * The Promise resolves with a `React\Stream\Stream` instance on success or * rejects with an `Exception` if the connection is not successful. * - * The returned Promise SHOULD be implemented in such a way that it can be - * cancelled when it is still pending. Cancelling a pending promise SHOULD + * The returned Promise MUST be implemented in such a way that it can be + * cancelled when it is still pending. Cancelling a pending promise MUST * reject its value with an Exception. It SHOULD clean up any underlying * resources and references as applicable. * diff --git a/src/DnsConnector.php b/src/DnsConnector.php index 21a4623b..b40f032f 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -33,10 +33,11 @@ public function connect($uri) $that = $this; $host = trim($parts['host'], '[]'); + $connector = $this->connector; return $this ->resolveHostname($host) - ->then(function ($ip) use ($that, $parts) { + ->then(function ($ip) use ($connector, $parts) { $uri = ''; // prepend original scheme if known @@ -71,7 +72,7 @@ public function connect($uri) $uri .= '#' . $parts['fragment']; } - return $that->connectTcp($uri); + return $connector->connect($uri); }); } @@ -99,31 +100,4 @@ function ($_, $reject) use ($promise) { } ); } - - /** @internal */ - public function connectTcp($uri) - { - $promise = $this->connector->connect($uri); - - return new Promise\Promise( - function ($resolve, $reject) use ($promise) { - // resolve/reject with result of TCP/IP connection - $promise->then($resolve, $reject); - }, - function ($_, $reject) use ($promise) { - // cancellation should reject connection attempt - $reject(new \RuntimeException('Connection attempt cancelled during TCP/IP connection')); - - // forefully close TCP/IP connection if it completes despite cancellation - $promise->then(function (Stream $stream) { - $stream->close(); - }); - - // (try to) cancel pending TCP/IP connection - if ($promise instanceof CancellablePromiseInterface) { - $promise->cancel(); - } - } - ); - } } diff --git a/src/SecureConnector.php b/src/SecureConnector.php index bf06064d..09882e87 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -52,7 +52,7 @@ public function connect($uri) } $encryption = $this->streamEncryption; - return $this->connectTcp($uri)->then(function (Stream $stream) use ($context, $encryption) { + return $this->connector->connect($uri)->then(function (Stream $stream) use ($context, $encryption) { // (unencrypted) TCP/IP connection succeeded // set required SSL/TLS context options @@ -68,30 +68,4 @@ public function connect($uri) }); }); } - - private function connectTcp($uri) - { - $promise = $this->connector->connect($uri); - - return new Promise\Promise( - function ($resolve, $reject) use ($promise) { - // resolve/reject with result of TCP/IP connection - $promise->then($resolve, $reject); - }, - function ($_, $reject) use ($promise) { - // cancellation should reject connection attempt - $reject(new \RuntimeException('Connection attempt cancelled during TCP/IP connection')); - - // forefully close TCP/IP connection if it completes despite cancellation - $promise->then(function (Stream $stream) { - $stream->close(); - }); - - // (try to) cancel pending TCP/IP connection - if ($promise instanceof CancellablePromiseInterface) { - $promise->cancel(); - } - } - ); - } } diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index 8088f83a..f2bd25cf 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -24,27 +24,6 @@ public function __construct(ConnectorInterface $connector, $timeout, LoopInterfa public function connect($uri) { - $promise = $this->connector->connect($uri); - - return Timer\timeout(new Promise( - function ($resolve, $reject) use ($promise) { - // resolve/reject with result of TCP/IP connection - $promise->then($resolve, $reject); - }, - function ($_, $reject) use ($promise) { - // cancellation should reject connection attempt - $reject(new \RuntimeException('Connection attempt cancelled during connection')); - - // forefully close TCP/IP connection if it completes despite cancellation - $promise->then(function (Stream $stream) { - $stream->close(); - }); - - // (try to) cancel pending TCP/IP connection - if ($promise instanceof CancellablePromiseInterface) { - $promise->cancel(); - } - } - ), $this->timeout, $this->loop); + return Timer\timeout($this->connector->connect($uri), $this->timeout, $this->loop); } } diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index 737ed9a8..3ffae41d 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -73,7 +73,7 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() { $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue($pending)); - $this->tcp->expects($this->never())->method('resolve'); + $this->tcp->expects($this->never())->method('connect'); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); @@ -83,25 +83,7 @@ public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() public function testCancelDuringTcpConnectionCancelsTcpConnection() { - $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); - $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->will($this->returnValue($pending)); - - $promise = $this->connector->connect('example.com:80'); - $promise->cancel(); - - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); - } - - public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() - { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock(); - $stream->expects($this->once())->method('close'); - - $pending = new Promise\Promise(function () { }, function ($resolve) use ($stream) { - $resolve($stream); - }); - + $pending = new Promise\Promise(function () { }, function () { throw new \Exception(); }); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->will($this->returnValue($pending)); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index 1756f431..b05af083 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -51,24 +51,7 @@ public function testConnectionToInvalidSchemeWillReject() public function testCancelDuringTcpConnectionCancelsTcpConnection() { - $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); - - $promise = $this->connector->connect('example.com:80'); - $promise->cancel(); - - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); - } - - public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() - { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock(); - $stream->expects($this->once())->method('close'); - - $pending = new Promise\Promise(function () { }, function ($resolve) use ($stream) { - $resolve($stream); - }); - + $pending = new Promise\Promise(function () { }, function () { throw new \Exception(); }); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); $promise = $this->connector->connect('example.com:80'); diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index d00f5018..633b33a7 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -86,29 +86,7 @@ public function testRejectsAndCancelsPendingPromiseOnTimeout() public function testCancelsPendingPromiseOnCancel() { - $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); - - $connector = $this->getMock('React\SocketClient\ConnectorInterface'); - $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); - - $loop = Factory::create(); - - $timeout = new TimeoutConnector($connector, 0.01, $loop); - - $out = $timeout->connect('google.com:80'); - $out->cancel(); - - $out->then($this->expectCallableNever(), $this->expectCallableOnce()); - } - - public function testCancelClosesStreamIfTcpResolvesDespiteCancellation() - { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock(); - $stream->expects($this->once())->method('close'); - - $promise = new Promise\Promise(function () { }, function ($resolve) use ($stream) { - $resolve($stream); - }); + $promise = new Promise\Promise(function () { }, function () { throw new \Exception(); }); $connector = $this->getMock('React\SocketClient\ConnectorInterface'); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); From 5996b69e4e2fb0a9acff45c649791a5c9066c6a7 Mon Sep 17 00:00:00 2001 From: Shaun Bramley Date: Sat, 14 Jan 2017 16:24:59 -0500 Subject: [PATCH 121/146] add phpunit 4.8 to require-dev, force travisci to use local phpunit --- .travis.yml | 2 +- composer.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3464291c..15e341e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,4 +14,4 @@ install: - composer install --no-interaction script: - - phpunit --coverage-text + - ./vendor/bin/phpunit --coverage-text diff --git a/composer.json b/composer.json index a77d17b6..7908ea4c 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ }, "require-dev": { "clue/block-react": "^1.1", - "react/socket": "^0.4.5" + "react/socket": "^0.4.5", + "phpunit/phpunit": "~4.8" } } From 75e9885ac5b6b1084bcbf5ebdad5406e84a465c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 Feb 2017 09:42:29 +0100 Subject: [PATCH 122/146] Update Socket component to v0.5 --- composer.json | 2 +- tests/SecureIntegrationTest.php | 25 ++++++++++++------------- tests/TcpConnectorTest.php | 21 ++++++++------------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index 7908ea4c..b90f7173 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ }, "require-dev": { "clue/block-react": "^1.1", - "react/socket": "^0.4.5", + "react/socket": "^0.5", "phpunit/phpunit": "~4.8" } } diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index f4533f28..f64e46a8 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -21,7 +21,7 @@ class SecureIntegrationTest extends TestCase private $loop; private $server; private $connector; - private $portSecure; + private $address; public function setUp() { @@ -30,26 +30,25 @@ public function setUp() } $this->loop = LoopFactory::create(); - $this->server = new Server($this->loop); + $this->server = new Server(0, $this->loop); $this->server = new SecureServer($this->server, $this->loop, array( 'local_cert' => __DIR__ . '/localhost.pem' )); - $this->server->listen(0); - $this->portSecure = $this->server->getPort(); + $this->address = $this->server->getAddress(); $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false)); } public function tearDown() { if ($this->server !== null) { - $this->server->shutdown(); + $this->server->close(); $this->server = null; } } public function testConnectToServer() { - $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->close(); @@ -59,7 +58,7 @@ public function testConnectToServerEmitsConnection() { $promiseServer = $this->createPromiseForEvent($this->server, 'connection', $this->expectCallableOnce()); - $promiseClient = $this->connector->connect('127.0.0.1:' . $this->portSecure); + $promiseClient = $this->connector->connect($this->address); list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop, self::TIMEOUT); /* @var $client Stream */ @@ -77,7 +76,7 @@ public function testSendSmallDataToServerReceivesOneChunk() }); }); - $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); /* @var $client Stream */ $client->write('hello'); @@ -103,7 +102,7 @@ public function testSendDataWithEndToServerReceivesAllData() }); }); - $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('a', 200000); @@ -124,7 +123,7 @@ public function testSendDataWithoutEndingToServerReceivesAllData() }); }); - $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); /* @var $client Stream */ $data = str_repeat('d', 200000); @@ -144,7 +143,7 @@ public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() $peer->write('hello'); }); - $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await client to report one "data" event @@ -161,7 +160,7 @@ public function testConnectToServerWhichSendsDataWithEndReceivesAllData() $peer->end($data); }); - $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop, self::TIMEOUT); + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); /* @var $client Stream */ // await data from client until it closes @@ -177,7 +176,7 @@ public function testConnectToServerWhichSendsDataWithoutEndingReceivesAllData() $peer->write($data); }); - $client = Block\await($this->connector->connect('127.0.0.1:' . $this->portSecure), $this->loop); + $client = Block\await($this->connector->connect($this->address), $this->loop); /* @var $client Stream */ // buffer incoming data for 0.1s (should be plenty of time) diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 66f1143d..75bef72b 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -28,12 +28,9 @@ public function connectionToTcpServerShouldSucceed() { $loop = new StreamSelectLoop(); - $server = new Server($loop); + $server = new Server(9999, $loop); $server->on('connection', $this->expectCallableOnce()); - $server->on('connection', function () use ($server, $loop) { - $server->shutdown(); - }); - $server->listen(9999); + $server->on('connection', array($server, 'close')); $connector = new TcpConnector($loop); @@ -62,16 +59,15 @@ public function connectionToIp6TcpServerShouldSucceed() { $loop = new StreamSelectLoop(); - $server = new Server($loop); - $server->on('connection', $this->expectCallableOnce()); - $server->on('connection', array($server, 'shutdown')); - try { - $server->listen(9999, '::1'); + $server = new Server('[::1]:9999', $loop); } catch (\Exception $e) { $this->markTestSkipped('Unable to start IPv6 server socket (IPv6 not supported on this system?)'); } + $server->on('connection', $this->expectCallableOnce()); + $server->on('connection', array($server, 'close')); + $connector = new TcpConnector($loop); $stream = Block\await($connector->connect('[::1]:9999'), $loop, self::TIMEOUT); @@ -137,10 +133,9 @@ public function cancellingConnectionShouldRejectPromise() $loop = new StreamSelectLoop(); $connector = new TcpConnector($loop); - $server = new Server($loop); - $server->listen(0); + $server = new Server(0, $loop); - $promise = $connector->connect('127.0.0.1:' . $server->getPort()); + $promise = $connector->connect($server->getAddress()); $promise->cancel(); $this->setExpectedException('RuntimeException', 'Cancelled'); From 87c77d05944dd0b0aac111dee33aafde16fa4ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Feb 2017 08:24:53 +0100 Subject: [PATCH 123/146] Connections now resolve with a ConnectionInterface --- README.md | 124 +++++++++++++++++++++++++++++----- examples/01-http.php | 10 +-- examples/02-https.php | 10 +-- examples/03-netcat.php | 12 ++-- src/ConnectionInterface.php | 102 ++++++++++++++++++++++++++++ src/ConnectorInterface.php | 29 ++++++-- src/SecureConnector.php | 13 ++-- src/StreamConnection.php | 39 +++++++++++ src/TcpConnector.php | 2 +- tests/IntegrationTest.php | 3 + tests/SecureConnectorTest.php | 12 ++++ tests/TcpConnectorTest.php | 73 ++++++++++++++++++-- 12 files changed, 382 insertions(+), 47 deletions(-) create mode 100644 src/ConnectionInterface.php create mode 100644 src/StreamConnection.php diff --git a/README.md b/README.md index cfe35e7f..2b1346a3 100644 --- a/README.md +++ b/README.md @@ -50,15 +50,16 @@ The interface only offers a single method: #### connect() -The `connect(string $uri): PromiseInterface` method -can be used to establish a streaming connection. +The `connect(string $uri): PromiseInterface` method +can be used to create a streaming connection to the given remote address. + It returns a [Promise](https://github.com/reactphp/promise) which either -fulfills with a [Stream](https://github.com/reactphp/stream) or -rejects with an `Exception`: +fulfills with a stream implementing [`ConnectionInterface`](#connectioninterface) +on success or rejects with an `Exception` if the connection is not successful: ```php $connector->connect('google.com:443')->then( - function (Stream $stream) { + function (ConnectionInterface $connection) { // connection successfully established }, function (Exception $error) { @@ -67,6 +68,8 @@ $connector->connect('google.com:443')->then( ); ``` +See also [`ConnectionInterface`](#connectioninterface) for more details. + The returned Promise MUST be implemented in such a way that it can be cancelled when it is still pending. Cancelling a pending promise MUST reject its value with an `Exception`. It SHOULD clean up any underlying @@ -78,6 +81,95 @@ $promise = $connector->connect($uri); $promise->cancel(); ``` +### ConnectionInterface + +The `ConnectionInterface` is used to represent any outgoing connection, +such as a normal TCP/IP connection. + +An outgoing connection is a duplex stream (both readable and writable) that +implements React's +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). +It contains additional properties for the local and remote address +where this connection has been established to. + +Most commonly, instances implementing this `ConnectionInterface` are returned +by all classes implementing the [`ConnectorInterface`](#connectorinterface). + +> Note that this interface is only to be used to represent the client-side end +of an outgoing connection. +It MUST NOT be used to represent an incoming connection in a server-side context. +If you want to accept incoming connections, +use the [`Socket`](https://github.com/reactphp/socket) component instead. + +Because the `ConnectionInterface` implements the underlying +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) +you can use any of its events and methods as usual: + +```php +$connection->on('data', function ($chunk) { + echo $data; +}); + +$conenction->on('close', function () { + echo 'closed'; +}); + +$connection->write($data); +$connection->end($data = null); +$connection->close(); +// … +``` + +For more details, see the +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + +#### getRemoteAddress() + +The `getRemoteAddress(): ?string` method can be used to +return the remote address (IP and port) where this connection has been +established to. + +```php +$address = $connection->getRemoteAddress(); +echo 'Connected to ' . $address . PHP_EOL; +``` + +If the remote address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full remote address as a string value. +If this is a TCP/IP based connection and you only want the remote IP, you may +use something like this: + +```php +$address = $connection->getRemoteAddress(); +$ip = trim(parse_url('tcp://' . $address, PHP_URL_HOST), '[]'); +echo 'Connected to ' . $ip . PHP_EOL; +``` + +#### getLocalAddress() + +The `getLocalAddress(): ?string` method can be used to +return the full local address (IP and port) where this connection has been +established from. + +```php +$address = $connection->getLocalAddress(); +echo 'Connected via ' . $address . PHP_EOL; +``` + +If the local address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full local address as a string value. + +This method complements the [`getRemoteAddress()`](#getremoteaddress) method, +so they should not be confused. + +If your system has multiple interfaces (e.g. a WAN and a LAN interface), +you can use this method to find out which interface was actually +used for this connection. + ### Async TCP/IP connections The `React\SocketClient\TcpConnector` class implements the @@ -87,9 +179,9 @@ TCP/IP connections to any IP-port-combination: ```php $tcpConnector = new React\SocketClient\TcpConnector($loop); -$tcpConnector->connect('127.0.0.1:80')->then(function (React\Stream\Stream $stream) { - $stream->write('...'); - $stream->end(); +$tcpConnector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); }); $loop->run(); @@ -140,9 +232,9 @@ $dns = $dnsResolverFactory->connectCached('8.8.8.8', $loop); $dnsConnector = new React\SocketClient\DnsConnector($tcpConnector, $dns); -$dnsConnector->connect('www.google.com:80')->then(function (React\Stream\Stream $stream) { - $stream->write('...'); - $stream->end(); +$dnsConnector->connect('www.google.com:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); }); $loop->run(); @@ -184,8 +276,8 @@ stream. ```php $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop); -$secureConnector->connect('www.google.com:443')->then(function (React\Stream\Stream $stream) { - $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); +$secureConnector->connect('www.google.com:443')->then(function (ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); ... }); @@ -237,7 +329,7 @@ underlying connection attempt if it takes too long. ```php $timeoutConnector = new React\SocketClient\TimeoutConnector($connector, 3.0, $loop); -$timeoutConnector->connect('google.com:80')->then(function (React\Stream\Stream $stream) { +$timeoutConnector->connect('google.com:80')->then(function (ConnectionInterface $connection) { // connection succeeded within 3.0 seconds }); ``` @@ -264,8 +356,8 @@ Unix domain socket (UDS) paths like this: ```php $connector = new React\SocketClient\UnixConnector($loop); -$connector->connect('/tmp/demo.sock')->then(function (React\Stream\Stream $stream) { - $stream->write("HELLO\n"); +$connector->connect('/tmp/demo.sock')->then(function (ConnectionInterface $connection) { + $connection->write("HELLO\n"); }); $loop->run(); diff --git a/examples/01-http.php b/examples/01-http.php index 6a2f9311..be7b1c0b 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -3,8 +3,8 @@ use React\EventLoop\Factory; use React\SocketClient\TcpConnector; use React\SocketClient\DnsConnector; -use React\Stream\Stream; use React\SocketClient\TimeoutConnector; +use React\SocketClient\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -19,15 +19,15 @@ // time out connection attempt in 3.0s $dns = new TimeoutConnector($dns, 3.0, $loop); -$dns->create('www.google.com', 80)->then(function (Stream $stream) { - $stream->on('data', function ($data) { +$dns->create('www.google.com', 80)->then(function (ConnectionInterface $connection) { + $connection->on('data', function ($data) { echo $data; }); - $stream->on('close', function () { + $connection->on('close', function () { echo '[CLOSED]' . PHP_EOL; }); - $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); + $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); }, 'printf'); $loop->run(); diff --git a/examples/02-https.php b/examples/02-https.php index c70ddcd7..d18dce0e 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -4,8 +4,8 @@ use React\SocketClient\TcpConnector; use React\SocketClient\DnsConnector; use React\SocketClient\SecureConnector; -use React\Stream\Stream; use React\SocketClient\TimeoutConnector; +use React\SocketClient\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -21,15 +21,15 @@ // time out connection attempt in 3.0s $tls = new TimeoutConnector($tls, 3.0, $loop); -$tls->create('www.google.com', 443)->then(function (Stream $stream) { - $stream->on('data', function ($data) { +$tls->create('www.google.com', 443)->then(function (ConnectionInterface $connection) { + $connection->on('data', function ($data) { echo $data; }); - $stream->on('close', function () { + $connection->on('close', function () { echo '[CLOSED]' . PHP_EOL; }); - $stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); + $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); }, 'printf'); $loop->run(); diff --git a/examples/03-netcat.php b/examples/03-netcat.php index 8ef34ad6..5ede41a7 100644 --- a/examples/03-netcat.php +++ b/examples/03-netcat.php @@ -3,8 +3,8 @@ use React\EventLoop\Factory; use React\SocketClient\TcpConnector; use React\SocketClient\DnsConnector; -use React\Stream\Stream; use React\SocketClient\TimeoutConnector; +use React\SocketClient\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -33,21 +33,21 @@ $stderr->write('Connecting' . PHP_EOL); -$dns->create($argv[1], $argv[2])->then(function (Stream $stream) use ($stdin, $stdout, $stderr) { +$dns->create($argv[1], $argv[2])->then(function (ConnectionInterface $connection) use ($stdin, $stdout, $stderr) { // pipe everything from STDIN into connection $stdin->resume(); - $stdin->pipe($stream); + $stdin->pipe($connection); // pipe everything from connection to STDOUT - $stream->pipe($stdout); + $connection->pipe($stdout); // report errors to STDERR - $stream->on('error', function ($error) use ($stderr) { + $connection->on('error', function ($error) use ($stderr) { $stderr->write('Stream ERROR: ' . $error . PHP_EOL); }); // report closing and stop reading from input - $stream->on('close', function () use ($stderr, $stdin) { + $connection->on('close', function () use ($stderr, $stdin) { $stderr->write('[CLOSED]' . PHP_EOL); $stdin->close(); }); diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php new file mode 100644 index 00000000..0687e754 --- /dev/null +++ b/src/ConnectionInterface.php @@ -0,0 +1,102 @@ + Note that this interface is only to be used to represent the client-side end + * of an outgoing connection. + * It MUST NOT be used to represent an incoming connection in a server-side context. + * If you want to accept incoming connections, + * use the [`Socket`](https://github.com/reactphp/socket) component instead. + * + * Because the `ConnectionInterface` implements the underlying + * [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) + * you can use any of its events and methods as usual: + * + * ```php + * $connection->on('data', function ($chunk) { + * echo $data; + * }); + * + * $conenction->on('close', function () { + * echo 'closed'; + * }); + * + * $connection->write($data); + * $connection->end($data = null); + * $connection->close(); + * // … + * ``` + * + * For more details, see the + * [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + * + * @see DuplexStreamInterface + * @see ConnectorInterface + */ +interface ConnectionInterface extends DuplexStreamInterface +{ + /** + * Returns the remote address (IP and port) where this connection has been established to + * + * ```php + * $address = $connection->getRemoteAddress(); + * echo 'Connected to ' . $address . PHP_EOL; + * ``` + * + * If the remote address can not be determined or is unknown at this time (such as + * after the connection has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full remote address as a string value. + * If this is a TCP/IP based connection and you only want the remote IP, you may + * use something like this: + * + * ```php + * $address = $connection->getRemoteAddress(); + * $ip = trim(parse_url('tcp://' . $address, PHP_URL_HOST), '[]'); + * echo 'Connected to ' . $ip . PHP_EOL; + * ``` + * + * @return ?string remote address (IP and port) or null if unknown + */ + public function getRemoteAddress(); + + /** + * Returns the full local address (IP and port) where this connection has been established from + * + * ```php + * $address = $connection->getLocalAddress(); + * echo 'Connected via ' . $address . PHP_EOL; + * ``` + * + * If the local address can not be determined or is unknown at this time (such as + * after the connection has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full local address as a string value. + * + * This method complements the [`getRemoteAddress()`](#getremoteaddress) method, + * so they should not be confused. + * + * If your system has multiple interfaces (e.g. a WAN and a LAN interface), + * you can use this method to find out which interface was actually + * used for this connection. + * + * @return ?string local address (IP and port) or null if unknown + * @see self::getRemoteAddress() + */ + public function getLocalAddress(); +} diff --git a/src/ConnectorInterface.php b/src/ConnectorInterface.php index 46b0182d..57002979 100644 --- a/src/ConnectorInterface.php +++ b/src/ConnectorInterface.php @@ -16,22 +16,43 @@ * swap this implementation against any other implementation of this interface. * * The interface only offers a single `connect()` method. + * + * @see ConnectionInterface */ interface ConnectorInterface { /** - * Creates a Promise which resolves with a stream once the connection to the given remote address succeeds + * Creates a streaming connection to the given remote address + * + * If returns a Promise which either fulfills with a stream implementing + * `ConnectionInterface` on success or rejects with an `Exception` if the + * connection is not successful. * - * The Promise resolves with a `React\Stream\Stream` instance on success or - * rejects with an `Exception` if the connection is not successful. + * ```php + * $connector->connect('google.com:443')->then( + * function (ConnectionInterface $connection) { + * // connection successfully established + * }, + * function (Exception $error) { + * // failed to connect due to $error + * } + * ); + * ``` * * The returned Promise MUST be implemented in such a way that it can be * cancelled when it is still pending. Cancelling a pending promise MUST * reject its value with an Exception. It SHOULD clean up any underlying * resources and references as applicable. * + * ```php + * $promise = $connector->connect($uri); + * + * $promise->cancel(); + * ``` + * * @param string $uri - * @return React\Promise\PromiseInterface resolves with a Stream on success or rejects with an Exception on error + * @return React\Promise\PromiseInterface resolves with a stream implementing ConnectionInterface on success or rejects with an Exception on error + * @see ConnectionInterface */ public function connect($uri); } diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 09882e87..17046749 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -52,18 +52,23 @@ public function connect($uri) } $encryption = $this->streamEncryption; - return $this->connector->connect($uri)->then(function (Stream $stream) use ($context, $encryption) { + return $this->connector->connect($uri)->then(function (ConnectionInterface $connection) use ($context, $encryption) { // (unencrypted) TCP/IP connection succeeded + if (!$connection instanceof Stream) { + $connection->close(); + throw new \UnexpectedValueException('Connection MUST extend Stream in order to access underlying stream resource'); + } + // set required SSL/TLS context options foreach ($context as $name => $value) { - stream_context_set_option($stream->stream, 'ssl', $name, $value); + stream_context_set_option($connection->stream, 'ssl', $name, $value); } // try to enable encryption - return $encryption->enable($stream)->then(null, function ($error) use ($stream) { + return $encryption->enable($connection)->then(null, function ($error) use ($connection) { // establishing encryption failed => close invalid connection and return error - $stream->close(); + $connection->close(); throw $error; }); }); diff --git a/src/StreamConnection.php b/src/StreamConnection.php new file mode 100644 index 00000000..4d883da8 --- /dev/null +++ b/src/StreamConnection.php @@ -0,0 +1,39 @@ +sanitizeAddress(@stream_socket_get_name($this->stream, true)); + } + + public function getLocalAddress() + { + return $this->sanitizeAddress(@stream_socket_get_name($this->stream, false)); + } + + private function sanitizeAddress($address) + { + if ($address === false) { + return null; + } + + // check if this is an IPv6 address which includes multiple colons but no square brackets + $pos = strrpos($address, ':'); + if ($pos !== false && strpos($address, ':') < $pos && substr($address, 0, 1) !== '[') { + $port = substr($address, $pos + 1); + $address = '[' . substr($address, 0, $pos) . ']:' . $port; + } + + return $address; + } +} diff --git a/src/TcpConnector.php b/src/TcpConnector.php index d13f02ee..8e4890d4 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -96,6 +96,6 @@ public function checkConnectedSocket($socket) /** @internal */ public function handleConnectedSocket($socket) { - return new Stream($socket, $this->loop); + return new StreamConnection($socket, $this->loop); } } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 4c64973e..70951c89 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -26,6 +26,9 @@ public function gettingStuffFromGoogleShouldWork() $conn = Block\await($connector->connect('google.com:80'), $loop); + $this->assertContains(':80', $conn->getRemoteAddress()); + $this->assertNotEquals('google.com:80', $conn->getRemoteAddress()); + $conn->write("GET / HTTP/1.0\r\n\r\n"); $response = Block\await(BufferedSink::createPromise($conn), $loop, self::TIMEOUT); diff --git a/tests/SecureConnectorTest.php b/tests/SecureConnectorTest.php index b05af083..ad7de593 100644 --- a/tests/SecureConnectorTest.php +++ b/tests/SecureConnectorTest.php @@ -59,4 +59,16 @@ public function testCancelDuringTcpConnectionCancelsTcpConnection() $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); } + + public function testConnectionWillBeClosedAndRejectedIfConnectioIsNoStream() + { + $connection = $this->getMockBuilder('React\SocketClient\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('close'); + + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->willReturn(Promise\resolve($connection)); + + $promise = $this->connector->connect('example.com:80'); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } } diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 75bef72b..5e48febe 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -5,6 +5,7 @@ use React\EventLoop\StreamSelectLoop; use React\Socket\Server; use React\SocketClient\TcpConnector; +use React\SocketClient\ConnectionInterface; use Clue\React\Block; class TcpConnectorTest extends TestCase @@ -34,11 +35,67 @@ public function connectionToTcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); - $this->assertInstanceOf('React\Stream\Stream', $stream); + $this->assertInstanceOf('React\SocketClient\ConnectionInterface', $connection); - $stream->close(); + $connection->close(); + } + + /** @test */ + public function connectionToTcpServerShouldSucceedWithRemoteAdressSameAsTarget() + { + $loop = new StreamSelectLoop(); + + $server = new Server(9999, $loop); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $this->assertEquals('127.0.0.1:9999', $connection->getRemoteAddress()); + + $connection->close(); + } + + /** @test */ + public function connectionToTcpServerShouldSucceedWithLocalAdressOnLocalhost() + { + $loop = new StreamSelectLoop(); + + $server = new Server(9999, $loop); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $this->assertContains('127.0.0.1:', $connection->getLocalAddress()); + $this->assertNotEquals('127.0.0.1:9999', $connection->getLocalAddress()); + + $connection->close(); + } + + /** @test */ + public function connectionToTcpServerShouldSucceedWithNullAddressesAfterConnectionClosed() + { + $loop = new StreamSelectLoop(); + + $server = new Server(9999, $loop); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $connection->close(); + + $this->assertNull($connection->getRemoteAddress()); + $this->assertNull($connection->getLocalAddress()); } /** @test */ @@ -70,11 +127,15 @@ public function connectionToIp6TcpServerShouldSucceed() $connector = new TcpConnector($loop); - $stream = Block\await($connector->connect('[::1]:9999'), $loop, self::TIMEOUT); + $connection = Block\await($connector->connect('[::1]:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $this->assertEquals('[::1]:9999', $connection->getRemoteAddress()); - $this->assertInstanceOf('React\Stream\Stream', $stream); + $this->assertContains('[::1]:', $connection->getLocalAddress()); + $this->assertNotEquals('[::1]:9999', $connection->getLocalAddress()); - $stream->close(); + $connection->close(); } /** @test */ From c9b7f2255031568205d99775ee74c77029530ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 16 Feb 2017 11:36:56 +0100 Subject: [PATCH 124/146] Remove superfluous and undocumented ConnectionException --- README.md | 8 ++++++++ src/ConnectionException.php | 7 ------- src/TcpConnector.php | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 src/ConnectionException.php diff --git a/README.md b/README.md index 2b1346a3..47b77e64 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,14 @@ $tcpConnector = new React\SocketClient\TcpConnector($loop, array( ``` Note that this class only allows you to connect to IP-port-combinations. +If the given URI is invalid, does not contain a valid IP address and port +or contains any other scheme, it will reject with an +`InvalidArgumentException`: + +If the given URI appears to be valid, but connecting to it fails (such as if +the remote host rejects the connection etc.), it will reject with a +`RuntimeException`. + If you want to connect to hostname-port-combinations, see also the following chapter. ### DNS resolution diff --git a/src/ConnectionException.php b/src/ConnectionException.php deleted file mode 100644 index b5f9f47b..00000000 --- a/src/ConnectionException.php +++ /dev/null @@ -1,7 +0,0 @@ - Date: Thu, 16 Feb 2017 11:40:12 +0100 Subject: [PATCH 125/146] Mark all connector classes as final Classes should be used via composition rather than extension. This reduces our API footprint and avoids future BC breaks by avoiding exposing its internal assumptions. --- src/Connector.php | 2 +- src/DnsConnector.php | 3 +-- src/SecureConnector.php | 3 +-- src/TcpConnector.php | 4 +--- src/TimeoutConnector.php | 5 +---- src/UnixConnector.php | 2 +- 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Connector.php b/src/Connector.php index a1b79d25..4a07c81e 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -15,7 +15,7 @@ * @see DnsConnector for the newer replacement * @see ConnectorInterface for the base interface */ -class Connector implements ConnectorInterface +final class Connector implements ConnectorInterface { private $connector; diff --git a/src/DnsConnector.php b/src/DnsConnector.php index b40f032f..a91329fb 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -3,11 +3,10 @@ namespace React\SocketClient; use React\Dns\Resolver\Resolver; -use React\Stream\Stream; use React\Promise; use React\Promise\CancellablePromiseInterface; -class DnsConnector implements ConnectorInterface +final class DnsConnector implements ConnectorInterface { private $connector; private $resolver; diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 17046749..2dee858f 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -5,9 +5,8 @@ use React\EventLoop\LoopInterface; use React\Stream\Stream; use React\Promise; -use React\Promise\CancellablePromiseInterface; -class SecureConnector implements ConnectorInterface +final class SecureConnector implements ConnectorInterface { private $connector; private $streamEncryption; diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 8e4890d4..9b29852c 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -3,12 +3,10 @@ namespace React\SocketClient; use React\EventLoop\LoopInterface; -use React\Dns\Resolver\Resolver; use React\Stream\Stream; use React\Promise; -use React\Promise\Deferred; -class TcpConnector implements ConnectorInterface +final class TcpConnector implements ConnectorInterface { private $loop; private $context; diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php index f2bd25cf..67e4f9f0 100644 --- a/src/TimeoutConnector.php +++ b/src/TimeoutConnector.php @@ -5,11 +5,8 @@ use React\SocketClient\ConnectorInterface; use React\EventLoop\LoopInterface; use React\Promise\Timer; -use React\Stream\Stream; -use React\Promise\Promise; -use React\Promise\CancellablePromiseInterface; -class TimeoutConnector implements ConnectorInterface +final class TimeoutConnector implements ConnectorInterface { private $connector; private $timeout; diff --git a/src/UnixConnector.php b/src/UnixConnector.php index 44d225a0..9da4590f 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -14,7 +14,7 @@ * Unix domain sockets use atomic operations, so we can as well emulate * async behavior. */ -class UnixConnector implements ConnectorInterface +final class UnixConnector implements ConnectorInterface { private $loop; From 8e3cd4f07e9c4291f5ee36bb49597dcfab41376b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Feb 2017 16:17:41 +0100 Subject: [PATCH 126/146] Prepare v0.6.0 release --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 64 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 96 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9570ae..b10e1ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # Changelog +## 0.6.0 (2017-02-17) + +* Feature / BC break: Use `connect($uri)` instead of `create($host, $port)` + and resolve with a `ConnectionInterface` instead of `Stream` + and expose remote and local addresses through this interface + and remove superfluous and undocumented `ConnectionException`. + (#74, #82 and #84 by @clue) + + ```php + // old + $connector->create('google.com', 80)->then(function (Stream $conn) { + echo 'Connected' . PHP_EOL; + $conn->write("GET / HTTP/1.0\r\n\r\n"); + }); + + // new + $connector->connect('google.com:80')->then(function (ConnectionInterface $conn) { + echo 'Connected to ' . $conn->getRemoteAddress() . PHP_EOL; + $conn->write("GET / HTTP/1.0\r\n\r\n"); + }); + ``` + + > Note that both the old `Stream` and the new `ConnectionInterface` implement + the same underlying `DuplexStreamInterface`, so their streaming behavior is + actually equivalent. + In order to upgrade, simply use the new typehints. + Existing stream handlers should continue to work unchanged. + +* Feature / BC break: All connectors now MUST offer cancellation support. + You can now rely on getting a rejected promise when calling `cancel()` on a + pending connection attempt. + (#79 by @clue) + + ```php + // old: promise resolution not enforced and thus unreliable + $promise = $connector->create($host, $port); + $promise->cancel(); + $promise->then(/* MAY still be called */, /* SHOULD be called */); + + // new: rejecting after cancellation is mandatory + $promise = $connector->connect($uri); + $promise->cancel(); + $promise->then(/* MUST NOT be called */, /* MUST be called */); + ``` + + > Note that this behavior is only mandatory for *pending* connection attempts. + Once the promise is settled (resolved), calling `cancel()` will have no effect. + +* BC break: All connector classes are now marked `final` + and you can no longer `extend` them + (which was never documented or recommended anyway). + Please use composition instead of extension. + (#85 by @clue) + ## 0.5.3 (2016-12-24) * Fix: Skip IPv6 tests if not supported by the system diff --git a/README.md b/README.md index 47b77e64..b4c8dc2c 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,35 @@ [![Build Status](https://secure.travis-ci.org/reactphp/socket-client.png?branch=master)](http://travis-ci.org/reactphp/socket-client) [![Code Climate](https://codeclimate.com/github/reactphp/socket-client/badges/gpa.svg)](https://codeclimate.com/github/reactphp/socket-client) -Async Connector to open TCP/IP and SSL/TLS based connections. +Async, streaming plaintext TCP/IP and secure TLS based connections for [ReactPHP](https://reactphp.org/) -> The master branch contains the code for the upcoming 0.6 release. -For the code of the current stable 0.5.x release, checkout the -[0.5 branch](https://github.com/reactphp/socket-client/tree/0.5). - -## Introduction - -Think of this library as an async version of +You can think of this library as an async version of [`fsockopen()`](http://www.php.net/function.fsockopen) or [`stream_socket_client()`](http://php.net/function.stream-socket-client). - -Before you can actually transmit and receive data to/from a remote server, you -have to establish a connection to the remote end. Establishing this connection -through the internet/network takes some time as it requires several steps in -order to complete: - -1. Resolve remote target hostname via DNS (+cache) -2. Complete TCP handshake (2 roundtrips) with remote target IP:port -3. Optionally enable SSL/TLS on the new resulting connection +If you want to transmit and receive data to/from a remote server, you first +have to establish a connection to the remote end. +Establishing this connection through the internet/network may take some time +as it requires several steps (such as resolving target hostname, completing +TCP/IP handshake and enabling TLS) in order to complete. +This component provides an async version of all this so you can establish and +handle multiple connections without blocking. + +**Table of Contents** + +* [Usage](#usage) + * [ConnectorInterface](#connectorinterface) + * [connect()](#connect) + * [ConnectionInterface](#connectioninterface) + * [getRemoteAddress()](#getremoteaddress) + * [getLocalAddress()](#getlocaladdress) + * [Plaintext TCP/IP connections](#plaintext-tcpip-connections) + * [DNS resolution](#dns-resolution) + * [Secure TLS connections](#secure-tls-connections) + * [Connection timeout](#connection-timeouts) + * [Unix domain sockets](#unix-domain-sockets) +* [Install](#install) +* [Tests](#tests) +* [License](#license) ## Usage @@ -170,7 +179,7 @@ If your system has multiple interfaces (e.g. a WAN and a LAN interface), you can use this method to find out which interface was actually used for this connection. -### Async TCP/IP connections +### Plaintext TCP/IP connections The `React\SocketClient\TcpConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create plaintext @@ -271,7 +280,7 @@ $connector = new React\SocketClient\Connector($loop, $dns); $connector->connect('www.google.com:80')->then($callback); ``` -### Async SSL/TLS connections +### Secure TLS connections The `SecureConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create secure @@ -383,15 +392,26 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/socket-client:^0.5.3 +$ composer require react/socket-client:^0.6 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). ## Tests -To run the test suite, you need PHPUnit. Go to the project root and run: +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](http://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: ```bash -$ phpunit +$ php vendor/bin/phpunit ``` + +## License + +MIT, see [LICENSE file](LICENSE). From 8d6921f287c22606e80a1ea7f28f9bc117c8af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Mar 2017 16:14:26 +0100 Subject: [PATCH 127/146] Fix examples to use updated API --- examples/01-http.php | 2 +- examples/02-https.php | 2 +- examples/03-netcat.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/01-http.php b/examples/01-http.php index be7b1c0b..779c31eb 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -19,7 +19,7 @@ // time out connection attempt in 3.0s $dns = new TimeoutConnector($dns, 3.0, $loop); -$dns->create('www.google.com', 80)->then(function (ConnectionInterface $connection) { +$dns->connect('www.google.com:80')->then(function (ConnectionInterface $connection) { $connection->on('data', function ($data) { echo $data; }); diff --git a/examples/02-https.php b/examples/02-https.php index d18dce0e..9c92a9ad 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -21,7 +21,7 @@ // time out connection attempt in 3.0s $tls = new TimeoutConnector($tls, 3.0, $loop); -$tls->create('www.google.com', 443)->then(function (ConnectionInterface $connection) { +$tls->connect('www.google.com:443')->then(function (ConnectionInterface $connection) { $connection->on('data', function ($data) { echo $data; }); diff --git a/examples/03-netcat.php b/examples/03-netcat.php index 5ede41a7..e0c633cf 100644 --- a/examples/03-netcat.php +++ b/examples/03-netcat.php @@ -8,8 +8,8 @@ require __DIR__ . '/../vendor/autoload.php'; -if (!isset($argv[2])) { - fwrite(STDERR, 'Usage error: required arguments ' . PHP_EOL); +if (!isset($argv[1])) { + fwrite(STDERR, 'Usage error: required argument ' . PHP_EOL); exit(1); } @@ -33,7 +33,7 @@ $stderr->write('Connecting' . PHP_EOL); -$dns->create($argv[1], $argv[2])->then(function (ConnectionInterface $connection) use ($stdin, $stdout, $stderr) { +$dns->connect($argv[1])->then(function (ConnectionInterface $connection) use ($stdin, $stdout, $stderr) { // pipe everything from STDIN into connection $stdin->resume(); $stdin->pipe($connection); From 1fce3430c96c33584bdd60f475de63931b9a02a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Mar 2017 16:17:28 +0100 Subject: [PATCH 128/146] Forward compatibility with Stream v0.5 and upcoming v0.6 --- README.md | 12 ++++++++++-- composer.json | 2 +- src/ConnectionInterface.php | 12 ++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b4c8dc2c..efd58a36 100644 --- a/README.md +++ b/README.md @@ -116,10 +116,18 @@ you can use any of its events and methods as usual: ```php $connection->on('data', function ($chunk) { - echo $data; + echo $chunk; }); -$conenction->on('close', function () { +$connection->on('end', function () { + echo 'ended'; +}); + +$connection->on('error', function (Exception $e) { + echo 'error: ' . $e->getMessage(); +}); + +$connection->on('close', function () { echo 'closed'; }); diff --git a/composer.json b/composer.json index b90f7173..3671b721 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=5.3.0", "react/dns": "0.4.*|0.3.*", "react/event-loop": "0.4.*|0.3.*", - "react/stream": "^0.4.5", + "react/stream": "^0.6 || ^0.5 || ^0.4.5", "react/promise": "^2.1 || ^1.2", "react/promise-timer": "~1.0" }, diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php index 0687e754..ad33b2b6 100644 --- a/src/ConnectionInterface.php +++ b/src/ConnectionInterface.php @@ -29,10 +29,18 @@ * * ```php * $connection->on('data', function ($chunk) { - * echo $data; + * echo $chunk; * }); * - * $conenction->on('close', function () { + * $connection->on('end', function () { + * echo 'ended'; + * }); + * + * $connection->on('error', function (Exception $e) { + * echo 'error: ' . $e->getMessage(); + * }); + * + * $connection->on('close', function () { * echo 'closed'; * }); * From a141bb1c260414ac81d9b6318eaf795d6943cdec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Mar 2017 08:45:13 +0100 Subject: [PATCH 129/146] Prepare v0.6.1 release --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- composer.json | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b10e1ac4..29eed84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.6.1 (2017-03-10) + +* Feature: Forward compatibility with Stream v0.5 and upcoming v0.6 + (#89 by @clue) + +* Fix: Fix examples to use updated API + (#88 by @clue) + ## 0.6.0 (2017-02-17) * Feature / BC break: Use `connect($uri)` instead of `create($host, $port)` diff --git a/README.md b/README.md index efd58a36..1f738cdc 100644 --- a/README.md +++ b/README.md @@ -400,7 +400,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/socket-client:^0.6 +$ composer require react/socket-client:^0.6.1 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). diff --git a/composer.json b/composer.json index 3671b721..6e6eeb2f 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "react/socket-client", - "description": "Async connector to open TCP/IP and SSL/TLS based connections.", - "keywords": ["socket"], + "description": "Async, streaming plaintext TCP/IP and secure TLS based connections for ReactPHP", + "keywords": ["async", "socket", "stream", "connection", "ReactPHP"], "license": "MIT", "require": { "php": ">=5.3.0", From cdbdfe9a86bf9edfd48d53faefd76bacc692a213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Mar 2017 23:48:32 +0100 Subject: [PATCH 130/146] HTTP/HTTPS examples accept target host --- examples/01-http.php | 6 ++++-- examples/02-https.php | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/01-http.php b/examples/01-http.php index 779c31eb..9b06cc28 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -19,7 +19,9 @@ // time out connection attempt in 3.0s $dns = new TimeoutConnector($dns, 3.0, $loop); -$dns->connect('www.google.com:80')->then(function (ConnectionInterface $connection) { +$target = isset($argv[1]) ? $argv[1] : 'www.google.com:80'; + +$dns->connect($target)->then(function (ConnectionInterface $connection) use ($target) { $connection->on('data', function ($data) { echo $data; }); @@ -27,7 +29,7 @@ echo '[CLOSED]' . PHP_EOL; }); - $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); + $connection->write("GET / HTTP/1.0\r\nHost: $target\r\n\r\n"); }, 'printf'); $loop->run(); diff --git a/examples/02-https.php b/examples/02-https.php index 9c92a9ad..96721923 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -21,7 +21,9 @@ // time out connection attempt in 3.0s $tls = new TimeoutConnector($tls, 3.0, $loop); -$tls->connect('www.google.com:443')->then(function (ConnectionInterface $connection) { +$target = isset($argv[1]) ? $argv[1] : 'www.google.com:443'; + +$tls->connect($target)->then(function (ConnectionInterface $connection) use ($target) { $connection->on('data', function ($data) { echo $data; }); @@ -29,7 +31,7 @@ echo '[CLOSED]' . PHP_EOL; }); - $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); + $connection->write("GET / HTTP/1.0\r\nHost: $target\r\n\r\n"); }, 'printf'); $loop->run(); From 7c91c7a7d5d2e08b6d3d58832b2bd5ccabf2d70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Mar 2017 00:09:38 +0100 Subject: [PATCH 131/146] Pass through original host to underlying TcpConnector for TLS setup --- README.md | 29 ++++++++++++++++++++++++----- src/DnsConnector.php | 11 +++++++++-- src/SecureConnector.php | 17 ++--------------- src/TcpConnector.php | 35 ++++++++++++++++++++++++++++++++++- tests/DnsConnectorTest.php | 22 +++++++++++++++++++--- tests/IntegrationTest.php | 30 ++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1f738cdc..dd7446f4 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,13 @@ the remote host rejects the connection etc.), it will reject with a If you want to connect to hostname-port-combinations, see also the following chapter. +> Advanced usage: Internally, the `TcpConnector` allocates an empty *context* +resource for each stream resource. +If the destination URI contains a `hostname` query parameter, its value will +be used to set up the TLS peer name. +This is used by the `SecureConnector` and `DnsConnector` to verify the peer +name and can also be used if you want a custom TLS peer name. + ### DNS resolution The `DnsConnector` class implements the @@ -288,6 +295,17 @@ $connector = new React\SocketClient\Connector($loop, $dns); $connector->connect('www.google.com:80')->then($callback); ``` +> Advanced usage: Internally, the `DnsConnector` relies on a `Resolver` to +look up the IP address for the given hostname. +It will then replace the hostname in the destination URI with this IP and +append a `hostname` query parameter and pass this updated URI to the underlying +connector. +The underlying connector is thus responsible for creating a connection to the +target IP address, while this query parameter can be used to check the original +hostname and is used by the `TcpConnector` to set up the TLS peer name. +If a `hostname` is given explicitly, this query parameter will not be modified, +which can be useful if you want a custom TLS peer name. + ### Secure TLS connections The `SecureConnector` class implements the @@ -333,13 +351,14 @@ $secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop, )); ``` -> Advanced usage: Internally, the `SecureConnector` has to set the required -*context options* on the underlying stream resource. +> Advanced usage: Internally, the `SecureConnector` relies on setting up the +required *context options* on the underlying stream resource. It should therefor be used with a `TcpConnector` somewhere in the connector stack so that it can allocate an empty *context* resource for each stream -resource. -Failing to do so may result in some hard to trace race conditions, because all -stream resources will use a single, shared *default context* resource otherwise. +resource and verify the peer name. +Failing to do so may result in a TLS peer name mismatch error or some hard to +trace race conditions, because all stream resources will use a single, shared +*default context* resource otherwise. ### Connection timeouts diff --git a/src/DnsConnector.php b/src/DnsConnector.php index a91329fb..14c3bca6 100644 --- a/src/DnsConnector.php +++ b/src/DnsConnector.php @@ -30,13 +30,12 @@ public function connect($uri) return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); } - $that = $this; $host = trim($parts['host'], '[]'); $connector = $this->connector; return $this ->resolveHostname($host) - ->then(function ($ip) use ($connector, $parts) { + ->then(function ($ip) use ($connector, $host, $parts) { $uri = ''; // prepend original scheme if known @@ -66,6 +65,14 @@ public function connect($uri) $uri .= '?' . $parts['query']; } + // append original hostname as query if resolved via DNS and if + // destination URI does not contain "hostname" query param already + $args = array(); + parse_str(isset($parts['query']) ? $parts['query'] : '', $args); + if ($host !== $ip && !isset($args['hostname'])) { + $uri .= (isset($parts['query']) ? '&' : '?') . 'hostname=' . rawurlencode($host); + } + // append original fragment if known if (isset($parts['fragment'])) { $uri .= '#' . $parts['fragment']; diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 2dee858f..3c0b9ea3 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -30,25 +30,12 @@ public function connect($uri) } $parts = parse_url($uri); - if (!$parts || !isset($parts['host']) || $parts['scheme'] !== 'tls') { + if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') { return Promise\reject(new \InvalidArgumentException('Given URI "' . $uri . '" is invalid')); } $uri = str_replace('tls://', '', $uri); - $host = trim($parts['host'], '[]'); - - $context = $this->context + array( - 'SNI_enabled' => true, - 'peer_name' => $host - ); - - // legacy PHP < 5.6 ignores peer_name and requires legacy context options instead - if (PHP_VERSION_ID < 50600) { - $context += array( - 'SNI_server_name' => $host, - 'CN_match' => $host - ); - } + $context = $this->context; $encryption = $this->streamEncryption; return $this->connector->connect($uri)->then(function (ConnectionInterface $connection) use ($context, $encryption) { diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 454a9c34..4633b795 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -33,13 +33,46 @@ public function connect($uri) return Promise\reject(new \InvalidArgumentException('Given URI "' . $ip . '" does not contain a valid host IP')); } + // use context given in constructor + $context = array( + 'socket' => $this->context + ); + + // parse arguments from query component of URI + $args = array(); + if (isset($parts['query'])) { + parse_str($parts['query'], $args); + } + + // If an original hostname has been given, use this for TLS setup. + // This can happen due to layers of nested connectors, such as a + // DnsConnector reporting its original hostname. + // These context options are here in case TLS is enabled later on this stream. + // If TLS is not enabled later, this doesn't hurt either. + if (isset($args['hostname'])) { + $context['ssl'] = array( + 'SNI_enabled' => true, + 'peer_name' => $args['hostname'] + ); + + // Legacy PHP < 5.6 ignores peer_name and requires legacy context options instead. + // The SNI_server_name context option has to be set here during construction, + // as legacy PHP ignores any values set later. + if (PHP_VERSION_ID < 50600) { + $context['ssl'] += array( + 'SNI_server_name' => $args['hostname'], + 'CN_match' => $args['hostname'] + ); + } + } + $socket = @stream_socket_client( $uri, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT, - stream_context_create(array('socket' => $this->context)) + stream_context_create($context) ); if (false === $socket) { diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index 3ffae41d..5592ef42 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -30,7 +30,7 @@ public function testPassByResolverIfGivenIp() public function testPassThroughResolverIfGivenHost() { $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=google.com'))->will($this->returnValue(Promise\reject())); $this->connector->connect('google.com:80'); } @@ -38,7 +38,7 @@ public function testPassThroughResolverIfGivenHost() public function testPassThroughResolverIfGivenHostWhichResolvesToIpv6() { $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('::1'))); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('[::1]:80'))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('[::1]:80?hostname=google.com'))->will($this->returnValue(Promise\reject())); $this->connector->connect('google.com:80'); } @@ -51,6 +51,22 @@ public function testPassByResolverIfGivenCompleteUri() $this->connector->connect('scheme://127.0.0.1:80/path?query#fragment'); } + public function testPassThroughResolverIfGivenCompleteUri() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://1.2.3.4:80/path?query&hostname=google.com#fragment'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('scheme://google.com:80/path?query#fragment'); + } + + public function testPassThroughResolverIfGivenExplicitHost() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://1.2.3.4:80/?hostname=google.de'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('scheme://google.com:80/?hostname=google.de'); + } + public function testRejectsImmediatelyIfUriIsInvalid() { $this->resolver->expects($this->never())->method('resolve'); @@ -85,7 +101,7 @@ public function testCancelDuringTcpConnectionCancelsTcpConnection() { $pending = new Promise\Promise(function () { }, function () { throw new \Exception(); }); $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); - $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->will($this->returnValue($pending)); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->will($this->returnValue($pending)); $promise = $this->connector->connect('example.com:80'); $promise->cancel(); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 70951c89..6796874e 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -10,6 +10,7 @@ use React\SocketClient\TcpConnector; use React\Stream\BufferedSink; use Clue\React\Block; +use React\SocketClient\DnsConnector; class IntegrationTest extends TestCase { @@ -62,6 +63,35 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $this->assertRegExp('#^HTTP/1\.0#', $response); } + /** @test */ + public function gettingEncryptedStuffFromGoogleShouldWorkIfHostIsResolvedFirst() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = new StreamSelectLoop(); + + $factory = new Factory(); + $dns = $factory->create('8.8.8.8', $loop); + + $connector = new DnsConnector( + new SecureConnector( + new TcpConnector($loop), + $loop + ), + $dns + ); + + $conn = Block\await($connector->connect('google.com:443'), $loop); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = Block\await(BufferedSink::createPromise($conn), $loop, self::TIMEOUT); + + $this->assertRegExp('#^HTTP/1\.0#', $response); + } + /** @test */ public function testSelfSignedRejectsIfVerificationIsEnabled() { From f797b6af982c720da31903e09b3c14a50dfb71b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Mar 2017 17:26:17 +0100 Subject: [PATCH 132/146] Work around HHVM being unable to parse URIs with query but no path --- src/TcpConnector.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 4633b795..dbf8e751 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -66,6 +66,12 @@ public function connect($uri) } } + // HHVM fails to parse URIs with a query but no path, so let's add a dummy path + // See also https://3v4l.org/jEhLF + if (defined('HHVM_VERSION') && isset($parts['query']) && !isset($parts['path'])) { + $uri = str_replace('?', '/?', $uri); + } + $socket = @stream_socket_client( $uri, $errno, From 2589d0f8bd3d8527d2c3ee4b7868a0efdc68098a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Mar 2017 13:53:09 +0100 Subject: [PATCH 133/146] Documentation for supported PHP versions --- README.md | 30 ++++++++++++++++++++++++++++++ tests/SecureIntegrationTest.php | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd7446f4..a2e73d55 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,36 @@ $ composer require react/socket-client:^0.6.1 More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). +This project supports running on legacy PHP 5.3 through current PHP 7+ and HHVM. +It's *highly recommended to use PHP 7+* for this project, partly due to its vast +performance improvements and partly because legacy PHP versions require several +workarounds as described below. + +Secure TLS connections received some major upgrades starting with PHP 5.6, with +the defaults now being more secure, while older versions required explicit +context options. +This library does not take responsibility over these context options, so it's +up to consumers of this library to take care of setting appropriate context +options as described above. + +All versions of PHP prior to 5.6.8 suffered from a buffering issue where reading +from a streaming TLS connection could be one `data` event behind. +This library implements a work-around to try to flush the complete incoming +data buffers on these versions, but we have seen reports of people saying this +could still affect some older versions (`5.5.23`, `5.6.7`, and `5.6.8`). +Note that this only affects *some* higher-level streaming protocols, such as +IRC over TLS, but should not affect HTTP over TLS (HTTPS). +Further investigation of this issue is needed. +For more insights, this issue is also covered by our test suite. + +This project also supports running on HHVM. +Note that really old HHVM < 3.8 does not support secure TLS connections, as it +lacks the required `stream_socket_enable_crypto()` function. +As such, trying to create a secure TLS connections on affected versions will +return a rejected promise instead. +This issue is also covered by our test suite, which will skip related tests +on affected versions. + ## Tests To run the test suite, you first need to clone this repo and then install all diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index f64e46a8..e883d004 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -176,7 +176,7 @@ public function testConnectToServerWhichSendsDataWithoutEndingReceivesAllData() $peer->write($data); }); - $client = Block\await($this->connector->connect($this->address), $this->loop); + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); /* @var $client Stream */ // buffer incoming data for 0.1s (should be plenty of time) From e9efc9e85d5cf6453fa82e190f8f3213695c86bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 17 Mar 2017 15:00:55 +0100 Subject: [PATCH 134/146] Prepare v0.6.2 release --- CHANGELOG.md | 6 ++++++ README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29eed84f..2b516c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.6.2 (2017-03-17) + +* Feature / Fix: Support SNI on legacy PHP < 5.6 and add documentation for + supported PHP and HHVM versions. + (#90 and #91 by @clue) + ## 0.6.1 (2017-03-10) * Feature: Forward compatibility with Stream v0.5 and upcoming v0.6 diff --git a/README.md b/README.md index a2e73d55..bb4e5f1b 100644 --- a/README.md +++ b/README.md @@ -419,7 +419,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/socket-client:^0.6.1 +$ composer require react/socket-client:^0.6.2 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From 4c6bc517f96a9a5fb78da17248696be7a85376f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Mar 2017 17:07:12 +0100 Subject: [PATCH 135/146] Connector class now supports plaintext TCP and secure TLS connections --- README.md | 109 +++++++++++++++++++++++++++++++++++--- examples/04-web.php | 48 +++++++++++++++++ src/Connector.php | 49 +++++++++++++---- tests/IntegrationTest.php | 27 ++-------- 4 files changed, 194 insertions(+), 39 deletions(-) create mode 100644 examples/04-web.php diff --git a/README.md b/README.md index bb4e5f1b..5e7bcfe4 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ handle multiple connections without blocking. * [ConnectionInterface](#connectioninterface) * [getRemoteAddress()](#getremoteaddress) * [getLocalAddress()](#getlocaladdress) + * [Connector](#connector) * [Plaintext TCP/IP connections](#plaintext-tcpip-connections) * [DNS resolution](#dns-resolution) * [Secure TLS connections](#secure-tls-connections) @@ -34,13 +35,6 @@ handle multiple connections without blocking. ## Usage -In order to use this project, you'll need the following react boilerplate code -to initialize the main loop. - -```php -$loop = React\EventLoop\Factory::create(); -``` - ### ConnectorInterface The `ConnectorInterface` is responsible for providing an interface for @@ -187,6 +181,105 @@ If your system has multiple interfaces (e.g. a WAN and a LAN interface), you can use this method to find out which interface was actually used for this connection. +### Connector + +The `Connector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create any kind +of streaming connections, such as plaintext TCP/IP, secure TLS or local Unix +connection streams. + +It binds to the main event loop and can be used like this: + +```php +$loop = React\EventLoop\Factory::create(); +$connector = new Connector($loop); + +$connector->connect($uri)->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); + +$loop->run(); +``` + +In order to create a plaintext TCP/IP connection, you can simply pass a host +and port combination like this: + +```php +$connector->connect('www.google.com:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> If you do no specify a URI scheme in the destination URI, it will assume + `tcp://` as a default and establish a plaintext TCP/IP connection. + Note that TCP/IP connections require as host and port part in the destination + URI like above, all other URI components are optional. + +In order to create a secure TLS connection, you can use the `tls://` URI scheme +like this: + +```php +$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +In order to create a local Unix domain socket connection, you can use the +`unix://` URI scheme like this: + +```php +$connector->connect('unix:///tmp/demo.sock')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +Under the hood, the `Connector` is implemented as a *higher-level facade* +for the lower-level connectors implemented in this package. This means it +also shares all of their features and implementation details. +If you want to typehint in your higher-level protocol implementation, you SHOULD +use the generic [`ConnectorInterface`](#connectorinterface) instead. + +In particular, the `Connector` class uses Google's public DNS server `8.8.8.8` +to resolve all hostnames into underlying IP addresses by default. +This implies that it also ignores your `hosts` file and `resolve.conf`, which +means you won't be able to connect to `localhost` and other non-public +hostnames by default. +If you want to use a custom DNS server (such as a local DNS relay), you can set +up the `Connector` like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$dns = $dnsResolverFactory->createCached('127.0.1.1', $loop); + +$tcpConnector = new TcpConnector($loop); +$dnsConnector = new DnsConnector($tcpConnector, $dns); +$connector = new Connector($loop, $dnsConnector); + +$connector->connect('localhost:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +If you do not want to use a DNS resolver and want to connect to IP addresses +only, you can also set up your `Connector` like this: + +```php +$connector = new Connector( + $loop, + new TcpConnector($loop) +); + +$connector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + ### Plaintext TCP/IP connections The `React\SocketClient\TcpConnector` class implements the @@ -260,7 +353,7 @@ Make sure to set up your DNS resolver and underlying TCP connector like this: ```php $dnsResolverFactory = new React\Dns\Resolver\Factory(); -$dns = $dnsResolverFactory->connectCached('8.8.8.8', $loop); +$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); $dnsConnector = new React\SocketClient\DnsConnector($tcpConnector, $dns); diff --git a/examples/04-web.php b/examples/04-web.php new file mode 100644 index 00000000..faaf5ed9 --- /dev/null +++ b/examples/04-web.php @@ -0,0 +1,48 @@ +' . PHP_EOL); + exit(1); +} + +$loop = Factory::create(); +$connector = new Connector($loop); + +if (!isset($parts['port'])) { + $parts['port'] = $parts['scheme'] === 'https' ? 443 : 80; +} + +$host = $parts['host']; +if (($parts['scheme'] === 'http' && $parts['port'] !== 80) || ($parts['scheme'] === 'https' && $parts['port'] !== 443)) { + $host .= ':' . $parts['port']; +} +$target = ($parts['scheme'] === 'https' ? 'tls' : 'tcp') . '://' . $parts['host'] . ':' . $parts['port']; +$resource = isset($parts['path']) ? $parts['path'] : '/'; +if (isset($parts['query'])) { + $resource .= '?' . $parts['query']; +} + +$stdout = new Stream(STDOUT, $loop); +$stdout->pause(); + +$connector->connect($target)->then(function (ConnectionInterface $connection) use ($resource, $host, $stdout) { + $connection->pipe($stdout); + + $connection->write("GET $resource HTTP/1.0\r\nHost: $host\r\n\r\n"); +}, 'printf'); + +$loop->run(); diff --git a/src/Connector.php b/src/Connector.php index 4a07c81e..067a2b12 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -4,28 +4,59 @@ use React\EventLoop\LoopInterface; use React\Dns\Resolver\Resolver; +use React\Dns\Resolver\Factory; +use InvalidArgumentException; /** - * Legacy Connector + * The `Connector` class implements the `ConnectorInterface` and allows you to + * create any kind of streaming connections, such as plaintext TCP/IP, secure + * TLS or local Unix connection streams. * - * This class is not to be confused with the ConnectorInterface and should not - * be used as a typehint. + * Under the hood, the `Connector` is implemented as a *higher-level facade* + * or the lower-level connectors implemented in this package. This means it + * also shares all of their features and implementation details. + * If you want to typehint in your higher-level protocol implementation, you SHOULD + * use the generic [`ConnectorInterface`](#connectorinterface) instead. * - * @deprecated Exists for BC only, consider using the newer DnsConnector instead - * @see DnsConnector for the newer replacement * @see ConnectorInterface for the base interface */ final class Connector implements ConnectorInterface { - private $connector; + private $tcp; + private $tls; + private $unix; - public function __construct(LoopInterface $loop, Resolver $resolver) + public function __construct(LoopInterface $loop, ConnectorInterface $tcp = null) { - $this->connector = new DnsConnector(new TcpConnector($loop), $resolver); + if ($tcp === null) { + $factory = new Factory(); + $resolver = $factory->create('8.8.8.8', $loop); + + $tcp = new DnsConnector(new TcpConnector($loop), $resolver); + } + + $this->tcp = $tcp; + $this->tls = new SecureConnector($tcp, $loop); + $this->unix = new UnixConnector($loop); } public function connect($uri) { - return $this->connector->connect($uri); + if (strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + } + + $scheme = (string)substr($uri, 0, strpos($uri, '://')); + + if ($scheme === 'tcp') { + return $this->tcp->connect($uri); + } elseif ($scheme === 'tls') { + return $this->tls->connect($uri); + } elseif ($scheme === 'unix') { + return $this->unix->connect($uri); + } else{ + return Promise\reject(new InvalidArgumentException('Unknown URI scheme given')); + } } } + diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 6796874e..fd8c8677 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -4,7 +4,6 @@ use React\Dns\Resolver\Factory; use React\EventLoop\StreamSelectLoop; -use React\Socket\Server; use React\SocketClient\Connector; use React\SocketClient\SecureConnector; use React\SocketClient\TcpConnector; @@ -20,10 +19,7 @@ class IntegrationTest extends TestCase public function gettingStuffFromGoogleShouldWork() { $loop = new StreamSelectLoop(); - - $factory = new Factory(); - $dns = $factory->create('8.8.8.8', $loop); - $connector = new Connector($loop, $dns); + $connector = new Connector($loop); $conn = Block\await($connector->connect('google.com:80'), $loop); @@ -45,16 +41,9 @@ public function gettingEncryptedStuffFromGoogleShouldWork() } $loop = new StreamSelectLoop(); + $secureConnector = new Connector($loop); - $factory = new Factory(); - $dns = $factory->create('8.8.8.8', $loop); - - $secureConnector = new SecureConnector( - new Connector($loop, $dns), - $loop - ); - - $conn = Block\await($secureConnector->connect('google.com:443'), $loop); + $conn = Block\await($secureConnector->connect('tls://google.com:443'), $loop); $conn->write("GET / HTTP/1.0\r\n\r\n"); @@ -101,11 +90,8 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() $loop = new StreamSelectLoop(); - $factory = new Factory(); - $dns = $factory->create('8.8.8.8', $loop); - $secureConnector = new SecureConnector( - new Connector($loop, $dns), + new Connector($loop), $loop, array( 'verify_peer' => true @@ -125,11 +111,8 @@ public function testSelfSignedResolvesIfVerificationIsDisabled() $loop = new StreamSelectLoop(); - $factory = new Factory(); - $dns = $factory->create('8.8.8.8', $loop); - $secureConnector = new SecureConnector( - new Connector($loop, $dns), + new Connector($loop), $loop, array( 'verify_peer' => false From 90d5d1ecd405bba7b7573f9eb320b840751f4303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 11 Mar 2017 00:33:15 +0100 Subject: [PATCH 136/146] Connector is now main class, everything else is advanced usage --- README.md | 32 ++++++++++++++++++-------------- examples/01-http.php | 20 +++++--------------- examples/02-https.php | 22 +++++----------------- examples/03-netcat.php | 17 ++++------------- src/Connector.php | 8 +++++--- 5 files changed, 37 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 5e7bcfe4..b5e4b091 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,12 @@ handle multiple connections without blocking. * [getRemoteAddress()](#getremoteaddress) * [getLocalAddress()](#getlocaladdress) * [Connector](#connector) - * [Plaintext TCP/IP connections](#plaintext-tcpip-connections) - * [DNS resolution](#dns-resolution) - * [Secure TLS connections](#secure-tls-connections) - * [Connection timeout](#connection-timeouts) - * [Unix domain sockets](#unix-domain-sockets) +* [Advanced Usage](#advanced-usage) + * [TcpConnector](#tcpconnector) + * [DnsConnector](#dnsconnector) + * [SecureConnector](#secureconnector) + * [TimeoutConnector](#timeoutconnector) + * [UnixConnector](#unixconnector) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -183,10 +184,11 @@ used for this connection. ### Connector -The `Connector` class implements the -[`ConnectorInterface`](#connectorinterface) and allows you to create any kind -of streaming connections, such as plaintext TCP/IP, secure TLS or local Unix -connection streams. +The `Connector` class is the main class in this package that implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create streaming connections. + +You can use this connector to create any kind of streaming connections, such +as plaintext TCP/IP, secure TLS or local Unix connection streams. It binds to the main event loop and can be used like this: @@ -280,7 +282,9 @@ $connector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connect }); ``` -### Plaintext TCP/IP connections +## Advanced Usage + +### TcpConnector The `React\SocketClient\TcpConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create plaintext @@ -339,7 +343,7 @@ be used to set up the TLS peer name. This is used by the `SecureConnector` and `DnsConnector` to verify the peer name and can also be used if you want a custom TLS peer name. -### DNS resolution +### DnsConnector The `DnsConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create plaintext @@ -399,7 +403,7 @@ hostname and is used by the `TcpConnector` to set up the TLS peer name. If a `hostname` is given explicitly, this query parameter will not be modified, which can be useful if you want a custom TLS peer name. -### Secure TLS connections +### SecureConnector The `SecureConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create secure @@ -453,7 +457,7 @@ Failing to do so may result in a TLS peer name mismatch error or some hard to trace race conditions, because all stream resources will use a single, shared *default context* resource otherwise. -### Connection timeouts +### TimeoutConnector The `TimeoutConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to add timeout @@ -484,7 +488,7 @@ $promise->cancel(); Calling `cancel()` on a pending promise will cancel the underlying connection attempt, abort the timer and reject the resulting promise. -### Unix domain sockets +### UnixConnector The `UnixConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to connect to diff --git a/examples/01-http.php b/examples/01-http.php index 9b06cc28..95519c91 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -1,27 +1,17 @@ create('8.8.8.8', $loop); - -$tcp = new TcpConnector($loop); -$dns = new DnsConnector($tcp, $resolver); - -// time out connection attempt in 3.0s -$dns = new TimeoutConnector($dns, 3.0, $loop); - -$target = isset($argv[1]) ? $argv[1] : 'www.google.com:80'; - -$dns->connect($target)->then(function (ConnectionInterface $connection) use ($target) { +$connector->connect($target)->then(function (ConnectionInterface $connection) use ($target) { $connection->on('data', function ($data) { echo $data; }); diff --git a/examples/02-https.php b/examples/02-https.php index 96721923..a6abd2a9 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -1,29 +1,17 @@ create('8.8.8.8', $loop); - -$tcp = new TcpConnector($loop); -$dns = new DnsConnector($tcp, $resolver); -$tls = new SecureConnector($dns, $loop); - -// time out connection attempt in 3.0s -$tls = new TimeoutConnector($tls, 3.0, $loop); - -$target = isset($argv[1]) ? $argv[1] : 'www.google.com:443'; - -$tls->connect($target)->then(function (ConnectionInterface $connection) use ($target) { +$connector->connect($target)->then(function (ConnectionInterface $connection) use ($target) { $connection->on('data', function ($data) { echo $data; }); diff --git a/examples/03-netcat.php b/examples/03-netcat.php index e0c633cf..42c12346 100644 --- a/examples/03-netcat.php +++ b/examples/03-netcat.php @@ -1,10 +1,9 @@ create('8.8.8.8', $loop); - -$tcp = new TcpConnector($loop); -$dns = new DnsConnector($tcp, $resolver); - -// time out connection attempt in 3.0s -$dns = new TimeoutConnector($dns, 3.0, $loop); +$connector = new Connector($loop); $stdin = new Stream(STDIN, $loop); $stdin->pause(); @@ -33,7 +24,7 @@ $stderr->write('Connecting' . PHP_EOL); -$dns->connect($argv[1])->then(function (ConnectionInterface $connection) use ($stdin, $stdout, $stderr) { +$connector->connect($argv[1])->then(function (ConnectionInterface $connection) use ($stdin, $stdout, $stderr) { // pipe everything from STDIN into connection $stdin->resume(); $stdin->pipe($connection); diff --git a/src/Connector.php b/src/Connector.php index 067a2b12..6166655f 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -8,9 +8,11 @@ use InvalidArgumentException; /** - * The `Connector` class implements the `ConnectorInterface` and allows you to - * create any kind of streaming connections, such as plaintext TCP/IP, secure - * TLS or local Unix connection streams. + * The `Connector` class is the main class in this package that implements the + * `ConnectorInterface` and allows you to create streaming connections. + * + * You can use this connector to create any kind of streaming connections, such + * as plaintext TCP/IP, secure TLS or local Unix connection streams. * * Under the hood, the `Connector` is implemented as a *higher-level facade* * or the lower-level connectors implemented in this package. This means it From 07ec6840768fab46e803941189961b1d8747de46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Mar 2017 15:00:12 +0100 Subject: [PATCH 137/146] Simplify DNS setup by using underlying connector hash map --- README.md | 39 ++++++++++++++++-------- examples/02-https.php | 4 +-- src/Connector.php | 63 ++++++++++++++++++++++++--------------- tests/ConnectorTest.php | 33 ++++++++++++++++++++ tests/IntegrationTest.php | 20 +++++++++++++ 5 files changed, 120 insertions(+), 39 deletions(-) create mode 100644 tests/ConnectorTest.php diff --git a/README.md b/README.md index b5e4b091..1396ad37 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ $connector->connect('www.google.com:80')->then(function (ConnectionInterface $co > If you do no specify a URI scheme in the destination URI, it will assume `tcp://` as a default and establish a plaintext TCP/IP connection. - Note that TCP/IP connections require as host and port part in the destination + Note that TCP/IP connections require a host and port part in the destination URI like above, all other URI components are optional. In order to create a secure TLS connection, you can use the `tls://` URI scheme @@ -254,12 +254,9 @@ If you want to use a custom DNS server (such as a local DNS relay), you can set up the `Connector` like this: ```php -$dnsResolverFactory = new React\Dns\Resolver\Factory(); -$dns = $dnsResolverFactory->createCached('127.0.1.1', $loop); - -$tcpConnector = new TcpConnector($loop); -$dnsConnector = new DnsConnector($tcpConnector, $dns); -$connector = new Connector($loop, $dnsConnector); +$connector = new Connector($loop, array( + 'dns' => '127.0.1.1' +)); $connector->connect('localhost:80')->then(function (ConnectionInterface $connection) { $connection->write('...'); @@ -267,14 +264,13 @@ $connector->connect('localhost:80')->then(function (ConnectionInterface $connect }); ``` -If you do not want to use a DNS resolver and want to connect to IP addresses -only, you can also set up your `Connector` like this: +If you do not want to use a DNS resolver at all and want to connect to IP +addresses only, you can also set up your `Connector` like this: ```php -$connector = new Connector( - $loop, - new TcpConnector($loop) -); +$connector = new Connector($loop, array( + 'dns' => false +)); $connector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connection) { $connection->write('...'); @@ -282,6 +278,23 @@ $connector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connect }); ``` +Advanced: If you need a custom DNS `Resolver` instance, you can also set up +your `Connector` like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$resolver = $dnsResolverFactory->createCached('127.0.1.1', $loop); + +$connector = new Connector($loop, array( + 'dns' => $resolver +)); + +$connector->connect('localhost:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + ## Advanced Usage ### TcpConnector diff --git a/examples/02-https.php b/examples/02-https.php index a6abd2a9..b1780de6 100644 --- a/examples/02-https.php +++ b/examples/02-https.php @@ -4,14 +4,14 @@ use React\SocketClient\Connector; use React\SocketClient\ConnectionInterface; -$target = 'tls://' . (isset($argv[1]) ? $argv[1] : 'www.google.com:443'); +$target = isset($argv[1]) ? $argv[1] : 'www.google.com:443'; require __DIR__ . '/../vendor/autoload.php'; $loop = Factory::create(); $connector = new Connector($loop); -$connector->connect($target)->then(function (ConnectionInterface $connection) use ($target) { +$connector->connect('tls://' . $target)->then(function (ConnectionInterface $connection) use ($target) { $connection->on('data', function ($data) { echo $data; }); diff --git a/src/Connector.php b/src/Connector.php index 6166655f..fae7c860 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -5,7 +5,8 @@ use React\EventLoop\LoopInterface; use React\Dns\Resolver\Resolver; use React\Dns\Resolver\Factory; -use InvalidArgumentException; +use React\Promise; +use RuntimeException; /** * The `Connector` class is the main class in this package that implements the @@ -24,41 +25,55 @@ */ final class Connector implements ConnectorInterface { - private $tcp; - private $tls; - private $unix; + private $connectors; - public function __construct(LoopInterface $loop, ConnectorInterface $tcp = null) + public function __construct(LoopInterface $loop, array $options = array()) { - if ($tcp === null) { - $factory = new Factory(); - $resolver = $factory->create('8.8.8.8', $loop); + // apply default options if not explicitly given + $options += array( + 'dns' => true + ); - $tcp = new DnsConnector(new TcpConnector($loop), $resolver); + $tcp = new TcpConnector($loop); + if ($options['dns'] !== false) { + if ($options['dns'] instanceof Resolver) { + $resolver = $options['dns']; + } else { + $factory = new Factory(); + $resolver = $factory->create( + $options['dns'] === true ? '8.8.8.8' : $options['dns'], + $loop + ); + } + + $tcp = new DnsConnector($tcp, $resolver); } - $this->tcp = $tcp; - $this->tls = new SecureConnector($tcp, $loop); - $this->unix = new UnixConnector($loop); + $tls = new SecureConnector($tcp, $loop); + + $unix = new UnixConnector($loop); + + $this->connectors = array( + 'tcp' => $tcp, + 'tls' => $tls, + 'unix' => $unix + ); } public function connect($uri) { - if (strpos($uri, '://') === false) { - $uri = 'tcp://' . $uri; + $scheme = 'tcp'; + if (strpos($uri, '://') !== false) { + $scheme = (string)substr($uri, 0, strpos($uri, '://')); } - $scheme = (string)substr($uri, 0, strpos($uri, '://')); - - if ($scheme === 'tcp') { - return $this->tcp->connect($uri); - } elseif ($scheme === 'tls') { - return $this->tls->connect($uri); - } elseif ($scheme === 'unix') { - return $this->unix->connect($uri); - } else{ - return Promise\reject(new InvalidArgumentException('Unknown URI scheme given')); + if (!isset($this->connectors[$scheme])) { + return Promise\reject(new RuntimeException( + 'No connector available for URI scheme "' . $scheme . '"' + )); } + + return $this->connectors[$scheme]->connect($uri); } } diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php new file mode 100644 index 00000000..19b87fe4 --- /dev/null +++ b/tests/ConnectorTest.php @@ -0,0 +1,33 @@ +getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop); + + $promise = $connector->connect('unknown://google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorUsesGivenResolverInstance() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $promise = new Promise(function () { }); + $resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); + $resolver->expects($this->once())->method('resolve')->with('google.com')->willReturn($promise); + + $connector = new Connector($loop, array( + 'dns' => $resolver + )); + + $connector->connect('google.com:80'); + } +} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index fd8c8677..4451d30e 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -81,6 +81,26 @@ public function gettingEncryptedStuffFromGoogleShouldWorkIfHostIsResolvedFirst() $this->assertRegExp('#^HTTP/1\.0#', $response); } + /** @test */ + public function testConnectingFailsIfDnsUsesInvalidResolver() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = new StreamSelectLoop(); + + $factory = new Factory(); + $dns = $factory->create('demo.invalid', $loop); + + $connector = new Connector($loop, array( + 'dns' => $dns + )); + + $this->setExpectedException('RuntimeException'); + Block\await($connector->connect('google.com:80'), $loop, self::TIMEOUT); + } + /** @test */ public function testSelfSignedRejectsIfVerificationIsEnabled() { From 46c763541c4f3d2a68516296c6073b4fdbbfe6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 27 Mar 2017 23:46:12 +0200 Subject: [PATCH 138/146] Support disabling certain URI schemes --- README.md | 18 +++++++++++++++++ src/Connector.php | 25 ++++++++++++++--------- tests/ConnectorTest.php | 44 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1396ad37..afc6d265 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,24 @@ $connector->connect('localhost:80')->then(function (ConnectionInterface $connect }); ``` +By default, the `Connector` supports the `tcp://`, `tls://` and `unix://` +URI schemes. If you want to explicitly prohibit any of these, you can simply +pass boolean flags like this: + +```php +// only allow secure TLS connections +$connector = new Connector($loop, array( + 'tcp' => false, + 'tls' => true, + 'unix' => false, +)); + +$connector->connect('tcp://localhost:443')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + ## Advanced Usage ### TcpConnector diff --git a/src/Connector.php b/src/Connector.php index fae7c860..f7e9bea9 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -25,13 +25,16 @@ */ final class Connector implements ConnectorInterface { - private $connectors; + private $connectors = array(); public function __construct(LoopInterface $loop, array $options = array()) { // apply default options if not explicitly given $options += array( - 'dns' => true + 'tcp' => true, + 'dns' => true, + 'tls' => true, + 'unix' => true, ); $tcp = new TcpConnector($loop); @@ -49,15 +52,19 @@ public function __construct(LoopInterface $loop, array $options = array()) $tcp = new DnsConnector($tcp, $resolver); } - $tls = new SecureConnector($tcp, $loop); + if ($options['tcp'] !== false) { + $this->connectors['tcp'] = $tcp; + } - $unix = new UnixConnector($loop); + if ($options['tls'] !== false) { + $tls = new SecureConnector($tcp, $loop); + $this->connectors['tls'] = $tls; + } - $this->connectors = array( - 'tcp' => $tcp, - 'tls' => $tls, - 'unix' => $unix - ); + if ($options['unix'] !== false) { + $unix = new UnixConnector($loop); + $this->connectors['unix'] = $unix; + } } public function connect($uri) diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php index 19b87fe4..5205b791 100644 --- a/tests/ConnectorTest.php +++ b/tests/ConnectorTest.php @@ -16,6 +16,50 @@ public function testConnectorWithUnknownSchemeAlwaysFails() $promise->then(null, $this->expectCallableOnce()); } + public function testConnectorWithDisabledTcpDefaultSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'tcp' => false + )); + + $promise = $connector->connect('google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorWithDisabledTcpSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'tcp' => false + )); + + $promise = $connector->connect('tcp://google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorWithDisabledTlsSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'tls' => false + )); + + $promise = $connector->connect('tls://google.com:443'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorWithDisabledUnixSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'unix' => false + )); + + $promise = $connector->connect('unix://demo.sock'); + $promise->then(null, $this->expectCallableOnce()); + } + public function testConnectorUsesGivenResolverInstance() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); From 6c88baf77cffa6a8b66572324a69bd327333e520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Mar 2017 15:22:27 +0100 Subject: [PATCH 139/146] Allow setting TCP and TLS context options --- README.md | 29 ++++++++++++++++++++++++++++- src/Connector.php | 11 +++++++++-- tests/IntegrationTest.php | 20 ++++++++------------ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index afc6d265..6289c360 100644 --- a/README.md +++ b/README.md @@ -307,12 +307,39 @@ $connector = new Connector($loop, array( 'unix' => false, )); -$connector->connect('tcp://localhost:443')->then(function (ConnectionInterface $connection) { +$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $connection) { $connection->write('...'); $connection->end(); }); ``` +The `tcp://` and `tls://` also accept additional context options passed to +the underlying connectors. +If you want to explicitly pass additional context options, you can simply +pass arrays of context options like this: + +```php +// allow insecure TLS connections +$connector = new Connector($loop, array( + 'tcp' => array( + 'bindto' => '192.168.0.1:0' + ), + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ), +)); + +$connector->connect('tls://localhost:443')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> For more details about context options, please refer to the PHP documentation + about [socket context options](http://php.net/manual/en/context.socket.php) + and [SSL context options](http://php.net/manual/en/context.ssl.php). + ## Advanced Usage ### TcpConnector diff --git a/src/Connector.php b/src/Connector.php index f7e9bea9..cf093862 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -37,7 +37,10 @@ public function __construct(LoopInterface $loop, array $options = array()) 'unix' => true, ); - $tcp = new TcpConnector($loop); + $tcp = new TcpConnector( + $loop, + is_array($options['tcp']) ? $options['tcp'] : array() + ); if ($options['dns'] !== false) { if ($options['dns'] instanceof Resolver) { $resolver = $options['dns']; @@ -57,7 +60,11 @@ public function __construct(LoopInterface $loop, array $options = array()) } if ($options['tls'] !== false) { - $tls = new SecureConnector($tcp, $loop); + $tls = new SecureConnector( + $tcp, + $loop, + is_array($options['tls']) ? $options['tls'] : array() + ); $this->connectors['tls'] = $tls; } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 4451d30e..bbc0a51a 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -110,16 +110,14 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() $loop = new StreamSelectLoop(); - $secureConnector = new SecureConnector( - new Connector($loop), - $loop, - array( + $connector = new Connector($loop, array( + 'tls' => array( 'verify_peer' => true ) - ); + )); $this->setExpectedException('RuntimeException'); - Block\await($secureConnector->connect('self-signed.badssl.com:443'), $loop, self::TIMEOUT); + Block\await($connector->connect('tls://self-signed.badssl.com:443'), $loop, self::TIMEOUT); } /** @test */ @@ -131,15 +129,13 @@ public function testSelfSignedResolvesIfVerificationIsDisabled() $loop = new StreamSelectLoop(); - $secureConnector = new SecureConnector( - new Connector($loop), - $loop, - array( + $connector = new Connector($loop, array( + 'tls' => array( 'verify_peer' => false ) - ); + )); - $conn = Block\await($secureConnector->connect('self-signed.badssl.com:443'), $loop, self::TIMEOUT); + $conn = Block\await($connector->connect('tls://self-signed.badssl.com:443'), $loop, self::TIMEOUT); $conn->close(); } From 91198c97bd893d21dc5a4d061698cd5d3c2ef533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 25 Mar 2017 15:55:13 +0100 Subject: [PATCH 140/146] Support explicitly passing connectors --- README.md | 38 ++++++++++++++++++++++++++++++++ src/Connector.php | 33 +++++++++++++++++----------- tests/ConnectorTest.php | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6289c360..3657f1f0 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,44 @@ $connector->connect('tls://localhost:443')->then(function (ConnectionInterface $ about [socket context options](http://php.net/manual/en/context.socket.php) and [SSL context options](http://php.net/manual/en/context.ssl.php). +Advanced: By default, the `Connector` supports the `tcp://`, `tls://` and +`unix://` URI schemes. +For this, it sets up the required connector classes automatically. +If you want to explicitly pass custom connectors for any of these, you can simply +pass an instance implementing the `ConnectorInterface` like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$resolver = $dnsResolverFactory->createCached('127.0.1.1', $loop); +$tcp = new DnsConnector(new TcpConnector($loop), $resolver); + +$tls = new SecureConnector($tcp, $loop); + +$unix = new UnixConnector($loop); + +$connector = new Connector($loop, array( + 'tcp' => $tcp, + 'dns' => false, + 'tls' => $tls, + 'unix' => $unix, +)); + +$connector->connect('google.com:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> Internally, the `tcp://` connector will always be wrapped by the DNS resolver, + unless you disable DNS like in the above example. In this case, the `tcp://` + connector receives the actual hostname instead of only the resolved IP address + and is thus responsible for performing the lookup. + Internally, the automatically created `tls://` connector will always wrap the + underlying `tcp://` connector for establishing the underlying plaintext + TCP/IP connection before enabling secure TLS mode. If you want to use a custom + underlying `tcp://` connector for secure TLS connections only, you may + explicitly pass a `tls://` connector like above instead. + ## Advanced Usage ### TcpConnector diff --git a/src/Connector.php b/src/Connector.php index cf093862..f4c45aa3 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -37,10 +37,15 @@ public function __construct(LoopInterface $loop, array $options = array()) 'unix' => true, ); - $tcp = new TcpConnector( - $loop, - is_array($options['tcp']) ? $options['tcp'] : array() - ); + if ($options['tcp'] instanceof ConnectorInterface) { + $tcp = $options['tcp']; + } else { + $tcp = new TcpConnector( + $loop, + is_array($options['tcp']) ? $options['tcp'] : array() + ); + } + if ($options['dns'] !== false) { if ($options['dns'] instanceof Resolver) { $resolver = $options['dns']; @@ -60,17 +65,21 @@ public function __construct(LoopInterface $loop, array $options = array()) } if ($options['tls'] !== false) { - $tls = new SecureConnector( - $tcp, - $loop, - is_array($options['tls']) ? $options['tls'] : array() - ); - $this->connectors['tls'] = $tls; + if (!$options['tls'] instanceof ConnectorInterface) { + $options['tls'] = new SecureConnector( + $tcp, + $loop, + is_array($options['tls']) ? $options['tls'] : array() + ); + } + $this->connectors['tls'] = $options['tls']; } if ($options['unix'] !== false) { - $unix = new UnixConnector($loop); - $this->connectors['unix'] = $unix; + if (!$options['unix'] instanceof ConnectorInterface) { + $options['unix'] = new UnixConnector($loop); + } + $this->connectors['unix'] = $options['unix']; } } diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php index 5205b791..0672068a 100644 --- a/tests/ConnectorTest.php +++ b/tests/ConnectorTest.php @@ -7,6 +7,35 @@ class ConnectorTest extends TestCase { + public function testConnectorUsesTcpAsDefaultScheme() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); + $tcp->expects($this->once())->method('connect')->with('127.0.0.1:80'); + + $connector = new Connector($loop, array( + 'tcp' => $tcp + )); + + $connector->connect('127.0.0.1:80'); + } + + public function testConnectorPassedThroughHostnameIfDnsIsDisabled() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); + $tcp->expects($this->once())->method('connect')->with('tcp://google.com:80'); + + $connector = new Connector($loop, array( + 'tcp' => $tcp, + 'dns' => false + )); + + $connector->connect('tcp://google.com:80'); + } + public function testConnectorWithUnknownSchemeAlwaysFails() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -74,4 +103,23 @@ public function testConnectorUsesGivenResolverInstance() $connector->connect('google.com:80'); } + + public function testConnectorUsesResolvedHostnameIfDnsIsUsed() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $promise = new Promise(function ($resolve) { $resolve('127.0.0.1'); }); + $resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); + $resolver->expects($this->once())->method('resolve')->with('google.com')->willReturn($promise); + + $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); + $tcp->expects($this->once())->method('connect')->with('tcp://127.0.0.1:80?hostname=google.com'); + + $connector = new Connector($loop, array( + 'tcp' => $tcp, + 'dns' => $resolver + )); + + $connector->connect('tcp://google.com:80'); + } } From 03504a1d59fdc2c232431998e418d81494c15bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 27 Mar 2017 13:32:13 +0200 Subject: [PATCH 141/146] Add timeout handling --- README.md | 25 ++++++++++++++++++++++++- src/Connector.php | 29 +++++++++++++++++++++++++++-- tests/ConnectorTest.php | 9 ++++++--- tests/IntegrationTest.php | 21 +++++++++++++++++---- 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3657f1f0..bfd0cc33 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,25 @@ $connector->connect('localhost:80')->then(function (ConnectionInterface $connect }); ``` +By default, the `tcp://` and `tls://` URI schemes will use timeout value that +repects your `default_socket_timeout` ini setting (which defaults to 60s). +If you want a custom timeout value, you can simply pass this like this: + +```php +$connector = new Connector($loop, array( + 'timeout' => 10.0 +)); +``` + +Similarly, if you do not want to apply a timeout at all and let the operating +system handle this, you can pass a boolean flag like this: + +```php +$connector = new Connector($loop, array( + 'timeout' => false +)); +``` + By default, the `Connector` supports the `tcp://`, `tls://` and `unix://` URI schemes. If you want to explicitly prohibit any of these, you can simply pass boolean flags like this: @@ -357,9 +376,11 @@ $unix = new UnixConnector($loop); $connector = new Connector($loop, array( 'tcp' => $tcp, - 'dns' => false, 'tls' => $tls, 'unix' => $unix, + + 'dns' => false, + 'timeout' => false, )); $connector->connect('google.com:80')->then(function (ConnectionInterface $connection) { @@ -377,6 +398,8 @@ $connector->connect('google.com:80')->then(function (ConnectionInterface $connec TCP/IP connection before enabling secure TLS mode. If you want to use a custom underlying `tcp://` connector for secure TLS connections only, you may explicitly pass a `tls://` connector like above instead. + Internally, the `tcp://` and `tls://` connectors will always be wrapped by + `TimeoutConnector`, unless you disable timeouts like in the above example. ## Advanced Usage diff --git a/src/Connector.php b/src/Connector.php index f4c45aa3..7a6d81d8 100644 --- a/src/Connector.php +++ b/src/Connector.php @@ -32,11 +32,17 @@ public function __construct(LoopInterface $loop, array $options = array()) // apply default options if not explicitly given $options += array( 'tcp' => true, - 'dns' => true, 'tls' => true, 'unix' => true, + + 'dns' => true, + 'timeout' => true, ); + if ($options['timeout'] === true) { + $options['timeout'] = (float)ini_get("default_socket_timeout"); + } + if ($options['tcp'] instanceof ConnectorInterface) { $tcp = $options['tcp']; } else { @@ -61,7 +67,17 @@ public function __construct(LoopInterface $loop, array $options = array()) } if ($options['tcp'] !== false) { - $this->connectors['tcp'] = $tcp; + $options['tcp'] = $tcp; + + if ($options['timeout'] !== false) { + $options['tcp'] = new TimeoutConnector( + $options['tcp'], + $options['timeout'], + $loop + ); + } + + $this->connectors['tcp'] = $options['tcp']; } if ($options['tls'] !== false) { @@ -72,6 +88,15 @@ public function __construct(LoopInterface $loop, array $options = array()) is_array($options['tls']) ? $options['tls'] : array() ); } + + if ($options['timeout'] !== false) { + $options['tls'] = new TimeoutConnector( + $options['tls'], + $options['timeout'], + $loop + ); + } + $this->connectors['tls'] = $options['tls']; } diff --git a/tests/ConnectorTest.php b/tests/ConnectorTest.php index 0672068a..ea167ad3 100644 --- a/tests/ConnectorTest.php +++ b/tests/ConnectorTest.php @@ -11,8 +11,9 @@ public function testConnectorUsesTcpAsDefaultScheme() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $promise = new Promise(function () { }); $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); - $tcp->expects($this->once())->method('connect')->with('127.0.0.1:80'); + $tcp->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise); $connector = new Connector($loop, array( 'tcp' => $tcp @@ -25,8 +26,9 @@ public function testConnectorPassedThroughHostnameIfDnsIsDisabled() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $promise = new Promise(function () { }); $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); - $tcp->expects($this->once())->method('connect')->with('tcp://google.com:80'); + $tcp->expects($this->once())->method('connect')->with('tcp://google.com:80')->willReturn($promise); $connector = new Connector($loop, array( 'tcp' => $tcp, @@ -112,8 +114,9 @@ public function testConnectorUsesResolvedHostnameIfDnsIsUsed() $resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); $resolver->expects($this->once())->method('resolve')->with('google.com')->willReturn($promise); + $promise = new Promise(function () { }); $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); - $tcp->expects($this->once())->method('connect')->with('tcp://127.0.0.1:80?hostname=google.com'); + $tcp->expects($this->once())->method('connect')->with('tcp://127.0.0.1:80?hostname=google.com')->willReturn($promise); $connector = new Connector($loop, array( 'tcp' => $tcp, diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index bbc0a51a..a11447bc 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -84,10 +84,6 @@ public function gettingEncryptedStuffFromGoogleShouldWorkIfHostIsResolvedFirst() /** @test */ public function testConnectingFailsIfDnsUsesInvalidResolver() { - if (!function_exists('stream_socket_enable_crypto')) { - $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); - } - $loop = new StreamSelectLoop(); $factory = new Factory(); @@ -101,6 +97,23 @@ public function testConnectingFailsIfDnsUsesInvalidResolver() Block\await($connector->connect('google.com:80'), $loop, self::TIMEOUT); } + /** @test */ + public function testConnectingFailsIfTimeoutIsTooSmall() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = new StreamSelectLoop(); + + $connector = new Connector($loop, array( + 'timeout' => 0.001 + )); + + $this->setExpectedException('RuntimeException'); + Block\await($connector->connect('google.com:80'), $loop, self::TIMEOUT); + } + /** @test */ public function testSelfSignedRejectsIfVerificationIsEnabled() { From 697fdd9b2a5642fdd93fe14ee81d03a332a5e801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Mar 2017 01:09:49 +0200 Subject: [PATCH 142/146] Update examples to use Stream v0.6 API --- README.md | 10 ---------- composer.json | 3 ++- examples/03-netcat.php | 11 +++++------ examples/04-web.php | 5 ++--- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index bfd0cc33..d8b6b30f 100644 --- a/README.md +++ b/README.md @@ -501,16 +501,6 @@ $promise->cancel(); Calling `cancel()` on a pending promise will cancel the underlying DNS lookup and/or the underlying TCP/IP connection and reject the resulting promise. -The legacy `Connector` class can be used for backwards-compatiblity reasons. -It works very much like the newer `DnsConnector` but instead has to be -set up like this: - -```php -$connector = new React\SocketClient\Connector($loop, $dns); - -$connector->connect('www.google.com:80')->then($callback); -``` - > Advanced usage: Internally, the `DnsConnector` relies on a `Resolver` to look up the IP address for the given hostname. It will then replace the hostname in the destination URI with this IP and diff --git a/composer.json b/composer.json index 6e6eeb2f..b271f4b7 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "require-dev": { "clue/block-react": "^1.1", "react/socket": "^0.5", - "phpunit/phpunit": "~4.8" + "phpunit/phpunit": "~4.8", + "react/stream": "^0.6" } } diff --git a/examples/03-netcat.php b/examples/03-netcat.php index 42c12346..6ee70fab 100644 --- a/examples/03-netcat.php +++ b/examples/03-netcat.php @@ -3,7 +3,8 @@ use React\EventLoop\Factory; use React\SocketClient\Connector; use React\SocketClient\ConnectionInterface; -use React\Stream\Stream; +use React\Stream\ReadableResourceStream; +use React\Stream\WritableResourceStream; require __DIR__ . '/../vendor/autoload.php'; @@ -15,12 +16,10 @@ $loop = Factory::create(); $connector = new Connector($loop); -$stdin = new Stream(STDIN, $loop); +$stdin = new ReadableResourceStream(STDIN, $loop); $stdin->pause(); -$stdout = new Stream(STDOUT, $loop); -$stdout->pause(); -$stderr = new Stream(STDERR, $loop); -$stderr->pause(); +$stdout = new WritableResourceStream(STDOUT, $loop); +$stderr = new WritableResourceStream(STDERR, $loop); $stderr->write('Connecting' . PHP_EOL); diff --git a/examples/04-web.php b/examples/04-web.php index faaf5ed9..ab5a68d3 100644 --- a/examples/04-web.php +++ b/examples/04-web.php @@ -3,7 +3,7 @@ use React\EventLoop\Factory; use React\SocketClient\ConnectionInterface; use React\SocketClient\Connector; -use React\Stream\Stream; +use React\Stream\WritableResourceStream; require __DIR__ . '/../vendor/autoload.php'; @@ -36,8 +36,7 @@ $resource .= '?' . $parts['query']; } -$stdout = new Stream(STDOUT, $loop); -$stdout->pause(); +$stdout = new WritableResourceStream(STDOUT, $loop); $connector->connect($target)->then(function (ConnectionInterface $connection) use ($resource, $host, $stdout) { $connection->pipe($stdout); From 8ad621ef80fb23d10330c8cc9232c5a5e17af60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 2 Apr 2017 22:32:08 +0200 Subject: [PATCH 143/146] Prepare v0.7.0 release --- CHANGELOG.md | 22 ++++++++++++++++++++++ README.md | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b516c0a..74ed7d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.7.0 (2017-04-02) + +* Feature / BC break: Add main `Connector` facade + (#93 by @clue) + + The new `Connector` class acts as a facade for all underlying connectors, + which are now marked as "advanced usage", but continue to work unchanged. + This now makes it trivially easy to create plaintext TCP/IP, secure TLS and + Unix domain socket (UDS) connection streams simply like this: + + ```php + $connector = new Connector($loop); + + $connector->connect('tls://google.com:443')->then(function (ConnectionInterface $conn) { + $conn->write("GET / HTTP/1.0\r\n\r\n"); + }); + ``` + + Optionally, it accepts options to configure all underlying connectors, such + as using a custom DNS setup, timeout values and disabling certain protocols + and much more. See the README for more details. + ## 0.6.2 (2017-03-17) * Feature / Fix: Support SNI on legacy PHP < 5.6 and add documentation for diff --git a/README.md b/README.md index d8b6b30f..970a1ac9 100644 --- a/README.md +++ b/README.md @@ -625,7 +625,7 @@ The recommended way to install this library is [through Composer](http://getcomp This will install the latest supported version: ```bash -$ composer require react/socket-client:^0.6.2 +$ composer require react/socket-client:^0.7 ``` More details about version upgrades can be found in the [CHANGELOG](CHANGELOG.md). From 57bfe77208de36795f77a41ac4c0707fe84fac23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 2 Apr 2017 23:46:27 +0200 Subject: [PATCH 144/146] Prepare merging by updating namespace to Socket This causes some tests to fail due to the StreamEncryption being present in this package and the original Socket component. This will be fixed in the follow-up commit. --- README.md | 18 +++++++++--------- composer.json | 2 +- examples/01-http.php | 4 ++-- examples/02-https.php | 4 ++-- examples/03-netcat.php | 4 ++-- examples/04-web.php | 4 ++-- src/ConnectionInterface.php | 2 +- src/Connector.php | 2 +- src/ConnectorInterface.php | 2 +- src/DnsConnector.php | 2 +- src/SecureConnector.php | 2 +- src/StreamConnection.php | 4 ++-- src/StreamEncryption.php | 4 ++-- src/TcpConnector.php | 2 +- src/TimeoutConnector.php | 4 ++-- src/UnixConnector.php | 4 ++-- tests/CallableStub.php | 2 +- tests/ConnectorTest.php | 10 +++++----- tests/DnsConnectorTest.php | 6 +++--- tests/IntegrationTest.php | 10 +++++----- tests/SecureConnectorTest.php | 8 ++++---- tests/SecureIntegrationTest.php | 6 +++--- tests/TcpConnectorTest.php | 8 ++++---- tests/TestCase.php | 4 ++-- tests/TimeoutConnectorTest.php | 14 +++++++------- tests/UnixConnectorTest.php | 4 ++-- tests/bootstrap.php | 2 +- 27 files changed, 69 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 970a1ac9..af6dff78 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SocketClient Component +# Socket Component [![Build Status](https://secure.travis-ci.org/reactphp/socket-client.png?branch=master)](http://travis-ci.org/reactphp/socket-client) [![Code Climate](https://codeclimate.com/github/reactphp/socket-client/badges/gpa.svg)](https://codeclimate.com/github/reactphp/socket-client) @@ -405,12 +405,12 @@ $connector->connect('google.com:80')->then(function (ConnectionInterface $connec ### TcpConnector -The `React\SocketClient\TcpConnector` class implements the +The `React\Socket\TcpConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create plaintext TCP/IP connections to any IP-port-combination: ```php -$tcpConnector = new React\SocketClient\TcpConnector($loop); +$tcpConnector = new React\Socket\TcpConnector($loop); $tcpConnector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connection) { $connection->write('...'); @@ -439,7 +439,7 @@ You can optionally pass additional to the constructor like this: ```php -$tcpConnector = new React\SocketClient\TcpConnector($loop, array( +$tcpConnector = new React\Socket\TcpConnector($loop, array( 'bindto' => '192.168.0.1:0' )); ``` @@ -478,7 +478,7 @@ Make sure to set up your DNS resolver and underlying TCP connector like this: $dnsResolverFactory = new React\Dns\Resolver\Factory(); $dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); -$dnsConnector = new React\SocketClient\DnsConnector($tcpConnector, $dns); +$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns); $dnsConnector->connect('www.google.com:80')->then(function (ConnectionInterface $connection) { $connection->write('...'); @@ -523,7 +523,7 @@ creates a plaintext TCP/IP connection and then enables TLS encryption on this stream. ```php -$secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop); +$secureConnector = new React\Socket\SecureConnector($dnsConnector, $loop); $secureConnector->connect('www.google.com:443')->then(function (ConnectionInterface $connection) { $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); @@ -551,7 +551,7 @@ You can optionally pass additional to the constructor like this: ```php -$secureConnector = new React\SocketClient\SecureConnector($dnsConnector, $loop, array( +$secureConnector = new React\Socket\SecureConnector($dnsConnector, $loop, array( 'verify_peer' => false, 'verify_peer_name' => false )); @@ -577,7 +577,7 @@ instance and starting a timer that will automatically reject and abort any underlying connection attempt if it takes too long. ```php -$timeoutConnector = new React\SocketClient\TimeoutConnector($connector, 3.0, $loop); +$timeoutConnector = new React\Socket\TimeoutConnector($connector, 3.0, $loop); $timeoutConnector->connect('google.com:80')->then(function (ConnectionInterface $connection) { // connection succeeded within 3.0 seconds @@ -604,7 +604,7 @@ The `UnixConnector` class implements the Unix domain socket (UDS) paths like this: ```php -$connector = new React\SocketClient\UnixConnector($loop); +$connector = new React\Socket\UnixConnector($loop); $connector->connect('/tmp/demo.sock')->then(function (ConnectionInterface $connection) { $connection->write("HELLO\n"); diff --git a/composer.json b/composer.json index b271f4b7..62815165 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ }, "autoload": { "psr-4": { - "React\\SocketClient\\": "src" + "React\\Socket\\": "src" } }, "require-dev": { diff --git a/examples/01-http.php b/examples/01-http.php index 95519c91..72c585a0 100644 --- a/examples/01-http.php +++ b/examples/01-http.php @@ -1,8 +1,8 @@ getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $promise = new Promise(function () { }); - $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); + $tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $tcp->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise); $connector = new Connector($loop, array( @@ -27,7 +27,7 @@ public function testConnectorPassedThroughHostnameIfDnsIsDisabled() $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $promise = new Promise(function () { }); - $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); + $tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $tcp->expects($this->once())->method('connect')->with('tcp://google.com:80')->willReturn($promise); $connector = new Connector($loop, array( @@ -115,7 +115,7 @@ public function testConnectorUsesResolvedHostnameIfDnsIsUsed() $resolver->expects($this->once())->method('resolve')->with('google.com')->willReturn($promise); $promise = new Promise(function () { }); - $tcp = $this->getMockBuilder('React\SocketClient\ConnectorInterface')->getMock(); + $tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $tcp->expects($this->once())->method('connect')->with('tcp://127.0.0.1:80?hostname=google.com')->willReturn($promise); $connector = new Connector($loop, array( diff --git a/tests/DnsConnectorTest.php b/tests/DnsConnectorTest.php index 5592ef42..c33850b1 100644 --- a/tests/DnsConnectorTest.php +++ b/tests/DnsConnectorTest.php @@ -1,8 +1,8 @@ tcp = $this->getMock('React\SocketClient\ConnectorInterface'); + $this->tcp = $this->getMock('React\Socket\ConnectorInterface'); $this->resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); $this->connector = new DnsConnector($this->tcp, $this->resolver); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index a11447bc..d5999693 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -1,15 +1,15 @@ loop = $this->getMock('React\EventLoop\LoopInterface'); - $this->tcp = $this->getMock('React\SocketClient\ConnectorInterface'); + $this->tcp = $this->getMock('React\Socket\ConnectorInterface'); $this->connector = new SecureConnector($this->tcp, $this->loop); } @@ -62,7 +62,7 @@ public function testCancelDuringTcpConnectionCancelsTcpConnection() public function testConnectionWillBeClosedAndRejectedIfConnectioIsNoStream() { - $connection = $this->getMockBuilder('React\SocketClient\ConnectionInterface')->getMock(); + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('close'); $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->willReturn(Promise\resolve($connection)); diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index e883d004..aa7004e6 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -1,12 +1,12 @@ connect('127.0.0.1:9999'), $loop, self::TIMEOUT); - $this->assertInstanceOf('React\SocketClient\ConnectionInterface', $connection); + $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection); $connection->close(); } diff --git a/tests/TestCase.php b/tests/TestCase.php index bc3fc8bb..1a29e548 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,6 +1,6 @@ getMock('React\Tests\SocketClient\CallableStub'); + return $this->getMock('React\Tests\Socket\CallableStub'); } } diff --git a/tests/TimeoutConnectorTest.php b/tests/TimeoutConnectorTest.php index 633b33a7..333e8bfd 100644 --- a/tests/TimeoutConnectorTest.php +++ b/tests/TimeoutConnectorTest.php @@ -1,8 +1,8 @@ getMock('React\SocketClient\ConnectorInterface'); + $connector = $this->getMock('React\Socket\ConnectorInterface'); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); @@ -31,7 +31,7 @@ public function testRejectsWhenConnectorRejects() { $promise = Promise\reject(new \RuntimeException()); - $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector = $this->getMock('React\Socket\ConnectorInterface'); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); @@ -50,7 +50,7 @@ public function testResolvesWhenConnectorResolves() { $promise = Promise\resolve(); - $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector = $this->getMock('React\Socket\ConnectorInterface'); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); @@ -69,7 +69,7 @@ public function testRejectsAndCancelsPendingPromiseOnTimeout() { $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); - $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector = $this->getMock('React\Socket\ConnectorInterface'); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); @@ -88,7 +88,7 @@ public function testCancelsPendingPromiseOnCancel() { $promise = new Promise\Promise(function () { }, function () { throw new \Exception(); }); - $connector = $this->getMock('React\SocketClient\ConnectorInterface'); + $connector = $this->getMock('React\Socket\ConnectorInterface'); $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); $loop = Factory::create(); diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index 9ade7204..deb826d7 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -1,8 +1,8 @@ addPsr4('React\\Tests\\SocketClient\\', __DIR__); +$loader->addPsr4('React\\Tests\\Socket\\', __DIR__); From 7bcdfde5b067654e6033bd102f16c99ac8f1abd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 30 Mar 2017 15:32:17 +0200 Subject: [PATCH 145/146] Re-structure to ease getting started with merged component --- README.md | 224 +++++++++--------- examples/{01-http.php => 11-http.php} | 0 examples/{02-https.php => 12-https.php} | 0 examples/{03-netcat.php => 13-netcat.php} | 0 examples/{04-web.php => 14-web.php} | 0 ...signed.php => 99-generate-self-signed.php} | 0 src/StreamConnection.php | 39 --- src/TcpConnector.php | 2 +- src/UnixConnector.php | 3 +- tests/SecureIntegrationTest.php | 2 +- tests/localhost.pem | 49 ---- 11 files changed, 118 insertions(+), 201 deletions(-) rename examples/{01-http.php => 11-http.php} (100%) rename examples/{02-https.php => 12-https.php} (100%) rename examples/{03-netcat.php => 13-netcat.php} (100%) rename examples/{04-web.php => 14-web.php} (100%) rename examples/{10-generate-self-signed.php => 99-generate-self-signed.php} (100%) delete mode 100644 src/StreamConnection.php delete mode 100644 tests/localhost.pem diff --git a/README.md b/README.md index a22c91cd..5d76b0c5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ handle multiple concurrent connections without blocking. **Table of Contents** * [Quickstart example](#quickstart-example) -* [Usage](#usage) +* [Connection usage](#connection-usage) + * [ConnectionInterface](#connectioninterface) + * [getRemoteAddress()](#getremoteaddress) + * [getLocalAddress()](#getlocaladdress) +* [Server usage](#server-usage) * [ServerInterface](#serverinterface) * [connection event](#connection-event) * [error event](#error-event) @@ -30,18 +34,16 @@ handle multiple concurrent connections without blocking. * [SecureServer](#secureserver) * [LimitingServer](#limitingserver) * [getConnections()](#getconnections) - * [ConnectionInterface](#connectioninterface) - * [getRemoteAddress()](#getremoteaddress) - * [getLocalAddress()](#getlocaladdress) +* [Client usage](#client-usage) * [ConnectorInterface](#connectorinterface) * [connect()](#connect) * [Connector](#connector) -* [Advanced Usage](#advanced-usage) - * [TcpConnector](#tcpconnector) - * [DnsConnector](#dnsconnector) - * [SecureConnector](#secureconnector) - * [TimeoutConnector](#timeoutconnector) - * [UnixConnector](#unixconnector) + * [Advanced client usage](#advanced-client-usage) + * [TcpConnector](#tcpconnector) + * [DnsConnector](#dnsconnector) + * [SecureConnector](#secureconnector) + * [TimeoutConnector](#timeoutconnector) + * [UnixConnector](#unixconnector) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -84,7 +86,102 @@ $connector->connect('127.0.0.1:8080')->then(function (ConnectionInterface $conn) $loop->run(); ``` -## Usage +## Connection usage + +### ConnectionInterface + +The `ConnectionInterface` is used to represent any incoming and outgoing +connection, such as a normal TCP/IP connection. + +An incoming or outgoing connection is a duplex stream (both readable and +writable) that implements React's +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). +It contains additional properties for the local and remote address (client IP) +where this connection has been established to/from. + +Most commonly, instances implementing this `ConnectionInterface` are emitted +by all classes implementing the [`ServerInterface`](#serverinterface) and +used by all classes implementing the [`ConnectorInterface`](#connectorinterface). + +Because the `ConnectionInterface` implements the underlying +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) +you can use any of its events and methods as usual: + +```php +$connection->on('data', function ($chunk) { + echo $chunk; +}); + +$connection->on('end', function () { + echo 'ended'; +}); + +$connection->on('error', function (Exception $e) { + echo 'error: ' . $e->getMessage(); +}); + +$connection->on('close', function () { + echo 'closed'; +}); + +$connection->write($data); +$connection->end($data = null); +$connection->close(); +// … +``` + +For more details, see the +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + +#### getRemoteAddress() + +The `getRemoteAddress(): ?string` method returns the full remote address +(client IP and port) where this connection has been established with. + +```php +$address = $connection->getRemoteAddress(); +echo 'Connection with ' . $address . PHP_EOL; +``` + +If the remote address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full remote address as a string value. +If this is a TCP/IP based connection and you only want the remote IP, you may +use something like this: + +```php +$address = $connection->getRemoteAddress(); +$ip = trim(parse_url('tcp://' . $address, PHP_URL_HOST), '[]'); +echo 'Connection with ' . $ip . PHP_EOL; +``` + +#### getLocalAddress() + +The `getLocalAddress(): ?string` method returns the full local address +(client IP and port) where this connection has been established with. + +```php +$address = $connection->getLocalAddress(); +echo 'Connection with ' . $address . PHP_EOL; +``` + +If the local address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full local address as a string value. + +This method complements the [`getRemoteAddress()`](#getremoteaddress) method, +so they should not be confused. +If your `Server` instance is listening on multiple interfaces (e.g. using +the address `0.0.0.0`), you can use this method to find out which interface +actually accepted this connection (such as a public or local interface). + +If your system has multiple interfaces (e.g. a WAN and a LAN interface), +you can use this method to find out which interface was actually +used for this connection. + +## Server usage ### ServerInterface @@ -473,98 +570,7 @@ foreach ($server->getConnection() as $connection) { } ``` -### ConnectionInterface - -The `ConnectionInterface` is used to represent any incoming and outgoing -connection, such as a normal TCP/IP connection. - -An incoming or outgoing connection is a duplex stream (both readable and -writable) that implements React's -[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). -It contains additional properties for the local and remote address (client IP) -where this connection has been established to/from. - -Most commonly, instances implementing this `ConnectionInterface` are emitted -by all classes implementing the [`ServerInterface`](#serverinterface) and -used by all classes implementing the [`ConnectorInterface`](#connectorinterface). - -Because the `ConnectionInterface` implements the underlying -[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) -you can use any of its events and methods as usual: - -```php -$connection->on('data', function ($chunk) { - echo $chunk; -}); - -$connection->on('end', function () { - echo 'ended'; -}); - -$connection->on('error', function (Exception $e) { - echo 'error: ' . $e->getMessage(); -}); - -$connection->on('close', function () { - echo 'closed'; -}); - -$connection->write($data); -$connection->end($data = null); -$connection->close(); -// … -``` - -For more details, see the -[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). - -#### getRemoteAddress() - -The `getRemoteAddress(): ?string` method returns the full remote address -(client IP and port) where this connection has been established with. - -```php -$address = $connection->getRemoteAddress(); -echo 'Connection with ' . $address . PHP_EOL; -``` - -If the remote address can not be determined or is unknown at this time (such as -after the connection has been closed), it MAY return a `NULL` value instead. - -Otherwise, it will return the full remote address as a string value. -If this is a TCP/IP based connection and you only want the remote IP, you may -use something like this: - -```php -$address = $connection->getRemoteAddress(); -$ip = trim(parse_url('tcp://' . $address, PHP_URL_HOST), '[]'); -echo 'Connection with ' . $ip . PHP_EOL; -``` - -#### getLocalAddress() - -The `getLocalAddress(): ?string` method returns the full local address -(client IP and port) where this connection has been established with. - -```php -$address = $connection->getLocalAddress(); -echo 'Connection with ' . $address . PHP_EOL; -``` - -If the local address can not be determined or is unknown at this time (such as -after the connection has been closed), it MAY return a `NULL` value instead. - -Otherwise, it will return the full local address as a string value. - -This method complements the [`getRemoteAddress()`](#getremoteaddress) method, -so they should not be confused. -If your `Server` instance is listening on multiple interfaces (e.g. using -the address `0.0.0.0`), you can use this method to find out which interface -actually accepted this connection (such as a public or local interface). - -If your system has multiple interfaces (e.g. a WAN and a LAN interface), -you can use this method to find out which interface was actually -used for this connection. +## Client usage ### ConnectorInterface @@ -834,9 +840,9 @@ $connector->connect('google.com:80')->then(function (ConnectionInterface $connec Internally, the `tcp://` and `tls://` connectors will always be wrapped by `TimeoutConnector`, unless you disable timeouts like in the above example. -## Advanced Usage +### Advanced client usage -### TcpConnector +#### TcpConnector The `React\Socket\TcpConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create plaintext @@ -895,7 +901,7 @@ be used to set up the TLS peer name. This is used by the `SecureConnector` and `DnsConnector` to verify the peer name and can also be used if you want a custom TLS peer name. -### DnsConnector +#### DnsConnector The `DnsConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create plaintext @@ -945,7 +951,7 @@ hostname and is used by the `TcpConnector` to set up the TLS peer name. If a `hostname` is given explicitly, this query parameter will not be modified, which can be useful if you want a custom TLS peer name. -### SecureConnector +#### SecureConnector The `SecureConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to create secure @@ -999,7 +1005,7 @@ Failing to do so may result in a TLS peer name mismatch error or some hard to trace race conditions, because all stream resources will use a single, shared *default context* resource otherwise. -### TimeoutConnector +#### TimeoutConnector The `TimeoutConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to add timeout @@ -1030,7 +1036,7 @@ $promise->cancel(); Calling `cancel()` on a pending promise will cancel the underlying connection attempt, abort the timer and reject the resulting promise. -### UnixConnector +#### UnixConnector The `UnixConnector` class implements the [`ConnectorInterface`](#connectorinterface) and allows you to connect to diff --git a/examples/01-http.php b/examples/11-http.php similarity index 100% rename from examples/01-http.php rename to examples/11-http.php diff --git a/examples/02-https.php b/examples/12-https.php similarity index 100% rename from examples/02-https.php rename to examples/12-https.php diff --git a/examples/03-netcat.php b/examples/13-netcat.php similarity index 100% rename from examples/03-netcat.php rename to examples/13-netcat.php diff --git a/examples/04-web.php b/examples/14-web.php similarity index 100% rename from examples/04-web.php rename to examples/14-web.php diff --git a/examples/10-generate-self-signed.php b/examples/99-generate-self-signed.php similarity index 100% rename from examples/10-generate-self-signed.php rename to examples/99-generate-self-signed.php diff --git a/src/StreamConnection.php b/src/StreamConnection.php deleted file mode 100644 index 6aa82c2b..00000000 --- a/src/StreamConnection.php +++ /dev/null @@ -1,39 +0,0 @@ -sanitizeAddress(@stream_socket_get_name($this->stream, true)); - } - - public function getLocalAddress() - { - return $this->sanitizeAddress(@stream_socket_get_name($this->stream, false)); - } - - private function sanitizeAddress($address) - { - if ($address === false) { - return null; - } - - // check if this is an IPv6 address which includes multiple colons but no square brackets - $pos = strrpos($address, ':'); - if ($pos !== false && strpos($address, ':') < $pos && substr($address, 0, 1) !== '[') { - $port = substr($address, $pos + 1); - $address = '[' . substr($address, 0, $pos) . ']:' . $port; - } - - return $address; - } -} diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 56c10b2d..1622b048 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -133,6 +133,6 @@ public function checkConnectedSocket($socket) /** @internal */ public function handleConnectedSocket($socket) { - return new StreamConnection($socket, $this->loop); + return new Connection($socket, $this->loop); } } diff --git a/src/UnixConnector.php b/src/UnixConnector.php index 1d6d985d..bb00f8dd 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -3,7 +3,6 @@ namespace React\Socket; use React\Socket\ConnectorInterface; -use React\Stream\Stream; use React\EventLoop\LoopInterface; use React\Promise; use RuntimeException; @@ -37,6 +36,6 @@ public function connect($path) return Promise\reject(new RuntimeException('Unable to connect to unix domain socket "' . $path . '": ' . $errstr, $errno)); } - return Promise\resolve(new Stream($resource, $this->loop)); + return Promise\resolve(new Connection($resource, $this->loop)); } } diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php index aa7004e6..ba290ca8 100644 --- a/tests/SecureIntegrationTest.php +++ b/tests/SecureIntegrationTest.php @@ -32,7 +32,7 @@ public function setUp() $this->loop = LoopFactory::create(); $this->server = new Server(0, $this->loop); $this->server = new SecureServer($this->server, $this->loop, array( - 'local_cert' => __DIR__ . '/localhost.pem' + 'local_cert' => __DIR__ . '/../examples/localhost.pem' )); $this->address = $this->server->getAddress(); $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false)); diff --git a/tests/localhost.pem b/tests/localhost.pem deleted file mode 100644 index be692792..00000000 --- a/tests/localhost.pem +++ /dev/null @@ -1,49 +0,0 @@ ------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----- From 1529ce8beede24d05064f750d58660dbc351cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 5 Apr 2017 00:14:13 +0200 Subject: [PATCH 146/146] Work around legacy HHVM < 3.8 (does not support TLS connections) --- src/SecureConnector.php | 2 +- src/SecureServer.php | 5 +++++ src/StreamEncryption.php | 4 ---- src/TcpConnector.php | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/SecureConnector.php b/src/SecureConnector.php index 4cf508ab..e418864d 100644 --- a/src/SecureConnector.php +++ b/src/SecureConnector.php @@ -22,7 +22,7 @@ public function __construct(ConnectorInterface $connector, LoopInterface $loop, public function connect($uri) { if (!function_exists('stream_socket_enable_crypto')) { - return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); + return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); // @codeCoverageIgnore } if (strpos($uri, '://') === false) { diff --git a/src/SecureServer.php b/src/SecureServer.php index c117985c..2b7b0dc9 100644 --- a/src/SecureServer.php +++ b/src/SecureServer.php @@ -112,11 +112,16 @@ final class SecureServer extends EventEmitter implements ServerInterface * @param ServerInterface|Server $tcp * @param LoopInterface $loop * @param array $context + * @throws \BadMethodCallException for legacy HHVM < 3.8 due to lack of support * @see Server * @link http://php.net/manual/en/context.ssl.php for TLS context options */ public function __construct(ServerInterface $tcp, LoopInterface $loop, array $context) { + if (!function_exists('stream_socket_enable_crypto')) { + throw new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); // @codeCoverageIgnore + } + // default to empty passphrase to surpress blocking passphrase prompt $context += array( 'passphrase' => '' diff --git a/src/StreamEncryption.php b/src/StreamEncryption.php index 68be4fb3..ff804699 100644 --- a/src/StreamEncryption.php +++ b/src/StreamEncryption.php @@ -26,10 +26,6 @@ class StreamEncryption public function __construct(LoopInterface $loop, $server = true) { - if (!function_exists('stream_socket_enable_crypto')) { - throw new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); - } - $this->loop = $loop; $this->server = $server; diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 1622b048..e248a45f 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -69,7 +69,7 @@ public function connect($uri) // HHVM fails to parse URIs with a query but no path, so let's add a dummy path // See also https://3v4l.org/jEhLF if (defined('HHVM_VERSION') && isset($parts['query']) && !isset($parts['path'])) { - $uri = str_replace('?', '/?', $uri); + $uri = str_replace('?', '/?', $uri); // @codeCoverageIgnore } $socket = @stream_socket_client(