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

7
vendor/autoload.php vendored Executable file
View File

@@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit331ae81537359437b02a96bc74f11b80::getLoader();

View File

@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2015 Sebastian Mößler <code@binsoul.de>
> 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.

145
vendor/binsoul/net-mqtt-client-react/README.md vendored Executable file
View File

@@ -0,0 +1,145 @@
# net-mqtt-client-react
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Software License][ico-license]](LICENSE.md)
[![Total Downloads][ico-downloads]][link-downloads]
This package provides an asynchronous MQTT client built on the [React socket](https://github.com/reactphp/socket) library. All client methods return a promise which is fulfilled if the operation succeeded or rejected if the operation failed. Incoming messages of subscribed topics are delivered via the "message" event.
## Install
Via composer:
``` bash
$ composer require binsoul/net-mqtt-client-react
```
## Example
Connect to a public broker and run forever.
``` php
<?php
use BinSoul\Net\Mqtt\Client\React\ReactMqttClient;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\DefaultMessage;
use BinSoul\Net\Mqtt\DefaultSubscription;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Subscription;
use React\Socket\DnsConnector;
use React\Socket\TcpConnector;
include 'vendor/autoload.php';
// Setup client
$loop = \React\EventLoop\Factory::create();
$dnsResolverFactory = new \React\Dns\Resolver\Factory();
$connector = new DnsConnector(new TcpConnector($loop), $dnsResolverFactory->createCached('8.8.8.8', $loop));
$client = new ReactMqttClient($connector, $loop);
// Bind to events
$client->on('open', function () use ($client) {
// Network connection established
echo sprintf("Open: %s:%s\n", $client->getHost(), $client->getPort());
});
$client->on('close', function () use ($client, $loop) {
// Network connection closed
echo sprintf("Close: %s:%s\n", $client->getHost(), $client->getPort());
$loop->stop();
});
$client->on('connect', function (Connection $connection) {
// Broker connected
echo sprintf("Connect: client=%s\n", $connection->getClientID());
});
$client->on('disconnect', function (Connection $connection) {
// Broker disconnected
echo sprintf("Disconnect: client=%s\n", $connection->getClientID());
});
$client->on('message', function (Message $message) {
// Incoming message
echo 'Message';
if ($message->isDuplicate()) {
echo ' (duplicate)';
}
if ($message->isRetained()) {
echo ' (retained)';
}
echo ': '.$message->getTopic().' => '.mb_strimwidth($message->getPayload(), 0, 50, '...');
echo "\n";
});
$client->on('warning', function (\Exception $e) {
echo sprintf("Warning: %s\n", $e->getMessage());
});
$client->on('error', function (\Exception $e) use ($loop) {
echo sprintf("Error: %s\n", $e->getMessage());
$loop->stop();
});
// Connect to broker
$client->connect('test.mosquitto.org')->then(
function () use ($client) {
// Subscribe to all topics
$client->subscribe(new DefaultSubscription('#'))
->then(function (Subscription $subscription) {
echo sprintf("Subscribe: %s\n", $subscription->getFilter());
})
->otherwise(function (\Exception $e) {
echo sprintf("Error: %s\n", $e->getMessage());
});
// Publish humidity once
$client->publish(new DefaultMessage('sensors/humidity', '55%'))
->then(function (Message $message) {
echo sprintf("Publish: %s => %s\n", $message->getTopic(), $message->getPayload());
})
->otherwise(function (\Exception $e) {
echo sprintf("Error: %s\n", $e->getMessage());
});
// Publish a random temperature every 10 seconds
$generator = function () {
return mt_rand(-20, 30);
};
$client->publishPeriodically(10, new DefaultMessage('sensors/temperature'), $generator)
->progress(function (Message $message) {
echo sprintf("Publish: %s => %s\n", $message->getTopic(), $message->getPayload());
})
->otherwise(function (\Exception $e) {
echo sprintf("Error: %s\n", $e->getMessage());
});
}
);
$loop->run();
```
## Testing
``` bash
$ composer test
```
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
[ico-version]: https://img.shields.io/packagist/v/binsoul/net-mqtt-client-react.svg?style=flat-square
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/binsoul/net-mqtt-client-react.svg?style=flat-square
[link-packagist]: https://packagist.org/packages/binsoul/net-mqtt-client-react
[link-downloads]: https://packagist.org/packages/binsoul/net-mqtt-client-react
[link-author]: https://github.com/binsoul

View File

@@ -0,0 +1,51 @@
{
"name": "binsoul/net-mqtt-client-react",
"description": "Asynchronous MQTT client built on React",
"keywords": [
"net",
"mqtt",
"client"
],
"homepage": "https://github.com/binsoul/net-mqtt-client-react",
"license": "MIT",
"authors": [
{
"name": "Sebastian Mößler",
"email": "code@binsoul.de",
"homepage": "https://github.com/binsoul",
"role": "Developer"
}
],
"require": {
"php": "~5.6|~7.0",
"binsoul/net-mqtt": "~0.2",
"react/promise": "~2.0",
"react/socket": "~0.8"
},
"require-dev": {
"phpunit/phpunit": "~4.0||~5.0",
"friendsofphp/php-cs-fixer": "~1.0"
},
"autoload": {
"psr-4": {
"BinSoul\\Net\\Mqtt\\Client\\React\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"BinSoul\\Test\\Net\\Mqtt\\Client\\React\\": "tests"
}
},
"scripts": {
"test": "phpunit",
"fix-style": [
"php-cs-fixer fix src",
"php-cs-fixer fix tests"
]
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace BinSoul\Net\Mqtt\Client\React;
use BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet;
use React\Promise\Deferred;
/**
* Decorates flows with data required for the {@see ReactMqttClient} class.
*/
class ReactFlow implements Flow
{
/** @var Flow */
private $decorated;
/** @var Deferred */
private $deferred;
/** @var Packet */
private $packet;
/** @var bool */
private $isSilent;
/**
* Constructs an instance of this class.
*
* @param Flow $decorated
* @param Deferred $deferred
* @param Packet $packet
* @param bool $isSilent
*/
public function __construct(Flow $decorated, Deferred $deferred, Packet $packet = null, $isSilent = false)
{
$this->decorated = $decorated;
$this->deferred = $deferred;
$this->packet = $packet;
$this->isSilent = $isSilent;
}
public function getCode()
{
return $this->decorated->getCode();
}
public function start()
{
$this->packet = $this->decorated->start();
return $this->packet;
}
public function accept(Packet $packet)
{
return $this->decorated->accept($packet);
}
public function next(Packet $packet)
{
$this->packet = $this->decorated->next($packet);
return $this->packet;
}
public function isFinished()
{
return $this->decorated->isFinished();
}
public function isSuccess()
{
return $this->decorated->isSuccess();
}
public function getResult()
{
return $this->decorated->getResult();
}
public function getErrorMessage()
{
return $this->decorated->getErrorMessage();
}
/**
* Returns the associated deferred.
*
* @return Deferred
*/
public function getDeferred()
{
return $this->deferred;
}
/**
* Returns the current packet.
*
* @return Packet
*/
public function getPacket()
{
return $this->packet;
}
/**
* Indicates if the flow should emit events.
*
* @return bool
*/
public function isSilent()
{
return $this->isSilent;
}
}

View File

@@ -0,0 +1,701 @@
<?php
namespace BinSoul\Net\Mqtt\Client\React;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\DefaultConnection;
use BinSoul\Net\Mqtt\DefaultIdentifierGenerator;
use BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Flow\IncomingPublishFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingConnectFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingDisconnectFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingPingFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingPublishFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingSubscribeFlow;
use BinSoul\Net\Mqtt\Flow\OutgoingUnsubscribeFlow;
use BinSoul\Net\Mqtt\DefaultMessage;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PublishRequestPacket;
use BinSoul\Net\Mqtt\StreamParser;
use BinSoul\Net\Mqtt\Subscription;
use Evenement\EventEmitter;
use React\EventLoop\Timer\TimerInterface;
use React\Promise\Deferred;
use React\EventLoop\LoopInterface;
use React\Promise\ExtendedPromiseInterface;
use React\Promise\RejectedPromise;
use React\Socket\ConnectorInterface;
use React\Stream\DuplexStreamInterface;
/**
* Connects to a MQTT broker and subscribes to topics or publishes messages.
*
* The following events are emitted:
* - open - The network connection to the server is established.
* - close - The network connection to the server is closed.
* - warning - An event of severity "warning" occurred.
* - error - An event of severity "error" occurred.
*
* If a flow finishes it's result is also emitted, e.g.:
* - connect - The client connected to the broker.
* - disconnect - The client disconnected from the broker.
* - subscribe - The client subscribed to a topic filter.
* - unsubscribe - The client unsubscribed from topic filter.
* - publish - A message was published.
* - message - A message was received.
*/
class ReactMqttClient extends EventEmitter
{
/** @var ConnectorInterface */
private $connector;
/** @var LoopInterface */
private $loop;
/** @var DuplexStreamInterface */
private $stream;
/** @var StreamParser */
private $parser;
/** @var IdentifierGenerator */
private $identifierGenerator;
/** @var string */
private $host;
/** @var int */
private $port;
/** @var Connection */
private $connection;
/** @var bool */
private $isConnected = false;
/** @var bool */
private $isConnecting = false;
/** @var bool */
private $isDisconnecting = false;
/** @var TimerInterface[] */
private $timer = [];
/** @var ReactFlow[] */
private $receivingFlows = [];
/** @var ReactFlow[] */
private $sendingFlows = [];
/** @var ReactFlow */
private $writtenFlow;
/**
* Constructs an instance of this class.
*
* @param ConnectorInterface $connector
* @param LoopInterface $loop
* @param IdentifierGenerator $identifierGenerator
* @param StreamParser $parser
*/
public function __construct(
ConnectorInterface $connector,
LoopInterface $loop,
IdentifierGenerator $identifierGenerator = null,
StreamParser $parser = null
) {
$this->connector = $connector;
$this->loop = $loop;
$this->parser = $parser;
if ($this->parser === null) {
$this->parser = new StreamParser();
}
$this->parser->onError(function (\Exception $e) {
$this->emitWarning($e);
});
$this->identifierGenerator = $identifierGenerator;
if ($this->identifierGenerator === null) {
$this->identifierGenerator = new DefaultIdentifierGenerator();
}
}
/**
* Return the host.
*
* @return string
*/
public function getHost()
{
return $this->host;
}
/**
* Return the port.
*
* @return string
*/
public function getPort()
{
return $this->port;
}
/**
* Indicates if the client is connected.
*
* @return bool
*/
public function isConnected()
{
return $this->isConnected;
}
/**
* Returns the underlying stream or null if the client is not connected.
*
* @return DuplexStreamInterface|null
*/
public function getStream()
{
return $this->stream;
}
/**
* Connects to a broker.
*
* @param string $host
* @param int $port
* @param Connection $connection
* @param int $timeout
*
* @return ExtendedPromiseInterface
*/
public function connect($host, $port = 1883, Connection $connection = null, $timeout = 5)
{
if ($this->isConnected || $this->isConnecting) {
return new RejectedPromise(new \LogicException('The client is already connected.'));
}
$this->isConnecting = true;
$this->isConnected = false;
$this->host = $host;
$this->port = $port;
if ($connection === null) {
$connection = new DefaultConnection();
}
if ($connection->isCleanSession()) {
$this->cleanPreviousSession();
}
if ($connection->getClientID() === '') {
$connection = $connection->withClientID($this->identifierGenerator->generateClientID());
}
$deferred = new Deferred();
$this->establishConnection($this->host, $this->port, $timeout)
->then(function (DuplexStreamInterface $stream) use ($connection, $deferred, $timeout) {
$this->stream = $stream;
$this->emit('open', [$connection, $this]);
$this->registerClient($connection, $timeout)
->then(function (Connection $connection) use ($deferred) {
$this->isConnecting = false;
$this->isConnected = true;
$this->connection = $connection;
$this->emit('connect', [$connection, $this]);
$deferred->resolve($this->connection);
})
->otherwise(function (\Exception $e) use ($deferred, $connection) {
$this->isConnecting = false;
$this->emitError($e);
$deferred->reject($e);
if ($this->stream !== null) {
$this->stream->close();
}
$this->emit('close', [$connection, $this]);
});
})
->otherwise(function (\Exception $e) use ($deferred) {
$this->isConnecting = false;
$this->emitError($e);
$deferred->reject($e);
});
return $deferred->promise();
}
/**
* Disconnects from a broker.
*
* @return ExtendedPromiseInterface
*/
public function disconnect()
{
if (!$this->isConnected || $this->isDisconnecting) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
$this->isDisconnecting = true;
$deferred = new Deferred();
$this->startFlow(new OutgoingDisconnectFlow($this->connection), true)
->then(function (Connection $connection) use ($deferred) {
$this->isDisconnecting = false;
$this->isConnected = false;
$this->emit('disconnect', [$connection, $this]);
$deferred->resolve($connection);
if ($this->stream !== null) {
$this->stream->close();
}
})
->otherwise(function () use ($deferred) {
$this->isDisconnecting = false;
$deferred->reject($this->connection);
});
return $deferred->promise();
}
/**
* Subscribes to a topic filter.
*
* @param Subscription $subscription
*
* @return ExtendedPromiseInterface
*/
public function subscribe(Subscription $subscription)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
return $this->startFlow(new OutgoingSubscribeFlow([$subscription], $this->identifierGenerator));
}
/**
* Unsubscribes from a topic filter.
*
* @param Subscription $subscription
*
* @return ExtendedPromiseInterface
*/
public function unsubscribe(Subscription $subscription)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
return $this->startFlow(new OutgoingUnsubscribeFlow([$subscription], $this->identifierGenerator));
}
/**
* Publishes a message.
*
* @param Message $message
*
* @return ExtendedPromiseInterface
*/
public function publish(Message $message)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
return $this->startFlow(new OutgoingPublishFlow($message, $this->identifierGenerator));
}
/**
* Calls the given generator periodically and publishes the return value.
*
* @param int $interval
* @param Message $message
* @param callable $generator
*
* @return ExtendedPromiseInterface
*/
public function publishPeriodically($interval, Message $message, callable $generator)
{
if (!$this->isConnected) {
return new RejectedPromise(new \LogicException('The client is not connected.'));
}
$deferred = new Deferred();
$this->timer[] = $this->loop->addPeriodicTimer(
$interval,
function () use ($message, $generator, $deferred) {
$this->publish($message->withPayload($generator($message->getTopic())))->then(
function ($value) use ($deferred) {
$deferred->notify($value);
},
function (\Exception $e) use ($deferred) {
$deferred->reject($e);
}
);
}
);
return $deferred->promise();
}
/**
* Emits warnings.
*
* @param \Exception $e
*/
private function emitWarning(\Exception $e)
{
$this->emit('warning', [$e, $this]);
}
/**
* Emits errors.
*
* @param \Exception $e
*/
private function emitError(\Exception $e)
{
$this->emit('error', [$e, $this]);
}
/**
* Establishes a network connection to a server.
*
* @param string $host
* @param int $port
* @param int $timeout
*
* @return ExtendedPromiseInterface
*/
private function establishConnection($host, $port, $timeout)
{
$deferred = new Deferred();
$timer = $this->loop->addTimer(
$timeout,
function () use ($deferred, $timeout) {
$exception = new \RuntimeException(sprintf('Connection timed out after %d seconds.', $timeout));
$deferred->reject($exception);
}
);
$this->connector->connect($host.':'.$port)
->always(function () use ($timer) {
$this->loop->cancelTimer($timer);
})
->then(function (DuplexStreamInterface $stream) use ($deferred) {
$stream->on('data', function ($data) {
$this->handleReceive($data);
});
$stream->on('close', function () {
$this->handleClose();
});
$stream->on('error', function (\Exception $e) {
$this->handleError($e);
});
$deferred->resolve($stream);
})
->otherwise(function (\Exception $e) use ($deferred) {
$deferred->reject($e);
});
return $deferred->promise();
}
/**
* Registers a new client with the broker.
*
* @param Connection $connection
* @param int $timeout
*
* @return ExtendedPromiseInterface
*/
private function registerClient(Connection $connection, $timeout)
{
$deferred = new Deferred();
$responseTimer = $this->loop->addTimer(
$timeout,
function () use ($deferred, $timeout) {
$exception = new \RuntimeException(sprintf('No response after %d seconds.', $timeout));
$deferred->reject($exception);
}
);
$this->startFlow(new OutgoingConnectFlow($connection, $this->identifierGenerator), true)
->always(function () use ($responseTimer) {
$this->loop->cancelTimer($responseTimer);
})->then(function (Connection $connection) use ($deferred) {
$this->timer[] = $this->loop->addPeriodicTimer(
floor($connection->getKeepAlive() * 0.75),
function () {
$this->startFlow(new OutgoingPingFlow());
}
);
$deferred->resolve($connection);
})->otherwise(function (\Exception $e) use ($deferred) {
$deferred->reject($e);
});
return $deferred->promise();
}
/**
* Handles incoming data.
*
* @param string $data
*/
private function handleReceive($data)
{
if (!$this->isConnected && !$this->isConnecting) {
return;
}
$flowCount = count($this->receivingFlows);
$packets = $this->parser->push($data);
foreach ($packets as $packet) {
$this->handlePacket($packet);
}
if ($flowCount > count($this->receivingFlows)) {
$this->receivingFlows = array_values($this->receivingFlows);
}
$this->handleSend();
}
/**
* Handles an incoming packet.
*
* @param Packet $packet
*/
private function handlePacket(Packet $packet)
{
switch ($packet->getPacketType()) {
case Packet::TYPE_PUBLISH:
/* @var PublishRequestPacket $packet */
$message = new DefaultMessage(
$packet->getTopic(),
$packet->getPayload(),
$packet->getQosLevel(),
$packet->isRetained(),
$packet->isDuplicate()
);
$this->startFlow(new IncomingPublishFlow($message, $packet->getIdentifier()));
break;
case Packet::TYPE_CONNACK:
case Packet::TYPE_PINGRESP:
case Packet::TYPE_SUBACK:
case Packet::TYPE_UNSUBACK:
case Packet::TYPE_PUBREL:
case Packet::TYPE_PUBACK:
case Packet::TYPE_PUBREC:
case Packet::TYPE_PUBCOMP:
$flowFound = false;
foreach ($this->receivingFlows as $index => $flow) {
if ($flow->accept($packet)) {
$flowFound = true;
unset($this->receivingFlows[$index]);
$this->continueFlow($flow, $packet);
break;
}
}
if (!$flowFound) {
$this->emitWarning(
new \LogicException(sprintf('Received unexpected packet of type %d.', $packet->getPacketType()))
);
}
break;
default:
$this->emitWarning(
new \LogicException(sprintf('Cannot handle packet of type %d.', $packet->getPacketType()))
);
}
}
/**
* Handles outgoing packets.
*/
private function handleSend()
{
$flow = null;
if ($this->writtenFlow !== null) {
$flow = $this->writtenFlow;
$this->writtenFlow = null;
}
if (count($this->sendingFlows) > 0) {
$this->writtenFlow = array_shift($this->sendingFlows);
$this->stream->write($this->writtenFlow->getPacket());
}
if ($flow !== null) {
if ($flow->isFinished()) {
$this->loop->nextTick(function () use ($flow) {
$this->finishFlow($flow);
});
} else {
$this->receivingFlows[] = $flow;
}
}
}
/**
* Handles closing of the stream.
*/
private function handleClose()
{
foreach ($this->timer as $timer) {
$this->loop->cancelTimer($timer);
}
$this->timer = [];
$connection = $this->connection;
$this->isConnecting = false;
$this->isDisconnecting = false;
$this->isConnected = false;
$this->connection = null;
$this->stream = null;
if ($connection !== null) {
$this->emit('close', [$connection, $this]);
}
}
/**
* Handles errors of the stream.
*
* @param \Exception $e
*/
private function handleError(\Exception $e)
{
$this->emitError($e);
}
/**
* Starts the given flow.
*
* @param Flow $flow
* @param bool $isSilent
*
* @return ExtendedPromiseInterface
*/
private function startFlow(Flow $flow, $isSilent = false)
{
try {
$packet = $flow->start();
} catch (\Exception $e) {
$this->emitError($e);
return new RejectedPromise($e);
}
$deferred = new Deferred();
$internalFlow = new ReactFlow($flow, $deferred, $packet, $isSilent);
if ($packet !== null) {
if ($this->writtenFlow !== null) {
$this->sendingFlows[] = $internalFlow;
} else {
$this->stream->write($packet);
$this->writtenFlow = $internalFlow;
$this->handleSend();
}
} else {
$this->loop->nextTick(function () use ($internalFlow) {
$this->finishFlow($internalFlow);
});
}
return $deferred->promise();
}
/**
* Continues the given flow.
*
* @param ReactFlow $flow
* @param Packet $packet
*/
private function continueFlow(ReactFlow $flow, Packet $packet)
{
try {
$response = $flow->next($packet);
} catch (\Exception $e) {
$this->emitError($e);
return;
}
if ($response !== null) {
if ($this->writtenFlow !== null) {
$this->sendingFlows[] = $flow;
} else {
$this->stream->write($response);
$this->writtenFlow = $flow;
$this->handleSend();
}
} elseif ($flow->isFinished()) {
$this->loop->nextTick(function () use ($flow) {
$this->finishFlow($flow);
});
}
}
/**
* Finishes the given flow.
*
* @param ReactFlow $flow
*/
private function finishFlow(ReactFlow $flow)
{
if ($flow->isSuccess()) {
if (!$flow->isSilent()) {
$this->emit($flow->getCode(), [$flow->getResult(), $this]);
}
$flow->getDeferred()->resolve($flow->getResult());
} else {
$result = new \RuntimeException($flow->getErrorMessage());
$this->emitWarning($result);
$flow->getDeferred()->reject($result);
}
}
/**
* Cleans previous session by rejecting all pending flows.
*/
private function cleanPreviousSession()
{
$error = new \RuntimeException('Connection has been closed.');
foreach ($this->receivingFlows as $receivingFlow) {
$receivingFlow->getDeferred()->reject($error);
}
foreach ($this->sendingFlows as $sendingFlow) {
$sendingFlow->getDeferred()->reject($error);
}
$this->receivingFlows = [];
$this->sendingFlows = [];
}
}

