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