diff --git a/README.md b/README.md index c4966d7..a474300 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Async HTTP CONNECT proxy connector, use any TCP/IP protocol through an HTTP prox * [Quickstart example](#quickstart-example) * [Usage](#usage) * [ConnectorInterface](#connectorinterface) - * [create()](#create) + * [connect()](#connect) * [ProxyConnector](#proxyconnector) * [Install](#install) * [Tests](#tests) @@ -25,7 +25,7 @@ $connector = new TcpConnector($loop); $proxy = new ProxyConnector('127.0.0.1:8080', $connector); $ssl = new SecureConnector($proxy, $loop); -$ssl->create('google.com', 443)->then(function (Stream $stream) { +$ssl->connect('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; @@ -59,17 +59,17 @@ HTTP CONNECT proxy. The interface only offers a single method: -#### create() +#### connect() -The `create(string $host, int $port): PromiseInterface` method +The `connect(string $uri): 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 +fulfills with a [ConnectionInterface](https://github.com/reactphp/socket-client#connectioninterface) or rejects with an `Exception`: ```php -$connector->create('google.com', 443)->then( - function (Stream $stream) { +$connector->connect('google.com:443')->then( + function (ConnectionInterface $stream) { // connection successfully established }, function (Exception $error) { @@ -121,7 +121,7 @@ connector is actually inherently a general-purpose plain TCP/IP connector: ```php $proxy = new ProxyConnector('127.0.0.1:8080', $connector); -$proxy->create('smtp.googlemail.com', 587)->then(function (Stream $stream) { +$proxy->connect('smtp.googlemail.com:587')->then(function (ConnectionInterface $stream) { $stream->write("EHLO local\r\n"); $stream->on('data', function ($chunk) use ($stream) { echo $chunk; @@ -141,7 +141,7 @@ instance: $proxy = new ProxyConnector('127.0.0.1:8080', $connector); $ssl = new SecureConnector($proxy, $loop); -$ssl->create('smtp.googlemail.com', 465)->then(function (Stream $stream) { +$ssl->connect('smtp.googlemail.com:465')->then(function (ConnectionInterface $stream) { $stream->write("EHLO local\r\n"); $stream->on('data', function ($chunk) use ($stream) { echo $chunk; @@ -163,7 +163,7 @@ instance to create a secure connection to the proxy: $ssl = new SecureConnector($connector, $loop); $proxy = new ProxyConnector('127.0.0.1:443', $ssl); -$proxy->create('smtp.googlemail.com', 587); +$proxy->connect('smtp.googlemail.com:587'); ``` ## Install diff --git a/composer.json b/composer.json index 6092d34..8f06bb3 100644 --- a/composer.json +++ b/composer.json @@ -18,15 +18,13 @@ }, "require": { "php": ">=5.3", - "react/socket-client": "^0.5 || ^0.4 || ^0.3", + "react/socket-client": "^0.7 || ^0.6", "react/event-loop": "^0.4 || ^0.3", - "react/stream": "^0.4 || ^0.3", "react/promise": " ^2.1 || ^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { "phpunit/phpunit": "^5.0 || ^4.8", - "react/socket-client": "^0.5", "clue/block-react": "^1.1" } } diff --git a/examples/01-proxy-https.php b/examples/01-proxy-https.php index df57098..92d1aee 100644 --- a/examples/01-proxy-https.php +++ b/examples/01-proxy-https.php @@ -4,9 +4,9 @@ // The proxy can be given as first argument and defaults to localhost:8080 otherwise. use Clue\React\HttpProxy\ProxyConnector; -use React\Stream\Stream; use React\SocketClient\TcpConnector; use React\SocketClient\SecureConnector; +use React\SocketClient\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -18,7 +18,7 @@ $proxy = new ProxyConnector($url, $connector); $ssl = new SecureConnector($proxy, $loop); -$ssl->create('google.com', 443)->then(function (Stream $stream) { +$ssl->connect('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 9daa6df..accee91 100644 --- a/examples/02-optional-proxy-https.php +++ b/examples/02-optional-proxy-https.php @@ -8,11 +8,11 @@ // network protocol otherwise. use Clue\React\HttpProxy\ProxyConnector; -use React\Stream\Stream; use React\SocketClient\TcpConnector; use React\SocketClient\SecureConnector; use React\SocketClient\DnsConnector; use React\Dns\Resolver\Factory; +use React\SocketClient\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -31,7 +31,7 @@ $connector = new SecureConnector($dns, $loop); } -$connector->create('google.com', 443)->then(function (Stream $stream) { +$connector->connect('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 2d6b38e..c307b4c 100644 --- a/examples/11-proxy-smtp.php +++ b/examples/11-proxy-smtp.php @@ -5,8 +5,8 @@ // Please note that MANY public proxies do not allow SMTP connections, YMMV. use Clue\React\HttpProxy\ProxyConnector; -use React\Stream\Stream; use React\SocketClient\TcpConnector; +use React\SocketClient\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -17,7 +17,7 @@ $connector = new TcpConnector($loop); $proxy = new ProxyConnector($url, $connector); -$proxy->create('smtp.googlemail.com', 587)->then(function (Stream $stream) { +$proxy->connect('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 2730e6b..247c6c0 100644 --- a/examples/12-proxy-smtps.php +++ b/examples/12-proxy-smtps.php @@ -8,9 +8,9 @@ // Please note that MANY public proxies do not allow SMTP connections, YMMV. use Clue\React\HttpProxy\ProxyConnector; -use React\Stream\Stream; use React\SocketClient\TcpConnector; use React\SocketClient\SecureConnector; +use React\SocketClient\ConnectionInterface; require __DIR__ . '/../vendor/autoload.php'; @@ -22,7 +22,7 @@ $proxy = new ProxyConnector($url, $connector); $ssl = new SecureConnector($proxy, $loop); -$ssl->create('smtp.googlemail.com', 465)->then(function (Stream $stream) { +$ssl->connect('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 f921b0f..44f53a8 100644 --- a/src/ProxyConnector.php +++ b/src/ProxyConnector.php @@ -3,12 +3,12 @@ namespace Clue\React\HttpProxy; use React\SocketClient\ConnectorInterface; -use React\Stream\Stream; use Exception; use InvalidArgumentException; use RuntimeException; use RingCentral\Psr7; use React\Promise\Deferred; +use React\SocketClient\ConnectionInterface; /** * A simple Connector that uses an HTTP CONNECT proxy to create plain TCP/IP connections to any destination @@ -40,8 +40,7 @@ class ProxyConnector implements ConnectorInterface { private $connector; - private $proxyHost; - private $proxyPort; + private $proxyUri; /** * Instantiate a new ProxyConnector which uses the given $proxyUrl @@ -61,7 +60,7 @@ public function __construct($proxyUrl, ConnectorInterface $connector) } $parts = parse_url($proxyUrl); - if (!$parts || !isset($parts['host'])) { + if (!$parts || !isset($parts['scheme'], $parts['host'])) { throw new InvalidArgumentException('Invalid proxy URL'); } @@ -70,13 +69,51 @@ public function __construct($proxyUrl, ConnectorInterface $connector) } $this->connector = $connector; - $this->proxyHost = $parts['host']; - $this->proxyPort = $parts['port']; + $this->proxyUri = $parts['host'] . ':' . $parts['port']; } - public function create($host, $port) + public function connect($uri) { - return $this->connector->create($this->proxyHost, $this->proxyPort)->then(function (Stream $stream) use ($host, $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('Invalid target URI specified')); + } + + $host = trim($parts['host'], '[]'); + $port = $parts['port']; + + // construct URI to HTTP CONNECT proxy server to connect to + $proxyUri = $this->proxyUri; + + // append path from URI if given + if (isset($parts['path'])) { + $proxyUri .= $parts['path']; + } + + // parse query args + $args = array(); + if (isset($parts['query'])) { + parse_str($parts['query'], $args); + } + + // append hostname from URI to query string unless explicitly given + if (!isset($args['hostname'])) { + $args['hostname'] = $parts['host']; + } + + // append query string + $proxyUri .= '?' . http_build_query($args, '', '&');; + + // append fragment from URI if given + if (isset($parts['fragment'])) { + $proxyUri .= '#' . $parts['fragment']; + } + + return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port) { $deferred = new Deferred(function ($_, $reject) use ($stream) { $reject(new RuntimeException('Operation canceled while waiting for response from proxy')); $stream->close(); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 6e46977..db3f1c5 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -31,7 +31,7 @@ public function testPlainGoogleDoesNotAcceptConnectMethod() { $proxy = new ProxyConnector('google.com', $this->dnsConnector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $this->setExpectedException('RuntimeException', 'Method Not Allowed', 405); Block\await($promise, $this->loop, 3.0); @@ -46,7 +46,7 @@ public function testSecureGoogleDoesNotAcceptConnectMethod() $secure = new SecureConnector($this->dnsConnector, $this->loop); $proxy = new ProxyConnector('google.com:443', $secure); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $this->setExpectedException('RuntimeException', 'Method Not Allowed', 405); Block\await($promise, $this->loop, 3.0); @@ -56,7 +56,7 @@ public function testSecureGoogleDoesNotAcceptPlainStream() { $proxy = new ProxyConnector('google.com:443', $this->dnsConnector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $this->setExpectedException('RuntimeException', 'Connection to proxy lost'); Block\await($promise, $this->loop, 3.0); diff --git a/tests/ProxyConnectorTest.php b/tests/ProxyConnectorTest.php index 66cf6de..4abd70e 100644 --- a/tests/ProxyConnectorTest.php +++ b/tests/ProxyConnectorTest.php @@ -4,7 +4,7 @@ use Clue\React\HttpProxy\ProxyConnector; use React\Promise\Promise; -use React\Stream\Stream; +use React\SocketClient\ConnectionInterface; class ProxyConnectorTest extends AbstractTestCase { @@ -26,31 +26,41 @@ public function testInvalidProxy() public function testCreatesConnectionToHttpPort() { $promise = new Promise(function () { }); - $this->connector->expects($this->once())->method('create')->with('proxy.example.com', 80)->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->with('proxy.example.com:80?hostname=google.com')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $proxy->create('google.com', 80); + $proxy->connect('google.com:80'); + } + + public function testCreatesConnectionToHttpPortAndObeysExplicitHostname() + { + $promise = new Promise(function () { }); + $this->connector->expects($this->once())->method('connect')->with('proxy.example.com:80?hostname=www.google.com')->willReturn($promise); + + $proxy = new ProxyConnector('proxy.example.com', $this->connector); + + $proxy->connect('google.com:80?hostname=www.google.com'); } public function testCreatesConnectionToHttpsPort() { $promise = new Promise(function () { }); - $this->connector->expects($this->once())->method('create')->with('proxy.example.com', 443)->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->with('proxy.example.com:443?hostname=google.com')->willReturn($promise); $proxy = new ProxyConnector('https://proxy.example.com', $this->connector); - $proxy->create('google.com', 80); + $proxy->connect('google.com:80'); } public function testCancelPromiseWillCancelPendingConnection() { $promise = new Promise(function () { }, $this->expectCallableOnce()); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $this->assertInstanceOf('React\Promise\CancellablePromiseInterface', $promise); @@ -59,27 +69,27 @@ public function testCancelPromiseWillCancelPendingConnection() public function testWillWriteToOpenConnection() { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream = $this->getMockBuilder('React\SocketClient\StreamConnection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); $stream->expects($this->once())->method('write'); $promise = \React\Promise\resolve($stream); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $proxy->create('google.com', 80); + $proxy->connect('google.com:80'); } public function testRejectsAndClosesIfStreamWritesNonHttp() { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream = $this->getMockBuilder('React\SocketClient\StreamConnection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); $promise = \React\Promise\resolve($stream); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $stream->expects($this->once())->method('close'); $stream->emit('data', array("invalid\r\n\r\n")); @@ -89,14 +99,14 @@ public function testRejectsAndClosesIfStreamWritesNonHttp() public function testRejectsAndClosesIfStreamWritesTooMuchData() { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream = $this->getMockBuilder('React\SocketClient\StreamConnection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); $promise = \React\Promise\resolve($stream); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $stream->expects($this->once())->method('close'); $stream->emit('data', array(str_repeat('*', 100000))); @@ -106,14 +116,14 @@ public function testRejectsAndClosesIfStreamWritesTooMuchData() public function testRejectsAndClosesIfStreamReturnsNonSuccess() { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream = $this->getMockBuilder('React\SocketClient\StreamConnection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); $promise = \React\Promise\resolve($stream); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $stream->expects($this->once())->method('close'); $stream->emit('data', array("HTTP/1.1 403 Not allowed\r\n\r\n")); @@ -123,18 +133,18 @@ public function testRejectsAndClosesIfStreamReturnsNonSuccess() public function testResolvesIfStreamReturnsSuccess() { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream = $this->getMockBuilder('React\SocketClient\StreamConnection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); $promise = \React\Promise\resolve($stream); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $promise->then($this->expectCallableOnce('React\Stream\Stream')); $never = $this->expectCallableNever(); - $promise->then(function (Stream $stream) use ($never) { + $promise->then(function (ConnectionInterface $stream) use ($never) { $stream->on('data', $never); }); @@ -143,17 +153,17 @@ public function testResolvesIfStreamReturnsSuccess() public function testResolvesIfStreamReturnsSuccessAndEmitsExcessiveData() { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream = $this->getMockBuilder('React\SocketClient\StreamConnection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); $promise = \React\Promise\resolve($stream); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $once = $this->expectCallableOnceWith('hello!'); - $promise->then(function (Stream $stream) use ($once) { + $promise->then(function (ConnectionInterface $stream) use ($once) { $stream->on('data', $once); }); @@ -162,15 +172,15 @@ public function testResolvesIfStreamReturnsSuccessAndEmitsExcessiveData() public function testCancelPromiseWillCloseOpenConnectionAndReject() { - $stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream = $this->getMockBuilder('React\SocketClient\StreamConnection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); $stream->expects($this->once())->method('close'); $promise = \React\Promise\resolve($stream); - $this->connector->expects($this->once())->method('create')->willReturn($promise); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector); - $promise = $proxy->create('google.com', 80); + $promise = $proxy->connect('google.com:80'); $this->assertInstanceOf('React\Promise\CancellablePromiseInterface', $promise);