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 1/7] 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 bb4e5f1..5e7bcfe 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 0000000..faaf5ed --- /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 4a07c81..067a2b1 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 6796874..fd8c867 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 2/7] 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 5e7bcfe..b5e4b09 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 9b06cc2..95519c9 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 9672192..a6abd2a 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 e0c633c..42c1234 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 067a2b1..6166655 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 3/7] 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 b5e4b09..1396ad3 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 a6abd2a..b1780de 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 6166655..fae7c86 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 0000000..19b87fe --- /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 fd8c867..4451d30 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 4/7] 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 1396ad3..afc6d26 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 fae7c86..f7e9bea 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 19b87fe..5205b79 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 5/7] 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 afc6d26..6289c36 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 f7e9bea..cf09386 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 4451d30..bbc0a51 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 6/7] 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 6289c36..3657f1f 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 cf09386..f4c45aa 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 5205b79..0672068 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 7/7] 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 3657f1f..bfd0cc3 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 f4c45aa..7a6d81d 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 0672068..ea167ad 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 bbc0a51..a11447b 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() {