diff --git a/README.md b/README.md index 7defa2f..75a0d7c 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ Async HTTP CONNECT proxy connector, use any TCP/IP protocol through an HTTP prox * [ConnectorInterface](#connectorinterface) * [connect()](#connect) * [ProxyConnector](#proxyconnector) + * [Plain TCP connections](#plain-tcp-connections) + * [Secure TLS connections](#secure-tls-connections) + * [Connection timeout](#connection-timeout) + * [DNS resolution](#dns-resolution) + * [Advanced secure proxy connections](#advanced-secure-proxy-connections) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -21,11 +26,15 @@ secure HTTPS request to google.com through a local HTTP proxy server: ```php $loop = React\EventLoop\Factory::create(); -$connector = new TcpConnector($loop); -$proxy = new ProxyConnector('127.0.0.1:8080', $connector); -$ssl = new SecureConnector($proxy, $loop); -$ssl->connect('google.com:443')->then(function (ConnectionInterface $stream) { +$proxy = new ProxyConnector('127.0.0.1:8080', new Connector($loop)); +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'timeout' => 3.0, + 'dns' => false +)); + +$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) { $stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n"); $stream->on('data', function ($chunk) { echo $chunk; @@ -91,7 +100,7 @@ Its constructor simply accepts an HTTP proxy URL and a connector used to connect to the proxy server address: ```php -$connector = new TcpConnector($loop); +$connector = new Connector($loop); $proxy = new ProxyConnector('127.0.0.1:8080', $connector); ``` @@ -99,7 +108,7 @@ The proxy URL may or may not contain a scheme and port definition. The default port will be `80` for HTTP (or `443` for HTTPS), but many common HTTP proxy servers use custom ports. In its most simple form, the given connector will be a -[`TcpConnector`](https://github.com/reactphp/socket#tcpconnector) if you +[`\React\Socket\Connector`](https://github.com/reactphp/socket#connector) if you want to connect to a given IP address as above. This is the main class in this package. @@ -114,14 +123,36 @@ higher-level component: + $client = new SomeClient($proxy); ``` +#### Plain TCP connections + This is most frequently used to issue HTTPS requests to your destination. However, this is actually performed on a higher protocol layer and this -connector is actually inherently a general-purpose plain TCP/IP connector: +connector is actually inherently a general-purpose plain TCP/IP connector. + +The `ProxyConnector` implements the [`ConnectorInterface`](#connectorinterface) and +hence provides a single public method, the [`connect()`](#connect) method. ```php $proxy = new ProxyConnector('127.0.0.1:8080', $connector); -$proxy->connect('smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) { +$proxy->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) { + $stream->write("EHLO local\r\n"); + $stream->on('data', function ($chunk) use ($stream) { + echo $chunk; + }); +}); +``` + +You can either use the `ProxyConnector` directly or you may want to wrap this connector +in React's [`Connector`](https://github.com/reactphp/socket#connector): + +```php +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); + +$connector->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) { $stream->write("EHLO local\r\n"); $stream->on('data', function ($chunk) use ($stream) { echo $chunk; @@ -132,16 +163,21 @@ $proxy->connect('smtp.googlemail.com:587')->then(function (ConnectionInterface $ Note that HTTP CONNECT proxies often restrict which ports one may connect to. Many (public) proxy servers do in fact limit this to HTTPS (443) only. +#### Secure TLS connections + If you want to establish a TLS connection (such as HTTPS) between you and -your destination, you may want to wrap this connector in a -[`SecureConnector`](https://github.com/reactphp/socket#secureconnector) -instance: +your destination, you may want to wrap this connector in React's +[`Connector`](https://github.com/reactphp/socket#connector) or the low-level +[`SecureConnector`](https://github.com/reactphp/socket#secureconnector): ```php $proxy = new ProxyConnector('127.0.0.1:8080', $connector); -$ssl = new SecureConnector($proxy, $loop); +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); -$ssl->connect('smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) { +$connector->connect('tls://smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) { $stream->write("EHLO local\r\n"); $stream->on('data', function ($chunk) use ($stream) { echo $chunk; @@ -149,6 +185,90 @@ $ssl->connect('smtp.googlemail.com:465')->then(function (ConnectionInterface $st }); ``` +> Also note how secure TLS connections are in fact entirely handled outside of + this HTTP CONNECT client implementation. + +#### Connection timeout + +By default, the `ProxyConnector` does not implement any timeouts for establishing remote +connections. +Your underlying operating system may impose limits on pending and/or idle TCP/IP +connections, anywhere in a range of a few minutes to several hours. + +Many use cases require more control over the timeout and likely values much +smaller, usually in the range of a few seconds only. + +You can use React's [`Connector`](https://github.com/reactphp/socket#connector) +or the low-level +[`TimeoutConnector`](https://github.com/reactphp/socket#timeoutconnector) +to decorate any given `ConnectorInterface` instance. +It provides the same `connect()` method, but will automatically reject the +underlying connection attempt if it takes too long: + +```php +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false, + 'timeout' => 3.0 +)); + +$connector->connect('tcp://google.com:80')->then(function ($stream) { + // connection succeeded within 3.0 seconds +}); +``` + +See also any of the [examples](examples). + +> Also note how connection timeout is in fact entirely handled outside of this + HTTP CONNECT client implementation. + +#### DNS resolution + +By default, the `ProxyConnector` does not perform any DNS resolution at all and simply +forwards any hostname you're trying to connect to the remote proxy server. +The remote proxy server is thus responsible for looking up any hostnames via DNS +(this default mode is thus called *remote DNS resolution*). + +As an alternative, you can also send the destination IP to the remote proxy +server. +In this mode you either have to stick to using IPs only (which is ofen unfeasable) +or perform any DNS lookups locally and only transmit the resolved destination IPs +(this mode is thus called *local DNS resolution*). + +The default *remote DNS resolution* is useful if your local `ProxyConnector` either can +not resolve target hostnames because it has no direct access to the internet or +if it should not resolve target hostnames because its outgoing DNS traffic might +be intercepted. + +As noted above, the `ProxyConnector` defaults to using remote DNS resolution. +However, wrapping the `ProxyConnector` in React's +[`Connector`](https://github.com/reactphp/socket#connector) actually +performs local DNS resolution unless explicitly defined otherwise. +Given that remote DNS resolution is assumed to be the preferred mode, all +other examples explicitly disable DNS resoltion like this: + +```php +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'dns' => false +)); +``` + +If you want to explicitly use *local DNS resolution*, you can use the following code: + +```php +// set up Connector which uses Google's public DNS (8.8.8.8) +$connector = Connector($loop, array( + 'tcp' => $proxy, + 'dns' => '8.8.8.8' +)); +``` + +> Also note how local DNS resolution is in fact entirely handled outside of this + HTTP CONNECT client implementation. + +#### Advanced secure proxy connections + Note that communication between the client and the proxy is usually via an unencrypted, plain TCP/IP HTTP connection. Note that this is the most common setup, because you can still establish a TLS connection between you and the diff --git a/examples/01-proxy-https.php b/examples/01-proxy-https.php index f5cc1ce..c07ea0d 100644 --- a/examples/01-proxy-https.php +++ b/examples/01-proxy-https.php @@ -4,8 +4,7 @@ // The proxy can be given as first argument and defaults to localhost:8080 otherwise. use Clue\React\HttpProxy\ProxyConnector; -use React\Socket\TcpConnector; -use React\Socket\SecureConnector; +use React\Socket\Connector; use React\Socket\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -14,11 +13,14 @@ $loop = React\EventLoop\Factory::create(); -$connector = new TcpConnector($loop); -$proxy = new ProxyConnector($url, $connector); -$ssl = new SecureConnector($proxy, $loop); +$proxy = new ProxyConnector($url, new Connector($loop)); +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'timeout' => 3.0, + 'dns' => false +)); -$ssl->connect('google.com:443')->then(function (ConnectionInterface $stream) { +$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) { $stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n"); $stream->on('data', function ($chunk) { echo $chunk; diff --git a/examples/02-optional-proxy-https.php b/examples/02-optional-proxy-https.php index 4e83d73..c65e69a 100644 --- a/examples/02-optional-proxy-https.php +++ b/examples/02-optional-proxy-https.php @@ -8,30 +8,26 @@ // network protocol otherwise. use Clue\React\HttpProxy\ProxyConnector; -use React\Socket\TcpConnector; -use React\Socket\SecureConnector; -use React\Socket\DnsConnector; -use React\Dns\Resolver\Factory; +use React\Socket\Connector; use React\Socket\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; $loop = React\EventLoop\Factory::create(); -$tcp = new TcpConnector($loop); -$dnsFactory = new Factory(); -$resolver = $dnsFactory->create('8.8.8.8', $loop); -$dns = new DnsConnector($tcp, $resolver); +$connector = new Connector($loop); // first argument given? use this as the proxy URL if (isset($argv[1])) { - $proxy = new ProxyConnector($argv[1], $dns); - $connector = new SecureConnector($proxy, $loop); -} else { - $connector = new SecureConnector($dns, $loop); + $proxy = new ProxyConnector($argv[1], $connector); + $connector = new Connector($loop, array( + 'tcp' => $proxy, + 'timeout' => 3.0, + 'dns' => false + )); } -$connector->connect('google.com:443')->then(function (ConnectionInterface $stream) { +$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) { $stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n"); $stream->on('data', function ($chunk) { echo $chunk; diff --git a/examples/11-proxy-smtp.php b/examples/11-proxy-smtp.php index 703ca66..3225491 100644 --- a/examples/11-proxy-smtp.php +++ b/examples/11-proxy-smtp.php @@ -5,7 +5,7 @@ // Please note that MANY public proxies do not allow SMTP connections, YMMV. use Clue\React\HttpProxy\ProxyConnector; -use React\Socket\TcpConnector; +use React\Socket\Connector; use React\Socket\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -14,10 +14,14 @@ $loop = React\EventLoop\Factory::create(); -$connector = new TcpConnector($loop); -$proxy = new ProxyConnector($url, $connector); +$proxy = new ProxyConnector($url, new Connector($loop)); +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'timeout' => 3.0, + 'dns' => false +)); -$proxy->connect('smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) { +$connector->connect('tcp://smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) { $stream->write("EHLO local\r\n"); $stream->on('data', function ($chunk) use ($stream) { echo $chunk; diff --git a/examples/12-proxy-smtps.php b/examples/12-proxy-smtps.php index 6719f75..462dba3 100644 --- a/examples/12-proxy-smtps.php +++ b/examples/12-proxy-smtps.php @@ -8,8 +8,7 @@ // Please note that MANY public proxies do not allow SMTP connections, YMMV. use Clue\React\HttpProxy\ProxyConnector; -use React\Socket\TcpConnector; -use React\Socket\SecureConnector; +use React\Socket\Connector; use React\Socket\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -18,11 +17,14 @@ $loop = React\EventLoop\Factory::create(); -$connector = new TcpConnector($loop); -$proxy = new ProxyConnector($url, $connector); -$ssl = new SecureConnector($proxy, $loop); +$proxy = new ProxyConnector($url, new Connector($loop)); +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'timeout' => 3.0, + 'dns' => false +)); -$ssl->connect('smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) { +$connector->connect('tls://smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) { $stream->write("EHLO local\r\n"); $stream->on('data', function ($chunk) use ($stream) { echo $chunk; diff --git a/src/ProxyConnector.php b/src/ProxyConnector.php index ff1f6af..f8e27be 100644 --- a/src/ProxyConnector.php +++ b/src/ProxyConnector.php @@ -49,8 +49,8 @@ class ProxyConnector implements ConnectorInterface * port definition. The default port will be `80` for HTTP (or `443` for * HTTPS), but many common HTTP proxy servers use custom ports. * @param ConnectorInterface $connector In its most simple form, the given - * connector will be a TcpConnector if you want to connect to a given IP - * address. + * connector will be a \React\Socket\Connector if you want to connect to + * a given IP address. * @throws InvalidArgumentException if the proxy URL is invalid */ public function __construct($proxyUrl, ConnectorInterface $connector)