This commit is contained in:
2022-10-23 01:39:27 +02:00
parent 8c17aab483
commit 1929b84685
4130 changed files with 479334 additions and 0 deletions

2
vendor/clue/http-proxy-react/.gitignore vendored Executable file
View File

@@ -0,0 +1,2 @@
/vendor/
/composer.lock

27
vendor/clue/http-proxy-react/.travis.yml vendored Executable file
View File

@@ -0,0 +1,27 @@
language: php
php:
# - 5.3 # requires old distro, see below
- 5.4
- 5.5
- 5.6
- 7
- hhvm # ignore errors, see below
# lock distro so new future defaults will not break the build
dist: trusty
matrix:
include:
- php: 5.3
dist: precise
allow_failures:
- php: hhvm
sudo: false
install:
- composer install --no-interaction
script:
- vendor/bin/phpunit --coverage-text

103
vendor/clue/http-proxy-react/CHANGELOG.md vendored Executable file
View File

@@ -0,0 +1,103 @@
# Changelog
## 1.3.0 (2018-02-13)
* Feature: Support communication over Unix domain sockets (UDS)
(#20 by @clue)
```php
// new: now supports communication over Unix domain sockets (UDS)
$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $connector);
```
* Reduce memory consumption by avoiding circular reference from stream reader
(#18 by @valga)
* Improve documentation
(#19 by @clue)
## 1.2.0 (2017-08-30)
* Feature: Use socket error codes for connection rejections
(#17 by @clue)
```php
$promise = $proxy->connect('imap.example.com:143');
$promise->then(null, function (Exeption $e) {
if ($e->getCode() === SOCKET_EACCES) {
echo 'Failed to authenticate with proxy!';
}
throw $e;
});
```
* Improve test suite by locking Travis distro so new defaults will not break the build and
optionally exclude tests that rely on working internet connection
(#15 and #16 by @clue)
## 1.1.0 (2017-06-11)
* Feature: Support proxy authentication if proxy URL contains username/password
(#14 by @clue)
```php
// new: username/password will now be passed to HTTP proxy server
$proxy = new ProxyConnector('user:pass@127.0.0.1:8080', $connector);
```
## 1.0.0 (2017-06-10)
* First stable release, now following SemVer
> Contains no other changes, so it's actually fully compatible with the v0.3.2 release.
## 0.3.2 (2017-06-10)
* Fix: Fix rejecting invalid URIs and unexpected URI schemes
(#13 by @clue)
* Fix HHVM build for now again and ignore future HHVM build errors
(#12 by @clue)
* Documentation for Connector concepts (TCP/TLS, timeouts, DNS resolution)
(#11 by @clue)
## 0.3.1 (2017-05-10)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#10 by @clue)
## 0.3.0 (2017-04-10)
* Feature / BC break: Replace deprecated SocketClient with new Socket component
(#9 by @clue)
This implies that the `ProxyConnector` from this package now implements the
`React\Socket\ConnectorInterface` instead of the legacy
`React\SocketClient\ConnectorInterface`.
## 0.2.0 (2017-04-10)
* Feature / BC break: Update SocketClient to v0.7 or v0.6 and
use `connect($uri)` instead of `create($host, $port)`
(#8 by @clue)
```php
// old
$connector->create($host, $port)->then(function (Stream $conn) {
$conn->write("…");
});
// new
$connector->connect($uri)->then(function (ConnectionInterface $conn) {
$conn->write("…");
});
```
* Improve test suite by adding PHPUnit to require-dev
(#7 by @clue)
## 0.1.0 (2016-11-01)
* First tagged release

21
vendor/clue/http-proxy-react/LICENSE vendored Executable file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Christian Lück
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.

422
vendor/clue/http-proxy-react/README.md vendored Executable file
View File

@@ -0,0 +1,422 @@
# clue/http-proxy-react [![Build Status](https://travis-ci.org/clue/php-http-proxy-react.svg?branch=master)](https://travis-ci.org/clue/php-http-proxy-react)
Async HTTP proxy connector, use any TCP/IP-based protocol through an HTTP
CONNECT proxy server, built on top of [ReactPHP](https://reactphp.org).
HTTP CONNECT proxy servers (also commonly known as "HTTPS proxy" or "SSL proxy")
are commonly used to tunnel HTTPS traffic through an intermediary ("proxy"), to
conceal the origin address (anonymity) or to circumvent address blocking
(geoblocking). While many (public) HTTP CONNECT proxy servers often limit this
to HTTPS port `443` only, this can technically be used to tunnel any
TCP/IP-based protocol (HTTP, SMTP, IMAP etc.).
This library provides a simple API to create these tunneled connection for you.
Because it implements ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
This makes it fairly simple to add HTTP CONNECT proxy support to pretty much any
existing higher-level protocol implementation.
* **Async execution of connections** -
Send any number of HTTP CONNECT requests in parallel and process their
responses as soon as results come in.
The Promise-based design provides a *sane* interface to working with out of
bound responses and possible connection errors.
* **Standard interfaces** -
Allows easy integration with existing higher-level components by implementing
ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface).
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](http://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
Builds on top of well-tested components and well-established concepts instead of reinventing the wheel.
* **Good test coverage** -
Comes with an automated tests suite and is regularly tested against actual proxy servers in the wild
**Table of contents**
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [ProxyConnector](#proxyconnector)
* [Plain TCP connections](#plain-tcp-connections)
* [Secure TLS connections](#secure-tls-connections)
* [Connection timeout](#connection-timeout)
* [DNS resolution](#dns-resolution)
* [Authentication](#authentication)
* [Advanced secure proxy connections](#advanced-secure-proxy-connections)
* [Advanced Unix domain sockets](#advanced-unix-domain-sockets)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
* [More](#more)
### Quickstart example
The following example code demonstrates how this library can be used to send a
secure HTTPS request to google.com through a local HTTP proxy server:
```php
$loop = React\EventLoop\Factory::create();
$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;
});
}, 'printf');
$loop->run();
```
See also the [examples](examples).
## Usage
### ProxyConnector
The `ProxyConnector` is responsible for creating plain TCP/IP connections to
any destination by using an intermediary HTTP CONNECT proxy.
```
[you] -> [proxy] -> [destination]
```
Its constructor simply accepts an HTTP proxy URL and a connector used to connect
to the proxy server address:
```php
$connector = new Connector($loop);
$proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
```
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 (often the alternative HTTP port `8080`).
In its most simple form, the given connector will be a
[`\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.
Because it implements ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
Accordingly, it provides only a single public method, the
[`connect()`](https://github.com/reactphp/socket#connect) method.
The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>`
method can be used to establish a streaming connection.
It returns a [Promise](https://github.com/reactphp/promise) which either
fulfills with a [ConnectionInterface](https://github.com/reactphp/socket#connectioninterface)
on success or rejects with an `Exception` on error.
This makes it fairly simple to add HTTP CONNECT proxy support to pretty much any
higher-level component:
```diff
- $client = new SomeClient($connector);
+ $proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
+ $client = new SomeClient($proxy);
```
#### Plain TCP connections
HTTP CONNECT proxies are 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.
As documented above, you can simply invoke its `connect()` method to establish
a streaming plain TCP/IP connection and use any higher level protocol like so:
```php
$proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
$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 ReactPHP'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;
});
});
```
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
This class can also be used if you want to establish a secure TLS connection
(formerly known as SSL) between you and your destination, such as when using
secure HTTPS to your destination site. You can simply wrap this connector in
ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector) or the
low-level [`SecureConnector`](https://github.com/reactphp/socket#secureconnector):
```php
$proxy = new ProxyConnector('http://127.0.0.1:8080', $connector);
$connector = new Connector($loop, array(
'tcp' => $proxy,
'dns' => false
));
$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;
});
});
```
> 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 ReactPHP'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).
> 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 ReactPHP'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'
));
```
> Note how local DNS resolution is in fact entirely handled outside of this
HTTP CONNECT client implementation.
#### Authentication
If your HTTP proxy server requires authentication, you may pass the username and
password as part of the HTTP proxy URL like this:
```php
$proxy = new ProxyConnector('http://user:pass@127.0.0.1:8080', $connector);
```
Note that both the username and password must be percent-encoded if they contain
special characters:
```php
$user = 'he:llo';
$pass = 'p@ss';
$proxy = new ProxyConnector(
rawurlencode($user) . ':' . rawurlencode($pass) . '@127.0.0.1:8080',
$connector
);
```
> The authentication details will be used for basic authentication and will be
transferred in the `Proxy-Authorization` HTTP request header for each
connection attempt.
If the authentication details are missing or not accepted by the remote HTTP
proxy server, it is expected to reject each connection attempt with a
`407` (Proxy Authentication Required) response status code and an exception
error code of `SOCKET_EACCES` (13).
#### 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
destination host as above.
If you want to connect to a (rather rare) HTTPS proxy, you may want use the
`https://` scheme (HTTPS default port 443) and use ReactPHP's
[`Connector`](https://github.com/reactphp/socket#connector) or the low-level
[`SecureConnector`](https://github.com/reactphp/socket#secureconnector)
instance to create a secure connection to the proxy:
```php
$connector = new Connector($loop);
$proxy = new ProxyConnector('https://127.0.0.1:443', $connector);
$proxy->connect('tcp://smtp.googlemail.com:587');
```
#### Advanced Unix domain sockets
HTTP CONNECT proxy servers support forwarding TCP/IP based connections and
higher level protocols.
In some advanced cases, it may be useful to let your HTTP CONNECT proxy server
listen on a Unix domain socket (UDS) path instead of a IP:port combination.
For example, this allows you to rely on file system permissions instead of
having to rely on explicit [authentication](#authentication).
You can simply use the `http+unix://` URI scheme like this:
```php
$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $connector);
$proxy->connect('tcp://google.com:80')->then(function (ConnectionInterface $stream) {
// connected…
});
```
Similarly, you can also combine this with [authentication](#authentication)
like this:
```php
$proxy = new ProxyConnector('http+unix://user:pass@/tmp/proxy.sock', $connector);
```
> Note that Unix domain sockets (UDS) are considered advanced usage and PHP only
has limited support for this.
In particular, enabling [secure TLS](#secure-tls-connections) may not be
supported.
> Note that the HTTP CONNECT protocol does not support the notion of UDS paths.
The above works reasonably well because UDS is only used for the connection between
client and proxy server and the path will not actually passed over the protocol.
This implies that this does not support connecting to UDS destination paths.
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](http://semver.org/).
This will install the latest supported version:
```bash
$ composer require clue/http-proxy-react:^1.3
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 7+ and
HHVM.
It's *highly recommended to use PHP 7+* for this project.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org):
```bash
$ composer install
```
To run the test suite, go to the project root and run:
```bash
$ php vendor/bin/phpunit
```
The test suite contains tests that rely on a working internet connection,
alternatively you can also run it like this:
```bash
$ php vendor/bin/phpunit --exclude-group internet
```
## License
MIT
## More
* If you want to learn more about how the
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
and its usual implementations look like, refer to the documentation of the underlying
[react/socket](https://github.com/reactphp/socket) component.
* If you want to learn more about processing streams of data, refer to the
documentation of the underlying
[react/stream](https://github.com/reactphp/stream) component.
* As an alternative to an HTTP CONNECT proxy, you may also want to look into
using a SOCKS (SOCKS4/SOCKS5) proxy instead.
You may want to use [clue/socks-react](https://github.com/clue/php-socks-react)
which also provides an implementation of the same
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
so that supporting either proxy protocol should be fairly trivial.
* If you're dealing with public proxies, you'll likely have to work with mixed
quality and unreliable proxies. You may want to look into using
[clue/connection-manager-extra](https://github.com/clue/php-connection-manager-extra)
which allows retrying unreliable ones, implying connection timeouts,
concurrently working with multiple connectors and more.
* If you're looking for an end-user HTTP CONNECT proxy server daemon, you may
want to use [LeProxy](https://leproxy.org/).

30
vendor/clue/http-proxy-react/composer.json vendored Executable file
View File

@@ -0,0 +1,30 @@
{
"name": "clue/http-proxy-react",
"description": "Async HTTP proxy connector, use any TCP/IP-based protocol through an HTTP CONNECT proxy server, built on top of ReactPHP",
"keywords": ["HTTP", "CONNECT", "proxy", "ReactPHP", "async"],
"homepage": "https://github.com/clue/php-http-proxy-react",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"autoload": {
"psr-4": { "Clue\\React\\HttpProxy\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Tests\\Clue\\React\\HttpProxy\\": "tests/" }
},
"require": {
"php": ">=5.3",
"react/promise": " ^2.1 || ^1.2.1",
"react/socket": "^1.0 || ^0.8.4",
"ringcentral/psr7": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^5.0 || ^4.8",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"clue/block-react": "^1.1"
}
}

View File

@@ -0,0 +1,30 @@
<?php
// A simple example which requests https://google.com/ through an HTTP CONNECT proxy.
// The proxy can be given as first argument and defaults to localhost:8080 otherwise.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';
$loop = React\EventLoop\Factory::create();
$proxy = new ProxyConnector($url, 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;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,37 @@
<?php
// A simple example which requests https://google.com/ either directly or through
// an HTTP CONNECT proxy.
// The Proxy can be given as first argument or does not use a proxy otherwise.
// This example highlights how changing from direct connection to using a proxy
// actually adds very little complexity and does not mess with your actual
// network protocol otherwise.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$connector = new Connector($loop);
// first argument given? use this as the proxy URL
if (isset($argv[1])) {
$proxy = new ProxyConnector($argv[1], $connector);
$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;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,32 @@
<?php
// A simple example which uses a plain SMTP connection to Googlemail through a HTTP CONNECT proxy.
// Proxy can be given as first argument and defaults to localhost:8080 otherwise.
// Please note that MANY public proxies do not allow SMTP connections, YMMV.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';
$loop = React\EventLoop\Factory::create();
$proxy = new ProxyConnector($url, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'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;
$stream->write("QUIT\r\n");
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,35 @@
<?php
// A simple example which uses a secure SMTP connection to Googlemail through a HTTP CONNECT proxy.
// Proxy can be given as first argument and defaults to localhost:8080 otherwise.
// This example highlights how changing from plain connections (see previous
// example) to using a secure connection actually adds very little complexity
// and does not mess with your actual network protocol otherwise.
// Please note that MANY public proxies do not allow SMTP connections, YMMV.
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';
$loop = React\EventLoop\Factory::create();
$proxy = new ProxyConnector($url, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $proxy,
'timeout' => 3.0,
'dns' => false
));
$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;
$stream->write("QUIT\r\n");
});
}, 'printf');
$loop->run();

14
vendor/clue/http-proxy-react/phpunit.xml.dist vendored Executable file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite>
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,213 @@
<?php
namespace Clue\React\HttpProxy;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use RingCentral\Psr7;
use React\Promise;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface;
use React\Socket\ConnectorInterface;
use React\Socket\FixedUriConnector;
/**
* A simple Connector that uses an HTTP CONNECT proxy to create plain TCP/IP connections to any destination
*
* [you] -> [proxy] -> [destination]
*
* 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.
*
* 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.
*
* 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
* instance.
*
* 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
* destination host as above.
*
* If you want to connect to a (rather rare) HTTPS proxy, you may want use its
* HTTPS port (443) and use a SecureConnector instance to create a secure
* connection to the proxy.
*
* @link https://tools.ietf.org/html/rfc7231#section-4.3.6
*/
class ProxyConnector implements ConnectorInterface
{
private $connector;
private $proxyUri;
private $proxyAuth = '';
/**
* Instantiate a new ProxyConnector which uses the given $proxyUrl
*
* @param string $proxyUrl 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.
* @param ConnectorInterface $connector In its most simple form, the given
* 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)
{
// support `http+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^http\+unix:\/\/(.*?@)?(.+?)$/', $proxyUrl, $match)) {
// rewrite URI to parse authentication from dummy host
$proxyUrl = 'http://' . $match[1] . 'localhost';
// connector uses Unix transport scheme and explicit path given
$connector = new FixedUriConnector(
'unix://' . $match[2],
$connector
);
}
if (strpos($proxyUrl, '://') === false) {
$proxyUrl = 'http://' . $proxyUrl;
}
$parts = parse_url($proxyUrl);
if (!$parts || !isset($parts['scheme'], $parts['host']) || ($parts['scheme'] !== 'http' && $parts['scheme'] !== 'https')) {
throw new InvalidArgumentException('Invalid proxy URL "' . $proxyUrl . '"');
}
// apply default port and TCP/TLS transport for given scheme
if (!isset($parts['port'])) {
$parts['port'] = $parts['scheme'] === 'https' ? 443 : 80;
}
$parts['scheme'] = $parts['scheme'] === 'https' ? 'tls' : 'tcp';
$this->connector = $connector;
$this->proxyUri = $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'];
// prepare Proxy-Authorization header if URI contains username/password
if (isset($parts['user']) || isset($parts['pass'])) {
$this->proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode(
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
) . "\r\n";
}
}
public function connect($uri)
{
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'];
}
$auth = $this->proxyAuth;
return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port, $auth) {
$deferred = new Deferred(function ($_, $reject) use ($stream) {
$reject(new RuntimeException('Connection canceled while waiting for response from proxy (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103));
$stream->close();
});
// keep buffering data until headers are complete
$buffer = '';
$fn = function ($chunk) use (&$buffer, $deferred, $stream) {
$buffer .= $chunk;
$pos = strpos($buffer, "\r\n\r\n");
if ($pos !== false) {
// try to parse headers as response message
try {
$response = Psr7\parse_response(substr($buffer, 0, $pos));
} catch (Exception $e) {
$deferred->reject(new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $e));
$stream->close();
return;
}
if ($response->getStatusCode() === 407) {
// map status code 407 (Proxy Authentication Required) to EACCES
$deferred->reject(new RuntimeException('Proxy denied connection due to invalid authentication ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13));
return $stream->close();
} elseif ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
// map non-2xx status code to ECONNREFUSED
$deferred->reject(new RuntimeException('Proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111));
return $stream->close();
}
// all okay, resolve with stream instance
$deferred->resolve($stream);
// emit remaining incoming as data event
$buffer = (string)substr($buffer, $pos + 4);
if ($buffer !== '') {
$stream->emit('data', array($buffer));
$buffer = '';
}
return;
}
// stop buffering when 8 KiB have been read
if (isset($buffer[8192])) {
$deferred->reject(new RuntimeException('Proxy must not send more than 8 KiB of headers (EMSGSIZE)', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90));
$stream->close();
}
};
$stream->on('data', $fn);
$stream->on('error', function (Exception $e) use ($deferred) {
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy (EIO)', defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e));
});
$stream->on('close', function () use ($deferred) {
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
});
$stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n" . $auth . "\r\n");
return $deferred->promise()->then(function (ConnectionInterface $stream) use ($fn) {
// Stop buffering when connection has been established.
$stream->removeListener('data', $fn);
return new Promise\FulfilledPromise($stream);
});
}, function (Exception $e) use ($proxyUri) {
throw new RuntimeException('Unable to connect to proxy (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $e);
});
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Tests\Clue\React\HttpProxy;
use PHPUnit_Framework_TestCase;
abstract class AbstractTestCase extends PHPUnit_Framework_TestCase
{
protected function expectCallableNever()
{
$mock = $this->createCallableMock();
$mock
->expects($this->never())
->method('__invoke');
return $mock;
}
protected function expectCallableOnce()
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke');
return $mock;
}
protected function expectCallableOnceWith($value)
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->equalTo($value));
return $mock;
}
protected function expectCallableOnceWithExceptionCode($code)
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->callback(function ($e) use ($code) {
return $e->getCode() === $code;
}));
return $mock;
}
protected function expectCallableOnceParameter($type)
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->isInstanceOf($type));
return $mock;
}
/**
* @link https://github.com/reactphp/react/blob/master/tests/React/Tests/Socket/TestCase.php (taken from reactphp/react)
*/
protected function createCallableMock()
{
return $this->getMockBuilder('Tests\\Clue\\React\\HttpProxy\\CallableStub')->getMock();
}
}
class CallableStub
{
public function __invoke()
{
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Tests\Clue\React\HttpProxy;
use React\EventLoop\Factory;
use Clue\React\HttpProxy\ProxyConnector;
use React\Socket\TcpConnector;
use React\Socket\DnsConnector;
use Clue\React\Block;
use React\Socket\SecureConnector;
/** @group internet */
class FunctionalTest extends AbstractTestCase
{
private $loop;
private $tcpConnector;
private $dnsConnector;
public function setUp()
{
$this->loop = Factory::create();
$this->tcpConnector = new TcpConnector($this->loop);
$f = new \React\Dns\Resolver\Factory();
$resolver = $f->create('8.8.8.8', $this->loop);
$this->dnsConnector = new DnsConnector($this->tcpConnector, $resolver);
}
public function testNonListeningSocketRejectsConnection()
{
$proxy = new ProxyConnector('127.0.0.1:9999', $this->dnsConnector);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', 'Unable to connect to proxy', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}
public function testPlainGoogleDoesNotAcceptConnectMethod()
{
$proxy = new ProxyConnector('google.com', $this->dnsConnector);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}
public function testSecureGoogleDoesNotAcceptConnectMethod()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('TLS not supported on really old platforms (HHVM < 3.8)');
}
$secure = new SecureConnector($this->dnsConnector, $this->loop);
$proxy = new ProxyConnector('https://google.com:443', $secure);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}
public function testSecureGoogleDoesNotAcceptPlainStream()
{
$proxy = new ProxyConnector('google.com:443', $this->dnsConnector);
$promise = $proxy->connect('google.com:80');
$this->setExpectedException('RuntimeException', 'Connection to proxy lost', SOCKET_ECONNRESET);
Block\await($promise, $this->loop, 3.0);
}
}

View File

@@ -0,0 +1,333 @@
<?php
namespace Tests\Clue\React\HttpProxy;
use Clue\React\HttpProxy\ProxyConnector;
use React\Promise\Promise;
use React\Socket\ConnectionInterface;
class ProxyConnectorTest extends AbstractTestCase
{
private $connector;
public function setUp()
{
$this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidProxy()
{
new ProxyConnector('///', $this->connector);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidProxyScheme()
{
new ProxyConnector('ftp://example.com', $this->connector);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidHttpsUnixScheme()
{
new ProxyConnector('https+unix:///tmp/proxy.sock', $this->connector);
}
public function testCreatesConnectionToHttpPort()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('tcp://proxy.example.com:80?hostname=google.com')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testCreatesConnectionToHttpPortAndPassesThroughUriComponents()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('tcp://proxy.example.com:80/path?foo=bar&hostname=google.com#segment')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$proxy->connect('google.com:80/path?foo=bar#segment');
}
public function testCreatesConnectionToHttpPortAndObeysExplicitHostname()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('tcp://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('connect')->with('tls://proxy.example.com:443?hostname=google.com')->willReturn($promise);
$proxy = new ProxyConnector('https://proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testCreatesConnectionToUnixPath()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('unix:///tmp/proxy.sock')->willReturn($promise);
$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $this->connector);
$proxy->connect('google.com:80');
}
public function testCancelPromiseWillCancelPendingConnection()
{
$promise = new Promise(function () { }, $this->expectCallableOnce());
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$this->assertInstanceOf('React\Promise\CancellablePromiseInterface', $promise);
$promise->cancel();
}
public function testWillWriteToOpenConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthentication()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('user:pass@proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfProxyUriContainsOnlyUsernameWithoutPassword()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjo=\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('user@proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthenticationWithPercentEncoding()
{
$user = 'h@llÖ';
$pass = '%secret?';
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic " . base64_encode($user . ':' . $pass) . "\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector(rawurlencode($user) . ':' . rawurlencode($pass) . '@proxy.example.com', $this->connector);
$proxy->connect('google.com:80');
}
public function testWillProxyAuthorizationHeaderIfUnixProxyUriContainsAuthentication()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n");
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('unix:///tmp/proxy.sock')->willReturn($promise);
$proxy = new ProxyConnector('http+unix://user:pass@/tmp/proxy.sock', $this->connector);
$proxy->connect('google.com:80');
}
public function testRejectsInvalidUri()
{
$this->connector->expects($this->never())->method('connect');
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('///');
$promise->then(null, $this->expectCallableOnce());
}
public function testRejectsUriWithNonTcpScheme()
{
$this->connector->expects($this->never())->method('connect');
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('tls://google.com:80');
$promise->then(null, $this->expectCallableOnce());
}
public function testRejectsIfConnectorRejects()
{
$promise = \React\Promise\reject(new \RuntimeException());
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$promise->then(null, $this->expectCallableOnce());
}
public function testRejectsAndClosesIfStreamWritesNonHttp()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$stream->expects($this->once())->method('close');
$stream->emit('data', array("invalid\r\n\r\n"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
}
public function testRejectsAndClosesIfStreamWritesTooMuchData()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$stream->expects($this->once())->method('close');
$stream->emit('data', array(str_repeat('*', 100000)));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EMSGSIZE));
}
public function testRejectsAndClosesIfStreamReturnsProyAuthenticationRequired()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$stream->expects($this->once())->method('close');
$stream->emit('data', array("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EACCES));
}
public function testRejectsAndClosesIfStreamReturnsNonSuccess()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$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"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
}
public function testResolvesIfStreamReturnsSuccess()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$promise->then($this->expectCallableOnce('React\Stream\Stream'));
$never = $this->expectCallableNever();
$promise->then(function (ConnectionInterface $stream) use ($never) {
$stream->on('data', $never);
});
$stream->emit('data', array("HTTP/1.1 200 OK\r\n\r\n"));
}
public function testResolvesIfStreamReturnsSuccessAndEmitsExcessiveData()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$once = $this->expectCallableOnceWith('hello!');
$promise->then(function (ConnectionInterface $stream) use ($once) {
$stream->on('data', $once);
});
$stream->emit('data', array("HTTP/1.1 200 OK\r\n\r\nhello!"));
}
public function testCancelPromiseWillCloseOpenConnectionAndReject()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
$promise = $proxy->connect('google.com:80');
$this->assertInstanceOf('React\Promise\CancellablePromiseInterface', $promise);
$promise->cancel();
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNABORTED));
}
}