21
vendor/binsoul/net-mqtt/LICENSE.md vendored Executable file
View File

@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2015 Sebastian Mößler <code@binsoul.de>
> 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.

36
vendor/binsoul/net-mqtt/README.md vendored Executable file
View File

@@ -0,0 +1,36 @@
# net-mqtt
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Software License][ico-license]](LICENSE.md)
[![Total Downloads][ico-downloads]][link-downloads]
MQTT is a machine-to-machine (M2M) / Internet of Things (IoT) connectivity protocol. It provides a lightweight method of carrying out messaging using a publish/subscribe model.
This package implements the MQTT protocol versions 3.1 and 3.1.1.
## Install
Via composer:
``` bash
$ composer require binsoul/net-mqtt
```
## Testing
``` bash
$ composer test
```
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
[ico-version]: https://img.shields.io/packagist/v/binsoul/net-mqtt.svg?style=flat-square
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/binsoul/net-mqtt.svg?style=flat-square
[link-packagist]: https://packagist.org/packages/binsoul/net-mqtt
[link-downloads]: https://packagist.org/packages/binsoul/net-mqtt
[link-author]: https://github.com/binsoul

47
vendor/binsoul/net-mqtt/composer.json vendored Executable file
View File

@@ -0,0 +1,47 @@
{
"name": "binsoul/net-mqtt",
"description": "MQTT protocol implementation",
"keywords": [
"net",
"mqtt"
],
"homepage": "https://github.com/binsoul/net-mqtt",
"license": "MIT",
"authors": [
{
"name": "Sebastian Mößler",
"email": "code@binsoul.de",
"homepage": "https://github.com/binsoul",
"role": "Developer"
}
],
"require": {
"php": "~5.6|~7.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0||~5.0",
"friendsofphp/php-cs-fixer": "~1.0"
},
"autoload": {
"psr-4": {
"BinSoul\\Net\\Mqtt\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"BinSoul\\Test\\Net\\Mqtt\\": "tests"
}
},
"scripts": {
"test": "phpunit",
"fix-style": [
"php-cs-fixer fix src",
"php-cs-fixer fix tests"
]
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

90
vendor/binsoul/net-mqtt/src/Connection.php vendored Executable file
View File

@@ -0,0 +1,90 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents the connection of a MQTT client.
*/
interface Connection
{
/**
* @return int
*/
public function getProtocol();
/**
* @return string
*/
public function getClientID();
/**
* @return bool
*/
public function isCleanSession();
/**
* @return string
*/
public function getUsername();
/**
* @return string
*/
public function getPassword();
/**
* @return Message|null
*/
public function getWill();
/**
* @return int
*/
public function getKeepAlive();
/**
* Returns a new connection with the given protocol.
*
* @param int $protocol
*
* @return self
*/
public function withProtocol($protocol);
/**
* Returns a new connection with the given client id.
*
* @param string $clientID
*
* @return self
*/
public function withClientID($clientID);
/**
* Returns a new connection with the given credentials.
*
* @param string $username
* @param string $password
*
* @return self
*/
public function withCredentials($username, $password);
/**
* Returns a new connection with the given will.
*
* @param Message $will
*
* @return self
*/
public function withWill(Message $will);
/**
* Returns a new connection with the given keep alive timeout.
*
* @param int $timeout
*
* @return self
*/
public function withKeepAlive($timeout);
}

View File

@@ -0,0 +1,129 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see Connection} interface.
*/
class DefaultConnection implements Connection
{
/** @var string */
private $username;
/** @var string */
private $password;
/** @var Message|null */
private $will;
/** @var string */
private $clientID;
/** @var int */
private $keepAlive;
/** @var int */
private $protocol;
/** @var bool */
private $clean;
/**
* Constructs an instance of this class.
*
* @param string $username
* @param string $password
* @param Message|null $will
* @param string $clientID
* @param int $keepAlive
* @param int $protocol
* @param bool $clean
*/
public function __construct(
$username = '',
$password = '',
Message $will = null,
$clientID = '',
$keepAlive = 60,
$protocol = 4,
$clean = true
) {
$this->username = $username;
$this->password = $password;
$this->will = $will;
$this->clientID = $clientID;
$this->keepAlive = $keepAlive;
$this->protocol = $protocol;
$this->clean = $clean;
}
public function getProtocol()
{
return $this->protocol;
}
public function getClientID()
{
return $this->clientID;
}
public function isCleanSession()
{
return $this->clean;
}
public function getUsername()
{
return $this->username;
}
public function getPassword()
{
return $this->password;
}
public function getWill()
{
return $this->will;
}
public function getKeepAlive()
{
return $this->keepAlive;
}
public function withProtocol($protocol)
{
$result = clone $this;
$result->protocol = $protocol;
return $result;
}
public function withClientID($clientID)
{
$result = clone $this;
$result->clientID = $clientID;
return $result;
}
public function withCredentials($username, $password)
{
$result = clone $this;
$result->username = $username;
$result->password = $password;
return $result;
}
public function withWill(Message $will = null)
{
$result = clone $this;
$result->will = $will;
return $result;
}
public function withKeepAlive($timeout)
{
$result = clone $this;
$result->keepAlive = $timeout;
return $result;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see IdentifierGenerator} interface.
*/
class DefaultIdentifierGenerator implements IdentifierGenerator
{
/** @var int */
private $currentIdentifier = 0;
public function generatePacketID()
{
++$this->currentIdentifier;
if ($this->currentIdentifier > 0xFFFF) {
$this->currentIdentifier = 1;
}
return $this->currentIdentifier;
}
public function generateClientID()
{
if (function_exists('random_bytes')) {
$data = random_bytes(9);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$data = openssl_random_pseudo_bytes(9);
} else {
$data = '';
for ($i = 1; $i <= 8; ++$i) {
$data = chr(mt_rand(0, 255)).$data;
}
}
return 'BNMCR'.bin2hex($data);
}
}

119
vendor/binsoul/net-mqtt/src/DefaultMessage.php vendored Executable file
View File

@@ -0,0 +1,119 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see Message} interface.
*/
class DefaultMessage implements Message
{
/** @var string */
private $topic;
/** @var string */
private $payload;
/** @var bool */
private $isRetained;
/** @var bool */
private $isDuplicate = false;
/** @var int */
private $qosLevel;
/**
* Constructs an instance of this class.
*
* @param string $topic
* @param string $payload
* @param int $qosLevel
* @param bool $retain
* @param bool $isDuplicate
*/
public function __construct($topic, $payload = '', $qosLevel = 0, $retain = false, $isDuplicate = false)
{
$this->topic = $topic;
$this->payload = $payload;
$this->isRetained = $retain;
$this->qosLevel = $qosLevel;
$this->isDuplicate = $isDuplicate;
}
public function getTopic()
{
return $this->topic;
}
public function getPayload()
{
return $this->payload;
}
public function getQosLevel()
{
return $this->qosLevel;
}
public function isDuplicate()
{
return $this->isDuplicate;
}
public function isRetained()
{
return $this->isRetained;
}
public function withTopic($topic)
{
$result = clone $this;
$result->topic = $topic;
return $result;
}
public function withPayload($payload)
{
$result = clone $this;
$result->payload = $payload;
return $result;
}
public function withQosLevel($level)
{
$result = clone $this;
$result->qosLevel = $level;
return $result;
}
public function retain()
{
$result = clone $this;
$result->isRetained = true;
return $result;
}
public function release()
{
$result = clone $this;
$result->isRetained = false;
return $result;
}
public function duplicate()
{
$result = clone $this;
$result->isDuplicate = true;
return $result;
}
public function original()
{
$result = clone $this;
$result->isDuplicate = false;
return $result;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Provides a default implementation of the {@see Subscription} interface.
*/
class DefaultSubscription implements Subscription
{
/** @var string */
private $filter;
/** @var int */
private $qosLevel;
/**
* Constructs an instance of this class.
*
* @param string $filter
* @param int $qosLevel
*/
public function __construct($filter, $qosLevel = 0)
{
$this->filter = $filter;
$this->qosLevel = $qosLevel;
}
public function getFilter()
{
return $this->filter;
}
public function getQosLevel()
{
return $this->qosLevel;
}
public function withFilter($filter)
{
$result = clone $this;
$result->filter = $filter;
return $result;
}
public function withQosLevel($level)
{
$result = clone $this;
$result->qosLevel = $level;
return $result;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BinSoul\Net\Mqtt\Exception;
/**
* Will be thrown if the end of a stream is reached but more bytes were requested.
*/
class EndOfStreamException extends \Exception
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BinSoul\Net\Mqtt\Exception;
/**
* Will be thrown if a packet is malformed.
*/
class MalformedPacketException extends \Exception
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BinSoul\Net\Mqtt\Exception;
/**
* Will be thrown if a packet type is unknown.
*/
class UnknownPacketTypeException extends \Exception
{
}

71
vendor/binsoul/net-mqtt/src/Flow.php vendored Executable file
View File

@@ -0,0 +1,71 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents a sequence of packages exchanged between clients and brokers.
*/
interface Flow
{
/**
* Returns the unique code.
*
* @return string
*/
public function getCode();
/**
* Starts the flow.
*
* @return Packet|null First packet of the flow
*
* @throws \Exception
*/
public function start();
/**
* Indicates if the flow can handle the given packet.
*
* @param Packet $packet Packet to accept
*
* @return bool
*/
public function accept(Packet $packet);
/**
* Continues the flow.
*
* @param Packet $packet Packet to respond
*
* @return Packet|null Next packet of the flow
*/
public function next(Packet $packet);
/**
* Indicates if the flow is finished.
*
* @return bool
*/
public function isFinished();
/**
* Indicates if the flow finished successfully.
*
* @return bool
*/
public function isSuccess();
/**
* Returns the result of the flow if it finished successfully.
*
* @return mixed
*/
public function getResult();
/**
* Returns an error message if the flow didn't finish successfully.
*
* @return string
*/
public function getErrorMessage();
}

View File

@@ -0,0 +1,74 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet;
/**
* Provides an abstract implementation of the {@see Flow} interface.
*/
abstract class AbstractFlow implements Flow
{
/** @var bool */
private $isFinished = false;
/** @var bool */
private $isSuccess = false;
/** @var mixed */
private $result;
/** @var string */
private $error = '';
public function accept(Packet $packet)
{
return false;
}
public function next(Packet $packet)
{
}
public function isFinished()
{
return $this->isFinished;
}
public function isSuccess()
{
return $this->isFinished && $this->isSuccess;
}
public function getResult()
{
return $this->result;
}
public function getErrorMessage()
{
return $this->error;
}
/**
* Marks the flow as successful and sets the result.
*
* @param mixed|null $result
*/
protected function succeed($result = null)
{
$this->isFinished = true;
$this->isSuccess = true;
$this->result = $result;
}
/**
* Marks the flow as failed and sets the error message.
*
* @param string $error
*/
protected function fail($error = '')
{
$this->isFinished = true;
$this->isSuccess = false;
$this->error = $error;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet\PingResponsePacket;
/**
* Represents a flow starting with an incoming PING packet.
*/
class IncomingPingFlow extends AbstractFlow
{
public function getCode()
{
return 'pong';
}
public function start()
{
$this->succeed();
return new PingResponsePacket();
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PublishAckPacket;
use BinSoul\Net\Mqtt\Packet\PublishCompletePacket;
use BinSoul\Net\Mqtt\Packet\PublishReceivedPacket;
use BinSoul\Net\Mqtt\Packet\PublishReleasePacket;
/**
* Represents a flow starting with an incoming PUBLISH packet.
*/
class IncomingPublishFlow extends AbstractFlow
{
/** @var int|null */
private $identifier;
/** @var Message */
private $message;
/**
* Constructs an instance of this class.
*
* @param Message $message
* @param int|null $identifier
*/
public function __construct(Message $message, $identifier = null)
{
$this->message = $message;
$this->identifier = $identifier;
}
public function getCode()
{
return 'message';
}
public function start()
{
$packet = null;
$emit = true;
if ($this->message->getQosLevel() === 1) {
$packet = new PublishAckPacket();
} elseif ($this->message->getQosLevel() === 2) {
$packet = new PublishReceivedPacket();
$emit = false;
}
if ($packet !== null) {
$packet->setIdentifier($this->identifier);
}
if ($emit) {
$this->succeed($this->message);
}
return $packet;
}
public function accept(Packet $packet)
{
if ($this->message->getQosLevel() !== 2 || $packet->getPacketType() !== Packet::TYPE_PUBREL) {
return false;
}
/* @var PublishReleasePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
public function next(Packet $packet)
{
$this->succeed($this->message);
$response = new PublishCompletePacket();
$response->setIdentifier($this->identifier);
return $response;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\ConnectRequestPacket;
use BinSoul\Net\Mqtt\Packet\ConnectResponsePacket;
/**
* Represents a flow starting with an outgoing CONNECT packet.
*/
class OutgoingConnectFlow extends AbstractFlow
{
/** @var Connection */
private $connection;
/**
* Constructs an instance of this class.
*
* @param Connection $connection
* @param IdentifierGenerator $generator
*/
public function __construct(Connection $connection, IdentifierGenerator $generator)
{
$this->connection = $connection;
if ($this->connection->getClientID() === '') {
$this->connection = $this->connection->withClientID($generator->generateClientID());
}
}
public function getCode()
{
return 'connect';
}
public function start()
{
$packet = new ConnectRequestPacket();
$packet->setProtocolLevel($this->connection->getProtocol());
$packet->setKeepAlive($this->connection->getKeepAlive());
$packet->setClientID($this->connection->getClientID());
$packet->setCleanSession($this->connection->isCleanSession());
$packet->setUsername($this->connection->getUsername());
$packet->setPassword($this->connection->getPassword());
$will = $this->connection->getWill();
if ($will !== null && $will->getTopic() !== '' && $will->getPayload() !== '') {
$packet->setWill($will->getTopic(), $will->getPayload(), $will->getQosLevel(), $will->isRetained());
}
return $packet;
}
public function accept(Packet $packet)
{
return $packet->getPacketType() === Packet::TYPE_CONNACK;
}
public function next(Packet $packet)
{
/** @var ConnectResponsePacket $packet */
if ($packet->isSuccess()) {
$this->succeed($this->connection);
} else {
$this->fail($packet->getErrorName());
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Connection;
use BinSoul\Net\Mqtt\Packet\DisconnectRequestPacket;
/**
* Represents a flow starting with an outgoing DISCONNECT packet.
*/
class OutgoingDisconnectFlow extends AbstractFlow
{
/** @var Connection */
private $connection;
/**
* Constructs an instance of this class.
*
* @param Connection $connection
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function getCode()
{
return 'disconnect';
}
public function start()
{
$this->succeed($this->connection);
return new DisconnectRequestPacket();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PingRequestPacket;
/**
* Represents a flow starting with an outgoing PING packet.
*/
class OutgoingPingFlow extends AbstractFlow
{
public function getCode()
{
return 'ping';
}
public function start()
{
return new PingRequestPacket();
}
public function accept(Packet $packet)
{
return $packet->getPacketType() === Packet::TYPE_PINGRESP;
}
public function next(Packet $packet)
{
$this->succeed();
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Message;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\PublishAckPacket;
use BinSoul\Net\Mqtt\Packet\PublishCompletePacket;
use BinSoul\Net\Mqtt\Packet\PublishReceivedPacket;
use BinSoul\Net\Mqtt\Packet\PublishReleasePacket;
use BinSoul\Net\Mqtt\Packet\PublishRequestPacket;
/**
* Represents a flow starting with an outgoing PUBLISH packet.
*/
class OutgoingPublishFlow extends AbstractFlow
{
/** @var int|null */
private $identifier;
/** @var Message */
private $message;
/** @var bool */
private $receivedPubRec = false;
/**
* Constructs an instance of this class.
*
* @param Message $message
* @param IdentifierGenerator $generator
*/
public function __construct(Message $message, IdentifierGenerator $generator)
{
$this->message = $message;
if ($this->message->getQosLevel() > 0) {
$this->identifier = $generator->generatePacketID();
}
}
public function getCode()
{
return 'publish';
}
public function start()
{
$packet = new PublishRequestPacket();
$packet->setTopic($this->message->getTopic());
$packet->setPayload($this->message->getPayload());
$packet->setRetained($this->message->isRetained());
$packet->setDuplicate($this->message->isDuplicate());
$packet->setQosLevel($this->message->getQosLevel());
if ($this->message->getQosLevel() === 0) {
$this->succeed($this->message);
} else {
$packet->setIdentifier($this->identifier);
}
return $packet;
}
public function accept(Packet $packet)
{
if ($this->message->getQosLevel() === 0) {
return false;
}
$packetType = $packet->getPacketType();
if ($packetType === Packet::TYPE_PUBACK && $this->message->getQosLevel() === 1) {
/* @var PublishAckPacket $packet */
return $packet->getIdentifier() === $this->identifier;
} elseif ($this->message->getQosLevel() === 2) {
if ($packetType === Packet::TYPE_PUBREC) {
/* @var PublishReceivedPacket $packet */
return $packet->getIdentifier() === $this->identifier;
} elseif ($this->receivedPubRec && $packetType === Packet::TYPE_PUBCOMP) {
/* @var PublishCompletePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
}
return false;
}
public function next(Packet $packet)
{
$packetType = $packet->getPacketType();
if ($packetType === Packet::TYPE_PUBACK || $packetType === Packet::TYPE_PUBCOMP) {
$this->succeed($this->message);
} elseif ($packetType === Packet::TYPE_PUBREC) {
$this->receivedPubRec = true;
$response = new PublishReleasePacket();
$response->setIdentifier($this->identifier);
return $response;
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\SubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\SubscribeResponsePacket;
use BinSoul\Net\Mqtt\Subscription;
/**
* Represents a flow starting with an outgoing SUBSCRIBE packet.
*/
class OutgoingSubscribeFlow extends AbstractFlow
{
/** @var int */
private $identifier;
/** @var Subscription[] */
private $subscriptions;
/**
* Constructs an instance of this class.
*
* @param Subscription[] $subscriptions
* @param IdentifierGenerator $generator
*/
public function __construct(array $subscriptions, IdentifierGenerator $generator)
{
$this->subscriptions = array_values($subscriptions);
$this->identifier = $generator->generatePacketID();
}
public function getCode()
{
return 'subscribe';
}
public function start()
{
$packet = new SubscribeRequestPacket();
$packet->setTopic($this->subscriptions[0]->getFilter());
$packet->setQosLevel($this->subscriptions[0]->getQosLevel());
$packet->setIdentifier($this->identifier);
return $packet;
}
public function accept(Packet $packet)
{
if ($packet->getPacketType() !== Packet::TYPE_SUBACK) {
return false;
}
/* @var SubscribeResponsePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
public function next(Packet $packet)
{
/* @var SubscribeResponsePacket $packet */
$returnCodes = $packet->getReturnCodes();
if (count($returnCodes) !== count($this->subscriptions)) {
throw new \LogicException(
sprintf(
'SUBACK: Expected %d return codes but got %d.',
count($this->subscriptions),
count($returnCodes)
)
);
}
foreach ($returnCodes as $index => $code) {
if ($packet->isError($code)) {
$this->fail(sprintf('Failed to subscribe to "%s".', $this->subscriptions[$index]->getFilter()));
return;
}
}
$this->succeed($this->subscriptions[0]);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace BinSoul\Net\Mqtt\Flow;
use BinSoul\Net\Mqtt\IdentifierGenerator;
use BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet\UnsubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\UnsubscribeResponsePacket;
use BinSoul\Net\Mqtt\Subscription;
/**
* Represents a flow starting with an outgoing UNSUBSCRIBE packet.
*/
class OutgoingUnsubscribeFlow extends AbstractFlow
{
/** @var int */
private $identifier;
/** @var Subscription[] */
private $subscriptions;
/**
* Constructs an instance of this class.
*
* @param Subscription[] $subscriptions
* @param IdentifierGenerator $generator
*/
public function __construct(array $subscriptions, IdentifierGenerator $generator)
{
$this->subscriptions = array_values($subscriptions);
$this->identifier = $generator->generatePacketID();
}
public function getCode()
{
return 'unsubscribe';
}
public function start()
{
$packet = new UnsubscribeRequestPacket();
$packet->setTopic($this->subscriptions[0]->getFilter());
$packet->setIdentifier($this->identifier);
return $packet;
}
public function accept(Packet $packet)
{
if ($packet->getPacketType() !== Packet::TYPE_UNSUBACK) {
return false;
}
/* @var UnsubscribeResponsePacket $packet */
return $packet->getIdentifier() === $this->identifier;
}
public function next(Packet $packet)
{
$this->succeed($this->subscriptions[0]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Generates identifiers.
*/
interface IdentifierGenerator
{
/**
* Generates a packet identifier between 1 and 0xFFFF.
*
* @return int
*/
public function generatePacketID();
/**
* Generates a client identifier of up to 23 bytes.
*
* @return string
*/
public function generateClientID();
}

99
vendor/binsoul/net-mqtt/src/Message.php vendored Executable file
View File

@@ -0,0 +1,99 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents a message.
*/
interface Message
{
/**
* Returns the topic.
*
* @return string
*/
public function getTopic();
/**
* Returns the payload.
*
* @return string
*/
public function getPayload();
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel();
/**
* Indicates if the message is a duplicate.
*
* @return bool
*/
public function isDuplicate();
/**
* Indicates if the message is retained.
*
* @return bool
*/
public function isRetained();
/**
* Returns a new message with the given topic.
*
* @param string $topic
*
* @return self
*/
public function withTopic($topic);
/**
* Returns a new message with the given payload.
*
* @param string $payload
*
* @return self
*/
public function withPayload($payload);
/**
* Returns a new message with the given quality of service level.
*
* @param int $level
*
* @return self
*/
public function withQosLevel($level);
/**
* Returns a new message flagged as retained.
*
* @return self
*/
public function retain();
/**
* Returns a new message flagged as not retained.
*
* @return self
*/
public function release();
/**
* Returns a new message flagged as duplicate.
*
* @return self
*/
public function duplicate();
/**
* Returns a new message flagged as original.
*
* @return self
*/
public function original();
}

52
vendor/binsoul/net-mqtt/src/Packet.php vendored Executable file
View File

@@ -0,0 +1,52 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represent a packet of the MQTT protocol.
*/
interface Packet
{
const TYPE_CONNECT = 1;
const TYPE_CONNACK = 2;
const TYPE_PUBLISH = 3;
const TYPE_PUBACK = 4;
const TYPE_PUBREC = 5;
const TYPE_PUBREL = 6;
const TYPE_PUBCOMP = 7;
const TYPE_SUBSCRIBE = 8;
const TYPE_SUBACK = 9;
const TYPE_UNSUBSCRIBE = 10;
const TYPE_UNSUBACK = 11;
const TYPE_PINGREQ = 12;
const TYPE_PINGRESP = 13;
const TYPE_DISCONNECT = 14;
/**
* Returns the type of the packet.
*
* @return int
*/
public function getPacketType();
/**
* Reads the packet from the given stream.
*
* @param PacketStream $stream
*/
public function read(PacketStream $stream);
/**
* Writes the packet to the given stream.
*
* @param PacketStream $stream
*/
public function write(PacketStream $stream);
/**
* Returns the serialized form of the packet.
*
* @return string
*/
public function __toString();
}

View File

@@ -0,0 +1,279 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the base class for all packets.
*/
abstract class BasePacket implements Packet
{
/**
* Type of the packet. See {@see Packet}.
*
* @var int
*/
protected static $packetType = 0;
/**
* Flags of the packet.
*
* @var int
*/
protected $packetFlags = 0;
/**
* Number of bytes of a variable length packet.
*
* @var int
*/
protected $remainingPacketLength = 0;
public function __toString()
{
$output = new PacketStream();
$this->write($output);
return $output->getData();
}
public function read(PacketStream $stream)
{
$byte = $stream->readByte();
if ($byte >> 4 !== static::$packetType) {
throw new MalformedPacketException(
sprintf(
'Expected packet type %02x but got %02x.',
$byte >> 4,
static::$packetType
)
);
}
$this->packetFlags = $byte & 0x0F;
$this->readRemainingLength($stream);
}
public function write(PacketStream $stream)
{
$stream->writeByte(((static::$packetType & 0x0F) << 4) + ($this->packetFlags & 0x0F));
$this->writeRemainingLength($stream);
}
/**
* Reads the remaining length from the given stream.
*
* @param PacketStream $stream
*
* @throws MalformedPacketException
*/
private function readRemainingLength(PacketStream $stream)
{
$this->remainingPacketLength = 0;
$multiplier = 1;
do {
$encodedByte = $stream->readByte();
$this->remainingPacketLength += ($encodedByte & 127) * $multiplier;
$multiplier *= 128;
if ($multiplier > 128 * 128 * 128 * 128) {
throw new MalformedPacketException('Malformed remaining length.');
}
} while (($encodedByte & 128) !== 0);
}
/**
* Writes the remaining length to the given stream.
*
* @param PacketStream $stream
*/
private function writeRemainingLength(PacketStream $stream)
{
$x = $this->remainingPacketLength;
do {
$encodedByte = $x % 128;
$x = (int) ($x / 128);
if ($x > 0) {
$encodedByte |= 128;
}
$stream->writeByte($encodedByte);
} while ($x > 0);
}
public function getPacketType()
{
return static::$packetType;
}
/**
* Returns the packet flags.
*
* @return int
*/
public function getPacketFlags()
{
return $this->packetFlags;
}
/**
* Returns the remaining length.
*
* @return int
*/
public function getRemainingPacketLength()
{
return $this->remainingPacketLength;
}
/**
* Asserts that the packet flags have a specific value.
*
* @param int $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertPacketFlags($value, $fromPacket = true)
{
if ($this->packetFlags !== $value) {
$this->throwException(
sprintf(
'Expected flags %02x but got %02x.',
$value,
$this->packetFlags
),
$fromPacket
);
}
}
/**
* Asserts that the remaining length is greater than zero and has a specific value.
*
* @param int|null $value value to test or null if any value greater than zero is valid
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertRemainingPacketLength($value = null, $fromPacket = true)
{
if ($value === null && $this->remainingPacketLength === 0) {
$this->throwException('Expected payload but remaining packet length is zero.', $fromPacket);
}
if ($value !== null && $this->remainingPacketLength !== $value) {
$this->throwException(
sprintf(
'Expected remaining packet length of %d bytes but got %d.',
$value,
$this->remainingPacketLength
),
$fromPacket
);
}
}
/**
* Asserts that the given string is a well-formed MQTT string.
*
* @param string $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertValidStringLength($value, $fromPacket = true)
{
if (strlen($value) > 0xFFFF) {
$this->throwException(
sprintf(
'The string "%s" is longer than 65535 byte.',
substr($value, 0, 50)
),
$fromPacket
);
}
}
/**
* Asserts that the given string is a well-formed MQTT string.
*
* @param string $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertValidString($value, $fromPacket = true)
{
$this->assertValidStringLength($value, $fromPacket);
if (!mb_check_encoding($value, 'UTF-8')) {
$this->throwException(
sprintf(
'The string "%s" is not well-formed UTF-8.',
substr($value, 0, 50)
),
$fromPacket
);
}
if (preg_match('/[\xD8-\xDF][\x00-\xFF]|\x00\x00/x', $value)) {
$this->throwException(
sprintf(
'The string "%s" contains invalid characters.',
substr($value, 0, 50)
),
$fromPacket
);
}
}
/**
* Asserts that the given quality of service level is valid.
*
* @param int $level
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function assertValidQosLevel($level, $fromPacket = true)
{
if ($level < 0 || $level > 2) {
$this->throwException(
sprintf(
'Expected a quality of service level between 0 and 2 but got %d.',
$level
),
$fromPacket
);
}
}
/**
* Throws a MalformedPacketException for packet validation and an InvalidArgumentException otherwise.
*
* @param string $message
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
protected function throwException($message, $fromPacket)
{
if ($fromPacket) {
throw new MalformedPacketException($message);
}
throw new \InvalidArgumentException($message);
}
}

View File

@@ -0,0 +1,405 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the CONNECT packet.
*/
class ConnectRequestPacket extends BasePacket
{
/** @var int */
private $protocolLevel = 4;
/** @var string */
private $protocolName = 'MQTT';
/** @var int */
private $flags = 2;
/** @var string */
protected $clientID = '';
/** @var int */
private $keepAlive = 60;
/** @var string */
private $willTopic = '';
/** @var string */
private $willMessage = '';
/** @var string */
private $username = '';
/** @var string */
private $password = '';
protected static $packetType = Packet::TYPE_CONNECT;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength();
$this->protocolName = $stream->readString();
$this->protocolLevel = $stream->readByte();
$this->flags = $stream->readByte();
$this->keepAlive = $stream->readWord();
$this->clientID = $stream->readString();
if ($this->hasWill()) {
$this->willTopic = $stream->readString();
$this->willMessage = $stream->readString();
}
if ($this->hasUsername()) {
$this->username = $stream->readString();
}
if ($this->hasPassword()) {
$this->password = $stream->readString();
}
$this->assertValidWill();
$this->assertValidString($this->clientID);
$this->assertValidString($this->willTopic);
$this->assertValidString($this->username);
}
public function write(PacketStream $stream)
{
if ($this->clientID === '') {
$this->clientID = 'BinSoul'.mt_rand(100000, 999999);
}
$data = new PacketStream();
$data->writeString($this->protocolName);
$data->writeByte($this->protocolLevel);
$data->writeByte($this->flags);
$data->writeWord($this->keepAlive);
$data->writeString($this->clientID);
if ($this->hasWill()) {
$data->writeString($this->willTopic);
$data->writeString($this->willMessage);
}
if ($this->hasUsername()) {
$data->writeString($this->username);
}
if ($this->hasPassword()) {
$data->writeString($this->password);
}
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the protocol level.
*
* @return int
*/
public function getProtocolLevel()
{
return $this->protocolLevel;
}
/**
* Sets the protocol level.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setProtocolLevel($value)
{
if ($value < 3 || $value > 4) {
throw new \InvalidArgumentException(sprintf('Unknown protocol level %d.', $value));
}
$this->protocolLevel = $value;
if ($this->protocolLevel === 3) {
$this->protocolName = 'MQIsdp';
} elseif ($this->protocolLevel === 4) {
$this->protocolName = 'MQTT';
}
}
/**
* Returns the client id.
*
* @return string
*/
public function getClientID()
{
return $this->clientID;
}
/**
* Sets the client id.
*
* @param string $value
*/
public function setClientID($value)
{
$this->clientID = $value;
}
/**
* Returns the keep alive time in seconds.
*
* @return int
*/
public function getKeepAlive()
{
return $this->keepAlive;
}
/**
* Sets the keep alive time in seconds.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setKeepAlive($value)
{
if ($value > 65535) {
throw new \InvalidArgumentException(
sprintf(
'Expected a keep alive time lower than 65535 but got %d.',
$value
)
);
}
$this->keepAlive = $value;
}
/**
* Indicates if the clean session flag is set.
*
* @return bool
*/
public function isCleanSession()
{
return ($this->flags & 2) === 2;
}
/**
* Changes the clean session flag.
*
* @param bool $value
*/
public function setCleanSession($value)
{
if ($value) {
$this->flags |= 2;
} else {
$this->flags &= ~2;
}
}
/**
* Indicates if a will is set.
*
* @return bool
*/
public function hasWill()
{
return ($this->flags & 4) === 4;
}
/**
* Returns the desired quality of service level of the will.
*
* @return int
*/
public function getWillQosLevel()
{
return ($this->flags & 24) >> 3;
}
/**
* Indicates if the will should be retained.
*
* @return bool
*/
public function isWillRetained()
{
return ($this->flags & 32) === 32;
}
/**
* Returns the will topic.
*
* @return string
*/
public function getWillTopic()
{
return $this->willTopic;
}
/**
* Returns the will message.
*
* @return string
*/
public function getWillMessage()
{
return $this->willMessage;
}
/**
* Sets the will.
*
* @param string $topic
* @param string $message
* @param int $qosLevel
* @param bool $isRetained
*
* @throws \InvalidArgumentException
*/
public function setWill($topic, $message, $qosLevel = 0, $isRetained = false)
{
$this->assertValidString($topic, false);
if ($topic === '') {
throw new \InvalidArgumentException('The topic must not be empty.');
}
$this->assertValidStringLength($message, false);
if ($message === '') {
throw new \InvalidArgumentException('The message must not be empty.');
}
$this->assertValidQosLevel($qosLevel, false);
$this->willTopic = $topic;
$this->willMessage = $message;
$this->flags |= 4;
$this->flags |= ($qosLevel << 3);
if ($isRetained) {
$this->flags |= 32;
} else {
$this->flags &= ~32;
}
}
/**
* Removes the will.
*/
public function removeWill()
{
$this->flags &= ~60;
$this->willTopic = '';
$this->willMessage = '';
}
/**
* Indicates if a username is set.
*
* @return bool
*/
public function hasUsername()
{
return $this->flags & 64;
}
/**
* Returns the username.
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Sets the username.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setUsername($value)
{
$this->assertValidString($value, false);
$this->username = $value;
if ($this->username !== '') {
$this->flags |= 64;
} else {
$this->flags &= ~64;
}
}
/**
* Indicates if a password is set.
*
* @return bool
*/
public function hasPassword()
{
return $this->flags & 128;
}
/**
* Returns the password.
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Sets the password.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setPassword($value)
{
$this->assertValidStringLength($value, false);
$this->password = $value;
if ($this->password !== '') {
$this->flags |= 128;
} else {
$this->flags &= ~128;
}
}
/**
* Asserts that all will flags and quality of service are correct.
*
* @throws MalformedPacketException
*/
private function assertValidWill()
{
if ($this->hasWill()) {
$this->assertValidQosLevel($this->getWillQosLevel(), true);
} else {
if ($this->getWillQosLevel() > 0) {
$this->throwException(
sprintf(
'Expected a will quality of service level of zero but got %d.',
$this->getWillQosLevel()
),
true
);
}
if ($this->isWillRetained()) {
$this->throwException('There is not will but the will retain flag is set.', true);
}
}
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the CONNACK packet.
*/
class ConnectResponsePacket extends BasePacket
{
/** @var string[][] */
private static $returnCodes = [
0 => [
'Connection accepted',
'',
],
1 => [
'Unacceptable protocol version',
'The Server does not support the level of the MQTT protocol requested by the client.',
],
2 => [
'Identifier rejected',
'The client identifier is correct UTF-8 but not allowed by the server.',
],
3 => [
'Server unavailable',
'The network connection has been made but the MQTT service is unavailable',
],
4 => [
'Bad user name or password',
'The data in the user name or password is malformed.',
],
5 => [
'Not authorized',
'The client is not authorized to connect.',
],
];
/** @var int */
private $flags = 0;
/** @var int */
private $returnCode;
protected static $packetType = Packet::TYPE_CONNACK;
protected $remainingPacketLength = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(2);
$this->flags = $stream->readByte();
$this->returnCode = $stream->readByte();
}
public function write(PacketStream $stream)
{
$this->remainingPacketLength = 2;
parent::write($stream);
$stream->writeByte($this->flags);
$stream->writeByte($this->returnCode);
}
/**
* Returns the return code.
*
* @return int
*/
public function getReturnCode()
{
return $this->returnCode;
}
/**
* Indicates if the connection was successful.
*
* @return bool
*/
public function isSuccess()
{
return $this->returnCode === 0;
}
/**
* Indicates if the connection failed.
*
* @return bool
*/
public function isError()
{
return $this->returnCode > 0;
}
/**
* Returns a string representation of the returned error code.
*
* @return int
*/
public function getErrorName()
{
if (isset(self::$returnCodes[$this->returnCode])) {
return self::$returnCodes[$this->returnCode][0];
}
return 'Error '.$this->returnCode;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the DISCONNECT packet.
*/
class DisconnectRequestPacket extends BasePacket
{
protected static $packetType = Packet::TYPE_DISCONNECT;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(0);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
/**
* Provides methods for packets with an identifier.
*/
trait IdentifiablePacket
{
/** @var int */
private static $nextIdentifier = 0;
/** @var int|null */
protected $identifier;
/**
* Returns the identifier or generates a new one.
*
* @return int
*/
protected function generateIdentifier()
{
if ($this->identifier === null) {
++self::$nextIdentifier;
self::$nextIdentifier &= 0xFFFF;
$this->identifier = self::$nextIdentifier;
}
return $this->identifier;
}
/**
* Returns the identifier.
*
* @return int|null
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* Sets the identifier.
*
* @param int|null $value
*
* @throws \InvalidArgumentException
*/
public function setIdentifier($value)
{
if ($value !== null && ($value < 0 || $value > 0xFFFF)) {
throw new \InvalidArgumentException(
sprintf(
'Expected an identifier between 0x0000 and 0xFFFF but got %x',
$value
)
);
}
$this->identifier = $value;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
/**
* Provides a base class for PUB* packets.
*/
abstract class IdentifierOnlyPacket extends BasePacket
{
use IdentifiablePacket;
protected $remainingPacketLength = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags($this->getExpectedPacketFlags());
$this->assertRemainingPacketLength(2);
$this->identifier = $stream->readWord();
}
public function write(PacketStream $stream)
{
$this->remainingPacketLength = 2;
parent::write($stream);
$stream->writeWord($this->generateIdentifier());
}
/**
* Returns the expected packet flags.
*
* @return int
*/
protected function getExpectedPacketFlags()
{
return 0;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PINGREQ packet.
*/
class PingRequestPacket extends BasePacket
{
protected static $packetType = Packet::TYPE_PINGREQ;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(0);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PINGRESP packet.
*/
class PingResponsePacket extends BasePacket
{
protected static $packetType = Packet::TYPE_PINGRESP;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength(0);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBACK packet.
*/
class PublishAckPacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBACK;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBCOMP packet.
*/
class PublishCompletePacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBCOMP;
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBREC packet.
*/
class PublishReceivedPacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBREC;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBREL packet.
*/
class PublishReleasePacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_PUBREL;
protected $packetFlags = 2;
protected function getExpectedPacketFlags()
{
return 2;
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the PUBLISH packet.
*/
class PublishRequestPacket extends BasePacket
{
use IdentifiablePacket;
/** @var string */
private $topic;
/** @var string */
private $payload;
protected static $packetType = Packet::TYPE_PUBLISH;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertRemainingPacketLength();
$originalPosition = $stream->getPosition();
$this->topic = $stream->readString();
$this->identifier = null;
if ($this->getQosLevel() > 0) {
$this->identifier = $stream->readWord();
}
$payloadLength = $this->remainingPacketLength - ($stream->getPosition() - $originalPosition);
$this->payload = $stream->read($payloadLength);
$this->assertValidQosLevel($this->getQosLevel());
$this->assertValidString($this->topic);
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeString($this->topic);
if ($this->getQosLevel() > 0) {
$data->writeWord($this->generateIdentifier());
}
$data->write($this->payload);
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the topic.
*
* @return string
*/
public function getTopic()
{
return $this->topic;
}
/**
* Sets the topic.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setTopic($value)
{
$this->assertValidString($value, false);
if ($value === '') {
throw new \InvalidArgumentException('The topic must not be empty.');
}
$this->topic = $value;
}
/**
* Returns the payload.
*
* @return string
*/
public function getPayload()
{
return $this->payload;
}
/**
* Sets the payload.
*
* @param string $value
*/
public function setPayload($value)
{
$this->payload = $value;
}
/**
* Indicates if the packet is a duplicate.
*
* @return bool
*/
public function isDuplicate()
{
return ($this->packetFlags & 8) === 8;
}
/**
* Marks the packet as duplicate.
*
* @param bool $value
*/
public function setDuplicate($value)
{
if ($value) {
$this->packetFlags |= 8;
} else {
$this->packetFlags &= ~8;
}
}
/**
* Indicates if the packet is retained.
*
* @return bool
*/
public function isRetained()
{
return ($this->packetFlags & 1) === 1;
}
/**
* Marks the packet as retained.
*
* @param bool $value
*/
public function setRetained($value)
{
if ($value) {
$this->packetFlags |= 1;
} else {
$this->packetFlags &= ~1;
}
}
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel()
{
return ($this->packetFlags & 6) >> 1;
}
/**
* Sets the quality of service level.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setQosLevel($value)
{
$this->assertValidQosLevel($value, false);
$this->packetFlags |= ($value & 3) << 1;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
/**
* Represents the CONNECT packet with strict rules for client ids.
*/
class StrictConnectRequestPacket extends ConnectRequestPacket
{
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertValidClientID($this->clientID, true);
}
/**
* Sets the client id.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setClientID($value)
{
$this->assertValidClientID($value, false);
$this->clientID = $value;
}
/**
* Asserts that a client id is shorter than 24 bytes and only contains characters 0-9, a-z or A-Z.
*
* @param string $value
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
private function assertValidClientID($value, $fromPacket)
{
if (strlen($value) > 23) {
$this->throwException(
sprintf(
'Expected client id shorter than 24 bytes but got "%s".',
$value
),
$fromPacket
);
}
if ($value !== '' && !ctype_alnum($value)) {
$this->throwException(
sprintf(
'Expected a client id containing characters 0-9, a-z or A-Z but got "%s".',
$value
),
$fromPacket
);
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the SUBSCRIBE packet.
*/
class SubscribeRequestPacket extends BasePacket
{
use IdentifiablePacket;
/** @var string */
private $topic;
/** @var int */
private $qosLevel;
protected static $packetType = Packet::TYPE_SUBSCRIBE;
protected $packetFlags = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(2);
$this->assertRemainingPacketLength();
$this->identifier = $stream->readWord();
$this->topic = $stream->readString();
$this->qosLevel = $stream->readByte();
$this->assertValidQosLevel($this->qosLevel);
$this->assertValidString($this->topic);
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeWord($this->generateIdentifier());
$data->writeString($this->topic);
$data->writeByte($this->qosLevel);
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the topic.
*
* @return string
*/
public function getTopic()
{
return $this->topic;
}
/**
* Sets the topic.
*
* @param string $value
*
* @throws \InvalidArgumentException
*/
public function setTopic($value)
{
$this->assertValidString($value, false);
if ($value === '') {
throw new \InvalidArgumentException('The topic must not be empty.');
}
$this->topic = $value;
}
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel()
{
return $this->qosLevel;
}
/**
* Sets the quality of service level.
*
* @param int $value
*
* @throws \InvalidArgumentException
*/
public function setQosLevel($value)
{
$this->assertValidQosLevel($value, false);
$this->qosLevel = $value;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the SUBACK packet.
*/
class SubscribeResponsePacket extends BasePacket
{
use IdentifiablePacket;
private static $qosLevels = [
0 => ['Maximum QoS 0'],
1 => ['Maximum QoS 1'],
2 => ['Maximum QoS 2'],
128 => ['Failure'],
];
/** @var int[] */
private $returnCodes;
protected static $packetType = Packet::TYPE_SUBACK;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(0);
$this->assertRemainingPacketLength();
$this->identifier = $stream->readWord();
$returnCodeLength = $this->remainingPacketLength - 2;
for ($n = 0; $n < $returnCodeLength; ++$n) {
$returnCode = $stream->readByte();
$this->assertValidReturnCode($returnCode);
$this->returnCodes[] = $returnCode;
}
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeWord($this->generateIdentifier());
foreach ($this->returnCodes as $returnCode) {
$data->writeByte($returnCode);
}
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Indicates if the given return code is an error.
*
* @param int $returnCode
*
* @return bool
*/
public function isError($returnCode)
{
return $returnCode === 128;
}
/**
* Indicates if the given return code is an error.
*
* @param int $returnCode
*
* @return bool
*/
public function getReturnCodeName($returnCode)
{
if (isset(self::$qosLevels[$returnCode])) {
return self::$qosLevels[$returnCode][0];
}
return 'Unknown '.$returnCode;
}
/**
* Returns the return codes.
*
* @return int[]
*/
public function getReturnCodes()
{
return $this->returnCodes;
}
/**
* Sets the return codes.
*
* @param int[] $value
*
* @throws \InvalidArgumentException
*/
public function setReturnCodes(array $value)
{
foreach ($value as $returnCode) {
$this->assertValidReturnCode($returnCode, false);
}
$this->returnCodes = $value;
}
/**
* Asserts that a return code is valid.
*
* @param int $returnCode
* @param bool $fromPacket
*
* @throws MalformedPacketException
* @throws \InvalidArgumentException
*/
private function assertValidReturnCode($returnCode, $fromPacket = true)
{
if (!in_array($returnCode, [0, 1, 2, 128])) {
$this->throwException(
sprintf('Malformed return code %02x.', $returnCode),
$fromPacket
);
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\PacketStream;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the UNSUBSCRIBE packet.
*/
class UnsubscribeRequestPacket extends BasePacket
{
use IdentifiablePacket;
/** @var string */
private $topic;
protected static $packetType = Packet::TYPE_UNSUBSCRIBE;
protected $packetFlags = 2;
public function read(PacketStream $stream)
{
parent::read($stream);
$this->assertPacketFlags(2);
$this->assertRemainingPacketLength();
$originalPosition = $stream->getPosition();
do {
$this->identifier = $stream->readWord();
$this->topic = $stream->readString();
} while (($stream->getPosition() - $originalPosition) <= $this->remainingPacketLength);
}
public function write(PacketStream $stream)
{
$data = new PacketStream();
$data->writeWord($this->generateIdentifier());
$data->writeString($this->topic);
$this->remainingPacketLength = $data->length();
parent::write($stream);
$stream->write($data->getData());
}
/**
* Returns the topic.
*
* @return string
*/
public function getTopic()
{
return $this->topic;
}
/**
* Sets the topic.
*
* @param string $value
*/
public function setTopic($value)
{
$this->topic = $value;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace BinSoul\Net\Mqtt\Packet;
use BinSoul\Net\Mqtt\Packet;
/**
* Represents the UNSUBACK packet.
*/
class UnsubscribeResponsePacket extends IdentifierOnlyPacket
{
protected static $packetType = Packet::TYPE_UNSUBACK;
}

67
vendor/binsoul/net-mqtt/src/PacketFactory.php vendored Executable file
View File

@@ -0,0 +1,67 @@
<?php
namespace BinSoul\Net\Mqtt;
use BinSoul\Net\Mqtt\Exception\UnknownPacketTypeException;
use BinSoul\Net\Mqtt\Packet\ConnectRequestPacket;
use BinSoul\Net\Mqtt\Packet\ConnectResponsePacket;
use BinSoul\Net\Mqtt\Packet\DisconnectRequestPacket;
use BinSoul\Net\Mqtt\Packet\PingRequestPacket;
use BinSoul\Net\Mqtt\Packet\PingResponsePacket;
use BinSoul\Net\Mqtt\Packet\PublishRequestPacket;
use BinSoul\Net\Mqtt\Packet\PublishAckPacket;
use BinSoul\Net\Mqtt\Packet\PublishCompletePacket;
use BinSoul\Net\Mqtt\Packet\PublishReceivedPacket;
use BinSoul\Net\Mqtt\Packet\PublishReleasePacket;
use BinSoul\Net\Mqtt\Packet\SubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\SubscribeResponsePacket;
use BinSoul\Net\Mqtt\Packet\UnsubscribeRequestPacket;
use BinSoul\Net\Mqtt\Packet\UnsubscribeResponsePacket;
/**
* Builds instances of the {@see Packet} interface.
*/
class PacketFactory
{
/**
* Map of packet types to packet classes.
*
* @var string[]
*/
private static $mapping = [
Packet::TYPE_CONNECT => ConnectRequestPacket::class,
Packet::TYPE_CONNACK => ConnectResponsePacket::class,
Packet::TYPE_PUBLISH => PublishRequestPacket::class,
Packet::TYPE_PUBACK => PublishAckPacket::class,
Packet::TYPE_PUBREC => PublishReceivedPacket::class,
Packet::TYPE_PUBREL => PublishReleasePacket::class,
Packet::TYPE_PUBCOMP => PublishCompletePacket::class,
Packet::TYPE_SUBSCRIBE => SubscribeRequestPacket::class,
Packet::TYPE_SUBACK => SubscribeResponsePacket::class,
Packet::TYPE_UNSUBSCRIBE => UnsubscribeRequestPacket::class,
Packet::TYPE_UNSUBACK => UnsubscribeResponsePacket::class,
Packet::TYPE_PINGREQ => PingRequestPacket::class,
Packet::TYPE_PINGRESP => PingResponsePacket::class,
Packet::TYPE_DISCONNECT => DisconnectRequestPacket::class,
];
/**
* Builds a packet object for the given type.
*
* @param int $type
*
* @throws UnknownPacketTypeException
*
* @return Packet
*/
public function build($type)
{
if (!isset(self::$mapping[$type])) {
throw new UnknownPacketTypeException(sprintf('Unknown packet type %d.', $type));
}
$class = self::$mapping[$type];
return new $class();
}
}

216
vendor/binsoul/net-mqtt/src/PacketStream.php vendored Executable file
View File

@@ -0,0 +1,216 @@
<?php
namespace BinSoul\Net\Mqtt;
use BinSoul\Net\Mqtt\Exception\EndOfStreamException;
/**
* Provides methods to operate on a stream of bytes.
*/
class PacketStream
{
/** @var string */
private $data;
/** @var int */
private $position;
/**
* Constructs an instance of this class.
*
* @param string $data initial data of the stream
*/
public function __construct($data = '')
{
$this->data = $data;
$this->position = 0;
}
/**
* @return string
*/
public function __toString()
{
return $this->data;
}
/**
* Returns the desired number of bytes.
*
* @param int $count
*
* @throws EndOfStreamException
*
* @return string
*/
public function read($count)
{
$contentLength = strlen($this->data);
if ($this->position > $contentLength || $count > $contentLength - $this->position) {
throw new EndOfStreamException(
sprintf(
'End of stream reached when trying to read %d bytes. content length=%d, position=%d',
$count,
$contentLength,
$this->position
)
);
}
$chunk = substr($this->data, $this->position, $count);
if ($chunk === false) {
$chunk = '';
}
$readBytes = strlen($chunk);
$this->position += $readBytes;
return $chunk;
}
/**
* Returns a single byte.
*
* @return int
*/
public function readByte()
{
return ord($this->read(1));
}
/**
* Returns a single word.
*
* @return int
*/
public function readWord()
{
return ($this->readByte() << 8) + $this->readByte();
}
/**
* Returns a length prefixed string.
*
* @return string
*/
public function readString()
{
$length = $this->readWord();
return $this->read($length);
}
/**
* Appends the given value.
*
* @param string $value
*/
public function write($value)
{
$this->data .= $value;
}
/**
* Appends a single byte.
*
* @param int $value
*/
public function writeByte($value)
{
$this->write(chr($value));
}
/**
* Appends a single word.
*
* @param int $value
*/
public function writeWord($value)
{
$this->write(chr(($value & 0xFFFF) >> 8));
$this->write(chr($value & 0xFF));
}
/**
* Appends a length prefixed string.
*
* @param string $string
*/
public function writeString($string)
{
$this->writeWord(strlen($string));
$this->write($string);
}
/**
* Returns the length of the stream.
*
* @return int
*/
public function length()
{
return strlen($this->data);
}
/**
* Returns the number of bytes until the end of the stream.
*
* @return int
*/
public function getRemainingBytes()
{
return $this->length() - $this->position;
}
/**
* Returns the whole content of the stream.
*
* @return string
*/
public function getData()
{
return $this->data;
}
/**
* Changes the internal position of the stream relative to the current position.
*
* @param int $offset
*/
public function seek($offset)
{
$this->position += $offset;
}
/**
* Returns the internal position of the stream.
*
* @return int
*/
public function getPosition()
{
return $this->position;
}
/**
* Sets the internal position of the stream.
*
* @param int $value
*/
public function setPosition($value)
{
$this->position = $value;
}
/**
* Removes all bytes from the beginning to the current position.
*/
public function cut()
{
$this->data = substr($this->data, $this->position);
if ($this->data === false) {
$this->data = '';
}
$this->position = 0;
}
}

90
vendor/binsoul/net-mqtt/src/StreamParser.php vendored Executable file
View File

@@ -0,0 +1,90 @@
<?php
namespace BinSoul\Net\Mqtt;
use BinSoul\Net\Mqtt\Exception\EndOfStreamException;
use BinSoul\Net\Mqtt\Exception\MalformedPacketException;
use BinSoul\Net\Mqtt\Exception\UnknownPacketTypeException;
/**
* Provides methods to parse a stream of bytes into packets.
*/
class StreamParser
{
/** @var PacketStream */
private $buffer;
/** @var PacketFactory */
private $factory;
/** @var callable */
private $errorCallback;
/**
* Constructs an instance of this class.
*/
public function __construct()
{
$this->buffer = new PacketStream();
$this->factory = new PacketFactory();
}
/**
* Registers an error callback.
*
* @param callable $callback
*/
public function onError($callback)
{
$this->errorCallback = $callback;
}
/**
* Appends the given data to the internal buffer and parses it.
*
* @param string $data
*
* @return Packet[]
*/
public function push($data)
{
$this->buffer->write($data);
$result = [];
while ($this->buffer->getRemainingBytes() > 0) {
$type = $this->buffer->readByte() >> 4;
try {
$packet = $this->factory->build($type);
} catch (UnknownPacketTypeException $e) {
$this->handleError($e);
continue;
}
$this->buffer->seek(-1);
$position = $this->buffer->getPosition();
try {
$packet->read($this->buffer);
$result[] = $packet;
$this->buffer->cut();
} catch (EndOfStreamException $e) {
$this->buffer->setPosition($position);
break;
} catch (MalformedPacketException $e) {
$this->handleError($e);
}
}
return $result;
}
/**
* Executes the registered error callback.
*
* @param \Throwable $exception
*/
private function handleError($exception)
{
if ($this->errorCallback !== null) {
$callback = $this->errorCallback;
$callback($exception);
}
}
}

41
vendor/binsoul/net-mqtt/src/Subscription.php vendored Executable file
View File

@@ -0,0 +1,41 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Represents a subscription.
*/
interface Subscription
{
/**
* Returns the topic filter.
*
* @return string
*/
public function getFilter();
/**
* Returns the quality of service level.
*
* @return int
*/
public function getQosLevel();
/**
* Returns a new subscription with the given topic filter.
*
* @param string $filter
*
* @return self
*/
public function withFilter($filter);
/**
* Returns a new subscription with the given quality of service level.
*
* @param int $level
*
* @return self
*/
public function withQosLevel($level);
}

53
vendor/binsoul/net-mqtt/src/TopicMatcher.php vendored Executable file
View File

@@ -0,0 +1,53 @@
<?php
namespace BinSoul\Net\Mqtt;
/**
* Matches a topic filter with an actual topic.
*
* @author Alin Eugen Deac <ade@vestergaardcompany.com>
*/
class TopicMatcher
{
/**
* Check if the given topic matches the filter.
*
* @param string $filter e.g. A/B/+, A/B/#
* @param string $topic e.g. A/B/C, A/B/foo/bar/baz
*
* @return bool true if topic matches the pattern
*/
public function matches($filter, $topic)
{
// Created by Steffen (https://github.com/kernelguy)
$tokens = explode('/', $filter);
$parts = [];
for ($i = 0, $count = count($tokens); $i < $count; ++$i) {
$token = $tokens[$i];
switch ($token) {
case '+':
$parts[] = '[^/#\+]*';
break;
case '#':
if ($i === 0) {
$parts[] = '[^\+\$]*';
} else {
$parts[] = '[^\+]*';
}
break;
default:
$parts[] = str_replace('+', '\+', $token);
break;
}
}
$regex = implode('/', $parts);
$regex = str_replace('$', '\$', $regex);
$regex = ';^'.$regex.'$;';
return preg_match($regex, $topic) === 1;
}
}

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));
}
}

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

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

27
vendor/clue/socks-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

338
vendor/clue/socks-react/CHANGELOG.md vendored Executable file
View File

@@ -0,0 +1,338 @@
# Changelog
## 0.8.7 (2017-12-17)
* Feature: Support SOCKS over TLS (`sockss://` URI scheme)
(#70 and #71 by @clue)
```php
// new: now supports SOCKS over TLS
$client = new Client('socks5s://localhost', $connector);
```
* Feature: Support communication over Unix domain sockets (UDS)
(#69 by @clue)
```php
// new: now supports SOCKS over Unix domain sockets (UDS)
$client = new Client('socks5+unix:///tmp/proxy.sock', $connector);
```
* Improve test suite by adding forward compatibility with PHPUnit 6
(#68 by @clue)
## 0.8.6 (2017-09-17)
* Feature: Forward compatibility with Evenement v3.0
(#67 by @WyriHaximus)
## 0.8.5 (2017-09-01)
* Feature: Use socket error codes for connection rejections
(#63 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;
});
```
* Feature: Report matching SOCKS5 error codes for server side connection errors
(#62 by @clue)
* Fix: Fix SOCKS5 client receiving destination hostnames and
fix IPv6 addresses as hostnames for TLS certificates
(#64 and #65 by @clue)
* 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
(#61 and #66 by @clue)
## 0.8.4 (2017-07-27)
* Feature: Server now passes client source address to Connector
(#60 by @clue)
## 0.8.3 (2017-07-18)
* Feature: Pass full remote URI as parameter to authentication callback
(#58 by @clue)
```php
// new third parameter passed to authentication callback
$server->setAuth(function ($user, $pass, $remote) {
$ip = parse_url($remote, PHP_URL_HOST);
return ($ip === '127.0.0.1');
});
```
* Fix: Fix connecting to IPv6 address via SOCKS5 server and validate target
URI so hostname can not contain excessive URI components
(#59 by @clue)
* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors
(#57 by @clue)
## 0.8.2 (2017-05-09)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#56 by @clue)
## 0.8.1 (2017-04-21)
* Update examples to use URIs with default port 1080 and accept proxy URI arguments
(#54 by @clue)
* Remove now unneeded dependency on `react/stream`
(#55 by @clue)
## 0.8.0 (2017-04-18)
* Feature: Merge `Server` class from clue/socks-server
(#52 by @clue)
```php
$socket = new React\Socket\Server(1080, $loop);
$server = new Clue\React\Socks\Server($loop, $socket);
```
> Upgrading from [clue/socks-server](https://github.com/clue/php-socks-server)?
The classes have been moved as-is, so you can simply start using the new
class name `Clue\React\Socks\Server` with no other changes required.
## 0.7.0 (2017-04-14)
* Feature / BC break: Replace depreacted SocketClient with Socket v0.7 and
use `connect($uri)` instead of `create($host, $port)`
(#51 by @clue)
```php
// old
$connector = new React\SocketClient\TcpConnector($loop);
$client = new Client(1080, $connector);
$client->create('google.com', 80)->then(function (Stream $conn) {
$conn->write("…");
});
// new
$connector = new React\Socket\TcpConnector($loop);
$client = new Client(1080, $connector);
$client->connect('google.com:80')->then(function (ConnectionInterface $conn) {
$conn->write("…");
});
```
* Improve test suite by adding PHPUnit to require-dev
(#50 by @clue)
## 0.6.0 (2016-11-29)
* Feature / BC break: Pass connector into `Client` instead of loop, remove unneeded deps
(#49 by @clue)
```php
// old (connector is create implicitly)
$client = new Client('127.0.0.1', $loop);
// old (connector can optionally be passed)
$client = new Client('127.0.0.1', $loop, $connector);
// new (connector is now mandatory)
$connector = new React\SocketClient\TcpConnector($loop);
$client = new Client('127.0.0.1', $connector);
```
* Feature / BC break: `Client` now implements `ConnectorInterface`, remove `Connector` adapter
(#47 by @clue)
```php
// old (explicit connector functions as an adapter)
$connector = $client->createConnector();
$promise = $connector->create('google.com', 80);
// new (client can be used as connector right away)
$promise = $client->create('google.com', 80);
```
* Feature / BC break: Remove `createSecureConnector()`, use `SecureConnector` instead
(#47 by @clue)
```php
// old (tight coupling and hidden dependency)
$tls = $client->createSecureConnector();
$promise = $tls->create('google.com', 443);
// new (more explicit, loose coupling)
$tls = new React\SocketClient\SecureConnector($client, $loop);
$promise = $tls->create('google.com', 443);
```
* Feature / BC break: Remove `setResolveLocal()` and local DNS resolution and default to remote DNS resolution, use `DnsConnector` instead
(#44 by @clue)
```php
// old (implicitly defaults to true, can be disabled)
$client->setResolveLocal(false);
$tcp = $client->createConnector();
$promise = $tcp->create('google.com', 80);
// new (always disabled, can be re-enabled like this)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->createCached('8.8.8.8', $loop);
$tcp = new React\SocketClient\DnsConnector($client, $resolver);
$promise = $tcp->create('google.com', 80);
```
* Feature / BC break: Remove `setTimeout()`, use `TimeoutConnector` instead
(#45 by @clue)
```php
// old (timeout only applies to TCP/IP connection)
$client = new Client('127.0.0.1', …);
$client->setTimeout(3.0);
$tcp = $client->createConnector();
$promise = $tcp->create('google.com', 80);
// new (timeout can be added to any layer)
$client = new Client('127.0.0.1', …);
$tcp = new React\SocketClient\TimeoutConnector($client, 3.0, $loop);
$promise = $tcp->create('google.com', 80);
```
* Feature / BC break: Remove `setProtocolVersion()` and `setAuth()` mutators, only support SOCKS URI for protocol version and authentication (immutable API)
(#46 by @clue)
```php
// old (state can be mutated after instantiation)
$client = new Client('127.0.0.1', …);
$client->setProtocolVersion('5');
$client->setAuth('user', 'pass');
// new (immutable after construction, already supported as of v0.5.2 - now mandatory)
$client = new Client('socks5://user:pass@127.0.0.1', …);
```
## 0.5.2 (2016-11-25)
* Feature: Apply protocol version and username/password auth from SOCKS URI
(#43 by @clue)
```php
// explicitly use SOCKS5
$client = new Client('socks5://127.0.0.1', $loop);
// use authentication (automatically SOCKS5)
$client = new Client('user:pass@127.0.0.1', $loop);
```
* More explicit client examples, including proxy chaining
(#42 by @clue)
## 0.5.1 (2016-11-21)
* Feature: Support Promise cancellation
(#39 by @clue)
```php
$promise = $connector->create($host, $port);
$promise->cancel();
```
* Feature: Timeout now cancels pending connection attempt
(#39, #22 by @clue)
## 0.5.0 (2016-11-07)
* Remove / BC break: Split off Server to clue/socks-server
(#35 by @clue)
> Upgrading? Check [clue/socks-server](https://github.com/clue/php-socks-server) for details.
* Improve documentation and project structure
## 0.4.0 (2016-03-19)
* Feature: Support proper SSL/TLS connections with additional SSL context options
(#31, #33 by @clue)
* Documentation for advanced Connector setups (bindto, multihop)
(#32 by @clue)
## 0.3.0 (2015-06-20)
* BC break / Feature: Client ctor now accepts a SOCKS server URI
([#24](https://github.com/clue/php-socks-react/pull/24))
```php
// old
$client = new Client($loop, 'localhost', 9050);
// new
$client = new Client('localhost:9050', $loop);
```
* Feature: Automatically assume default SOCKS port (1080) if not given explicitly
([#26](https://github.com/clue/php-socks-react/pull/26))
* Improve documentation and test suite
## 0.2.1 (2014-11-13)
* Support React PHP v0.4 (while preserving BC with React PHP v0.3)
([#16](https://github.com/clue/php-socks-react/pull/16))
* Improve examples and add first class support for HHVM
([#15](https://github.com/clue/php-socks-react/pull/15) and [#17](https://github.com/clue/php-socks-react/pull/17))
## 0.2.0 (2014-09-27)
* BC break / Feature: Simplify constructors by making parameters optional.
([#10](https://github.com/clue/php-socks-react/pull/10))
The `Factory` has been removed, you can now create instances of the `Client`
and `Server` yourself:
```php
// old
$factory = new Factory($loop, $dns);
$client = $factory->createClient('localhost', 9050);
$server = $factory->createSever($socket);
// new
$client = new Client($loop, 'localhost', 9050);
$server = new Server($loop, $socket);
```
* BC break: Remove HTTP support and link to [clue/buzz-react](https://github.com/clue/php-buzz-react) instead.
([#9](https://github.com/clue/php-socks-react/pull/9))
HTTP operates on a different layer than this low-level SOCKS library.
Removing this reduces the footprint of this library.
> Upgrading? Check the [README](https://github.com/clue/php-socks-react#http-requests) for details.
* Fix: Refactored to support other, faster loops (libev/libevent)
([#12](https://github.com/clue/php-socks-react/pull/12))
* Explicitly list dependencies, clean up examples and extend test suite significantly
## 0.1.0 (2014-05-19)
* First stable release
* Async SOCKS `Client` and `Server` implementation
* Project was originally part of [clue/socks](https://github.com/clue/php-socks)
and was split off from its latest releave v0.4.0
([#1](https://github.com/clue/reactphp-socks/issues/1))
> Upgrading from clue/socks v0.4.0? Use namespace `Clue\React\Socks` instead of `Socks` and you're ready to go!
## 0.0.0 (2011-04-26)
* Initial concept, originally tracked as part of
[clue/socks](https://github.com/clue/php-socks)

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

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2011 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.

1017
vendor/clue/socks-react/README.md vendored Executable file

File diff suppressed because it is too large Load Diff

28
vendor/clue/socks-react/composer.json vendored Executable file
View File

@@ -0,0 +1,28 @@
{
"name": "clue/socks-react",
"description": "Async SOCKS4, SOCKS4a and SOCKS5 proxy client and server implementation, built on top of ReactPHP",
"keywords": ["socks client", "socks server", "proxy", "tcp tunnel", "socks protocol", "async", "ReactPHP"],
"homepage": "https://github.com/clue/php-socks-react",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"autoload": {
"psr-4": {"Clue\\React\\Socks\\": "src/"}
},
"require": {
"php": ">=5.3",
"react/socket": "^1.0 || ^0.8.6",
"react/promise": "^2.1 || ^1.2",
"evenement/evenement": "~3.0|~1.0|~2.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0 || ^5.7 || ^4.8.35",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"clue/connection-manager-extra": "^1.0 || ^0.7",
"clue/block-react": "^1.1"
}
}

30
vendor/clue/socks-react/examples/01-http.php vendored Executable file
View File

@@ -0,0 +1,30 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$proxy = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$loop = React\EventLoop\Factory::create();
$client = new Client($proxy, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $client,
'timeout' => 3.0,
'dns' => false
));
echo 'Demo SOCKS client connecting to SOCKS server ' . $proxy . PHP_EOL;
$connector->connect('tcp://www.google.com:80')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

30
vendor/clue/socks-react/examples/02-https.php vendored Executable file
View File

@@ -0,0 +1,30 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$proxy = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$loop = React\EventLoop\Factory::create();
$client = new Client($proxy, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $client,
'timeout' => 3.0,
'dns' => false
));
echo 'Demo SOCKS client connecting to SOCKS server ' . $proxy . PHP_EOL;
$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,46 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
if (!isset($argv[1])) {
echo 'No arguments given! Run with <proxy1> [<proxyN>...]' . PHP_EOL;
echo 'You can add 1..n proxies in the path' . PHP_EOL;
exit(1);
}
$path = array_slice($argv, 1);
// Alternatively, you can also hard-code this value like this:
//$path = array('127.0.0.1:9051', '127.0.0.1:9052', '127.0.0.1:9053');
$loop = React\EventLoop\Factory::create();
// set next SOCKS server chain via p1 -> p2 -> p3 -> destination
$connector = new Connector($loop);
foreach ($path as $proxy) {
$connector = new Client($proxy, $connector);
}
// please note how the client uses p3 (not p1!), which in turn then uses the complete chain
// this creates a TCP/IP connection to p1, which then connects to p2, then to p3, which then connects to the target
$connector = new Connector($loop, array(
'tcp' => $connector,
'timeout' => 3.0,
'dns' => false
));
echo 'Demo SOCKS client connecting to SOCKS proxy server chain ' . implode(' -> ', $path) . PHP_EOL;
$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,31 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$proxy = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$loop = React\EventLoop\Factory::create();
// set up DNS server to use (Google's public DNS)
$client = new Client($proxy, new Connector($loop));
$connector = new Connector($loop, array(
'tcp' => $client,
'timeout' => 3.0,
'dns' => '8.8.8.8'
));
echo 'Demo SOCKS client connecting to SOCKS server ' . $proxy . PHP_EOL;
$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,19 @@
<?php
use Clue\React\Socks\Server;
use React\Socket\Server as Socket;
require __DIR__ . '/../vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
// listen on 127.0.0.1:1080 or first argument
$listen = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$socket = new Socket($listen, $loop);
// start a new server listening for incoming connection on the given socket
$server = new Server($loop, $socket);
echo 'SOCKS server listening on ' . $socket->getAddress() . PHP_EOL;
$loop->run();

View File

@@ -0,0 +1,24 @@
<?php
use Clue\React\Socks\Server;
use React\Socket\Server as Socket;
require __DIR__ . '/../vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
// listen on 127.0.0.1:1080 or first argument
$listen = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$socket = new Socket($listen, $loop);
// start a new server listening for incoming connection on the given socket
// require authentication and hence make this a SOCKS5-only server
$server = new Server($loop, $socket);
$server->setAuthArray(array(
'tom' => 'god',
'user' => 'p@ssw0rd'
));
echo 'SOCKS5 server requiring authentication listening on ' . $socket->getAddress() . PHP_EOL;
$loop->run();

View File

@@ -0,0 +1,40 @@
<?php
// A SOCKS server that rejects connections to some domains (blacklist / filtering)
use React\EventLoop\Factory as LoopFactory;
use ConnectionManager\Extra\Multiple\ConnectionManagerSelective;
use React\Socket\Server as Socket;
use Clue\React\Socks\Server;
use ConnectionManager\Extra\ConnectionManagerReject;
use React\Socket\Connector;
require __DIR__ . '/../vendor/autoload.php';
$loop = LoopFactory::create();
// create a connector that rejects the connection
$reject = new ConnectionManagerReject();
// create an actual connector that establishes real connections
$permit = new Connector($loop);
// this connector selectively picks one of the the attached connectors depending on the target address
// reject youtube.com and unencrypted HTTP for google.com
// default connctor: permit everything
$connector = new ConnectionManagerSelective(array(
'*.youtube.com' => $reject,
'www.google.com:80' => $reject,
'*' => $permit
));
// listen on 127.0.0.1:1080 or first argument
$listen = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$socket = new Socket($listen, $loop);
// start the actual socks server on the given server socket and using our connection manager for outgoing connections
$server = new Server($loop, $socket, $connector);
echo 'SOCKS server listening on ' . $socket->getAddress() . PHP_EOL;
$loop->run();

View File

@@ -0,0 +1,42 @@
<?php
// A SOCKS server that forwards (proxy chaining) to other SOCKS servers
use Clue\React\Socks\Client;
use Clue\React\Socks\Server;
use React\Socket\Server as Socket;
use React\Socket\Connector;
require __DIR__ . '/../vendor/autoload.php';
if (!isset($argv[2])) {
echo 'No arguments given! Run with <listen> <proxy1> [<proxyN>...]' . PHP_EOL;
echo 'You can add 1..n proxies in the path' . PHP_EOL;
exit(1);
}
$listen = $argv[1];
$path = array_slice($argv, 2);
// Alternatively, you can also hard-code these values like this:
//$listen = '127.0.0.1:9050';
//$path = array('127.0.0.1:9051', '127.0.0.1:9052', '127.0.0.1:9053');
$loop = React\EventLoop\Factory::create();
// set next SOCKS server chain -> p1 -> p2 -> p3 -> destination
$connector = new Connector($loop);
foreach ($path as $proxy) {
$connector = new Client($proxy, $connector);
}
// listen on 127.0.0.1:1080 or first argument
$socket = new Socket($listen, $loop);
// start a new server which forwards all connections to the other SOCKS server
$server = new Server($loop, $socket, $connector);
echo 'SOCKS server listening on ' . $socket->getAddress() . PHP_EOL;
echo 'Forwarding via: ' . implode(' -> ', $path) . PHP_EOL;
$loop->run();

View File

@@ -0,0 +1,46 @@
<?php
// A SOCKS server that randomly forwards (proxy chaining) to a pool of SOCKS servers
use React\EventLoop\Factory as LoopFactory;
use ConnectionManager\Extra\Multiple\ConnectionManagerRandom;
use React\Socket\Server as Socket;
use Clue\React\Socks\Server;
use Clue\React\Socks\Client;
use React\Socket\Connector;
require __DIR__ . '/../vendor/autoload.php';
if (!isset($argv[3])) {
echo 'No arguments given! Run with <listen> <proxy1> <proxyN>...' . PHP_EOL;
echo 'You can add 2..n proxies in the pool' . PHP_EOL;
exit(1);
}
$listen = $argv[1];
$pool = array_slice($argv, 2);
// Alternatively, you can also hard-code these values like this:
//$listen = '127.0.0.1:9050';
//$pool = array('127.0.0.1:9051', '127.0.0.1:9052', '127.0.0.1:9053');
$loop = LoopFactory::create();
// forward to socks server listening on 127.0.0.1:9051-9053
// this connector randomly picks one of the the attached connectors from the pool
$connector = new Connector($loop);
$clients = array();
foreach ($pool as $proxy) {
$clients []= new Client($proxy, $connector);
}
$connector = new ConnectionManagerRandom($clients);
$socket = new Socket($listen, $loop);
// start the actual socks server on the given server socket and using our connection manager for outgoing connections
$server = new Server($loop, $socket, $connector);
echo 'SOCKS server listening on ' . $socket->getAddress() . PHP_EOL;
echo 'Randomly picking from: ' . implode(', ', $pool) . PHP_EOL;
$loop->run();

View File

@@ -0,0 +1,21 @@
<?php
use Clue\React\Socks\Server;
use React\Socket\Server as Socket;
require __DIR__ . '/../vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
// listen on tls://127.0.0.1:1080 or first argument
$listen = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$socket = new Socket('tls://' . $listen, $loop, array('tls' => array(
'local_cert' => __DIR__ . '/localhost.pem',
)));
// start a new server listening for incoming connection on the given socket
$server = new Server($loop, $socket);
echo 'SOCKS over TLS server listening on ' . str_replace('tls:', 'sockss:', $socket->getAddress()) . PHP_EOL;
$loop->run();

View File

@@ -0,0 +1,33 @@
<?php
use Clue\React\Socks\Client;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/../vendor/autoload.php';
$proxy = isset($argv[1]) ? $argv[1] : '127.0.0.1:1080';
$loop = React\EventLoop\Factory::create();
$client = new Client('sockss://' . $proxy, new Connector($loop, array('tls' => array(
'verify_peer' => false,
'verify_peer_name' => false
))));
$connector = new Connector($loop, array(
'tcp' => $client,
'timeout' => 3.0,
'dns' => false
));
echo 'Demo SOCKS over TLS client connecting to secure SOCKS server ' . $proxy . PHP_EOL;
$connector->connect('tcp://www.google.com:80')->then(function (ConnectionInterface $stream) {
echo 'connected' . PHP_EOL;
$stream->write("GET / HTTP/1.0\r\n\r\n");
$stream->on('data', function ($data) {
echo $data;
});
}, 'printf');
$loop->run();

View File

@@ -0,0 +1,49 @@
-----BEGIN CERTIFICATE-----
MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu
MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK
DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQ1OTA2WhcNMjYx
MjI4MTQ1OTA2WjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw
EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0
eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8SZWNS+Ktg0Py
W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN
2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9
zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0
UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4
wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY
YCUE54G/AgMBAAGjUDBOMB0GA1UdDgQWBBQ2GRz3QsQzdXaTMnPVCKfpigA10DAf
BgNVHSMEGDAWgBQ2GRz3QsQzdXaTMnPVCKfpigA10DAMBgNVHRMEBTADAQH/MA0G
CSqGSIb3DQEBBQUAA4IBAQA77iZ4KrpPY18Ezjt0mngYAuAxunKddXYdLZ2khywN
0uI/VzYnkFVtrsC7y2jLHSxlmE2/viPPGZDUplENV2acN6JNW+tlt7/bsrQHDQw3
7VCF27EWiDxHsaghhLkqC+kcop5YR5c0oDQTdEWEKSbow2zayUXDYbRRs76SClTe
824Yul+Ts8Mka+AX2PXDg47iZ84fJRN/nKavcJUTJ2iS1uYw0GNnFMge/uwsfMR3
V47qN0X5emky8fcq99FlMCbcy0gHAeSWAjClgr2dd2i0LDatUbj7YmdmFcskOgII
IwGfvuWR2yPevYGAE0QgFeLHniN3RW8zmpnX/XtrJ4a7
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8SZWNS+Ktg0Py
W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN
2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9
zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0
UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4
wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY
YCUE54G/AgMBAAECggEBAKiO/3FE1CMddkCLZVtUp8ShqJgRokx9WI5ecwFApAkV
ZHsjqDQQYRNmxhDUX/w0tOzLGyhde2xjJyZG29YviKsbHwu6zYwbeOzy/mkGOaK/
g6DmmMmRs9Z6juifoQCu4GIFZ6il2adIL2vF7OeJh+eKudQj/7NFRSB7mXzNrQWK
tZY3eux5zXWmio7pgZrx1HFZQiiL9NVLwT9J7oBnaoO3fREiu5J2xBpljG9Cr0j1
LLiVLhukWJYRlHDtGt1CzI9w8iKo44PCRzpKyxpbsOrQxeSyEWUYQRv9VHA59LC7
tVAJTbnTX1BNHkGZkOkoOpoZLwBaM2XbbDtcOGCAZMECgYEA+mTURFQ85/pxawvk
9ndqZ+5He1u/bMLYIJDp0hdB/vgD+vw3gb2UyRwp0I6Wc6Si4FEEnbY7L0pzWsiR
43CpLs+cyLfnD9NycuIasxs5fKb/1s1nGTkRAp7x9x/ZTtEf8v4YTmmMXFHzdo7V
pv+czO89ppEDkxEtMf/b5SifhO8CgYEAwIDIUvXLduGhL+RPDwjc2SKdydXGV6om
OEdt/V8oS801Z7k8l3gHXFm7zL/MpHmh9cag+F9dHK42kw2RSjDGsBlXXiAO1Z0I
2A34OdPw/kow8fmIKWTMu3+28Kca+3RmUqeyaq0vazQ/bWMO9px+Ud3YfLo1Tn5I
li0MecAx8DECgYEAvsLceKYYtL83c09fg2oc1ctSCCgw4WJcGAtvJ9DyRZacKbXH
b/+H/+OF8879zmKqd+0hcCnqUzAMTCisBLPLIM+o6b45ufPkqKObpcJi/JWaKgLY
vf2c+Psw6o4IF6T5Cz4MNIjzF06UBknxecYZpoPJ20F1kLCwVvxPgfl99l8CgYAb
XfOcv67WTstgiJ+oroTfJamy+P5ClkDqvVTosW+EHz9ZaJ8xlXHOcj9do2LPey9I
Rp250azmF+pQS5x9JKQKgv/FtN8HBVUtigbhCb14GUoODICMCfWFLmnumoMefnTR
iV+3BLn6Dqp5vZxx+NuIffZ5/Or5JsDhALSGVomC8QKBgAi3Z/dNQrDHfkXMNn/L
+EAoLuAbFgLs76r9VGgNaRQ/q5gex2bZEGoBj4Sxvs95NUIcfD9wKT7FF8HdxARv
y3o6Bfc8Xp9So9SlFXrje+gkdEJ0rQR67d+XBuJZh86bXJHVrMwpoNL+ahLGdVSe
81oh1uCH1YPLM29hPyaohxL8
-----END PRIVATE KEY-----

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

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

382
vendor/clue/socks-react/src/Client.php vendored Executable file
View File

@@ -0,0 +1,382 @@
<?php
namespace Clue\React\Socks;
use React\Promise;
use React\Promise\PromiseInterface;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface;
use React\Socket\ConnectorInterface;
use React\Socket\FixedUriConnector;
use \Exception;
use \InvalidArgumentException;
use RuntimeException;
class Client implements ConnectorInterface
{
/**
*
* @var ConnectorInterface
*/
private $connector;
private $socksUri;
private $protocolVersion = null;
private $auth = null;
public function __construct($socksUri, ConnectorInterface $connector)
{
// support `sockss://` scheme for SOCKS over TLS
// support `socks+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^(socks(?:5|4|4a)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) {
// rewrite URI to parse SOCKS scheme, authentication and dummy host
$socksUri = $match[1] . '://' . $match[3] . 'localhost';
// connector uses appropriate transport scheme and explicit host given
$connector = new FixedUriConnector(
($match[2] === 's' ? 'tls://' : 'unix://') . $match[4],
$connector
);
}
// assume default scheme if none is given
if (strpos($socksUri, '://') === false) {
$socksUri = 'socks://' . $socksUri;
}
// parse URI into individual parts
$parts = parse_url($socksUri);
if (!$parts || !isset($parts['scheme'], $parts['host'])) {
throw new \InvalidArgumentException('Invalid SOCKS server URI "' . $socksUri . '"');
}
// assume default port
if (!isset($parts['port'])) {
$parts['port'] = 1080;
}
// user or password in URI => SOCKS5 authentication
if (isset($parts['user']) || isset($parts['pass'])) {
if ($parts['scheme'] === 'socks') {
// default to using SOCKS5 if not given explicitly
$parts['scheme'] = 'socks5';
} elseif ($parts['scheme'] !== 'socks5') {
// fail if any other protocol version given explicitly
throw new InvalidArgumentException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication');
}
$parts += array('user' => '', 'pass' => '');
$this->setAuth(rawurldecode($parts['user']), rawurldecode($parts['pass']));
}
// check for valid protocol version from URI scheme
$this->setProtocolVersionFromScheme($parts['scheme']);
$this->socksUri = $parts['host'] . ':' . $parts['port'];
$this->connector = $connector;
}
private function setProtocolVersionFromScheme($scheme)
{
if ($scheme === 'socks' || $scheme === 'socks4a') {
$this->protocolVersion = '4a';
} elseif ($scheme === 'socks5') {
$this->protocolVersion = '5';
} elseif ($scheme === 'socks4') {
$this->protocolVersion = '4';
} else {
throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"');
}
}
/**
* set login data for username/password authentication method (RFC1929)
*
* @param string $username
* @param string $password
* @link http://tools.ietf.org/html/rfc1929
*/
private function setAuth($username, $password)
{
if (strlen($username) > 255 || strlen($password) > 255) {
throw new InvalidArgumentException('Both username and password MUST NOT exceed a length of 255 bytes each');
}
$this->auth = pack('C2', 0x01, strlen($username)) . $username . pack('C', strlen($password)) . $password;
}
/**
* Establish a TCP/IP connection to the given target URI through the SOCKS server
*
* Many higher-level networking protocols build on top of TCP. It you're dealing
* with one such client implementation, it probably uses/accepts an instance
* implementing React's `ConnectorInterface` (and usually its default `Connector`
* instance). In this case you can also pass this `Connector` instance instead
* to make this client implementation SOCKS-aware. That's it.
*
* @param string $uri
* @return PromiseInterface Promise<ConnectionInterface,Exception>
*/
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'];
if ($this->protocolVersion === '4' && false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return Promise\reject(new InvalidArgumentException('Requires an IPv4 address for SOCKS4'));
}
if (strlen($host) > 255 || $port > 65535 || $port < 0 || (string)$port !== (string)(int)$port) {
return Promise\reject(new InvalidArgumentException('Invalid target specified'));
}
// construct URI to SOCKS server to connect to
$socksUri = $this->socksUri;
// append path from URI if given
if (isset($parts['path'])) {
$socksUri .= $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'] = $host;
}
// append query string
$socksUri .= '?' . http_build_query($args, '', '&');
// append fragment from URI if given
if (isset($parts['fragment'])) {
$socksUri .= '#' . $parts['fragment'];
}
$that = $this;
// start TCP/IP connection to SOCKS server and then
// handle SOCKS protocol once connection is ready
// resolve plain connection once SOCKS protocol is completed
return $this->connector->connect($socksUri)->then(
function (ConnectionInterface $stream) use ($that, $host, $port) {
return $that->handleConnectedSocks($stream, $host, $port);
},
function (Exception $e) {
throw new RuntimeException('Unable to connect to proxy (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $e);
}
);
}
/**
* Internal helper used to handle the communication with the SOCKS server
*
* @param ConnectionInterface $stream
* @param string $host
* @param int $port
* @return Promise Promise<ConnectionInterface, Exception>
* @internal
*/
public function handleConnectedSocks(ConnectionInterface $stream, $host, $port)
{
$deferred = new Deferred(function ($_, $reject) {
$reject(new RuntimeException('Connection canceled while establishing SOCKS session (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103));
});
$reader = new StreamReader();
$stream->on('data', array($reader, 'write'));
$stream->on('error', $onError = 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', $onClose = function () use ($deferred) {
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
});
if ($this->protocolVersion === '5') {
$promise = $this->handleSocks5($stream, $host, $port, $reader);
} else {
$promise = $this->handleSocks4($stream, $host, $port, $reader);
}
$promise->then(function () use ($deferred, $stream) {
$deferred->resolve($stream);
}, function (Exception $error) use ($deferred) {
// pass custom RuntimeException through as-is, otherwise wrap in protocol error
if (!$error instanceof RuntimeException) {
$error = new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $error);
}
$deferred->reject($error);
});
return $deferred->promise()->then(
function (ConnectionInterface $stream) use ($reader, $onError, $onClose) {
$stream->removeListener('data', array($reader, 'write'));
$stream->removeListener('error', $onError);
$stream->removeListener('close', $onClose);
return $stream;
},
function ($error) use ($stream, $onClose) {
$stream->removeListener('close', $onClose);
$stream->close();
throw $error;
}
);
}
private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader)
{
// do not resolve hostname. only try to convert to IP
$ip = ip2long($host);
// send IP or (0.0.0.1) if invalid
$data = pack('C2nNC', 0x04, 0x01, $port, $ip === false ? 1 : $ip, 0x00);
if ($ip === false) {
// host is not a valid IP => send along hostname (SOCKS4a)
$data .= $host . pack('C', 0x00);
}
$stream->write($data);
return $reader->readBinary(array(
'null' => 'C',
'status' => 'C',
'port' => 'n',
'ip' => 'N'
))->then(function ($data) {
if ($data['null'] !== 0x00) {
throw new Exception('Invalid SOCKS response');
}
if ($data['status'] !== 0x5a) {
throw new RuntimeException('Proxy refused connection with SOCKS error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
}
});
}
private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader)
{
// protocol version 5
$data = pack('C', 0x05);
$auth = $this->auth;
if ($auth === null) {
// one method, no authentication
$data .= pack('C2', 0x01, 0x00);
} else {
// two methods, username/password and no authentication
$data .= pack('C3', 0x02, 0x02, 0x00);
}
$stream->write($data);
$that = $this;
return $reader->readBinary(array(
'version' => 'C',
'method' => 'C'
))->then(function ($data) use ($auth, $stream, $reader) {
if ($data['version'] !== 0x05) {
throw new Exception('Version/Protocol mismatch');
}
if ($data['method'] === 0x02 && $auth !== null) {
// username/password authentication requested and provided
$stream->write($auth);
return $reader->readBinary(array(
'version' => 'C',
'status' => 'C'
))->then(function ($data) {
if ($data['version'] !== 0x01 || $data['status'] !== 0x00) {
throw new RuntimeException('Username/Password authentication failed (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
}
});
} else if ($data['method'] !== 0x00) {
// any other method than "no authentication"
throw new RuntimeException('No acceptable authentication method found (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
}
})->then(function () use ($stream, $reader, $host, $port) {
// do not resolve hostname. only try to convert to (binary/packed) IP
$ip = @inet_pton($host);
$data = pack('C3', 0x05, 0x01, 0x00);
if ($ip === false) {
// not an IP, send as hostname
$data .= pack('C2', 0x03, strlen($host)) . $host;
} else {
// send as IPv4 / IPv6
$data .= pack('C', (strpos($host, ':') === false) ? 0x01 : 0x04) . $ip;
}
$data .= pack('n', $port);
$stream->write($data);
return $reader->readBinary(array(
'version' => 'C',
'status' => 'C',
'null' => 'C',
'type' => 'C'
));
})->then(function ($data) use ($reader) {
if ($data['version'] !== 0x05 || $data['null'] !== 0x00) {
throw new Exception('Invalid SOCKS response');
}
if ($data['status'] !== 0x00) {
// map limited list of SOCKS error codes to common socket error conditions
// @link https://tools.ietf.org/html/rfc1928#section-6
if ($data['status'] === Server::ERROR_GENERAL) {
throw new RuntimeException('SOCKS server reported a general server failure (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
} elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) {
throw new RuntimeException('SOCKS server reported connection is not allowed by ruleset (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
} elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) {
throw new RuntimeException('SOCKS server reported network unreachable (ENETUNREACH)', defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101);
} elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) {
throw new RuntimeException('SOCKS server reported host unreachable (EHOSTUNREACH)', defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113);
} elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) {
throw new RuntimeException('SOCKS server reported connection refused (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
} elseif ($data['status'] === Server::ERROR_TTL) {
throw new RuntimeException('SOCKS server reported TTL/timeout expired (ETIMEDOUT)', defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
} elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) {
throw new RuntimeException('SOCKS server does not support the CONNECT command (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
} elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) {
throw new RuntimeException('SOCKS server does not support this address type (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
}
throw new RuntimeException('SOCKS server reported an unassigned error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
}
if ($data['type'] === 0x01) {
// IPv4 address => skip IP and port
return $reader->readLength(6);
} elseif ($data['type'] === 0x03) {
// domain name => read domain name length
return $reader->readBinary(array(
'length' => 'C'
))->then(function ($data) use ($reader) {
// skip domain name and port
return $reader->readLength($data['length'] + 2);
});
} elseif ($data['type'] === 0x04) {
// IPv6 address => skip IP and port
return $reader->readLength(18);
} else {
throw new Exception('Invalid SOCKS reponse: Invalid address type');
}
});
}
}

422
vendor/clue/socks-react/src/Server.php vendored Executable file
View File

@@ -0,0 +1,422 @@
<?php
namespace Clue\React\Socks;
use Evenement\EventEmitter;
use React\Socket\ServerInterface;
use React\Promise;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
use React\EventLoop\LoopInterface;
use \UnexpectedValueException;
use \InvalidArgumentException;
use \Exception;
use React\Promise\Timer\TimeoutException;
class Server extends EventEmitter
{
// the following error codes are only used for SOCKS5 only
/** @internal */
const ERROR_GENERAL = 0x01;
/** @internal */
const ERROR_NOT_ALLOWED_BY_RULESET = 0x02;
/** @internal */
const ERROR_NETWORK_UNREACHABLE = 0x03;
/** @internal */
const ERROR_HOST_UNREACHABLE = 0x04;
/** @internal */
const ERROR_CONNECTION_REFUSED = 0x05;
/** @internal */
const ERROR_TTL = 0x06;
/** @internal */
const ERROR_COMMAND_UNSUPPORTED = 0x07;
/** @internal */
const ERROR_ADDRESS_UNSUPPORTED = 0x08;
protected $loop;
private $connector;
private $auth = null;
private $protocolVersion = null;
public function __construct(LoopInterface $loop, ServerInterface $serverInterface, ConnectorInterface $connector = null)
{
if ($connector === null) {
$connector = new Connector($loop);
}
$this->loop = $loop;
$this->connector = $connector;
$that = $this;
$serverInterface->on('connection', function ($connection) use ($that) {
$that->emit('connection', array($connection));
$that->onConnection($connection);
});
}
public function setProtocolVersion($version)
{
if ($version !== null) {
$version = (string)$version;
if (!in_array($version, array('4', '4a', '5'), true)) {
throw new InvalidArgumentException('Invalid protocol version given');
}
if ($version !== '5' && $this->auth !== null){
throw new UnexpectedValueException('Unable to change protocol version to anything but SOCKS5 while authentication is used. Consider removing authentication info or sticking to SOCKS5');
}
}
$this->protocolVersion = $version;
}
public function setAuth($auth)
{
if (!is_callable($auth)) {
throw new InvalidArgumentException('Given authenticator is not a valid callable');
}
if ($this->protocolVersion !== null && $this->protocolVersion !== '5') {
throw new UnexpectedValueException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication');
}
// wrap authentication callback in order to cast its return value to a promise
$this->auth = function($username, $password, $remote) use ($auth) {
$ret = call_user_func($auth, $username, $password, $remote);
if ($ret instanceof PromiseInterface) {
return $ret;
}
$deferred = new Deferred();
$ret ? $deferred->resolve() : $deferred->reject();
return $deferred->promise();
};
}
public function setAuthArray(array $login)
{
$this->setAuth(function ($username, $password) use ($login) {
return (isset($login[$username]) && (string)$login[$username] === $password);
});
}
public function unsetAuth()
{
$this->auth = null;
}
public function onConnection(ConnectionInterface $connection)
{
$that = $this;
$handling = $this->handleSocks($connection)->then(function($remote) use ($connection){
$connection->emit('ready',array($remote));
}, function ($error) use ($connection, $that) {
if (!($error instanceof \Exception)) {
$error = new \Exception($error);
}
$connection->emit('error', array($error));
$that->endConnection($connection);
});
$connection->on('close', function () use ($handling) {
$handling->cancel();
});
}
/**
* gracefully shutdown connection by flushing all remaining data and closing stream
*/
public function endConnection(ConnectionInterface $stream)
{
$tid = true;
$loop = $this->loop;
// cancel below timer in case connection is closed in time
$stream->once('close', function () use (&$tid, $loop) {
// close event called before the timer was set up, so everything is okay
if ($tid === true) {
// make sure to not start a useless timer
$tid = false;
} else {
$loop->cancelTimer($tid);
}
});
// shut down connection by pausing input data, flushing outgoing buffer and then exit
$stream->pause();
$stream->end();
// check if connection is not already closed
if ($tid === true) {
// fall back to forcefully close connection in 3 seconds if buffer can not be flushed
$tid = $loop->addTimer(3.0, array($stream,'close'));
}
}
private function handleSocks(ConnectionInterface $stream)
{
$reader = new StreamReader();
$stream->on('data', array($reader, 'write'));
$that = $this;
$that = $this;
$auth = $this->auth;
$protocolVersion = $this->protocolVersion;
// authentication requires SOCKS5
if ($auth !== null) {
$protocolVersion = '5';
}
return $reader->readByte()->then(function ($version) use ($stream, $that, $protocolVersion, $auth, $reader){
if ($version === 0x04) {
if ($protocolVersion === '5') {
throw new UnexpectedValueException('SOCKS4 not allowed due to configuration');
}
return $that->handleSocks4($stream, $protocolVersion, $reader);
} else if ($version === 0x05) {
if ($protocolVersion !== null && $protocolVersion !== '5') {
throw new UnexpectedValueException('SOCKS5 not allowed due to configuration');
}
return $that->handleSocks5($stream, $auth, $reader);
}
throw new UnexpectedValueException('Unexpected/unknown version number');
});
}
public function handleSocks4(ConnectionInterface $stream, $protocolVersion, StreamReader $reader)
{
// suppliying hostnames is only allowed for SOCKS4a (or automatically detected version)
$supportsHostname = ($protocolVersion === null || $protocolVersion === '4a');
$remote = $stream->getRemoteAddress();
if ($remote !== null) {
// remove transport scheme and prefix socks4:// instead
$secure = strpos($remote, 'tls://') === 0;
if (($pos = strpos($remote, '://')) !== false) {
$remote = substr($remote, $pos + 3);
}
$remote = 'socks4' . ($secure ? 's' : '') . '://' . $remote;
}
$that = $this;
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
return $reader->readBinary(array(
'port' => 'n',
'ipLong' => 'N',
'null' => 'C'
));
})->then(function ($data) use ($reader, $supportsHostname, $remote) {
if ($data['null'] !== 0x00) {
throw new Exception('Not a null byte');
}
if ($data['ipLong'] === 0) {
throw new Exception('Invalid IP');
}
if ($data['port'] === 0) {
throw new Exception('Invalid port');
}
if ($data['ipLong'] < 256 && $supportsHostname) {
// invalid IP => probably a SOCKS4a request which appends the hostname
return $reader->readStringNull()->then(function ($string) use ($data, $remote){
return array($string, $data['port'], $remote);
});
} else {
$ip = long2ip($data['ipLong']);
return array($ip, $data['port'], $remote);
}
})->then(function ($target) use ($stream, $that) {
return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){
$stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
return $remote;
}, function($error) use ($stream){
$stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
throw $error;
});
}, function($error) {
throw new UnexpectedValueException('SOCKS4 protocol error',0,$error);
});
}
public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamReader $reader)
{
$remote = $stream->getRemoteAddress();
if ($remote !== null) {
// remove transport scheme and prefix socks5:// instead
$secure = strpos($remote, 'tls://') === 0;
if (($pos = strpos($remote, '://')) !== false) {
$remote = substr($remote, $pos + 3);
}
$remote = 'socks5' . ($secure ? 's' : '') . '://' . $remote;
}
$that = $this;
return $reader->readByte()->then(function ($num) use ($reader) {
// $num different authentication mechanisms offered
return $reader->readLength($num);
})->then(function ($methods) use ($reader, $stream, $auth, &$remote) {
if ($auth === null && strpos($methods,"\x00") !== false) {
// accept "no authentication"
$stream->write(pack('C2', 0x05, 0x00));
return 0x00;
} else if ($auth !== null && strpos($methods,"\x02") !== false) {
// username/password authentication (RFC 1929) sub negotiation
$stream->write(pack('C2', 0x05, 0x02));
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
return $reader->readByte();
})->then(function ($length) use ($reader) {
return $reader->readLength($length);
})->then(function ($username) use ($reader, $auth, $stream, &$remote) {
return $reader->readByte()->then(function ($length) use ($reader) {
return $reader->readLength($length);
})->then(function ($password) use ($username, $auth, $stream, &$remote) {
// username and password given => authenticate
// prefix username/password to remote URI
if ($remote !== null) {
$remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
}
return $auth($username, $password, $remote)->then(function () use ($stream, $username) {
// accept
$stream->emit('auth', array($username));
$stream->write(pack('C2', 0x01, 0x00));
}, function() use ($stream) {
// reject => send any code but 0x00
$stream->end(pack('C2', 0x01, 0xFF));
throw new UnexpectedValueException('Unable to authenticate');
});
});
});
} else {
// reject all offered authentication methods
$stream->write(pack('C2', 0x05, 0xFF));
throw new UnexpectedValueException('No acceptable authentication mechanism found');
}
})->then(function ($method) use ($reader, $stream) {
return $reader->readBinary(array(
'version' => 'C',
'command' => 'C',
'null' => 'C',
'type' => 'C'
));
})->then(function ($data) use ($reader) {
if ($data['version'] !== 0x05) {
throw new UnexpectedValueException('Invalid SOCKS version');
}
if ($data['command'] !== 0x01) {
throw new UnexpectedValueException('Only CONNECT requests supported', Server::ERROR_COMMAND_UNSUPPORTED);
}
// if ($data['null'] !== 0x00) {
// throw new UnexpectedValueException('Reserved byte has to be NULL');
// }
if ($data['type'] === 0x03) {
// target hostname string
return $reader->readByte()->then(function ($len) use ($reader) {
return $reader->readLength($len);
});
} else if ($data['type'] === 0x01) {
// target IPv4
return $reader->readLength(4)->then(function ($addr) {
return inet_ntop($addr);
});
} else if ($data['type'] === 0x04) {
// target IPv6
return $reader->readLength(16)->then(function ($addr) {
return inet_ntop($addr);
});
} else {
throw new UnexpectedValueException('Invalid address type', Server::ERROR_ADDRESS_UNSUPPORTED);
}
})->then(function ($host) use ($reader, &$remote) {
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) {
return array($host, $data['port'], $remote);
});
})->then(function ($target) use ($that, $stream) {
return $that->connectTarget($stream, $target);
}, function($error) use ($stream) {
throw new UnexpectedValueException('SOCKS5 protocol error', $error->getCode(), $error);
})->then(function (ConnectionInterface $remote) use ($stream) {
$stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0));
return $remote;
}, function(Exception $error) use ($stream){
$stream->write(pack('C4Nn', 0x05, $error->getCode() === 0 ? Server::ERROR_GENERAL : $error->getCode(), 0x00, 0x01, 0, 0));
throw $error;
});
}
public function connectTarget(ConnectionInterface $stream, array $target)
{
$uri = $target[0];
if (strpos($uri, ':') !== false) {
$uri = '[' . $uri . ']';
}
$uri .= ':' . $target[1];
// validate URI so a string hostname can not pass excessive URI parts
$parts = parse_url('tcp://' . $uri);
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) {
return Promise\reject(new InvalidArgumentException('Invalid target URI given'));
}
if (isset($target[2])) {
$uri .= '?source=' . rawurlencode($target[2]);
}
$stream->emit('target', $target);
$that = $this;
$connecting = $this->connector->connect($uri);
$stream->on('close', function () use ($connecting) {
$connecting->cancel();
});
return $connecting->then(function (ConnectionInterface $remote) use ($stream, $that) {
$stream->pipe($remote, array('end'=>false));
$remote->pipe($stream, array('end'=>false));
// remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local
$remote->on('end', function() use ($stream, $that) {
$stream->emit('shutdown', array('remote', null));
$that->endConnection($stream);
});
// local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote
$stream->on('end', function() use ($remote, $that) {
$that->endConnection($remote);
});
// set bigger buffer size of 100k to improve performance
$stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024;
return $remote;
}, function(Exception $error) {
// default to general/unknown error
$code = Server::ERROR_GENERAL;
// map common socket error conditions to limited list of SOCKS error codes
if ((defined('SOCKET_EACCES') && $error->getCode() === SOCKET_EACCES) || $error->getCode() === 13) {
$code = Server::ERROR_NOT_ALLOWED_BY_RULESET;
} elseif ((defined('SOCKET_EHOSTUNREACH') && $error->getCode() === SOCKET_EHOSTUNREACH) || $error->getCode() === 113) {
$code = Server::ERROR_HOST_UNREACHABLE;
} elseif ((defined('SOCKET_ENETUNREACH') && $error->getCode() === SOCKET_ENETUNREACH) || $error->getCode() === 101) {
$code = Server::ERROR_NETWORK_UNREACHABLE;
} elseif ((defined('SOCKET_ECONNREFUSED') && $error->getCode() === SOCKET_ECONNREFUSED) || $error->getCode() === 111 || $error->getMessage() === 'Connection refused') {
// Socket component does not currently assign an error code for this, so we have to resort to checking the exception message
$code = Server::ERROR_CONNECTION_REFUSED;
} elseif ((defined('SOCKET_ETIMEDOUT') && $error->getCode() === SOCKET_ETIMEDOUT) || $error->getCode() === 110 || $error instanceof TimeoutException) {
// Socket component does not currently assign an error code for this, but we can rely on the TimeoutException
$code = Server::ERROR_TTL;
}
throw new UnexpectedValueException('Unable to connect to remote target', $code, $error);
});
}
}

149
vendor/clue/socks-react/src/StreamReader.php vendored Executable file
View File

@@ -0,0 +1,149 @@
<?php
namespace Clue\React\Socks;
use React\Promise\Deferred;
use \InvalidArgumentException;
use \UnexpectedValueException;
/**
* @internal
*/
class StreamReader
{
const RET_DONE = true;
const RET_INCOMPLETE = null;
private $buffer = '';
private $queue = array();
public function write($data)
{
$this->buffer .= $data;
do {
$current = reset($this->queue);
if ($current === false) {
break;
}
/* @var $current Closure */
$ret = $current($this->buffer);
if ($ret === self::RET_INCOMPLETE) {
// current is incomplete, so wait for further data to arrive
break;
} else {
// current is done, remove from list and continue with next
array_shift($this->queue);
}
} while (true);
}
public function readBinary($structure)
{
$length = 0;
$unpack = '';
foreach ($structure as $name=>$format) {
if ($length !== 0) {
$unpack .= '/';
}
$unpack .= $format . $name;
if ($format === 'C') {
++$length;
} else if ($format === 'n') {
$length += 2;
} else if ($format === 'N') {
$length += 4;
} else {
throw new InvalidArgumentException('Invalid format given');
}
}
return $this->readLength($length)->then(function ($response) use ($unpack) {
return unpack($unpack, $response);
});
}
public function readLength($bytes)
{
$deferred = new Deferred();
$this->readBufferCallback(function (&$buffer) use ($bytes, $deferred) {
if (strlen($buffer) >= $bytes) {
$deferred->resolve((string)substr($buffer, 0, $bytes));
$buffer = (string)substr($buffer, $bytes);
return StreamReader::RET_DONE;
}
});
return $deferred->promise();
}
public function readByte()
{
return $this->readBinary(array(
'byte' => 'C'
))->then(function ($data) {
return $data['byte'];
});
}
public function readByteAssert($expect)
{
return $this->readByte()->then(function ($byte) use ($expect) {
if ($byte !== $expect) {
throw new UnexpectedValueException('Unexpected byte encountered');
}
return $byte;
});
}
public function readStringNull()
{
$deferred = new Deferred();
$string = '';
$that = $this;
$readOne = function () use (&$readOne, $that, $deferred, &$string) {
$that->readByte()->then(function ($byte) use ($deferred, &$string, $readOne) {
if ($byte === 0x00) {
$deferred->resolve($string);
} else {
$string .= chr($byte);
$readOne();
}
});
};
$readOne();
return $deferred->promise();
}
public function readBufferCallback(/* callable */ $callable)
{
if (!is_callable($callable)) {
throw new InvalidArgumentException('Given function must be callable');
}
if ($this->queue) {
$this->queue []= $callable;
} else {
$this->queue = array($callable);
if ($this->buffer !== '') {
// this is the first element in the queue and the buffer is filled => trigger write procedure
$this->write('');
}
}
}
public function getBuffer()
{
return $this->buffer;
}
}

403
vendor/clue/socks-react/tests/ClientTest.php vendored Executable file
View File

@@ -0,0 +1,403 @@
<?php
use Clue\React\Socks\Client;
use React\Promise\Promise;
use Clue\React\Socks\Server;
class ClientTest extends TestCase
{
private $loop;
private $connector;
/** @var Client */
private $client;
public function setUp()
{
$this->loop = React\EventLoop\Factory::create();
$this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
$this->client = new Client('127.0.0.1:1080', $this->connector);
}
public function testCtorAcceptsUriWithHostAndPort()
{
$client = new Client('127.0.0.1:9050', $this->connector);
$this->assertTrue(true);
}
public function testCtorAcceptsUriWithScheme()
{
$client = new Client('socks://127.0.0.1:9050', $this->connector);
$this->assertTrue(true);
}
public function testCtorAcceptsUriWithHostOnlyAssumesDefaultPort()
{
$client = new Client('127.0.0.1', $this->connector);
$this->assertTrue(true);
}
public function testCtorAcceptsUriWithSecureScheme()
{
$client = new Client('sockss://127.0.0.1:9050', $this->connector);
$this->assertTrue(true);
}
public function testCtorAcceptsUriWithSecureVersionScheme()
{
$client = new Client('socks5s://127.0.0.1:9050', $this->connector);
$this->assertTrue(true);
}
public function testCtorAcceptsUriWithSocksUnixScheme()
{
$client = new Client('socks+unix:///tmp/socks.socket', $this->connector);
$this->assertTrue(true);
}
public function testCtorAcceptsUriWithSocks5UnixScheme()
{
$client = new Client('socks5+unix:///tmp/socks.socket', $this->connector);
$this->assertTrue(true);
}
/**
* @expectedException InvalidArgumentException
*/
public function testCtorThrowsForInvalidUri()
{
new Client('////', $this->connector);
}
public function testValidAuthFromUri()
{
$this->client = new Client('username:password@127.0.0.1', $this->connector);
$this->assertTrue(true);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidAuthInformation()
{
new Client(str_repeat('a', 256) . ':test@127.0.0.1', $this->connector);
}
public function testValidAuthAndVersionFromUri()
{
$this->client = new Client('socks5://username:password@127.0.0.1:9050', $this->connector);
$this->assertTrue(true);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidCanNotSetAuthenticationForSocks4Uri()
{
$this->client = new Client('socks4://username:password@127.0.0.1:9050', $this->connector);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidProtocolVersion()
{
$this->client = new Client('socks3://127.0.0.1:9050', $this->connector);
}
public function testCreateWillConnectToProxy()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=localhost')->willReturn($promise);
$promise = $this->client->connect('localhost:80');
$this->assertInstanceOf('\React\Promise\PromiseInterface', $promise);
}
public function testCreateWillConnectToProxyWithFullUri()
{
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080/?hostname=test#fragment')->willReturn($promise);
$promise = $this->client->connect('localhost:80/?hostname=test#fragment');
$this->assertInstanceOf('\React\Promise\PromiseInterface', $promise);
}
public function testCreateWithInvalidHostDoesNotConnect()
{
$promise = new Promise(function () { });
$this->connector->expects($this->never())->method('connect');
$promise = $this->client->connect(str_repeat('a', '256') . ':80');
$this->assertInstanceOf('\React\Promise\PromiseInterface', $promise);
}
public function testCreateWithInvalidPortDoesNotConnect()
{
$promise = new Promise(function () { });
$this->connector->expects($this->never())->method('connect');
$promise = $this->client->connect('some-random-site:some-random-port');
$this->assertInstanceOf('\React\Promise\PromiseInterface', $promise);
}
public function testConnectorRejectsWillRejectConnection()
{
$promise = \React\Promise\reject(new RuntimeException());
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$promise = $this->client->connect('google.com:80');
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
}
public function testCancelConnectionDuringConnectionWillCancelConnection()
{
$promise = new Promise(function () { }, function () {
throw new \RuntimeException();
});
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$promise = $this->client->connect('google.com:80');
$promise->cancel();
$this->expectPromiseReject($promise);
}
public function testCancelConnectionDuringSessionWillCloseStream()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->getMock();
$stream->expects($this->once())->method('close');
$promise = new Promise(function ($resolve) use ($stream) { $resolve($stream); });
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$promise = $this->client->connect('google.com:80');
$promise->cancel();
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNABORTED));
}
public function testEmitConnectionCloseDuringSessionWillRejectConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$promise = $this->client->connect('google.com:80');
$stream->emit('close');
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNRESET));
}
public function testEmitConnectionErrorDuringSessionWillRejectConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$promise = $this->client->connect('google.com:80');
$stream->emit('error', array(new RuntimeException()));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EIO));
}
public function testEmitInvalidSocks4DataDuringSessionWillRejectConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$promise = $this->client->connect('google.com:80');
$stream->emit('data', array("HTTP/1.1 400 Bad Request\r\n\r\n"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
}
public function testEmitInvalidSocks5DataDuringSessionWillRejectConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
$promise = $this->client->connect('google.com:80');
$stream->emit('data', array("HTTP/1.1 400 Bad Request\r\n\r\n"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
}
public function testEmitSocks5DataErrorDuringSessionWillRejectConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
$promise = $this->client->connect('google.com:80');
$stream->emit('data', array("\x05\x00" . "\x05\x01\x00\x00"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
}
public function testEmitSocks5DataInvalidAddressTypeWillRejectConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
$promise = $this->client->connect('google.com:80');
$stream->emit('data', array("\x05\x00" . "\x05\x00\x00\x00"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
}
public function testEmitSocks5DataIpv6AddressWillResolveConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->never())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=%3A%3A1')->willReturn($promise);
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
$promise = $this->client->connect('[::1]:80');
$stream->emit('data', array("\x05\x00" . "\x05\x00\x00\x04" . inet_pton('::1') . "\x00\x50"));
$promise->then($this->expectCallableOnce());
}
public function testEmitSocks5DataHostnameAddressWillResolveConnection()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->never())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
$promise = $this->client->connect('google.com:80');
$stream->emit('data', array("\x05\x00" . "\x05\x00\x00\x03\x0Agoogle.com\x00\x50"));
$promise->then($this->expectCallableOnce());
}
public function provideConnectionErrors()
{
return array(
array(
Server::ERROR_GENERAL,
SOCKET_ECONNREFUSED
),
array(
Server::ERROR_NOT_ALLOWED_BY_RULESET,
SOCKET_EACCES
),
array(
Server::ERROR_NETWORK_UNREACHABLE,
SOCKET_ENETUNREACH
),
array(
Server::ERROR_HOST_UNREACHABLE,
SOCKET_EHOSTUNREACH
),
array(
Server::ERROR_CONNECTION_REFUSED,
SOCKET_ECONNREFUSED
),
array(
Server::ERROR_TTL,
SOCKET_ETIMEDOUT
),
array(
Server::ERROR_COMMAND_UNSUPPORTED,
SOCKET_EPROTO
),
array(
Server::ERROR_ADDRESS_UNSUPPORTED,
SOCKET_EPROTO
),
array(
200,
SOCKET_ECONNREFUSED
)
);
}
/**
* @dataProvider provideConnectionErrors
* @param int $error
* @param int $expectedCode
*/
public function testEmitSocks5DataErrorMapsToExceptionCode($error, $expectedCode)
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
$stream->expects($this->once())->method('close');
$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
$promise = $this->client->connect('google.com:80');
$stream->emit('data', array("\x05\x00" . "\x05" . chr($error) . "\x00\x00"));
$promise->then(null, $this->expectCallableOnceWithExceptionCode($expectedCode));
}
}

View File

@@ -0,0 +1,437 @@
<?php
use Clue\React\Socks\Client;
use Clue\React\Socks\Server;
use Clue\React\Block;
use React\Socket\TimeoutConnector;
use React\Socket\SecureConnector;
use React\Socket\TcpConnector;
use React\Socket\UnixServer;
use React\Socket\Connector;
class FunctionalTest extends TestCase
{
private $loop;
private $connector;
private $client;
private $port;
private $server;
public function setUp()
{
$this->loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server(0, $this->loop);
$address = $socket->getAddress();
if (strpos($address, '://') === false) {
$address = 'tcp://' . $address;
}
$this->port = parse_url($address, PHP_URL_PORT);
$this->assertNotEquals(0, $this->port);
$this->server = new Server($this->loop, $socket);
$this->connector = new TcpConnector($this->loop);
$this->client = new Client('127.0.0.1:' . $this->port, $this->connector);
}
/** @group internet */
public function testConnection()
{
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionInvalid()
{
$this->assertRejectPromise($this->client->connect('www.google.com.invalid:80'));
}
public function testConnectionWithIpViaSocks4()
{
$this->server->setProtocolVersion('4');
$this->client = new Client('socks4://127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('127.0.0.1:' . $this->port));
}
/** @group internet */
public function testConnectionWithHostnameViaSocks4Fails()
{
$this->client = new Client('socks4://127.0.0.1:' . $this->port, $this->connector);
$this->assertRejectPromise($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionWithInvalidPortFails()
{
$this->assertRejectPromise($this->client->connect('www.google.com:100000'));
}
public function testConnectionWithIpv6ViaSocks4Fails()
{
$this->client = new Client('socks4://127.0.0.1:' . $this->port, $this->connector);
$this->assertRejectPromise($this->client->connect('[::1]:80'));
}
/** @group internet */
public function testConnectionSocks4a()
{
$this->server->setProtocolVersion('4a');
$this->client = new Client('socks4a://127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionSocks5()
{
$this->server->setProtocolVersion(5);
$this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionSocksOverTls()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Required function does not exist in your environment (HHVM?)');
}
$socket = new \React\Socket\Server('tls://127.0.0.1:0', $this->loop, array('tls' => array(
'local_cert' => __DIR__ . '/../examples/localhost.pem',
)));
$this->server = new Server($this->loop, $socket);
$this->connector = new Connector($this->loop, array('tls' => array(
'verify_peer' => false,
'verify_peer_name' => false
)));
$this->client = new Client(str_replace('tls:', 'sockss:', $socket->getAddress()), $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/**
* @group internet
* @requires PHP 5.6
*/
public function testConnectionSocksOverTlsUsesPeerNameFromSocksUri()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Required function does not exist in your environment (HHVM?)');
}
$socket = new \React\Socket\Server('tls://127.0.0.1:0', $this->loop, array('tls' => array(
'local_cert' => __DIR__ . '/../examples/localhost.pem',
)));
$this->server = new Server($this->loop, $socket);
$this->connector = new Connector($this->loop, array('tls' => array(
'verify_peer' => false,
'verify_peer_name' => true
)));
$this->client = new Client(str_replace('tls:', 'sockss:', $socket->getAddress()), $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionSocksOverUnix()
{
if (!in_array('unix', stream_get_transports())) {
$this->markTestSkipped('System does not support unix:// scheme');
}
$path = sys_get_temp_dir() . '/test' . mt_rand(1000, 9999) . '.sock';
$socket = new UnixServer($path, $this->loop);
$this->server = new Server($this->loop, $socket);
$this->connector = new Connector($this->loop);
$this->client = new Client('socks+unix://' . $path, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
unlink($path);
}
/** @group internet */
public function testConnectionSocks5OverUnix()
{
if (!in_array('unix', stream_get_transports())) {
$this->markTestSkipped('System does not support unix:// scheme');
}
$path = sys_get_temp_dir() . '/test' . mt_rand(1000, 9999) . '.sock';
$socket = new UnixServer($path, $this->loop);
$this->server = new Server($this->loop, $socket);
$this->server->setProtocolVersion(5);
$this->connector = new Connector($this->loop);
$this->client = new Client('socks5+unix://' . $path, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
unlink($path);
}
/** @group internet */
public function testConnectionSocksWithAuthenticationOverUnix()
{
if (!in_array('unix', stream_get_transports())) {
$this->markTestSkipped('System does not support unix:// scheme');
}
$path = sys_get_temp_dir() . '/test' . mt_rand(1000, 9999) . '.sock';
$socket = new UnixServer($path, $this->loop);
$this->server = new Server($this->loop, $socket);
$this->server->setAuthArray(array('name' => 'pass'));
$this->connector = new Connector($this->loop);
$this->client = new Client('socks+unix://name:pass@' . $path, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
unlink($path);
}
/** @group internet */
public function testConnectionAuthenticationFromUri()
{
$this->server->setAuthArray(array('name' => 'pass'));
$this->client = new Client('name:pass@127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionAuthenticationCallback()
{
$called = 0;
$that = $this;
$this->server->setAuth(function ($name, $pass, $remote) use ($that, &$called) {
++$called;
$that->assertEquals('name', $name);
$that->assertEquals('pass', $pass);
$that->assertStringStartsWith('socks5://name:pass@127.0.0.1:', $remote);
return true;
});
$this->client = new Client('name:pass@127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
$this->assertEquals(1, $called);
}
/** @group internet */
public function testConnectionAuthenticationCallbackWillNotBeInvokedIfClientsSendsNoAuth()
{
$called = 0;
$this->server->setAuth(function () use (&$called) {
++$called;
return true;
});
$this->client = new Client('127.0.0.1:' . $this->port, $this->connector);
$this->assertRejectPromise($this->client->connect('www.google.com:80'));
$this->assertEquals(0, $called);
}
/** @group internet */
public function testConnectionAuthenticationFromUriEncoded()
{
$this->server->setAuthArray(array('name' => 'p@ss:w0rd'));
$this->client = new Client(rawurlencode('name') . ':' . rawurlencode('p@ss:w0rd') . '@127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionAuthenticationFromUriWithOnlyUserAndNoPassword()
{
$this->server->setAuthArray(array('empty' => ''));
$this->client = new Client('empty@127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionAuthenticationEmptyPassword()
{
$this->server->setAuthArray(array('user' => ''));
$this->client = new Client('user@127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectionAuthenticationUnused()
{
$this->client = new Client('name:pass@127.0.0.1:' . $this->port, $this->connector);
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
public function testConnectionInvalidProtocolDoesNotMatchSocks5()
{
$this->server->setProtocolVersion(5);
$this->client = new Client('socks4a://127.0.0.1:' . $this->port, $this->connector);
$this->assertRejectPromise($this->client->connect('www.google.com:80'), null, SOCKET_ECONNRESET);
}
public function testConnectionInvalidProtocolDoesNotMatchSocks4()
{
$this->server->setProtocolVersion(4);
$this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector);
$this->assertRejectPromise($this->client->connect('www.google.com:80'), null, SOCKET_ECONNRESET);
}
public function testConnectionInvalidNoAuthentication()
{
$this->server->setAuthArray(array('name' => 'pass'));
$this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector);
$this->assertRejectPromise($this->client->connect('www.google.com:80'), null, SOCKET_EACCES);
}
public function testConnectionInvalidAuthenticationMismatch()
{
$this->server->setAuthArray(array('name' => 'pass'));
$this->client = new Client('user:pass@127.0.0.1:' . $this->port, $this->connector);
$this->assertRejectPromise($this->client->connect('www.google.com:80'), null, SOCKET_EACCES);
}
/** @group internet */
public function testConnectorOkay()
{
$this->assertResolveStream($this->client->connect('www.google.com:80'));
}
/** @group internet */
public function testConnectorInvalidDomain()
{
$this->assertRejectPromise($this->client->connect('www.google.commm:80'));
}
/** @group internet */
public function testConnectorCancelConnection()
{
$promise = $this->client->connect('www.google.com:80');
$promise->cancel();
$this->assertRejectPromise($promise);
}
/** @group internet */
public function testConnectorInvalidUnboundPortTimeout()
{
// time out the connection attempt in 0.1s (as expected)
$tcp = new TimeoutConnector($this->client, 0.1, $this->loop);
$this->assertRejectPromise($tcp->connect('www.google.com:8080'));
}
/** @group internet */
public function testSecureConnectorOkay()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Required function does not exist in your environment (HHVM?)');
}
$ssl = new SecureConnector($this->client, $this->loop);
$this->assertResolveStream($ssl->connect('www.google.com:443'));
}
/** @group internet */
public function testSecureConnectorToBadSslWithVerifyFails()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Required function does not exist in your environment (HHVM?)');
}
$ssl = new SecureConnector($this->client, $this->loop, array('verify_peer' => true));
$this->assertRejectPromise($ssl->connect('self-signed.badssl.com:443'));
}
/** @group internet */
public function testSecureConnectorToBadSslWithoutVerifyWorks()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Required function does not exist in your environment (HHVM?)');
}
$ssl = new SecureConnector($this->client, $this->loop, array('verify_peer' => false));
$this->assertResolveStream($ssl->connect('self-signed.badssl.com:443'));
}
/** @group internet */
public function testSecureConnectorInvalidPlaintextIsNotSsl()
{
if (!function_exists('stream_socket_enable_crypto')) {
$this->markTestSkipped('Required function does not exist in your environment (HHVM?)');
}
$ssl = new SecureConnector($this->client, $this->loop);
$this->assertRejectPromise($ssl->connect('www.google.com:80'));
}
/** @group internet */
public function testSecureConnectorInvalidUnboundPortTimeout()
{
$ssl = new SecureConnector($this->client, $this->loop);
// time out the connection attempt in 0.1s (as expected)
$ssl = new TimeoutConnector($ssl, 0.1, $this->loop);
$this->assertRejectPromise($ssl->connect('www.google.com:8080'));
}
private function assertResolveStream($promise)
{
$this->expectPromiseResolve($promise);
$promise->then(function ($stream) {
$stream->close();
});
Block\await($promise, $this->loop, 2.0);
}
private function assertRejectPromise($promise, $message = null, $code = null)
{
$this->expectPromiseReject($promise);
if (method_exists($this, 'expectException')) {
$this->expectException('Exception');
if ($message !== null) {
$this->expectExceptionMessage($message);
}
if ($code !== null) {
$this->expectExceptionCode($code);
}
} else {
$this->setExpectedException('Exception', $message, $code);
}
Block\await($promise, $this->loop, 2.0);
}
}

428
vendor/clue/socks-react/tests/ServerTest.php vendored Executable file
View File

@@ -0,0 +1,428 @@
<?php
use Clue\React\Socks\Server;
use React\Promise\Promise;
use React\Promise\Timer\TimeoutException;
class ServerTest extends TestCase
{
/** @var Server */
private $server;
private $connector;
public function setUp()
{
$socket = $this->getMockBuilder('React\Socket\ServerInterface')
->getMock();
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')
->getMock();
$this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')
->getMock();
$this->server = new Server($loop, $socket, $this->connector);
}
public function testSetProtocolVersion()
{
$this->server->setProtocolVersion(4);
$this->server->setProtocolVersion('4a');
$this->server->setProtocolVersion(5);
$this->server->setProtocolVersion(null);
$this->assertTrue(true);
}
/**
* @expectedException InvalidArgumentException
*/
public function testSetInvalidProtocolVersion()
{
$this->server->setProtocolVersion(6);
}
public function testSetAuthArray()
{
$this->server->setAuthArray(array());
$this->server->setAuthArray(array(
'name1' => 'password1',
'name2' => 'password2'
));
$this->assertTrue(true);
}
/**
* @expectedException InvalidArgumentException
*/
public function testSetAuthInvalid()
{
$this->server->setAuth(true);
}
/**
* @expectedException UnexpectedValueException
*/
public function testUnableToSetAuthIfProtocolDoesNotSupportAuth()
{
$this->server->setProtocolVersion(4);
$this->server->setAuthArray(array());
}
/**
* @expectedException UnexpectedValueException
*/
public function testUnableToSetProtocolWhichDoesNotSupportAuth()
{
$this->server->setAuthArray(array());
// this is okay
$this->server->setProtocolVersion(5);
$this->server->setProtocolVersion(4);
}
public function testConnectWillCreateConnection()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise);
$promise = $this->server->connectTarget($stream, array('google.com', 80));
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
}
public function testConnectWillCreateConnectionWithSourceUri()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('google.com:80?source=socks5%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
$promise = $this->server->connectTarget($stream, array('google.com', 80, 'socks5://10.20.30.40:5060'));
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
}
public function testConnectWillRejectIfConnectionFails()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$promise = new Promise(function ($_, $reject) { $reject(new \RuntimeException()); });
$this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise);
$promise = $this->server->connectTarget($stream, array('google.com', 80));
$promise->then(null, $this->expectCallableOnce());
}
public function testConnectWillCancelConnectionIfStreamCloses()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close'))->getMock();
$promise = new Promise(function () { }, function () {
throw new \RuntimeException();
});
$this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise);
$promise = $this->server->connectTarget($stream, array('google.com', 80));
$stream->emit('close');
$promise->then(null, $this->expectCallableOnce());
}
public function testConnectWillAbortIfPromiseIsCanceled()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$promise = new Promise(function () { }, function () {
throw new \RuntimeException();
});
$this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise);
$promise = $this->server->connectTarget($stream, array('google.com', 80));
$promise->cancel();
$promise->then(null, $this->expectCallableOnce());
}
public function provideConnectionErrors()
{
return array(
array(
new RuntimeException('', SOCKET_EACCES),
Server::ERROR_NOT_ALLOWED_BY_RULESET
),
array(
new RuntimeException('', SOCKET_ENETUNREACH),
Server::ERROR_NETWORK_UNREACHABLE
),
array(
new RuntimeException('', SOCKET_EHOSTUNREACH),
Server::ERROR_HOST_UNREACHABLE,
),
array(
new RuntimeException('', SOCKET_ECONNREFUSED),
Server::ERROR_CONNECTION_REFUSED
),
array(
new RuntimeException('Connection refused'),
Server::ERROR_CONNECTION_REFUSED
),
array(
new RuntimeException('', SOCKET_ETIMEDOUT),
Server::ERROR_TTL
),
array(
new TimeoutException(1.0),
Server::ERROR_TTL
),
array(
new RuntimeException(),
Server::ERROR_GENERAL
)
);
}
/**
* @dataProvider provideConnectionErrors
* @param Exception $error
* @param int $expectedCode
*/
public function testConnectWillReturnMappedSocks5ErrorCodeFromConnector($error, $expectedCode)
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$promise = \React\Promise\reject($error);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
$promise = $this->server->connectTarget($stream, array('google.com', 80));
$code = null;
$promise->then(null, function ($error) use (&$code) {
$code = $error->getCode();
});
$this->assertEquals($expectedCode, $code);
}
public function testHandleSocksConnectionWillEndOnInvalidData()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
$connection->expects($this->once())->method('pause');
$connection->expects($this->once())->method('end');
$this->server->onConnection($connection);
$connection->emit('data', array('asdasdasdasdasd'));
}
public function testHandleSocks4ConnectionWithIpv4WillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x04\x01" . "\x00\x50" . pack('N', ip2long('127.0.0.1')) . "\x00"));
}
public function testHandleSocks4aConnectionWithHostnameWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "example.com" . "\x00"));
}
public function testHandleSocks4aConnectionWithHostnameAndSourceAddressWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'getRemoteAddress'))->getMock();
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://10.20.30.40:5060');
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('example.com:80?source=socks4%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "example.com" . "\x00"));
}
public function testHandleSocks4aConnectionWithSecureTlsSourceAddressWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'getRemoteAddress'))->getMock();
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tls://10.20.30.40:5060');
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('example.com:80?source=socks4s%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "example.com" . "\x00"));
}
public function testHandleSocks4aConnectionWithInvalidHostnameWillNotEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
$this->connector->expects($this->never())->method('connect');
$this->server->onConnection($connection);
$connection->emit('data', array("\x04\x01" . "\x00\x50" . "\x00\x00\x00\x01" . "\x00" . "tls://example.com:80?" . "\x00"));
}
public function testHandleSocks5ConnectionWithIpv4WillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x01" . pack('N', ip2long('127.0.0.1')) . "\x00\x50"));
}
public function testHandleSocks5ConnectionWithIpv4AndSourceAddressWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write', 'getRemoteAddress'))->getMock();
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tcp://10.20.30.40:5060');
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80?source=socks5%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x01" . pack('N', ip2long('127.0.0.1')) . "\x00\x50"));
}
public function testHandleSocks5ConnectionWithSecureTlsIpv4AndSourceAddressWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write', 'getRemoteAddress'))->getMock();
$connection->expects($this->once())->method('getRemoteAddress')->willReturn('tls://10.20.30.40:5060');
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80?source=socks5s%3A%2F%2F10.20.30.40%3A5060')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x01" . pack('N', ip2long('127.0.0.1')) . "\x00\x50"));
}
public function testHandleSocks5ConnectionWithIpv6WillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('[::1]:80')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x04" . inet_pton('::1') . "\x00\x50"));
}
public function testHandleSocks5ConnectionWithHostnameWillEstablishOutgoingConnection()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
$promise = new Promise(function () { });
$this->connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x03\x0B" . "example.com" . "\x00\x50"));
}
public function testHandleSocks5ConnectionWithConnectorRefusedWillReturnReturnRefusedError()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
$promise = \React\Promise\reject(new RuntimeException('Connection refused'));
$this->connector->expects($this->once())->method('connect')->with('example.com:80')->willReturn($promise);
$this->server->onConnection($connection);
$connection->expects($this->exactly(2))->method('write')->withConsecutive(array("\x05\x00"), array("\x05\x05" . "\x00\x01\x00\x00\x00\x00\x00\x00"));
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x03\x0B" . "example.com" . "\x00\x50"));
}
public function testHandleSocks5UdpCommandWillNotEstablishOutgoingConnectionAndReturnCommandError()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
$this->connector->expects($this->never())->method('connect');
$this->server->onConnection($connection);
$connection->expects($this->exactly(2))->method('write')->withConsecutive(array("\x05\x00"), array("\x05\x07" . "\x00\x01\x00\x00\x00\x00\x00\x00"));
$connection->emit('data', array("\x05\x01\x00" . "\x05\x03\x00\x03\x0B" . "example.com" . "\x00\x50"));
}
public function testHandleSocks5ConnectionWithInvalidHostnameWillNotEstablishOutgoingConnectionAndReturnGeneralError()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end', 'write'))->getMock();
$this->connector->expects($this->never())->method('connect');
$this->server->onConnection($connection);
$connection->expects($this->exactly(2))->method('write')->withConsecutive(array("\x05\x00"), array("\x05\x01" . "\x00\x01\x00\x00\x00\x00\x00\x00"));
$connection->emit('data', array("\x05\x01\x00" . "\x05\x01\x00\x03\x15" . "tls://example.com:80?" . "\x00\x50"));
}
public function testHandleSocksConnectionWillCancelOutputConnectionIfIncomingCloses()
{
$connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock();
$promise = new Promise(function () { }, $this->expectCallableOnce());
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise);
$this->server->onConnection($connection);
$connection->emit('data', array("\x04\x01" . "\x00\x50" . pack('N', ip2long('127.0.0.1')) . "\x00"));
$connection->emit('close');
}
public function testUnsetAuth()
{
$this->server->unsetAuth();
$this->server->unsetAuth();
$this->assertTrue(true);
}
}

View File

@@ -0,0 +1,82 @@
<?php
use Clue\React\Socks\StreamReader;
class StreamReaderTest extends TestCase
{
private $reader;
public function setUp()
{
$this->reader = new StreamReader();
}
public function testReadByteAssertCorrect()
{
$this->reader->readByteAssert(0x01)->then($this->expectCallableOnce(0x01));
$this->reader->write("\x01");
}
public function testReadByteAssertInvalid()
{
$this->reader->readByteAssert(0x02)->then(null, $this->expectCallableOnce());
$this->reader->write("\x03");
}
public function testReadStringNull()
{
$this->reader->readStringNull()->then($this->expectCallableOnce('hello'));
$this->reader->write("hello\x00");
}
public function testReadStringLength()
{
$this->reader->readLength(5)->then($this->expectCallableOnce('hello'));
$this->reader->write('he');
$this->reader->write('ll');
$this->reader->write('o ');
$this->assertEquals(' ', $this->reader->getBuffer());
}
public function testReadBuffered()
{
$this->reader->write('hello');
$this->reader->readLength(5)->then($this->expectCallableOnce('hello'));
$this->assertEquals('', $this->reader->getBuffer());
}
public function testSequence()
{
$this->reader->readByte()->then($this->expectCallableOnce(ord('h')));
$this->reader->readByteAssert(ord('e'))->then($this->expectCallableOnce(ord('e')));
$this->reader->readLength(4)->then($this->expectCallableOnce('llo '));
$this->reader->readBinary(array('w'=>'C', 'o' => 'C'))->then($this->expectCallableOnce(array('w' => ord('w'), 'o' => ord('o'))));
$this->reader->write('hello world');
$this->assertEquals('rld', $this->reader->getBuffer());
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidStructure()
{
$this->reader->readBinary(array('invalid' => 'y'));
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidCallback()
{
$this->reader->readBufferCallback(array());
}
}

103
vendor/clue/socks-react/tests/bootstrap.php vendored Executable file
View File

@@ -0,0 +1,103 @@
<?php
(include_once __DIR__.'/../vendor/autoload.php') OR die(PHP_EOL.'ERROR: composer autoloader not found, run "composer install" or see README for instructions'.PHP_EOL);
class TestCase extends PHPUnit\Framework\TestCase
{
protected function expectCallableOnce()
{
$mock = $this->createCallableMock();
if (func_num_args() > 0) {
$mock
->expects($this->once())
->method('__invoke')
->with($this->equalTo(func_get_arg(0)));
} else {
$mock
->expects($this->once())
->method('__invoke');
}
return $mock;
}
protected function expectCallableNever()
{
$mock = $this->createCallableMock();
$mock
->expects($this->never())
->method('__invoke');
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('CallableStub')->getMock();
}
protected function expectPromiseResolve($promise)
{
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
$that = $this;
$promise->then(null, function($error) use ($that) {
$that->assertNull($error);
$that->fail('promise rejected');
});
$promise->then($this->expectCallableOnce(), $this->expectCallableNever());
return $promise;
}
protected function expectPromiseReject($promise)
{
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
$that = $this;
$promise->then(function($value) use ($that) {
$that->assertNull($value);
$that->fail('promise resolved');
});
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
return $promise;
}
}
class CallableStub
{
public function __invoke()
{
}
}

445
vendor/composer/ClassLoader.php vendored Executable file
View File

@@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

56
vendor/composer/LICENSE vendored Executable file
View File

@@ -0,0 +1,56 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Composer
Upstream-Contact: Jordi Boggiano <j.boggiano@seld.be>
Source: https://github.com/composer/composer
Files: *
Copyright: 2016, Nils Adermann <naderman@naderman.de>
2016, Jordi Boggiano <j.boggiano@seld.be>
License: Expat
Files: src/Composer/Util/TlsHelper.php
Copyright: 2016, Nils Adermann <naderman@naderman.de>
2016, Jordi Boggiano <j.boggiano@seld.be>
2013, Evan Coury <me@evancoury.com>
License: Expat and BSD-2-Clause
License: BSD-2-Clause
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
.
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License: Expat
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.

9
vendor/composer/autoload_classmap.php vendored Executable file
View File

@@ -0,0 +1,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

16
vendor/composer/autoload_files.php vendored Executable file
View File

@@ -0,0 +1,16 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php',
'972fda704d680a3a53c68e34e193cb22' => $vendorDir . '/react/promise-timer/src/functions_include.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
'ebf8799635f67b5d7248946fe2154f4a' => $vendorDir . '/ringcentral/psr7/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
);

10
vendor/composer/autoload_namespaces.php vendored Executable file
View File

@@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Evenement' => array($vendorDir . '/evenement/evenement/src'),
);

Some files were not shown because too many files have changed in this diff Show More