'use strict' var protocol = require('./constants') var Buffer = require('safe-buffer').Buffer var empty = Buffer.allocUnsafe(0) var zeroBuf = Buffer.from([0]) var numbers = require('./numbers') var nextTick = require('process-nextick-args').nextTick var numCache = numbers.cache var generateNumber = numbers.generateNumber var generateCache = numbers.generateCache var genBufVariableByteInt = numbers.genBufVariableByteInt var generate4ByteBuffer = numbers.generate4ByteBuffer var writeNumber = writeNumberCached var toGenerate = true function generate (packet, stream, opts) { if (stream.cork) { stream.cork() nextTick(uncork, stream) } if (toGenerate) { toGenerate = false generateCache() } switch (packet.cmd) { case 'connect': return connect(packet, stream, opts) case 'connack': return connack(packet, stream, opts) case 'publish': return publish(packet, stream, opts) case 'puback': case 'pubrec': case 'pubrel': case 'pubcomp': return confirmation(packet, stream, opts) case 'subscribe': return subscribe(packet, stream, opts) case 'suback': return suback(packet, stream, opts) case 'unsubscribe': return unsubscribe(packet, stream, opts) case 'unsuback': return unsuback(packet, stream, opts) case 'pingreq': case 'pingresp': return emptyPacket(packet, stream, opts) case 'disconnect': return disconnect(packet, stream, opts) case 'auth': return auth(packet, stream, opts) default: stream.emit('error', new Error('Unknown command')) return false } } /** * Controls numbers cache. * Set to "false" to allocate buffers on-the-flight instead of pre-generated cache */ Object.defineProperty(generate, 'cacheNumbers', { get: function () { return writeNumber === writeNumberCached }, set: function (value) { if (value) { if (!numCache || Object.keys(numCache).length === 0) toGenerate = true writeNumber = writeNumberCached } else { toGenerate = false writeNumber = writeNumberGenerated } } }) function uncork (stream) { stream.uncork() } function connect (packet, stream, opts) { var settings = packet || {} var protocolId = settings.protocolId || 'MQTT' var protocolVersion = settings.protocolVersion || 4 var will = settings.will var clean = settings.clean var keepalive = settings.keepalive || 0 var clientId = settings.clientId || '' var username = settings.username var password = settings.password /* mqtt5 new oprions */ var properties = settings.properties if (clean === undefined) clean = true var length = 0 // Must be a string and non-falsy if (!protocolId || (typeof protocolId !== 'string' && !Buffer.isBuffer(protocolId))) { stream.emit('error', new Error('Invalid protocolId')) return false } else length += protocolId.length + 2 // Must be 3 or 4 or 5 if (protocolVersion !== 3 && protocolVersion !== 4 && protocolVersion !== 5) { stream.emit('error', new Error('Invalid protocol version')) return false } else length += 1 // ClientId might be omitted in 3.1.1, but only if cleanSession is set to 1 if ((typeof clientId === 'string' || Buffer.isBuffer(clientId)) && (clientId || protocolVersion === 4) && (clientId || clean)) { length += clientId.length + 2 } else { if (protocolVersion < 4) { stream.emit('error', new Error('clientId must be supplied before 3.1.1')) return false } if ((clean * 1) === 0) { stream.emit('error', new Error('clientId must be given if cleanSession set to 0')) return false } } // Must be a two byte number if (typeof keepalive !== 'number' || keepalive < 0 || keepalive > 65535 || keepalive % 1 !== 0) { stream.emit('error', new Error('Invalid keepalive')) return false } else length += 2 // Connect flags length += 1 // Properties if (protocolVersion === 5) { var propertiesData = getProperties(stream, properties) length += propertiesData.length } // If will exists... if (will) { // It must be an object if (typeof will !== 'object') { stream.emit('error', new Error('Invalid will')) return false } // It must have topic typeof string if (!will.topic || typeof will.topic !== 'string') { stream.emit('error', new Error('Invalid will topic')) return false } else { length += Buffer.byteLength(will.topic) + 2 } // Payload length += 2 // payload length if (will.payload) { if (will.payload.length >= 0) { if (typeof will.payload === 'string') { length += Buffer.byteLength(will.payload) } else { length += will.payload.length } } else { stream.emit('error', new Error('Invalid will payload')) return false } } // will properties var willProperties = {} if (protocolVersion === 5) { willProperties = getProperties(stream, will.properties) length += willProperties.length } } // Username var providedUsername = false if (username != null) { if (isStringOrBuffer(username)) { providedUsername = true length += Buffer.byteLength(username) + 2 } else { stream.emit('error', new Error('Invalid username')) return false } } // Password if (password != null) { if (!providedUsername) { stream.emit('error', new Error('Username is required to use password')) return false } if (isStringOrBuffer(password)) { length += byteLength(password) + 2 } else { stream.emit('error', new Error('Invalid password')) return false } } // Generate header stream.write(protocol.CONNECT_HEADER) // Generate length writeVarByteInt(stream, length) // Generate protocol ID writeStringOrBuffer(stream, protocolId) stream.write( protocolVersion === 4 ? protocol.VERSION4 : protocolVersion === 5 ? protocol.VERSION5 : protocol.VERSION3 ) // Connect flags var flags = 0 flags |= (username != null) ? protocol.USERNAME_MASK : 0 flags |= (password != null) ? protocol.PASSWORD_MASK : 0 flags |= (will && will.retain) ? protocol.WILL_RETAIN_MASK : 0 flags |= (will && will.qos) ? will.qos << protocol.WILL_QOS_SHIFT : 0 flags |= will ? protocol.WILL_FLAG_MASK : 0 flags |= clean ? protocol.CLEAN_SESSION_MASK : 0 stream.write(Buffer.from([flags])) // Keepalive writeNumber(stream, keepalive) // Properties if (protocolVersion === 5) { propertiesData.write() } // Client ID writeStringOrBuffer(stream, clientId) // Will if (will) { if (protocolVersion === 5) { willProperties.write() } writeString(stream, will.topic) writeStringOrBuffer(stream, will.payload) } // Username and password if (username != null) { writeStringOrBuffer(stream, username) } if (password != null) { writeStringOrBuffer(stream, password) } // This is a small packet that happens only once on a stream // We assume the stream is always free to receive more data after this return true } function connack (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var rc = version === 5 ? settings.reasonCode : settings.returnCode var properties = settings.properties var length = 2 // length of rc and sessionHeader // Check return code if (typeof rc !== 'number') { stream.emit('error', new Error('Invalid return code')) return false } // mqtt5 properties var propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) length += propertiesData.length } stream.write(protocol.CONNACK_HEADER) // length writeVarByteInt(stream, length) stream.write(settings.sessionPresent ? protocol.SESSIONPRESENT_HEADER : zeroBuf) stream.write(Buffer.from([rc])) if (propertiesData != null) { propertiesData.write() } return true } function publish (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var qos = settings.qos || 0 var retain = settings.retain ? protocol.RETAIN_MASK : 0 var topic = settings.topic var payload = settings.payload || empty var id = settings.messageId var properties = settings.properties var length = 0 // Topic must be a non-empty string or Buffer if (typeof topic === 'string') length += Buffer.byteLength(topic) + 2 else if (Buffer.isBuffer(topic)) length += topic.length + 2 else { stream.emit('error', new Error('Invalid topic')) return false } // Get the payload length if (!Buffer.isBuffer(payload)) length += Buffer.byteLength(payload) else length += payload.length // Message ID must a number if qos > 0 if (qos && typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')) return false } else if (qos) length += 2 // mqtt5 properties var propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) length += propertiesData.length } // Header stream.write(protocol.PUBLISH_HEADER[qos][settings.dup ? 1 : 0][retain ? 1 : 0]) // Remaining length writeVarByteInt(stream, length) // Topic writeNumber(stream, byteLength(topic)) stream.write(topic) // Message ID if (qos > 0) writeNumber(stream, id) // Properties if (propertiesData != null) { propertiesData.write() } // Payload return stream.write(payload) } /* Puback, pubrec, pubrel and pubcomp */ function confirmation (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var type = settings.cmd || 'puback' var id = settings.messageId var dup = (settings.dup && type === 'pubrel') ? protocol.DUP_MASK : 0 var qos = 0 var reasonCode = settings.reasonCode var properties = settings.properties var length = version === 5 ? 3 : 2 if (type === 'pubrel') qos = 1 // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')) return false } // properies mqtt 5 var propertiesData = null if (version === 5) { propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) if (!propertiesData) { return false } length += propertiesData.length } // Header stream.write(protocol.ACKS[type][qos][dup][0]) // Length writeVarByteInt(stream, length) // Message ID writeNumber(stream, id) // reason code in header if (version === 5) { stream.write(Buffer.from([reasonCode])) } // properies mqtt 5 if (propertiesData !== null) { propertiesData.write() } return true } function subscribe (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var dup = settings.dup ? protocol.DUP_MASK : 0 var id = settings.messageId var subs = settings.subscriptions var properties = settings.properties var length = 0 // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')) return false } else length += 2 // properies mqtt 5 var propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) length += propertiesData.length } // Check subscriptions if (typeof subs === 'object' && subs.length) { for (var i = 0; i < subs.length; i += 1) { var itopic = subs[i].topic var iqos = subs[i].qos if (typeof itopic !== 'string') { stream.emit('error', new Error('Invalid subscriptions - invalid topic')) return false } if (typeof iqos !== 'number') { stream.emit('error', new Error('Invalid subscriptions - invalid qos')) return false } if (version === 5) { var nl = subs[i].nl || false if (typeof nl !== 'boolean') { stream.emit('error', new Error('Invalid subscriptions - invalid No Local')) return false } var rap = subs[i].rap || false if (typeof rap !== 'boolean') { stream.emit('error', new Error('Invalid subscriptions - invalid Retain as Published')) return false } var rh = subs[i].rh || 0 if (typeof rh !== 'number' || rh > 2) { stream.emit('error', new Error('Invalid subscriptions - invalid Retain Handling')) return false } } length += Buffer.byteLength(itopic) + 2 + 1 } } else { stream.emit('error', new Error('Invalid subscriptions')) return false } // Generate header stream.write(protocol.SUBSCRIBE_HEADER[1][dup ? 1 : 0][0]) // Generate length writeVarByteInt(stream, length) // Generate message ID writeNumber(stream, id) // properies mqtt 5 if (propertiesData !== null) { propertiesData.write() } var result = true // Generate subs for (var j = 0; j < subs.length; j++) { var sub = subs[j] var jtopic = sub.topic var jqos = sub.qos var jnl = +sub.nl var jrap = +sub.rap var jrh = sub.rh var joptions // Write topic string writeString(stream, jtopic) // options process joptions = protocol.SUBSCRIBE_OPTIONS_QOS[jqos] if (version === 5) { joptions |= jnl ? protocol.SUBSCRIBE_OPTIONS_NL : 0 joptions |= jrap ? protocol.SUBSCRIBE_OPTIONS_RAP : 0 joptions |= jrh ? protocol.SUBSCRIBE_OPTIONS_RH[jrh] : 0 } // Write options result = stream.write(Buffer.from([joptions])) } return result } function suback (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var id = settings.messageId var granted = settings.granted var properties = settings.properties var length = 0 // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')) return false } else length += 2 // Check granted qos vector if (typeof granted === 'object' && granted.length) { for (var i = 0; i < granted.length; i += 1) { if (typeof granted[i] !== 'number') { stream.emit('error', new Error('Invalid qos vector')) return false } length += 1 } } else { stream.emit('error', new Error('Invalid qos vector')) return false } // properies mqtt 5 var propertiesData = null if (version === 5) { propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) if (!propertiesData) { return false } length += propertiesData.length } // header stream.write(protocol.SUBACK_HEADER) // Length writeVarByteInt(stream, length) // Message ID writeNumber(stream, id) // properies mqtt 5 if (propertiesData !== null) { propertiesData.write() } return stream.write(Buffer.from(granted)) } function unsubscribe (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var id = settings.messageId var dup = settings.dup ? protocol.DUP_MASK : 0 var unsubs = settings.unsubscriptions var properties = settings.properties var length = 0 // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')) return false } else { length += 2 } // Check unsubs if (typeof unsubs === 'object' && unsubs.length) { for (var i = 0; i < unsubs.length; i += 1) { if (typeof unsubs[i] !== 'string') { stream.emit('error', new Error('Invalid unsubscriptions')) return false } length += Buffer.byteLength(unsubs[i]) + 2 } } else { stream.emit('error', new Error('Invalid unsubscriptions')) return false } // properies mqtt 5 var propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) length += propertiesData.length } // Header stream.write(protocol.UNSUBSCRIBE_HEADER[1][dup ? 1 : 0][0]) // Length writeVarByteInt(stream, length) // Message ID writeNumber(stream, id) // properies mqtt 5 if (propertiesData !== null) { propertiesData.write() } // Unsubs var result = true for (var j = 0; j < unsubs.length; j++) { result = writeString(stream, unsubs[j]) } return result } function unsuback (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var id = settings.messageId var dup = settings.dup ? protocol.DUP_MASK : 0 var granted = settings.granted var properties = settings.properties var type = settings.cmd var qos = 0 var length = 2 // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')) return false } // Check granted if (version === 5) { if (typeof granted === 'object' && granted.length) { for (var i = 0; i < granted.length; i += 1) { if (typeof granted[i] !== 'number') { stream.emit('error', new Error('Invalid qos vector')) return false } length += 1 } } else { stream.emit('error', new Error('Invalid qos vector')) return false } } // properies mqtt 5 var propertiesData = null if (version === 5) { propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) if (!propertiesData) { return false } length += propertiesData.length } // Header stream.write(protocol.ACKS[type][qos][dup][0]) // Length writeVarByteInt(stream, length) // Message ID writeNumber(stream, id) // properies mqtt 5 if (propertiesData !== null) { propertiesData.write() } // payload if (version === 5) { stream.write(Buffer.from(granted)) } return true } function emptyPacket (packet, stream, opts) { return stream.write(protocol.EMPTY[packet.cmd]) } function disconnect (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var reasonCode = settings.reasonCode var properties = settings.properties var length = version === 5 ? 1 : 0 // properies mqtt 5 var propertiesData = null if (version === 5) { propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) if (!propertiesData) { return false } length += propertiesData.length } // Header stream.write(Buffer.from([protocol.codes['disconnect'] << 4])) // Length writeVarByteInt(stream, length) // reason code in header if (version === 5) { stream.write(Buffer.from([reasonCode])) } // properies mqtt 5 if (propertiesData !== null) { propertiesData.write() } return true } function auth (packet, stream, opts) { var version = opts ? opts.protocolVersion : 4 var settings = packet || {} var reasonCode = settings.reasonCode var properties = settings.properties var length = version === 5 ? 1 : 0 if (version !== 5) stream.emit('error', new Error('Invalid mqtt version for auth packet')) // properies mqtt 5 var propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) if (!propertiesData) { return false } length += propertiesData.length // Header stream.write(Buffer.from([protocol.codes['auth'] << 4])) // Length writeVarByteInt(stream, length) // reason code in header stream.write(Buffer.from([reasonCode])) // properies mqtt 5 if (propertiesData !== null) { propertiesData.write() } return true } /** * writeVarByteInt - write an MQTT style variable byte integer to the buffer * * @param buffer - destination * @param pos - offset * @param length - length (>0) * @returns number of bytes written * * @api private */ var varByteIntCache = {} function writeVarByteInt (stream, num) { var buffer = varByteIntCache[num] if (!buffer) { buffer = genBufVariableByteInt(num).data if (num < 16384) varByteIntCache[num] = buffer } stream.write(buffer) } /** * writeString - write a utf8 string to the buffer * * @param buffer - destination * @param pos - offset * @param string - string to write * @return number of bytes written * * @api private */ function writeString (stream, string) { var strlen = Buffer.byteLength(string) writeNumber(stream, strlen) stream.write(string, 'utf8') } /** * writeStringPair - write a utf8 string pairs to the buffer * * @param buffer - destination * @param name - string name to write * @param value - string value to write * @return number of bytes written * * @api private */ function writeStringPair (stream, name, value) { writeString(stream, name) writeString(stream, value) } /** * writeNumber - write a two byte number to the buffer * * @param buffer - destination * @param pos - offset * @param number - number to write * @return number of bytes written * * @api private */ function writeNumberCached (stream, number) { return stream.write(numCache[number]) } function writeNumberGenerated (stream, number) { return stream.write(generateNumber(number)) } function write4ByteNumber (stream, number) { return stream.write(generate4ByteBuffer(number)) } /** * writeStringOrBuffer - write a String or Buffer with the its length prefix * * @param buffer - destination * @param pos - offset * @param toWrite - String or Buffer * @return number of bytes written */ function writeStringOrBuffer (stream, toWrite) { if (typeof toWrite === 'string') { writeString(stream, toWrite) } else if (toWrite) { writeNumber(stream, toWrite.length) stream.write(toWrite) } else writeNumber(stream, 0) } function getProperties (stream, properties) { /* connect properties */ if (typeof properties !== 'object' || properties.length != null) { return { length: 1, write: function () { writeProperties(stream, {}, 0) } } } var propertiesLength = 0 function getLengthProperty (name) { var type = protocol.propertiesTypes[name] var value = properties[name] var length = 0 switch (type) { case 'byte': { if (typeof value !== 'boolean') { stream.emit('error', new Error('Invalid ' + name)) return false } length += 1 + 1 break } case 'int8': { if (typeof value !== 'number') { stream.emit('error', new Error('Invalid ' + name)) return false } length += 1 + 1 break } case 'binary': { if (value && value === null) { stream.emit('error', new Error('Invalid ' + name)) return false } length += 1 + Buffer.byteLength(value) + 2 break } case 'int16': { if (typeof value !== 'number') { stream.emit('error', new Error('Invalid ' + name)) return false } length += 1 + 2 break } case 'int32': { if (typeof value !== 'number') { stream.emit('error', new Error('Invalid ' + name)) return false } length += 1 + 4 break } case 'var': { if (typeof value !== 'number') { stream.emit('error', new Error('Invalid ' + name)) return false } length += 1 + genBufVariableByteInt(value).length break } case 'string': { if (typeof value !== 'string') { stream.emit('error', new Error('Invalid ' + name)) return false } length += 1 + 2 + Buffer.byteLength(value.toString()) break } case 'pair': { if (typeof value !== 'object') { stream.emit('error', new Error('Invalid ' + name)) return false } length += Object.getOwnPropertyNames(value).reduce(function (result, name) { var currentValue = value[name] if (Array.isArray(currentValue)) { result += currentValue.reduce(function (currentLength, value) { currentLength += 1 + 2 + Buffer.byteLength(name.toString()) + 2 + Buffer.byteLength(value.toString()) return currentLength }, 0) } else { result += 1 + 2 + Buffer.byteLength(name.toString()) + 2 + Buffer.byteLength(value[name].toString()) } return result }, 0) break } default: { stream.emit('error', new Error('Invalid property ' + name)) return false } } return length } if (properties) { for (var propName in properties) { var propLength = getLengthProperty(propName) if (!propLength) return false propertiesLength += propLength } } var propertiesLengthLength = genBufVariableByteInt(propertiesLength).length return { length: propertiesLengthLength + propertiesLength, write: function () { writeProperties(stream, properties, propertiesLength) } } } function getPropertiesByMaximumPacketSize (stream, properties, opts, length) { var mayEmptyProps = ['reasonString', 'userProperties'] var maximumPacketSize = opts && opts.properties && opts.properties.maximumPacketSize ? opts.properties.maximumPacketSize : 0 var propertiesData = getProperties(stream, properties) if (maximumPacketSize) { while (length + propertiesData.length > maximumPacketSize) { var currentMayEmptyProp = mayEmptyProps.shift() if (currentMayEmptyProp && properties[currentMayEmptyProp]) { delete properties[currentMayEmptyProp] propertiesData = getProperties(stream, properties) } else { return false } } } return propertiesData } function writeProperties (stream, properties, propertiesLength) { /* write properties to stream */ writeVarByteInt(stream, propertiesLength) for (var propName in properties) { if (properties.hasOwnProperty(propName) && properties[propName] !== null) { var value = properties[propName] var type = protocol.propertiesTypes[propName] switch (type) { case 'byte': { stream.write(Buffer.from([protocol.properties[propName]])) stream.write(Buffer.from([+value])) break } case 'int8': { stream.write(Buffer.from([protocol.properties[propName]])) stream.write(Buffer.from([value])) break } case 'binary': { stream.write(Buffer.from([protocol.properties[propName]])) writeStringOrBuffer(stream, value) break } case 'int16': { stream.write(Buffer.from([protocol.properties[propName]])) writeNumber(stream, value) break } case 'int32': { stream.write(Buffer.from([protocol.properties[propName]])) write4ByteNumber(stream, value) break } case 'var': { stream.write(Buffer.from([protocol.properties[propName]])) writeVarByteInt(stream, value) break } case 'string': { stream.write(Buffer.from([protocol.properties[propName]])) writeString(stream, value) break } case 'pair': { Object.getOwnPropertyNames(value).forEach(function (name) { var currentValue = value[name] if (Array.isArray(currentValue)) { currentValue.forEach(function (value) { stream.write(Buffer.from([protocol.properties[propName]])) writeStringPair(stream, name.toString(), value.toString()) }) } else { stream.write(Buffer.from([protocol.properties[propName]])) writeStringPair(stream, name.toString(), currentValue.toString()) } }) break } default: { stream.emit('error', new Error('Invalid property ' + propName)) return false } } } } } function byteLength (bufOrString) { if (!bufOrString) return 0 else if (bufOrString instanceof Buffer) return bufOrString.length else return Buffer.byteLength(bufOrString) } function isStringOrBuffer (field) { return typeof field === 'string' || field instanceof Buffer } module.exports = generate