You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
637 lines
16 KiB
637 lines
16 KiB
'use strict' |
|
|
|
var bl = require('bl') |
|
var inherits = require('inherits') |
|
var EE = require('events').EventEmitter |
|
var Packet = require('./packet') |
|
var constants = require('./constants') |
|
|
|
function Parser (opt) { |
|
if (!(this instanceof Parser)) return new Parser(opt) |
|
|
|
this.settings = opt || {} |
|
|
|
this._states = [ |
|
'_parseHeader', |
|
'_parseLength', |
|
'_parsePayload', |
|
'_newPacket' |
|
] |
|
|
|
this._resetState() |
|
} |
|
|
|
inherits(Parser, EE) |
|
|
|
Parser.prototype._resetState = function () { |
|
this.packet = new Packet() |
|
this.error = null |
|
this._list = bl() |
|
this._stateCounter = 0 |
|
} |
|
|
|
Parser.prototype.parse = function (buf) { |
|
if (this.error) this._resetState() |
|
|
|
this._list.append(buf) |
|
|
|
while ((this.packet.length !== -1 || this._list.length > 0) && |
|
this[this._states[this._stateCounter]]() && |
|
!this.error) { |
|
this._stateCounter++ |
|
|
|
if (this._stateCounter >= this._states.length) this._stateCounter = 0 |
|
} |
|
|
|
return this._list.length |
|
} |
|
|
|
Parser.prototype._parseHeader = function () { |
|
// There is at least one byte in the buffer |
|
var zero = this._list.readUInt8(0) |
|
this.packet.cmd = constants.types[zero >> constants.CMD_SHIFT] |
|
this.packet.retain = (zero & constants.RETAIN_MASK) !== 0 |
|
this.packet.qos = (zero >> constants.QOS_SHIFT) & constants.QOS_MASK |
|
this.packet.dup = (zero & constants.DUP_MASK) !== 0 |
|
|
|
this._list.consume(1) |
|
|
|
return true |
|
} |
|
|
|
Parser.prototype._parseLength = function () { |
|
// There is at least one byte in the list |
|
var result = this._parseVarByteNum(true) |
|
|
|
if (result) { |
|
this.packet.length = result.value |
|
this._list.consume(result.bytes) |
|
} |
|
|
|
return !!result |
|
} |
|
|
|
Parser.prototype._parsePayload = function () { |
|
var result = false |
|
|
|
// Do we have a payload? Do we have enough data to complete the payload? |
|
// PINGs have no payload |
|
if (this.packet.length === 0 || this._list.length >= this.packet.length) { |
|
this._pos = 0 |
|
|
|
switch (this.packet.cmd) { |
|
case 'connect': |
|
this._parseConnect() |
|
break |
|
case 'connack': |
|
this._parseConnack() |
|
break |
|
case 'publish': |
|
this._parsePublish() |
|
break |
|
case 'puback': |
|
case 'pubrec': |
|
case 'pubrel': |
|
case 'pubcomp': |
|
this._parseConfirmation() |
|
break |
|
case 'subscribe': |
|
this._parseSubscribe() |
|
break |
|
case 'suback': |
|
this._parseSuback() |
|
break |
|
case 'unsubscribe': |
|
this._parseUnsubscribe() |
|
break |
|
case 'unsuback': |
|
this._parseUnsuback() |
|
break |
|
case 'pingreq': |
|
case 'pingresp': |
|
// These are empty, nothing to do |
|
break |
|
case 'disconnect': |
|
this._parseDisconnect() |
|
break |
|
case 'auth': |
|
this._parseAuth() |
|
break |
|
default: |
|
this._emitError(new Error('Not supported')) |
|
} |
|
|
|
result = true |
|
} |
|
|
|
return result |
|
} |
|
|
|
Parser.prototype._parseConnect = function () { |
|
var protocolId // Protocol ID |
|
var clientId // Client ID |
|
var topic // Will topic |
|
var payload // Will payload |
|
var password // Password |
|
var username // Username |
|
var flags = {} |
|
var packet = this.packet |
|
|
|
// Parse protocolId |
|
protocolId = this._parseString() |
|
|
|
if (protocolId === null) return this._emitError(new Error('Cannot parse protocolId')) |
|
if (protocolId !== 'MQTT' && protocolId !== 'MQIsdp') { |
|
return this._emitError(new Error('Invalid protocolId')) |
|
} |
|
|
|
packet.protocolId = protocolId |
|
|
|
// Parse constants version number |
|
if (this._pos >= this._list.length) return this._emitError(new Error('Packet too short')) |
|
|
|
packet.protocolVersion = this._list.readUInt8(this._pos) |
|
|
|
if (packet.protocolVersion !== 3 && packet.protocolVersion !== 4 && packet.protocolVersion !== 5) { |
|
return this._emitError(new Error('Invalid protocol version')) |
|
} |
|
|
|
this._pos++ |
|
|
|
if (this._pos >= this._list.length) { |
|
return this._emitError(new Error('Packet too short')) |
|
} |
|
|
|
// Parse connect flags |
|
flags.username = (this._list.readUInt8(this._pos) & constants.USERNAME_MASK) |
|
flags.password = (this._list.readUInt8(this._pos) & constants.PASSWORD_MASK) |
|
flags.will = (this._list.readUInt8(this._pos) & constants.WILL_FLAG_MASK) |
|
|
|
if (flags.will) { |
|
packet.will = {} |
|
packet.will.retain = (this._list.readUInt8(this._pos) & constants.WILL_RETAIN_MASK) !== 0 |
|
packet.will.qos = (this._list.readUInt8(this._pos) & |
|
constants.WILL_QOS_MASK) >> constants.WILL_QOS_SHIFT |
|
} |
|
|
|
packet.clean = (this._list.readUInt8(this._pos) & constants.CLEAN_SESSION_MASK) !== 0 |
|
this._pos++ |
|
|
|
// Parse keepalive |
|
packet.keepalive = this._parseNum() |
|
if (packet.keepalive === -1) return this._emitError(new Error('Packet too short')) |
|
|
|
// parse properties |
|
if (packet.protocolVersion === 5) { |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
// Parse clientId |
|
clientId = this._parseString() |
|
if (clientId === null) return this._emitError(new Error('Packet too short')) |
|
packet.clientId = clientId |
|
|
|
if (flags.will) { |
|
if (packet.protocolVersion === 5) { |
|
var willProperties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(willProperties).length) { |
|
packet.will.properties = willProperties |
|
} |
|
} |
|
// Parse will topic |
|
topic = this._parseString() |
|
if (topic === null) return this._emitError(new Error('Cannot parse will topic')) |
|
packet.will.topic = topic |
|
|
|
// Parse will payload |
|
payload = this._parseBuffer() |
|
if (payload === null) return this._emitError(new Error('Cannot parse will payload')) |
|
packet.will.payload = payload |
|
} |
|
|
|
// Parse username |
|
if (flags.username) { |
|
username = this._parseString() |
|
if (username === null) return this._emitError(new Error('Cannot parse username')) |
|
packet.username = username |
|
} |
|
|
|
// Parse password |
|
if (flags.password) { |
|
password = this._parseBuffer() |
|
if (password === null) return this._emitError(new Error('Cannot parse password')) |
|
packet.password = password |
|
} |
|
// need for right parse auth packet and self set up |
|
this.settings = packet |
|
|
|
return packet |
|
} |
|
|
|
Parser.prototype._parseConnack = function () { |
|
var packet = this.packet |
|
|
|
if (this._list.length < 2) return null |
|
|
|
packet.sessionPresent = !!(this._list.readUInt8(this._pos++) & constants.SESSIONPRESENT_MASK) |
|
if (this.settings.protocolVersion === 5) { |
|
packet.reasonCode = this._list.readUInt8(this._pos++) |
|
} else { |
|
packet.returnCode = this._list.readUInt8(this._pos++) |
|
} |
|
|
|
if (packet.returnCode === -1 || packet.reasonCode === -1) return this._emitError(new Error('Cannot parse return code')) |
|
// mqtt 5 properties |
|
if (this.settings.protocolVersion === 5) { |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
} |
|
|
|
Parser.prototype._parsePublish = function () { |
|
var packet = this.packet |
|
packet.topic = this._parseString() |
|
|
|
if (packet.topic === null) return this._emitError(new Error('Cannot parse topic')) |
|
|
|
// Parse messageId |
|
if (packet.qos > 0) if (!this._parseMessageId()) { return } |
|
|
|
// Properties mqtt 5 |
|
if (this.settings.protocolVersion === 5) { |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
|
|
packet.payload = this._list.slice(this._pos, packet.length) |
|
} |
|
|
|
Parser.prototype._parseSubscribe = function () { |
|
var packet = this.packet |
|
var topic |
|
var options |
|
var qos |
|
var rh |
|
var rap |
|
var nl |
|
var subscription |
|
|
|
if (packet.qos !== 1) { |
|
return this._emitError(new Error('Wrong subscribe header')) |
|
} |
|
|
|
packet.subscriptions = [] |
|
|
|
if (!this._parseMessageId()) { return } |
|
|
|
// Properties mqtt 5 |
|
if (this.settings.protocolVersion === 5) { |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
|
|
while (this._pos < packet.length) { |
|
// Parse topic |
|
topic = this._parseString() |
|
if (topic === null) return this._emitError(new Error('Cannot parse topic')) |
|
if (this._pos >= packet.length) return this._emitError(new Error('Malformed Subscribe Payload')) |
|
|
|
options = this._parseByte() |
|
qos = options & constants.SUBSCRIBE_OPTIONS_QOS_MASK |
|
nl = ((options >> constants.SUBSCRIBE_OPTIONS_NL_SHIFT) & constants.SUBSCRIBE_OPTIONS_NL_MASK) !== 0 |
|
rap = ((options >> constants.SUBSCRIBE_OPTIONS_RAP_SHIFT) & constants.SUBSCRIBE_OPTIONS_RAP_MASK) !== 0 |
|
rh = (options >> constants.SUBSCRIBE_OPTIONS_RH_SHIFT) & constants.SUBSCRIBE_OPTIONS_RH_MASK |
|
|
|
subscription = { topic: topic, qos: qos } |
|
|
|
// mqtt 5 options |
|
if (this.settings.protocolVersion === 5) { |
|
subscription.nl = nl |
|
subscription.rap = rap |
|
subscription.rh = rh |
|
} |
|
|
|
// Push pair to subscriptions |
|
packet.subscriptions.push(subscription) |
|
} |
|
} |
|
|
|
Parser.prototype._parseSuback = function () { |
|
var packet = this.packet |
|
this.packet.granted = [] |
|
|
|
if (!this._parseMessageId()) { return } |
|
|
|
// Properties mqtt 5 |
|
if (this.settings.protocolVersion === 5) { |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
|
|
// Parse granted QoSes |
|
while (this._pos < this.packet.length) { |
|
this.packet.granted.push(this._list.readUInt8(this._pos++)) |
|
} |
|
} |
|
|
|
Parser.prototype._parseUnsubscribe = function () { |
|
var packet = this.packet |
|
|
|
packet.unsubscriptions = [] |
|
|
|
// Parse messageId |
|
if (!this._parseMessageId()) { return } |
|
|
|
// Properties mqtt 5 |
|
if (this.settings.protocolVersion === 5) { |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
|
|
while (this._pos < packet.length) { |
|
var topic |
|
|
|
// Parse topic |
|
topic = this._parseString() |
|
if (topic === null) return this._emitError(new Error('Cannot parse topic')) |
|
|
|
// Push topic to unsubscriptions |
|
packet.unsubscriptions.push(topic) |
|
} |
|
} |
|
|
|
Parser.prototype._parseUnsuback = function () { |
|
var packet = this.packet |
|
if (!this._parseMessageId()) return this._emitError(new Error('Cannot parse messageId')) |
|
// Properties mqtt 5 |
|
if (this.settings.protocolVersion === 5) { |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
// Parse granted QoSes |
|
packet.granted = [] |
|
while (this._pos < this.packet.length) { |
|
this.packet.granted.push(this._list.readUInt8(this._pos++)) |
|
} |
|
} |
|
} |
|
|
|
// parse packets like puback, pubrec, pubrel, pubcomp |
|
Parser.prototype._parseConfirmation = function () { |
|
var packet = this.packet |
|
|
|
this._parseMessageId() |
|
|
|
if (this.settings.protocolVersion === 5) { |
|
if (packet.length > 2) { |
|
// response code |
|
packet.reasonCode = this._parseByte() |
|
// properies mqtt 5 |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
} |
|
|
|
return true |
|
} |
|
|
|
// parse disconnect packet |
|
Parser.prototype._parseDisconnect = function () { |
|
var packet = this.packet |
|
|
|
if (this.settings.protocolVersion === 5) { |
|
// response code |
|
packet.reasonCode = this._parseByte() |
|
// properies mqtt 5 |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
} |
|
|
|
return true |
|
} |
|
|
|
// parse auth packet |
|
Parser.prototype._parseAuth = function () { |
|
var packet = this.packet |
|
|
|
if (this.settings.protocolVersion !== 5) { |
|
return this._emitError(new Error('Not supported auth packet for this version MQTT')) |
|
} |
|
|
|
// response code |
|
packet.reasonCode = this._parseByte() |
|
// properies mqtt 5 |
|
var properties = this._parseProperties() |
|
if (Object.getOwnPropertyNames(properties).length) { |
|
packet.properties = properties |
|
} |
|
|
|
return true |
|
} |
|
|
|
Parser.prototype._parseMessageId = function () { |
|
var packet = this.packet |
|
|
|
packet.messageId = this._parseNum() |
|
|
|
if (packet.messageId === null) { |
|
this._emitError(new Error('Cannot parse messageId')) |
|
return false |
|
} |
|
|
|
return true |
|
} |
|
|
|
Parser.prototype._parseString = function (maybeBuffer) { |
|
var length = this._parseNum() |
|
var result |
|
var end = length + this._pos |
|
|
|
if (length === -1 || end > this._list.length || end > this.packet.length) return null |
|
|
|
result = this._list.toString('utf8', this._pos, end) |
|
this._pos += length |
|
|
|
return result |
|
} |
|
|
|
Parser.prototype._parseStringPair = function () { |
|
return { |
|
name: this._parseString(), |
|
value: this._parseString() |
|
} |
|
} |
|
|
|
Parser.prototype._parseBuffer = function () { |
|
var length = this._parseNum() |
|
var result |
|
var end = length + this._pos |
|
|
|
if (length === -1 || end > this._list.length || end > this.packet.length) return null |
|
|
|
result = this._list.slice(this._pos, end) |
|
|
|
this._pos += length |
|
|
|
return result |
|
} |
|
|
|
Parser.prototype._parseNum = function () { |
|
if (this._list.length - this._pos < 2) return -1 |
|
|
|
var result = this._list.readUInt16BE(this._pos) |
|
this._pos += 2 |
|
|
|
return result |
|
} |
|
|
|
Parser.prototype._parse4ByteNum = function () { |
|
if (this._list.length - this._pos < 4) return -1 |
|
|
|
var result = this._list.readUInt32BE(this._pos) |
|
this._pos += 4 |
|
|
|
return result |
|
} |
|
|
|
Parser.prototype._parseVarByteNum = function (fullInfoFlag) { |
|
var bytes = 0 |
|
var mul = 1 |
|
var length = 0 |
|
var result = true |
|
var current |
|
var padding = this._pos ? this._pos : 0 |
|
|
|
while (bytes < 5) { |
|
current = this._list.readUInt8(padding + bytes++) |
|
length += mul * (current & constants.LENGTH_MASK) |
|
mul *= 0x80 |
|
|
|
if ((current & constants.LENGTH_FIN_MASK) === 0) break |
|
if (this._list.length <= bytes) { |
|
result = false |
|
break |
|
} |
|
} |
|
|
|
if (padding) { |
|
this._pos += bytes |
|
} |
|
|
|
result = result |
|
? fullInfoFlag ? { |
|
bytes: bytes, |
|
value: length |
|
} : length |
|
: false |
|
|
|
return result |
|
} |
|
|
|
Parser.prototype._parseByte = function () { |
|
var result = this._list.readUInt8(this._pos) |
|
this._pos++ |
|
return result |
|
} |
|
|
|
Parser.prototype._parseByType = function (type) { |
|
switch (type) { |
|
case 'byte': { |
|
return this._parseByte() !== 0 |
|
} |
|
case 'int8': { |
|
return this._parseByte() |
|
} |
|
case 'int16': { |
|
return this._parseNum() |
|
} |
|
case 'int32': { |
|
return this._parse4ByteNum() |
|
} |
|
case 'var': { |
|
return this._parseVarByteNum() |
|
} |
|
case 'string': { |
|
return this._parseString() |
|
} |
|
case 'pair': { |
|
return this._parseStringPair() |
|
} |
|
case 'binary': { |
|
return this._parseBuffer() |
|
} |
|
} |
|
} |
|
|
|
Parser.prototype._parseProperties = function () { |
|
var length = this._parseVarByteNum() |
|
var start = this._pos |
|
var end = start + length |
|
var result = {} |
|
while (this._pos < end) { |
|
var type = this._parseByte() |
|
var name = constants.propertiesCodes[type] |
|
if (!name) { |
|
this._emitError(new Error('Unknown property')) |
|
return false |
|
} |
|
// user properties process |
|
if (name === 'userProperties') { |
|
if (!result[name]) { |
|
result[name] = {} |
|
} |
|
var currentUserProperty = this._parseByType(constants.propertiesTypes[name]) |
|
if (result[name][currentUserProperty.name]) { |
|
if (Array.isArray(result[name][currentUserProperty.name])) { |
|
result[name][currentUserProperty.name].push(currentUserProperty.value) |
|
} else { |
|
var currentValue = result[name][currentUserProperty.name] |
|
result[name][currentUserProperty.name] = [currentValue] |
|
result[name][currentUserProperty.name].push(currentUserProperty.value) |
|
} |
|
} else { |
|
result[name][currentUserProperty.name] = currentUserProperty.value |
|
} |
|
continue |
|
} |
|
result[name] = this._parseByType(constants.propertiesTypes[name]) |
|
} |
|
return result |
|
} |
|
|
|
Parser.prototype._newPacket = function () { |
|
if (this.packet) { |
|
this._list.consume(this.packet.length) |
|
this.emit('packet', this.packet) |
|
} |
|
|
|
this.packet = new Packet() |
|
|
|
this._pos = 0 |
|
|
|
return true |
|
} |
|
|
|
Parser.prototype._emitError = function (err) { |
|
this.error = err |
|
this.emit('error', err) |
|
} |
|
|
|
module.exports = Parser
|
|
|