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

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