pax_global_header00006660000000000000000000000064147615530240014521gustar00rootroot0000000000000052 comment=e39fb28c10628720fb50e5eb355492684ff0caaa mqtt-packet-9.0.2/000077500000000000000000000000001476155302400137635ustar00rootroot00000000000000mqtt-packet-9.0.2/.github/000077500000000000000000000000001476155302400153235ustar00rootroot00000000000000mqtt-packet-9.0.2/.github/workflows/000077500000000000000000000000001476155302400173605ustar00rootroot00000000000000mqtt-packet-9.0.2/.github/workflows/ci.yml000066400000000000000000000006721476155302400205030ustar00rootroot00000000000000name: ci on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install run: | npm install - name: Run tests run: | npm run ci mqtt-packet-9.0.2/.gitignore000066400000000000000000000010271476155302400157530ustar00rootroot00000000000000# Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Deployed apps should consider commenting this line out: # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules mqtt-packet-9.0.2/.npmrc000066400000000000000000000000231476155302400150760ustar00rootroot00000000000000package-lock=false mqtt-packet-9.0.2/CONTRIBUTING.md000066400000000000000000000023251476155302400162160ustar00rootroot00000000000000# mqtt-packet is an OPEN Open Source Project ----------------------------------------- ## What? Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. ## Rules There are a few basic ground-rules for contributors: 1. **No `--force` pushes** or modifying the Git history in any way. 1. **Non-master branches** ought to be used for ongoing work. 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 1. Contributors should attempt to adhere to the prevailing code-style. ## Releases Declaring formal releases remains the prerogative of the project maintainer. ## Changes to this arrangement This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. ----------------------------------------- mqtt-packet-9.0.2/LICENSE.md000066400000000000000000000023411476155302400153670ustar00rootroot00000000000000The MIT License (MIT) ===================== Copyright (c) 2014-2017 mqtt-packet contributors --------------------------------------- *mqtt-packet contributors listed at * 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. mqtt-packet-9.0.2/README.md000066400000000000000000000255741476155302400152570ustar00rootroot00000000000000mqtt-packet =========== Encode and Decode MQTT 3.1.1, 5.0 packets the node way. [![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) * Installation * Examples * Packets * API * Contributing * License & copyright This library is tested with node v6, v8, v10, v12 and v14. The last version to support older versions of node was mqtt-packet@4.1.2. Installation ------------ ```bash npm install mqtt-packet --save ``` Examples -------- ### Generating ```js const mqtt = require('mqtt-packet'); const object = { cmd: 'publish', retain: false, qos: 0, dup: false, length: 10, topic: 'test', payload: 'test' // Can also be a Buffer }; const opts = { protocolVersion: 4 }; // default is 4. Usually, opts is a connect packet console.log(mqtt.generate(object)) // Prints: // // // // Which is the same as: // // Buffer.from([ // 48, 10, // Header (publish) // 0, 4, // Topic length // 116, 101, 115, 116, // Topic (test) // 116, 101, 115, 116 // Payload (test) // ]) ``` ### Parsing ```js const mqtt = require('mqtt-packet'); const opts = { protocolVersion: 4 }; // default is 4. Usually, opts is a connect packet const parser = mqtt.parser(opts); // Synchronously emits all the parsed packets parser.on('packet', packet => { console.log(packet) // Prints: // // { // cmd: 'publish', // retain: false, // qos: 0, // dup: false, // length: 10, // topic: 'test', // payload: // } }) parser.parse(Buffer.from([ 48, 10, // Header (publish) 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 116, 101, 115, 116 // Payload (test) ])) // Returns the number of bytes left in the parser ``` API --- * mqtt#generate() * mqtt#writeToStream() * mqtt#parser() ### mqtt.generate(object, [opts]) Generates a `Buffer` containing an MQTT packet. The object must be one of the ones specified by the [packets](#packets) section. Throws an `Error` if a packet cannot be generated. ### mqtt.writeToStream(object, stream, [opts]) Writes the mqtt packet defined by `object` to the given stream. The object must be one of the ones specified by the [packets](#packets) section. Emits an `Error` on the stream if a packet cannot be generated. On node >= 0.12, this function automatically calls `cork()` on your stream, and then it calls `uncork()` on the next tick. By default cache for number buffers is enabled. It creates a list of buffers for faster write. To disable cache set `mqtt.writeToStream.cacheNumbers = false`. Should be set before any `writeToStream` calls. ### mqtt.parser([opts]) Returns a new `Parser` object. `Parser` inherits from `EventEmitter` and will emit: * `packet`, when a new packet is parsed, according to [packets](#packets) * `error`, if an error happens #### Parser.parse(buffer) Parses a given `Buffer` and emits synchronously all the MQTT packets that are included. Returns the number of bytes left to parse. If an error happens, an `error` event will be emitted, but no `packet` events will be emitted after that. Calling `parse()` again clears the error and previous buffer, as if you created a new `Parser`. Packets ------- This section describes the format of all packets emitted by the `Parser` and that you can input to `generate`. ### Connect ```js { cmd: 'connect', protocolId: 'MQTT', // Or 'MQIsdp' in MQTT 3.1 and 5.0 protocolVersion: 4, // Or 3 in MQTT 3.1, or 5 in MQTT 5.0 clean: true, // Can also be false clientId: 'my-device', keepalive: 0, // Seconds which can be any positive number, with 0 as the default setting username: 'matteo', password: Buffer.from('collina'), // Passwords are buffers will: { topic: 'mydevice/status', payload: Buffer.from('dead'), // Payloads are buffers properties: { // MQTT 5.0 willDelayInterval: 1234, payloadFormatIndicator: false, messageExpiryInterval: 4321, contentType: 'test', responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { 'test': 'test' } } }, properties: { // MQTT 5.0 properties sessionExpiryInterval: 1234, receiveMaximum: 432, maximumPacketSize: 100, topicAliasMaximum: 456, requestResponseInformation: true, requestProblemInformation: true, userProperties: { 'test': 'test' }, authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) } } ``` If `protocolVersion` is 3, `clientId` is mandatory and `generate` will throw if missing. If `password` or `will.payload` are passed as strings, they will automatically be converted into a `Buffer`. ### Connack ```js { cmd: 'connack', returnCode: 0, // Or whatever else you see fit MQTT < 5.0 sessionPresent: false, // Can also be true. reasonCode: 0, // reason code MQTT 5.0 properties: { // MQTT 5.0 properties sessionExpiryInterval: 1234, receiveMaximum: 432, maximumQoS: 1, retainAvailable: true, maximumPacketSize: 100, assignedClientIdentifier: 'test', topicAliasMaximum: 456, reasonString: 'test', userProperties: { 'test': 'test' }, wildcardSubscriptionAvailable: true, subscriptionIdentifiersAvailable: true, sharedSubscriptionAvailable: false, serverKeepAlive: 1234, responseInformation: 'test', serverReference: 'test', authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) } } ``` The only mandatory argument is `returnCode`, as `generate` will throw if missing. ### Subscribe ```js { cmd: 'subscribe', messageId: 42, properties: { // MQTT 5.0 properties subscriptionIdentifier: 145, userProperties: { test: 'test' } } subscriptions: [{ topic: 'test', qos: 0, nl: false, // no Local MQTT 5.0 flag rap: true, // Retain as Published MQTT 5.0 flag rh: 1 // Retain Handling MQTT 5.0 }] } ``` All properties are mandatory. ### Suback ```js { cmd: 'suback', messageId: 42, properties: { // MQTT 5.0 properties reasonString: 'test', userProperties: { 'test': 'test' } } granted: [0, 1, 2, 128] } ``` All the granted qos __must__ be < 256, as they are encoded as UInt8. All properties are mandatory. ### Unsubscribe ```js { cmd: 'unsubscribe', messageId: 42, properties: { // MQTT 5.0 properties userProperties: { 'test': 'test' } } unsubscriptions: [ 'test', 'a/topic' ] } ``` All properties are mandatory. ### Unsuback ```js { cmd: 'unsuback', messageId: 42, properties: { // MQTT 5.0 properties reasonString: 'test', userProperties: { 'test': 'test' } } } ``` All properties are mandatory. ### Publish ```js { cmd: 'publish', messageId: 42, qos: 2, dup: false, topic: 'test', payload: Buffer.from('test'), retain: false, properties: { // optional properties MQTT 5.0 payloadFormatIndicator: true, messageExpiryInterval: 4321, topicAlias: 100, responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { 'test': 'test' }, subscriptionIdentifier: 120, // can be an Array in message from broker, if message included in few another subscriptions contentType: 'test' } } ``` Only the `topic` property is mandatory. Both `topic` and `payload` can be `Buffer` objects instead of strings. `messageId` is mandatory for `qos > 0`. ### Puback ```js { cmd: 'puback', messageId: 42, reasonCode: 16, // only for MQTT 5.0 properties: { // MQTT 5.0 properties reasonString: 'test', userProperties: { 'test': 'test' } } } ``` The only mandatory property is `messageId`, as `generate` will throw if missing. ### Pubrec ```js { cmd: 'pubrec', messageId: 42, reasonCode: 16, // only for MQTT 5.0 properties: { // properties MQTT 5.0 reasonString: 'test', userProperties: { 'test': 'test' } } } ``` The only mandatory property is `messageId`, as `generate` will throw if missing. ### Pubrel ```js { cmd: 'pubrel', messageId: 42, reasonCode: 16, // only for MQTT 5.0 properties: { // properties MQTT 5.0 reasonString: 'test', userProperties: { 'test': 'test' } } } ``` The only mandatory property is `messageId`, as `generate` will throw if missing. ### Pubcomp ```js { cmd: 'pubcomp', messageId: 42, reasonCode: 16, // only for MQTT 5.0 properties: { // properties MQTT 5.0 reasonString: 'test', userProperties: { 'test': 'test' } } } ``` The only mandatory property is `messageId`, as `generate` will throw if missing. ### Pingreq ```js { cmd: 'pingreq' } ``` ### Pingresp ```js { cmd: 'pingresp' } ``` ### Disconnect ```js { cmd: 'disconnect', reasonCode: 0, // MQTT 5.0 code properties: { // properties MQTT 5.0 sessionExpiryInterval: 145, reasonString: 'test', userProperties: { 'test': 'test' }, serverReference: 'test' } } ``` ### Auth ```js { cmd: 'auth', reasonCode: 0, // MQTT 5.0 code properties: { // properties MQTT 5.0 authenticationMethod: 'test', authenticationData: Buffer.from([0, 1, 2, 3]), reasonString: 'test', userProperties: { 'test': 'test' } } } ``` Contributing ------------ mqtt-packet is an **OPEN Open Source Project**. This means that: > Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. See the [CONTRIBUTING.md](https://github.com/mqttjs/mqtt-packet/blob/master/CONTRIBUTING.md) file for more details. ### Contributors mqtt-packet is only possible due to the excellent work of the following contributors:
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
Adam RuddGitHub/adamvrTwitter/@adam_vr
Peter SorowkaGitHub/psorowkaTwitter/@psorowka
Siarhei BuntsevichGitHub/scarry1992
License ------- MIT mqtt-packet-9.0.2/benchmarks/000077500000000000000000000000001476155302400161005ustar00rootroot00000000000000mqtt-packet-9.0.2/benchmarks/generate.js000066400000000000000000000007021476155302400202270ustar00rootroot00000000000000const mqtt = require('../') const max = 100000 let i const buf = Buffer.from('test') // initialize it mqtt.generate({ cmd: 'publish', topic: 'test', payload: buf }) const start = Date.now() for (i = 0; i < max; i++) { mqtt.generate({ cmd: 'publish', topic: 'test', payload: buf }) } const time = Date.now() - start console.log('Total time', time) console.log('Total packets', max) console.log('Packet/s', max / time * 1000) mqtt-packet-9.0.2/benchmarks/generateNet.js000066400000000000000000000017401476155302400207010ustar00rootroot00000000000000const mqtt = require('../') const max = 1000000 let i = 0 const start = Date.now() let time const buf = Buffer.allocUnsafe(10) const net = require('net') const server = net.createServer(handle) let dest buf.fill('test') function handle (sock) { sock.resume() } server.listen(0, () => { dest = net.connect(server.address()) dest.on('connect', tickWait) dest.on('drain', tickWait) dest.on('finish', () => { time = Date.now() - start console.log('Total time', time) console.log('Total packets', max) console.log('Packet/s', max / time * 1000) server.close() }) }) function tickWait () { // console.log('tickWait', i) let res = true // var toSend = new Buffer(5 + buf.length) for (; i < max && res; i++) { res = dest.write(mqtt.generate({ cmd: 'publish', topic: 'test', payload: buf })) // buf.copy(toSend, 5) // res = dest.write(toSend, 'buffer') // console.log(res) } if (i >= max) { dest.end() } } mqtt-packet-9.0.2/benchmarks/parse.js000066400000000000000000000007471476155302400175600ustar00rootroot00000000000000const mqtt = require('../') const parser = mqtt.parser() const max = 10000000 let i const start = Date.now() / 1000 for (i = 0; i < max; i++) { parser.parse(Buffer.from([ 48, 10, // Header (publish) 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 116, 101, 115, 116 // Payload (test) ])) } const time = Date.now() / 1000 - start console.log('Total packets', max) console.log('Total time', Math.round(time * 100) / 100) console.log('Packet/s', max / time) mqtt-packet-9.0.2/benchmarks/writeToStream.js000066400000000000000000000016401476155302400212500ustar00rootroot00000000000000const mqtt = require('../') const max = 1000000 let i = 0 const start = Date.now() let time const buf = Buffer.allocUnsafe(10) const net = require('net') const server = net.createServer(handle) let dest function handle (sock) { sock.resume() } buf.fill('test') server.listen(0, () => { dest = net.connect(server.address()) dest.on('connect', tickWait) dest.on('drain', tickWait) dest.on('finish', () => { time = Date.now() - start console.log('Total time', time) console.log('Total packets', max) console.log('Packet/s', max / time * 1000) server.close() }) }) function tickWait () { let res = true // var toSend = new Buffer(5) for (; i < max && res; i++) { res = mqtt.writeToStream({ cmd: 'publish', topic: 'test', payload: buf }, dest) // dest.write(toSend, 'buffer') // res = dest.write(buf, 'buffer') } if (i >= max) { dest.end() } } mqtt-packet-9.0.2/constants.js000066400000000000000000000172551476155302400163470ustar00rootroot00000000000000/* Protocol - protocol constants */ const protocol = module.exports const { Buffer } = require('buffer') /* Command code => mnemonic */ protocol.types = { 0: 'reserved', 1: 'connect', 2: 'connack', 3: 'publish', 4: 'puback', 5: 'pubrec', 6: 'pubrel', 7: 'pubcomp', 8: 'subscribe', 9: 'suback', 10: 'unsubscribe', 11: 'unsuback', 12: 'pingreq', 13: 'pingresp', 14: 'disconnect', 15: 'auth' } protocol.requiredHeaderFlags = { 1: 0, // 'connect' 2: 0, // 'connack' 4: 0, // 'puback' 5: 0, // 'pubrec' 6: 2, // 'pubrel' 7: 0, // 'pubcomp' 8: 2, // 'subscribe' 9: 0, // 'suback' 10: 2, // 'unsubscribe' 11: 0, // 'unsuback' 12: 0, // 'pingreq' 13: 0, // 'pingresp' 14: 0, // 'disconnect' 15: 0 // 'auth' } protocol.requiredHeaderFlagsErrors = {} for (const k in protocol.requiredHeaderFlags) { const v = protocol.requiredHeaderFlags[k] protocol.requiredHeaderFlagsErrors[k] = 'Invalid header flag bits, must be 0x' + v.toString(16) + ' for ' + protocol.types[k] + ' packet' } /* Mnemonic => Command code */ protocol.codes = {} for (const k in protocol.types) { const v = protocol.types[k] protocol.codes[v] = k } /* Header */ protocol.CMD_SHIFT = 4 protocol.CMD_MASK = 0xF0 protocol.DUP_MASK = 0x08 protocol.QOS_MASK = 0x03 protocol.QOS_SHIFT = 1 protocol.RETAIN_MASK = 0x01 /* Length */ protocol.VARBYTEINT_MASK = 0x7F protocol.VARBYTEINT_FIN_MASK = 0x80 protocol.VARBYTEINT_MAX = 268435455 /* Connack */ protocol.SESSIONPRESENT_MASK = 0x01 protocol.SESSIONPRESENT_HEADER = Buffer.from([protocol.SESSIONPRESENT_MASK]) protocol.CONNACK_HEADER = Buffer.from([protocol.codes.connack << protocol.CMD_SHIFT]) /* Connect */ protocol.USERNAME_MASK = 0x80 protocol.PASSWORD_MASK = 0x40 protocol.WILL_RETAIN_MASK = 0x20 protocol.WILL_QOS_MASK = 0x18 protocol.WILL_QOS_SHIFT = 3 protocol.WILL_FLAG_MASK = 0x04 protocol.CLEAN_SESSION_MASK = 0x02 protocol.CONNECT_HEADER = Buffer.from([protocol.codes.connect << protocol.CMD_SHIFT]) /* Properties */ protocol.properties = { sessionExpiryInterval: 17, willDelayInterval: 24, receiveMaximum: 33, maximumPacketSize: 39, topicAliasMaximum: 34, requestResponseInformation: 25, requestProblemInformation: 23, userProperties: 38, authenticationMethod: 21, authenticationData: 22, payloadFormatIndicator: 1, messageExpiryInterval: 2, contentType: 3, responseTopic: 8, correlationData: 9, maximumQoS: 36, retainAvailable: 37, assignedClientIdentifier: 18, reasonString: 31, wildcardSubscriptionAvailable: 40, subscriptionIdentifiersAvailable: 41, sharedSubscriptionAvailable: 42, serverKeepAlive: 19, responseInformation: 26, serverReference: 28, topicAlias: 35, subscriptionIdentifier: 11 } protocol.propertiesCodes = {} for (const prop in protocol.properties) { const id = protocol.properties[prop] protocol.propertiesCodes[id] = prop } protocol.propertiesTypes = { sessionExpiryInterval: 'int32', willDelayInterval: 'int32', receiveMaximum: 'int16', maximumPacketSize: 'int32', topicAliasMaximum: 'int16', requestResponseInformation: 'byte', requestProblemInformation: 'byte', userProperties: 'pair', authenticationMethod: 'string', authenticationData: 'binary', payloadFormatIndicator: 'byte', messageExpiryInterval: 'int32', contentType: 'string', responseTopic: 'string', correlationData: 'binary', maximumQoS: 'int8', retainAvailable: 'byte', assignedClientIdentifier: 'string', reasonString: 'string', wildcardSubscriptionAvailable: 'byte', subscriptionIdentifiersAvailable: 'byte', sharedSubscriptionAvailable: 'byte', serverKeepAlive: 'int16', responseInformation: 'string', serverReference: 'string', topicAlias: 'int16', subscriptionIdentifier: 'var' } function genHeader (type) { return [0, 1, 2].map(qos => { return [0, 1].map(dup => { return [0, 1].map(retain => { const buf = Buffer.alloc(1) buf.writeUInt8( protocol.codes[type] << protocol.CMD_SHIFT | (dup ? protocol.DUP_MASK : 0) | qos << protocol.QOS_SHIFT | retain, 0, true) return buf }) }) }) } /* Publish */ protocol.PUBLISH_HEADER = genHeader('publish') /* Subscribe */ protocol.SUBSCRIBE_HEADER = genHeader('subscribe') protocol.SUBSCRIBE_OPTIONS_QOS_MASK = 0x03 protocol.SUBSCRIBE_OPTIONS_NL_MASK = 0x01 protocol.SUBSCRIBE_OPTIONS_NL_SHIFT = 2 protocol.SUBSCRIBE_OPTIONS_RAP_MASK = 0x01 protocol.SUBSCRIBE_OPTIONS_RAP_SHIFT = 3 protocol.SUBSCRIBE_OPTIONS_RH_MASK = 0x03 protocol.SUBSCRIBE_OPTIONS_RH_SHIFT = 4 protocol.SUBSCRIBE_OPTIONS_RH = [0x00, 0x10, 0x20] protocol.SUBSCRIBE_OPTIONS_NL = 0x04 protocol.SUBSCRIBE_OPTIONS_RAP = 0x08 protocol.SUBSCRIBE_OPTIONS_QOS = [0x00, 0x01, 0x02] /* Unsubscribe */ protocol.UNSUBSCRIBE_HEADER = genHeader('unsubscribe') /* Confirmations */ protocol.ACKS = { unsuback: genHeader('unsuback'), puback: genHeader('puback'), pubcomp: genHeader('pubcomp'), pubrel: genHeader('pubrel'), pubrec: genHeader('pubrec') } protocol.SUBACK_HEADER = Buffer.from([protocol.codes.suback << protocol.CMD_SHIFT]) /* Protocol versions */ protocol.VERSION3 = Buffer.from([3]) protocol.VERSION4 = Buffer.from([4]) protocol.VERSION5 = Buffer.from([5]) protocol.VERSION131 = Buffer.from([131]) protocol.VERSION132 = Buffer.from([132]) /* QoS */ protocol.QOS = [0, 1, 2].map(qos => { return Buffer.from([qos]) }) /* Empty packets */ protocol.EMPTY = { pingreq: Buffer.from([protocol.codes.pingreq << 4, 0]), pingresp: Buffer.from([protocol.codes.pingresp << 4, 0]), disconnect: Buffer.from([protocol.codes.disconnect << 4, 0]) } protocol.MQTT5_PUBACK_PUBREC_CODES = { 0x00: 'Success', 0x10: 'No matching subscribers', 0x80: 'Unspecified error', 0x83: 'Implementation specific error', 0x87: 'Not authorized', 0x90: 'Topic Name invalid', 0x91: 'Packet identifier in use', 0x97: 'Quota exceeded', 0x99: 'Payload format invalid' } protocol.MQTT5_PUBREL_PUBCOMP_CODES = { 0x00: 'Success', 0x92: 'Packet Identifier not found' } protocol.MQTT5_SUBACK_CODES = { 0x00: 'Granted QoS 0', 0x01: 'Granted QoS 1', 0x02: 'Granted QoS 2', 0x80: 'Unspecified error', 0x83: 'Implementation specific error', 0x87: 'Not authorized', 0x8F: 'Topic Filter invalid', 0x91: 'Packet Identifier in use', 0x97: 'Quota exceeded', 0x9E: 'Shared Subscriptions not supported', 0xA1: 'Subscription Identifiers not supported', 0xA2: 'Wildcard Subscriptions not supported' } protocol.MQTT5_UNSUBACK_CODES = { 0x00: 'Success', 0x11: 'No subscription existed', 0x80: 'Unspecified error', 0x83: 'Implementation specific error', 0x87: 'Not authorized', 0x8F: 'Topic Filter invalid', 0x91: 'Packet Identifier in use' } protocol.MQTT5_DISCONNECT_CODES = { 0x00: 'Normal disconnection', 0x04: 'Disconnect with Will Message', 0x80: 'Unspecified error', 0x81: 'Malformed Packet', 0x82: 'Protocol Error', 0x83: 'Implementation specific error', 0x87: 'Not authorized', 0x89: 'Server busy', 0x8B: 'Server shutting down', 0x8D: 'Keep Alive timeout', 0x8E: 'Session taken over', 0x8F: 'Topic Filter invalid', 0x90: 'Topic Name invalid', 0x93: 'Receive Maximum exceeded', 0x94: 'Topic Alias invalid', 0x95: 'Packet too large', 0x96: 'Message rate too high', 0x97: 'Quota exceeded', 0x98: 'Administrative action', 0x99: 'Payload format invalid', 0x9A: 'Retain not supported', 0x9B: 'QoS not supported', 0x9C: 'Use another server', 0x9D: 'Server moved', 0x9E: 'Shared Subscriptions not supported', 0x9F: 'Connection rate exceeded', 0xA0: 'Maximum connect time', 0xA1: 'Subscription Identifiers not supported', 0xA2: 'Wildcard Subscriptions not supported' } protocol.MQTT5_AUTH_CODES = { 0x00: 'Success', 0x18: 'Continue authentication', 0x19: 'Re-authenticate' } mqtt-packet-9.0.2/generate.js000066400000000000000000000023251476155302400161150ustar00rootroot00000000000000const writeToStream = require('./writeToStream') const { EventEmitter } = require('events') const { Buffer } = require('buffer') function generate (packet, opts) { const stream = new Accumulator() writeToStream(packet, stream, opts) return stream.concat() } class Accumulator extends EventEmitter { constructor () { super() this._array = new Array(20) this._i = 0 } write (chunk) { this._array[this._i++] = chunk return true } concat () { let length = 0 const lengths = new Array(this._array.length) const list = this._array let pos = 0 let i for (i = 0; i < list.length && list[i] !== undefined; i++) { if (typeof list[i] !== 'string') lengths[i] = list[i].length else lengths[i] = Buffer.byteLength(list[i]) length += lengths[i] } const result = Buffer.allocUnsafe(length) for (i = 0; i < list.length && list[i] !== undefined; i++) { if (typeof list[i] !== 'string') { list[i].copy(result, pos) pos += lengths[i] } else { result.write(list[i], pos) pos += lengths[i] } } return result } destroy (err) { if (err) this.emit('error', err) } } module.exports = generate mqtt-packet-9.0.2/mqtt.js000066400000000000000000000002101476155302400152770ustar00rootroot00000000000000exports.parser = require('./parser').parser exports.generate = require('./generate') exports.writeToStream = require('./writeToStream') mqtt-packet-9.0.2/numbers.js000066400000000000000000000024451476155302400160010ustar00rootroot00000000000000const { Buffer } = require('buffer') const max = 65536 const cache = {} // in node 6 Buffer.subarray returns a Uint8Array instead of a Buffer // later versions return a Buffer // alternative is Buffer.slice but that creates a new buffer // creating new buffers takes time // SubOk is only false on node < 8 const SubOk = Buffer.isBuffer(Buffer.from([1, 2]).subarray(0, 1)) function generateBuffer (i) { const buffer = Buffer.allocUnsafe(2) buffer.writeUInt8(i >> 8, 0) buffer.writeUInt8(i & 0x00FF, 0 + 1) return buffer } function generateCache () { for (let i = 0; i < max; i++) { cache[i] = generateBuffer(i) } } function genBufVariableByteInt (num) { const maxLength = 4 // max 4 bytes let digit = 0 let pos = 0 const buffer = Buffer.allocUnsafe(maxLength) do { digit = num % 128 | 0 num = num / 128 | 0 if (num > 0) digit = digit | 0x80 buffer.writeUInt8(digit, pos++) } while (num > 0 && pos < maxLength) if (num > 0) { pos = 0 } return SubOk ? buffer.subarray(0, pos) : buffer.slice(0, pos) } function generate4ByteBuffer (num) { const buffer = Buffer.allocUnsafe(4) buffer.writeUInt32BE(num, 0) return buffer } module.exports = { cache, generateCache, generateNumber: generateBuffer, genBufVariableByteInt, generate4ByteBuffer } mqtt-packet-9.0.2/package.json000066400000000000000000000023501476155302400162510ustar00rootroot00000000000000{ "name": "mqtt-packet", "version": "9.0.2", "description": "Parse and generate MQTT packets like a breeze", "main": "mqtt.js", "types": "types/index.d.ts", "contributors": [ "Matteo Collina (https://github.com/mcollina)", "Adam Rudd ", "Peter Sorowka (https://github.com/psorowka)", "Wouter Klijn (https://github.com/wuhkuh)", "Siarhei Buntsevich (https://github.com/scarry1992)" ], "scripts": { "test": "tape test.js | tap-spec && standard", "ci": "tape test.js && node testRandom && standard" }, "pre-commit": "test", "repository": { "type": "git", "url": "https://github.com/mqttjs/mqtt-packet.git" }, "keywords": [ "MQTT", "packet", "parse", "publish", "subscribe", "pubsub" ], "license": "MIT", "bugs": { "url": "https://github.com/mqttjs/mqtt-packet/issues" }, "homepage": "https://github.com/mqttjs/mqtt-packet", "devDependencies": { "pre-commit": "^1.2.2", "readable-stream": "^4.4.2", "standard": "^17.1.0", "tap-spec": "^5.0.0", "tape": "^5.7.2" }, "dependencies": { "bl": "^6.0.8", "debug": "^4.3.4", "process-nextick-args": "^2.0.1" } } mqtt-packet-9.0.2/packet.js000066400000000000000000000003261476155302400155710ustar00rootroot00000000000000class Packet { constructor () { this.cmd = null this.retain = false this.qos = 0 this.dup = false this.length = -1 this.topic = null this.payload = null } } module.exports = Packet mqtt-packet-9.0.2/parser.js000066400000000000000000000576301476155302400156300ustar00rootroot00000000000000const bl = require('bl') const { EventEmitter } = require('events') const Packet = require('./packet') const constants = require('./constants') const debug = require('debug')('mqtt-packet:parser') class Parser extends EventEmitter { constructor () { super() this.parser = this.constructor.parser } static parser (opt) { if (!(this instanceof Parser)) return (new Parser()).parser(opt) this.settings = opt || {} this._states = [ '_parseHeader', '_parseLength', '_parsePayload', '_newPacket' ] this._resetState() return this } _resetState () { debug('_resetState: resetting packet, error, _list, and _stateCounter') this.packet = new Packet() this.error = null this._list = bl() this._stateCounter = 0 } parse (buf) { if (this.error) this._resetState() this._list.append(buf) debug('parse: current state: %s', this._states[this._stateCounter]) while ((this.packet.length !== -1 || this._list.length > 0) && this[this._states[this._stateCounter]]() && !this.error) { this._stateCounter++ debug('parse: state complete. _stateCounter is now: %d', this._stateCounter) debug('parse: packet.length: %d, buffer list length: %d', this.packet.length, this._list.length) if (this._stateCounter >= this._states.length) this._stateCounter = 0 } debug('parse: exited while loop. packet: %d, buffer list length: %d', this.packet.length, this._list.length) return this._list.length } _parseHeader () { // There is at least one byte in the buffer const zero = this._list.readUInt8(0) const cmdIndex = zero >> constants.CMD_SHIFT this.packet.cmd = constants.types[cmdIndex] const headerFlags = zero & 0xf const requiredHeaderFlags = constants.requiredHeaderFlags[cmdIndex] if (requiredHeaderFlags != null && headerFlags !== requiredHeaderFlags) { // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] return this._emitError(new Error(constants.requiredHeaderFlagsErrors[cmdIndex])) } this.packet.retain = (zero & constants.RETAIN_MASK) !== 0 this.packet.qos = (zero >> constants.QOS_SHIFT) & constants.QOS_MASK if (this.packet.qos > 2) { return this._emitError(new Error('Packet must not have both QoS bits set to 1')) } this.packet.dup = (zero & constants.DUP_MASK) !== 0 debug('_parseHeader: packet: %o', this.packet) this._list.consume(1) return true } _parseLength () { // There is at least one byte in the list const result = this._parseVarByteNum(true) if (result) { this.packet.length = result.value this._list.consume(result.bytes) } debug('_parseLength %d', result.value) return !!result } _parsePayload () { debug('_parsePayload: payload %O', this._list) let 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 } debug('_parsePayload complete result: %s', result) return result } _parseConnect () { debug('_parseConnect') let topic // Will topic let payload // Will payload let password // Password let username // Username const flags = {} const packet = this.packet // Parse protocolId const 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 >= 128) { packet.bridgeMode = true packet.protocolVersion = packet.protocolVersion - 128 } 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')) } if (this._list.readUInt8(this._pos) & 0x1) { // The Server MUST validate that the reserved flag in the CONNECT Control Packet is set to zero and disconnect the Client if it is not zero [MQTT-3.1.2-3] return this._emitError(new Error('Connect flag bit 0 must be 0, but got 1')) } // 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) const willRetain = !!(this._list.readUInt8(this._pos) & constants.WILL_RETAIN_MASK) const willQos = (this._list.readUInt8(this._pos) & constants.WILL_QOS_MASK) >> constants.WILL_QOS_SHIFT if (flags.will) { packet.will = {} packet.will.retain = willRetain packet.will.qos = willQos } else { if (willRetain) { return this._emitError(new Error('Will Retain Flag must be set to zero when Will Flag is set to 0')) } if (willQos) { return this._emitError(new Error('Will QoS must be set to zero when Will Flag is set to 0')) } } 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) { const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } // Parse clientId const clientId = this._parseString() if (clientId === null) return this._emitError(new Error('Packet too short')) packet.clientId = clientId debug('_parseConnect: packet.clientId: %s', packet.clientId) if (flags.will) { if (packet.protocolVersion === 5) { const 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 debug('_parseConnect: packet.will.topic: %s', packet.will.topic) // Parse will payload payload = this._parseBuffer() if (payload === null) return this._emitError(new Error('Cannot parse will payload')) packet.will.payload = payload debug('_parseConnect: packet.will.paylaod: %s', packet.will.payload) } // Parse username if (flags.username) { username = this._parseString() if (username === null) return this._emitError(new Error('Cannot parse username')) packet.username = username debug('_parseConnect: packet.username: %s', packet.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 debug('_parseConnect: complete') return packet } _parseConnack () { debug('_parseConnack') const packet = this.packet if (this._list.length < 1) return null const flags = this._list.readUInt8(this._pos++) if (flags > 1) { return this._emitError(new Error('Invalid connack flags, bits 7-1 must be set to 0')) } packet.sessionPresent = !!(flags & constants.SESSIONPRESENT_MASK) if (this.settings.protocolVersion === 5) { if (this._list.length >= 2) { packet.reasonCode = this._list.readUInt8(this._pos++) } else { packet.reasonCode = 0 } } else { if (this._list.length < 2) return null 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) { const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } debug('_parseConnack: complete') } _parsePublish () { debug('_parsePublish') const 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) { const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } packet.payload = this._list.slice(this._pos, packet.length) debug('_parsePublish: payload from buffer list: %o', packet.payload) } _parseSubscribe () { debug('_parseSubscribe') const packet = this.packet let topic let options let qos let rh let rap let nl let subscription packet.subscriptions = [] if (!this._parseMessageId()) { return } // Properties mqtt 5 if (this.settings.protocolVersion === 5) { const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } if (packet.length <= 0) { return this._emitError(new Error('Malformed subscribe, no payload specified')) } 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() if (this.settings.protocolVersion === 5) { if (options & 0xc0) { return this._emitError(new Error('Invalid subscribe topic flag bits, bits 7-6 must be 0')) } } else { if (options & 0xfc) { return this._emitError(new Error('Invalid subscribe topic flag bits, bits 7-2 must be 0')) } } qos = options & constants.SUBSCRIBE_OPTIONS_QOS_MASK if (qos > 2) { return this._emitError(new Error('Invalid subscribe QoS, must be <= 2')) } 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 if (rh > 2) { return this._emitError(new Error('Invalid retain handling, must be <= 2')) } subscription = { topic, qos } // mqtt 5 options if (this.settings.protocolVersion === 5) { subscription.nl = nl subscription.rap = rap subscription.rh = rh } else if (this.settings.bridgeMode) { subscription.rh = 0 subscription.rap = true subscription.nl = true } // Push pair to subscriptions debug('_parseSubscribe: push subscription `%s` to subscription', subscription) packet.subscriptions.push(subscription) } } _parseSuback () { debug('_parseSuback') const packet = this.packet this.packet.granted = [] if (!this._parseMessageId()) { return } // Properties mqtt 5 if (this.settings.protocolVersion === 5) { const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } if (packet.length <= 0) { return this._emitError(new Error('Malformed suback, no payload specified')) } // Parse granted QoSes while (this._pos < this.packet.length) { const code = this._list.readUInt8(this._pos++) if (this.settings.protocolVersion === 5) { if (!constants.MQTT5_SUBACK_CODES[code]) { return this._emitError(new Error('Invalid suback code')) } } else { if (code > 2 && code !== 0x80) { return this._emitError(new Error('Invalid suback QoS, must be 0, 1, 2 or 128')) } } this.packet.granted.push(code) } } _parseUnsubscribe () { debug('_parseUnsubscribe') const packet = this.packet packet.unsubscriptions = [] // Parse messageId if (!this._parseMessageId()) { return } // Properties mqtt 5 if (this.settings.protocolVersion === 5) { const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } if (packet.length <= 0) { return this._emitError(new Error('Malformed unsubscribe, no payload specified')) } while (this._pos < packet.length) { // Parse topic const topic = this._parseString() if (topic === null) return this._emitError(new Error('Cannot parse topic')) // Push topic to unsubscriptions debug('_parseUnsubscribe: push topic `%s` to unsubscriptions', topic) packet.unsubscriptions.push(topic) } } _parseUnsuback () { debug('_parseUnsuback') const packet = this.packet if (!this._parseMessageId()) return this._emitError(new Error('Cannot parse messageId')) if ((this.settings.protocolVersion === 3 || this.settings.protocolVersion === 4) && packet.length !== 2) { return this._emitError(new Error('Malformed unsuback, payload length must be 2')) } if (packet.length <= 0) { return this._emitError(new Error('Malformed unsuback, no payload specified')) } // Properties mqtt 5 if (this.settings.protocolVersion === 5) { const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } // Parse granted QoSes packet.granted = [] while (this._pos < this.packet.length) { const code = this._list.readUInt8(this._pos++) if (!constants.MQTT5_UNSUBACK_CODES[code]) { return this._emitError(new Error('Invalid unsuback code')) } this.packet.granted.push(code) } } } // parse packets like puback, pubrec, pubrel, pubcomp _parseConfirmation () { debug('_parseConfirmation: packet.cmd: `%s`', this.packet.cmd) const packet = this.packet this._parseMessageId() if (this.settings.protocolVersion === 5) { if (packet.length > 2) { // response code packet.reasonCode = this._parseByte() switch (this.packet.cmd) { case 'puback': case 'pubrec': if (!constants.MQTT5_PUBACK_PUBREC_CODES[packet.reasonCode]) { return this._emitError(new Error('Invalid ' + this.packet.cmd + ' reason code')) } break case 'pubrel': case 'pubcomp': if (!constants.MQTT5_PUBREL_PUBCOMP_CODES[packet.reasonCode]) { return this._emitError(new Error('Invalid ' + this.packet.cmd + ' reason code')) } break } debug('_parseConfirmation: packet.reasonCode `%d`', packet.reasonCode) } else { packet.reasonCode = 0 } if (packet.length > 3) { // properies mqtt 5 const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } } return true } // parse disconnect packet _parseDisconnect () { const packet = this.packet debug('_parseDisconnect') if (this.settings.protocolVersion === 5) { // response code if (this._list.length > 0) { packet.reasonCode = this._parseByte() if (!constants.MQTT5_DISCONNECT_CODES[packet.reasonCode]) { this._emitError(new Error('Invalid disconnect reason code')) } } else { packet.reasonCode = 0 } // properies mqtt 5 const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } } debug('_parseDisconnect result: true') return true } // parse auth packet _parseAuth () { debug('_parseAuth') const 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() if (!constants.MQTT5_AUTH_CODES[packet.reasonCode]) { return this._emitError(new Error('Invalid auth reason code')) } // properies mqtt 5 const properties = this._parseProperties() if (Object.getOwnPropertyNames(properties).length) { packet.properties = properties } debug('_parseAuth: result: true') return true } _parseMessageId () { const packet = this.packet packet.messageId = this._parseNum() if (packet.messageId === null) { this._emitError(new Error('Cannot parse messageId')) return false } debug('_parseMessageId: packet.messageId %d', packet.messageId) return true } _parseString (maybeBuffer) { const length = this._parseNum() const end = length + this._pos if (length === -1 || end > this._list.length || end > this.packet.length) return null const result = this._list.toString('utf8', this._pos, end) this._pos += length debug('_parseString: result: %s', result) return result } _parseStringPair () { debug('_parseStringPair') return { name: this._parseString(), value: this._parseString() } } _parseBuffer () { const length = this._parseNum() const end = length + this._pos if (length === -1 || end > this._list.length || end > this.packet.length) return null const result = this._list.slice(this._pos, end) this._pos += length debug('_parseBuffer: result: %o', result) return result } _parseNum () { if (this._list.length - this._pos < 2) return -1 const result = this._list.readUInt16BE(this._pos) this._pos += 2 debug('_parseNum: result: %s', result) return result } _parse4ByteNum () { if (this._list.length - this._pos < 4) return -1 const result = this._list.readUInt32BE(this._pos) this._pos += 4 debug('_parse4ByteNum: result: %s', result) return result } _parseVarByteNum (fullInfoFlag) { debug('_parseVarByteNum') const maxBytes = 4 let bytes = 0 let mul = 1 let value = 0 let result = false let current const padding = this._pos ? this._pos : 0 while (bytes < maxBytes && (padding + bytes) < this._list.length) { current = this._list.readUInt8(padding + bytes++) value += mul * (current & constants.VARBYTEINT_MASK) mul *= 0x80 if ((current & constants.VARBYTEINT_FIN_MASK) === 0) { result = true break } if (this._list.length <= bytes) { break } } if (!result && bytes === maxBytes && this._list.length >= bytes) { this._emitError(new Error('Invalid variable byte integer')) } if (padding) { this._pos += bytes } if (result) { if (fullInfoFlag) { result = { bytes, value } } else { result = value } } else { result = false } debug('_parseVarByteNum: result: %o', result) return result } _parseByte () { let result if (this._pos < this._list.length) { result = this._list.readUInt8(this._pos) this._pos++ } debug('_parseByte: result: %o', result) return result } _parseByType (type) { debug('_parseByType: type: %s', 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() } } } _parseProperties () { debug('_parseProperties') const length = this._parseVarByteNum() const start = this._pos const end = start + length const result = {} while (this._pos < end) { const type = this._parseByte() if (!type) { this._emitError(new Error('Cannot parse property code type')) return false } const 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] = Object.create(null) } const 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 { const 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 } if (result[name]) { if (Array.isArray(result[name])) { result[name].push(this._parseByType(constants.propertiesTypes[name])) } else { result[name] = [result[name]] result[name].push(this._parseByType(constants.propertiesTypes[name])) } } else { result[name] = this._parseByType(constants.propertiesTypes[name]) } } return result } _newPacket () { debug('_newPacket') if (this.packet) { this._list.consume(this.packet.length) debug('_newPacket: parser emit packet: packet.cmd: %s, packet.payload: %s, packet.length: %d', this.packet.cmd, this.packet.payload, this.packet.length) this.emit('packet', this.packet) } debug('_newPacket: new packet') this.packet = new Packet() this._pos = 0 return true } _emitError (err) { debug('_emitError', err) this.error = err this.emit('error', err) } } module.exports = Parser mqtt-packet-9.0.2/test.js000066400000000000000000002364241476155302400153130ustar00rootroot00000000000000const util = require('util') const test = require('tape') const mqtt = require('./') const WS = require('readable-stream').Writable function normalExpectedObject (object) { if (object.username != null) object.username = object.username.toString() if (object.password != null) object.password = Buffer.from(object.password) return object } function testParseGenerate (name, object, buffer, opts) { test(`${name} parse`, t => { t.plan(2) const parser = mqtt.parser(opts) const expected = object const fixture = buffer parser.on('packet', packet => { if (packet.cmd !== 'publish') { delete packet.topic delete packet.payload } t.deepLooseEqual(packet, normalExpectedObject(expected), 'expected packet') }) parser.on('error', err => { t.fail(err) }) t.equal(parser.parse(fixture), 0, 'remaining bytes') }) test(`${name} generate`, t => { // For really large buffers, the expanded hex string can be so long as to // generate an error in nodejs 14.x, so only do the test with extra output // for relatively small buffers. const bigLength = 10000 const generatedBuffer = mqtt.generate(object, opts) if (generatedBuffer.length < bigLength && buffer.length < bigLength) { t.equal(generatedBuffer.toString('hex'), buffer.toString('hex')) } else { const bufferOkay = generatedBuffer.equals(buffer) if (bufferOkay) { t.pass() } else { // Output abbreviated representations of the buffers. t.comment('Expected:\n' + util.inspect(buffer)) t.comment('Got:\n' + util.inspect(generatedBuffer)) t.fail('Large buffers not equal') } } t.end() }) test(`${name} mirror`, t => { t.plan(2) const parser = mqtt.parser(opts) const expected = object const fixture = mqtt.generate(object, opts) parser.on('packet', packet => { if (packet.cmd !== 'publish') { delete packet.topic delete packet.payload } t.deepLooseEqual(packet, normalExpectedObject(expected), 'expected packet') }) parser.on('error', err => { t.fail(err) }) t.equal(parser.parse(fixture), 0, 'remaining bytes') }) test(`${name} writeToStream`, t => { const stream = WS() stream.write = () => true stream.on('error', (err) => t.fail(err)) const result = mqtt.writeToStream(object, stream, opts) t.equal(result, true, 'should return true') t.end() }) } // the API allows to pass strings as buffers to writeToStream and generate // parsing them back will result in a string so only generate and compare to buffer function testGenerateOnly (name, object, buffer, opts) { test(name, t => { t.equal(mqtt.generate(object, opts).toString('hex'), buffer.toString('hex')) t.end() }) } function testParseOnly (name, object, buffer, opts) { test(name, t => { const parser = mqtt.parser(opts) // const expected = object // const fixture = buffer t.plan(2 + Object.keys(object).length) parser.on('packet', packet => { t.equal(Object.keys(object).length, Object.keys(packet).length, 'key count') Object.keys(object).forEach(key => { t.deepEqual(packet[key], object[key], `expected packet property ${key}`) }) }) t.equal(parser.parse(buffer), 0, 'remaining bytes') t.end() }) } function testParseError (expected, fixture, opts) { test(expected, t => { t.plan(1) const parser = mqtt.parser(opts) parser.on('error', err => { t.equal(err.message, expected, 'expected error message') }) parser.on('packet', () => { t.fail('parse errors should not be followed by packet events') }) parser.parse(fixture) t.end() }) } function testGenerateError (expected, fixture, opts, name) { test(name || expected, t => { t.plan(1) try { mqtt.generate(fixture, opts) } catch (err) { t.equal(expected, err.message) } t.end() }) } function testGenerateErrorMultipleCmds (cmds, expected, fixture, opts) { cmds.forEach(cmd => { const obj = Object.assign({}, fixture) obj.cmd = cmd testGenerateError(expected, obj, opts, `${expected} on ${cmd}`) } ) } function testParseGenerateDefaults (name, object, buffer, generated, opts) { testParseOnly(`${name} parse`, generated, buffer, opts) testGenerateOnly(`${name} generate`, object, buffer, opts) } function testParseAndGenerate (name, object, buffer, opts) { testParseOnly(`${name} parse`, object, buffer, opts) testGenerateOnly(`${name} generate`, object, buffer, opts) } function testWriteToStreamError (expected, fixture) { test(`writeToStream ${expected} error`, t => { t.plan(2) const stream = WS() stream.write = () => t.fail('should not have called write') stream.on('error', () => t.pass('error emitted')) const result = mqtt.writeToStream(fixture, stream) t.false(result, 'result should be false') }) } test('cacheNumbers get/set/unset', t => { t.true(mqtt.writeToStream.cacheNumbers, 'initial state of cacheNumbers is enabled') mqtt.writeToStream.cacheNumbers = false t.false(mqtt.writeToStream.cacheNumbers, 'cacheNumbers can be disabled') mqtt.writeToStream.cacheNumbers = true t.true(mqtt.writeToStream.cacheNumbers, 'cacheNumbers can be enabled') t.end() }) test('disabled numbers cache', t => { const stream = WS() const message = { cmd: 'publish', retain: false, qos: 0, dup: false, length: 10, topic: Buffer.from('test'), payload: Buffer.from('test') } const expected = Buffer.from([ 48, 10, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 116, 101, 115, 116 // Payload (test) ]) let written = Buffer.alloc(0) stream.write = (chunk) => { written = Buffer.concat([written, chunk]) } mqtt.writeToStream.cacheNumbers = false mqtt.writeToStream(message, stream) t.deepEqual(written, expected, 'written buffer is expected') mqtt.writeToStream.cacheNumbers = true stream.end() t.end() }) testGenerateError('Unknown command', {}) testParseError('Not supported', Buffer.from([0, 1, 0]), {}) // Length header field testParseError('Invalid variable byte integer', Buffer.from( [16, 255, 255, 255, 255] ), {}) testParseError('Invalid variable byte integer', Buffer.from( [16, 255, 255, 255, 128] ), {}) testParseError('Invalid variable byte integer', Buffer.from( [16, 255, 255, 255, 255, 1] ), {}) testParseError('Invalid variable byte integer', Buffer.from( [16, 255, 255, 255, 255, 127] ), {}) testParseError('Invalid variable byte integer', Buffer.from( [16, 255, 255, 255, 255, 128] ), {}) testParseError('Invalid variable byte integer', Buffer.from( [16, 255, 255, 255, 255, 255, 1] ), {}) testParseGenerate('minimal connect', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 18, protocolId: 'MQIsdp', protocolVersion: 3, clean: false, keepalive: 30, clientId: 'test' }, Buffer.from([ 16, 18, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 0, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116 // Client ID ])) testGenerateOnly('minimal connect with clientId as Buffer', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 18, protocolId: 'MQIsdp', protocolVersion: 3, clean: false, keepalive: 30, clientId: Buffer.from('test') }, Buffer.from([ 16, 18, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 0, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116 // Client ID ])) testParseGenerate('connect MQTT bridge 131', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 18, protocolId: 'MQIsdp', protocolVersion: 3, bridgeMode: true, clean: false, keepalive: 30, clientId: 'test' }, Buffer.from([ 16, 18, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 131, // Protocol version 0, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116 // Client ID ])) testParseGenerate('connect MQTT bridge 132', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 18, protocolId: 'MQIsdp', protocolVersion: 4, bridgeMode: true, clean: false, keepalive: 30, clientId: 'test' }, Buffer.from([ 16, 18, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 132, // Protocol version 0, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116 // Client ID ])) testParseGenerate('connect MQTT 5', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 125, protocolId: 'MQTT', protocolVersion: 5, will: { retain: true, qos: 2, properties: { willDelayInterval: 1234, payloadFormatIndicator: false, messageExpiryInterval: 4321, contentType: 'test', responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' } }, topic: 'topic', payload: Buffer.from([4, 3, 2, 1]) }, clean: true, keepalive: 30, properties: { sessionExpiryInterval: 1234, receiveMaximum: 432, maximumPacketSize: 100, topicAliasMaximum: 456, requestResponseInformation: true, requestProblemInformation: true, userProperties: { test: 'test' }, authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) }, clientId: 'test' }, Buffer.from([ 16, 125, // Header 0, 4, // Protocol ID length 77, 81, 84, 84, // Protocol ID 5, // Protocol version 54, // Connect flags 0, 30, // Keepalive 47, // properties length 17, 0, 0, 4, 210, // sessionExpiryInterval 33, 1, 176, // receiveMaximum 39, 0, 0, 0, 100, // maximumPacketSize 34, 1, 200, // topicAliasMaximum 25, 1, // requestResponseInformation 23, 1, // requestProblemInformation, 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties, 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 22, 0, 4, 1, 2, 3, 4, // authenticationData 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 47, // will properties 24, 0, 0, 4, 210, // will delay interval 1, 0, // payload format indicator 2, 0, 0, 16, 225, // message expiry interval 3, 0, 4, 116, 101, 115, 116, // content type 8, 0, 5, 116, 111, 112, 105, 99, // response topic 9, 0, 4, 1, 2, 3, 4, // corelation data 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // user properties 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 4, // Will payload length 4, 3, 2, 1// Will payload ])) testParseGenerate('connect MQTT 5 with will properties but with empty will payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 121, protocolId: 'MQTT', protocolVersion: 5, will: { retain: true, qos: 2, properties: { willDelayInterval: 1234, payloadFormatIndicator: false, messageExpiryInterval: 4321, contentType: 'test', responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' } }, topic: 'topic', payload: Buffer.from([]) }, clean: true, keepalive: 30, properties: { sessionExpiryInterval: 1234, receiveMaximum: 432, maximumPacketSize: 100, topicAliasMaximum: 456, requestResponseInformation: true, requestProblemInformation: true, userProperties: { test: 'test' }, authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) }, clientId: 'test' }, Buffer.from([ 16, 121, // Header 0, 4, // Protocol ID length 77, 81, 84, 84, // Protocol ID 5, // Protocol version 54, // Connect flags 0, 30, // Keepalive 47, // properties length 17, 0, 0, 4, 210, // sessionExpiryInterval 33, 1, 176, // receiveMaximum 39, 0, 0, 0, 100, // maximumPacketSize 34, 1, 200, // topicAliasMaximum 25, 1, // requestResponseInformation 23, 1, // requestProblemInformation, 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties, 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 22, 0, 4, 1, 2, 3, 4, // authenticationData 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 47, // will properties 24, 0, 0, 4, 210, // will delay interval 1, 0, // payload format indicator 2, 0, 0, 16, 225, // message expiry interval 3, 0, 4, 116, 101, 115, 116, // content type 8, 0, 5, 116, 111, 112, 105, 99, // response topic 9, 0, 4, 1, 2, 3, 4, // corelation data 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // user properties 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 0 // Will payload length ])) testParseGenerate('connect MQTT 5 w/o will properties', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 78, protocolId: 'MQTT', protocolVersion: 5, will: { retain: true, qos: 2, topic: 'topic', payload: Buffer.from([4, 3, 2, 1]) }, clean: true, keepalive: 30, properties: { sessionExpiryInterval: 1234, receiveMaximum: 432, maximumPacketSize: 100, topicAliasMaximum: 456, requestResponseInformation: true, requestProblemInformation: true, userProperties: { test: 'test' }, authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) }, clientId: 'test' }, Buffer.from([ 16, 78, // Header 0, 4, // Protocol ID length 77, 81, 84, 84, // Protocol ID 5, // Protocol version 54, // Connect flags 0, 30, // Keepalive 47, // properties length 17, 0, 0, 4, 210, // sessionExpiryInterval 33, 1, 176, // receiveMaximum 39, 0, 0, 0, 100, // maximumPacketSize 34, 1, 200, // topicAliasMaximum 25, 1, // requestResponseInformation 23, 1, // requestProblemInformation, 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties, 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 22, 0, 4, 1, 2, 3, 4, // authenticationData 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, // will properties 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 4, // Will payload length 4, 3, 2, 1// Will payload ])) testParseGenerate('no clientId with 3.1.1', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 12, protocolId: 'MQTT', protocolVersion: 4, clean: true, keepalive: 30, clientId: '' }, Buffer.from([ 16, 12, // Header 0, 4, // Protocol ID length 77, 81, 84, 84, // Protocol ID 4, // Protocol version 2, // Connect flags 0, 30, // Keepalive 0, 0 // Client ID length ])) testParseGenerateDefaults('no clientId with 5.0', { cmd: 'connect', protocolId: 'MQTT', protocolVersion: 5, clean: true, keepalive: 60, properties: { receiveMaximum: 20 }, clientId: '' }, Buffer.from( [16, 16, 0, 4, 77, 81, 84, 84, 5, 2, 0, 60, 3, 33, 0, 20, 0, 0] ), { cmd: 'connect', retain: false, qos: 0, dup: false, length: 16, topic: null, payload: null, protocolId: 'MQTT', protocolVersion: 5, clean: true, keepalive: 60, properties: { receiveMaximum: 20 }, clientId: '' }, { protocolVersion: 5 }) testParseGenerateDefaults('utf-8 clientId with 5.0', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 23, protocolId: 'MQTT', protocolVersion: 4, clean: true, keepalive: 30, clientId: 'Ŧėśt🜄' }, Buffer.from([ 16, 23, // Header 0, 4, // Protocol ID length 77, 81, 84, 84, // Protocol ID 4, // Protocol version 2, // Connect flags 0, 30, // Keepalive 0, 11, // Client ID length 197, 166, // Ŧ (UTF-8: 0xc5a6) 196, 151, // ė (UTF-8: 0xc497) 197, 155, // ś (utf-8: 0xc59b) 116, // t (utf-8: 0x74) 240, 159, 156, 132 // 🜄 (utf-8: 0xf09f9c84) ]), { cmd: 'connect', retain: false, qos: 0, dup: false, length: 23, topic: null, payload: null, protocolId: 'MQTT', protocolVersion: 4, clean: true, keepalive: 30, clientId: 'Ŧėśt🜄' }, { protocol: 5 }) testParseGenerateDefaults('default connect', { cmd: 'connect', clientId: 'test' }, Buffer.from([ 16, 16, 0, 4, 77, 81, 84, 84, 4, 2, 0, 0, 0, 4, 116, 101, 115, 116 ]), { cmd: 'connect', retain: false, qos: 0, dup: false, length: 16, topic: null, payload: null, protocolId: 'MQTT', protocolVersion: 4, clean: true, keepalive: 0, clientId: 'test' }) testParseAndGenerate('Version 4 CONACK', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 2, topic: null, payload: null, sessionPresent: false, returnCode: 1 }, Buffer.from([ 32, 2, // Fixed Header (CONNACK, Remaining Length) 0, 1 // Variable Header (Session not present, Connection Refused - unacceptable protocol version) ]), {}) // Default protocolVersion (4) testParseAndGenerate('Version 5 CONACK', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 3, topic: null, payload: null, sessionPresent: false, reasonCode: 140 }, Buffer.from([ 32, 3, // Fixed Header (CONNACK, Remaining Length) 0, 140, // Variable Header (Session not present, Bad authentication method) 0 // Property Length Zero ]), { protocolVersion: 5 }) testParseOnly('Version 4 CONACK in Version 5 mode', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 2, topic: null, payload: null, sessionPresent: false, reasonCode: 1 // a version 4 return code stored in the version 5 reasonCode because this client is in version 5 }, Buffer.from([ 32, 2, // Fixed Header (CONNACK, Remaining Length) 0, 1 // Variable Header (Session not present, Connection Refused - unacceptable protocol version) ]), { protocolVersion: 5 }) // message is in version 4 format, but this client is in version 5 mode testParseOnly('Version 5 PUBACK test 1', { cmd: 'puback', messageId: 42, retain: false, qos: 0, dup: false, length: 2, topic: null, payload: null, reasonCode: 0 }, Buffer.from([ 64, 2, // Fixed Header (PUBACK, Remaining Length) 0, 42 // Variable Header (2 Bytes: Packet Identifier 42, Implied Reason code: Success, Implied no properties) ]), { protocolVersion: 5 } ) testParseAndGenerate('Version 5 PUBACK test 2', { cmd: 'puback', messageId: 42, retain: false, qos: 0, dup: false, length: 2, topic: null, payload: null, reasonCode: 0 }, Buffer.from([ 64, 2, // Fixed Header (PUBACK, Remaining Length) 0, 42 // Variable Header (2 Bytes: Packet Identifier 42, Implied reason code: 0 Success, Implied no properties) ]), { protocolVersion: 5 } ) testParseOnly('Version 5 PUBACK test 2.1', { cmd: 'puback', messageId: 42, retain: false, qos: 0, dup: false, length: 3, topic: null, payload: null, reasonCode: 0 }, Buffer.from([ 64, 3, // Fixed Header (PUBACK, Remaining Length) 0, 42, 0 // Variable Header (2 Bytes: Packet Identifier 42, Reason code: 0 Success, Implied no properties) ]), { protocolVersion: 5 } ) testParseOnly('Version 5 PUBACK test 3', { cmd: 'puback', messageId: 42, retain: false, qos: 0, dup: false, length: 4, topic: null, payload: null, reasonCode: 0 }, Buffer.from([ 64, 4, // Fixed Header (PUBACK, Remaining Length) 0, 42, 0, // Variable Header (2 Bytes: Packet Identifier 42, Reason code: 0 Success) 0 // no properties ]), { protocolVersion: 5 } ) testParseOnly('Version 5 CONNACK test 1', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 1, topic: null, payload: null, sessionPresent: true, reasonCode: 0 }, Buffer.from([ 32, 1, // Fixed Header (CONNACK, Remaining Length) 1 // Variable Header (Session Present: 1 => true, Implied Reason code: Success, Implied no properties) ]), { protocolVersion: 5 } ) testParseOnly('Version 5 CONNACK test 2', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 2, topic: null, payload: null, sessionPresent: true, reasonCode: 0 }, Buffer.from([ 32, 2, // Fixed Header (CONNACK, Remaining Length) 1, 0 // Variable Header (Session Present: 1 => true, Connect Reason code: Success, Implied no properties) ]), { protocolVersion: 5 } ) testParseAndGenerate('Version 5 CONNACK test 3', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 3, topic: null, payload: null, sessionPresent: true, reasonCode: 0 }, Buffer.from([ 32, 3, // Fixed Header (CONNACK, Remaining Length) 1, 0, // Variable Header (Session Present: 1 => true, Connect Reason code: Success) 0 // no properties ]), { protocolVersion: 5 } ) testParseOnly('Version 5 DISCONNECT test 1', { cmd: 'disconnect', retain: false, qos: 0, dup: false, length: 0, topic: null, payload: null, reasonCode: 0 }, Buffer.from([ 224, 0 // Fixed Header (DISCONNECT, Remaining Length), Implied Reason code: Normal Disconnection ]), { protocolVersion: 5 } ) testParseOnly('Version 5 DISCONNECT test 2', { cmd: 'disconnect', retain: false, qos: 0, dup: false, length: 1, topic: null, payload: null, reasonCode: 0 }, Buffer.from([ 224, 1, // Fixed Header (DISCONNECT, Remaining Length) 0 // reason Code (Normal disconnection) ]), { protocolVersion: 5 } ) testParseAndGenerate('Version 5 DISCONNECT test 3', { cmd: 'disconnect', retain: false, qos: 0, dup: false, length: 2, topic: null, payload: null, reasonCode: 0 }, Buffer.from([ 224, 2, // Fixed Header (DISCONNECT, Remaining Length) 0, // reason Code (Normal disconnection) 0 // no properties ]), { protocolVersion: 5 } ) testParseGenerate('empty will payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 47, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: Buffer.alloc(0) }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: Buffer.from('password') }, Buffer.from([ 16, 47, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 0, // Will payload length // Will payload 0, 8, // Username length 117, 115, 101, 114, 110, 97, 109, 101, // Username 0, 8, // Password length 112, 97, 115, 115, 119, 111, 114, 100 // Password ])) testParseGenerate('empty buffer username payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 20, protocolId: 'MQIsdp', protocolVersion: 3, clean: true, keepalive: 30, clientId: 'test', username: Buffer.from('') }, Buffer.from([ 16, 20, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 130, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 0 // Username length // Empty Username payload ])) testParseGenerate('empty string username payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 20, protocolId: 'MQIsdp', protocolVersion: 3, clean: true, keepalive: 30, clientId: 'test', username: '' }, Buffer.from([ 16, 20, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 130, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 0 // Username length // Empty Username payload ])) testParseGenerate('empty buffer password payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 30, protocolId: 'MQIsdp', protocolVersion: 3, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: Buffer.from('') }, Buffer.from([ 16, 30, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 194, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 8, // Username length 117, 115, 101, 114, 110, 97, 109, 101, // Username payload 0, 0 // Password length // Empty password payload ])) testParseGenerate('empty string password payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 30, protocolId: 'MQIsdp', protocolVersion: 3, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: '' }, Buffer.from([ 16, 30, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 194, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 8, // Username length 117, 115, 101, 114, 110, 97, 109, 101, // Username payload 0, 0 // Password length // Empty password payload ])) testParseGenerate('empty string username and password payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 22, protocolId: 'MQIsdp', protocolVersion: 3, clean: true, keepalive: 30, clientId: 'test', username: '', password: Buffer.from('') }, Buffer.from([ 16, 22, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 194, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 0, // Username length // Empty Username payload 0, 0 // Password length // Empty password payload ])) testParseGenerate('maximal connect', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: Buffer.from('payload') }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: Buffer.from('password') }, Buffer.from([ 16, 54, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 7, // Will payload length 112, 97, 121, 108, 111, 97, 100, // Will payload 0, 8, // Username length 117, 115, 101, 114, 110, 97, 109, 101, // Username 0, 8, // Password length 112, 97, 115, 115, 119, 111, 114, 100 // Password ])) testParseGenerate('max connect with special chars', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 57, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'tòpic', payload: Buffer.from('pay£oad') }, clean: true, keepalive: 30, clientId: 'te$t', username: 'u$ern4me', password: Buffer.from('p4$$w0£d') }, Buffer.from([ 16, 57, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 36, 116, // Client ID 0, 6, // Will topic length 116, 195, 178, 112, 105, 99, // Will topic 0, 8, // Will payload length 112, 97, 121, 194, 163, 111, 97, 100, // Will payload 0, 8, // Username length 117, 36, 101, 114, 110, 52, 109, 101, // Username 0, 9, // Password length 112, 52, 36, 36, 119, 48, 194, 163, 100 // Password ])) testGenerateOnly('connect all strings generate', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: 'password' }, Buffer.from([ 16, 54, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116, // Client ID 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 7, // Will payload length 112, 97, 121, 108, 111, 97, 100, // Will payload 0, 8, // Username length 117, 115, 101, 114, 110, 97, 109, 101, // Username 0, 8, // Password length 112, 97, 115, 115, 119, 111, 114, 100 // Password ])) testParseError('Cannot parse protocolId', Buffer.from([ 16, 4, 0, 6, 77, 81 ])) // missing protocol version on connect testParseError('Packet too short', Buffer.from([ 16, 8, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112 // Protocol ID ])) // missing keepalive on connect testParseError('Packet too short', Buffer.from([ 16, 10, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246 // Connect flags ])) // missing clientid on connect testParseError('Packet too short', Buffer.from([ 16, 10, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30 // Keepalive ])) // missing will topic on connect testParseError('Cannot parse will topic', Buffer.from([ 16, 16, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 2, // Will topic length 0, 0 // Will topic ])) // missing will payload on connect testParseError('Cannot parse will payload', Buffer.from([ 16, 23, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 2, // Will payload length 0, 0 // Will payload ])) // missing username on connect testParseError('Cannot parse username', Buffer.from([ 16, 32, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 7, // Will payload length 112, 97, 121, 108, 111, 97, 100, // Will payload 0, 2, // Username length 0, 0 // Username ])) // missing password on connect testParseError('Cannot parse password', Buffer.from([ 16, 42, // Header 0, 6, // Protocol ID length 77, 81, 73, 115, 100, 112, // Protocol ID 3, // Protocol version 246, // Connect flags 0, 30, // Keepalive 0, 5, // Will topic length 116, 111, 112, 105, 99, // Will topic 0, 7, // Will payload length 112, 97, 121, 108, 111, 97, 100, // Will payload 0, 8, // Username length 117, 115, 101, 114, 110, 97, 109, 101, // Username 0, 2, // Password length 0, 0 // Password ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for connect packet', Buffer.from([ 18, 10, // Header 0, 4, // Protocol ID length 0x4d, 0x51, 0x54, 0x54, // Protocol ID 3, // Protocol version 2, // Connect flags 0, 30 // Keepalive ])) // The Server MUST validate that the reserved flag in the CONNECT Control Packet is set to zero and disconnect the Client if it is not zero [MQTT-3.1.2-3] testParseError('Connect flag bit 0 must be 0, but got 1', Buffer.from([ 16, 10, // Header 0, 4, // Protocol ID length 0x4d, 0x51, 0x54, 0x54, // Protocol ID 3, // Protocol version 3, // Connect flags 0, 30 // Keepalive ])) // If the Will Flag is set to 0 the Will QoS and Will Retain fields in the Connect Flags MUST be set to zero and the Will Topic and Will Message fields MUST NOT be present in the payload [MQTT-3.1.2-11]. testParseError('Will Retain Flag must be set to zero when Will Flag is set to 0', Buffer.from([ 16, 10, // Header 0, 4, // Protocol ID length 0x4d, 0x51, 0x54, 0x54, // Protocol ID 3, // Protocol version 0x22, // Connect flags 0, 30 // Keepalive ])) // If the Will Flag is set to 0 the Will QoS and Will Retain fields in the Connect Flags MUST be set to zero and the Will Topic and Will Message fields MUST NOT be present in the payload [MQTT-3.1.2-11]. testParseError('Will QoS must be set to zero when Will Flag is set to 0', Buffer.from([ 16, 10, // Header 0, 4, // Protocol ID length 0x4d, 0x51, 0x54, 0x54, // Protocol ID 3, // Protocol version 0x12, // Connect flags 0, 30 // Keepalive ])) // If the Will Flag is set to 0 the Will QoS and Will Retain fields in the Connect Flags MUST be set to zero and the Will Topic and Will Message fields MUST NOT be present in the payload [MQTT-3.1.2-11]. testParseError('Will QoS must be set to zero when Will Flag is set to 0', Buffer.from([ 16, 10, // Header 0, 4, // Protocol ID length 0x4d, 0x51, 0x54, 0x54, // Protocol ID 3, // Protocol version 0xa, // Connect flags 0, 30 // Keepalive ])) // CONNECT, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK (v.5) packets must have payload // CONNECT testParseError('Packet too short', Buffer.from([ 16, // Header 8, // Packet length 0, 4, // Protocol ID length 77, 81, 84, 84, // MQTT 5, // Version 2, // Clean Start enabled 0, 0, // Keep-Alive 0, // Property Length 0, 0 // Properties // No payload ]), { protocolVersion: 5 }) // SUBSCRIBE testParseError('Malformed subscribe, no payload specified', Buffer.from([ 130, // Header 0 // Packet length ]), { protocolVersion: 5 }) // SUBACK testParseError('Malformed suback, no payload specified', Buffer.from([ 144, // Header 0 // Packet length ]), { protocolVersion: 5 }) // UNSUBSCRIBE testParseError('Malformed unsubscribe, no payload specified', Buffer.from([ 162, // Header 0 // Packet length ]), { protocolVersion: 5 }) // UNSUBACK (v.5) testParseError('Malformed unsuback, no payload specified', Buffer.from([ 176, // Header 0 // Packet length ]), { protocolVersion: 5 }) // UNSUBACK (v.4) testParseError('Malformed unsuback, payload length must be 2', Buffer.from([ 176, // Header 1, // Packet length 1 ]), { protocolVersion: 4 }) // UNSUBACK (v.3) testParseError('Malformed unsuback, payload length must be 2', Buffer.from([ 176, // Header 1, // Packet length 1 ]), { protocolVersion: 3 }) testParseGenerate('connack with return code 0', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 2, sessionPresent: false, returnCode: 0 }, Buffer.from([ 32, 2, 0, 0 ])) testParseGenerate('connack MQTT 5 with properties', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 87, sessionPresent: false, reasonCode: 0, properties: { sessionExpiryInterval: 1234, receiveMaximum: 432, maximumQoS: 2, retainAvailable: true, maximumPacketSize: 100, assignedClientIdentifier: 'test', topicAliasMaximum: 456, reasonString: 'test', userProperties: { test: 'test' }, wildcardSubscriptionAvailable: true, subscriptionIdentifiersAvailable: true, sharedSubscriptionAvailable: false, serverKeepAlive: 1234, responseInformation: 'test', serverReference: 'test', authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) } }, Buffer.from([ 32, 87, 0, 0, 84, // properties length 17, 0, 0, 4, 210, // sessionExpiryInterval 33, 1, 176, // receiveMaximum 36, 2, // Maximum qos 37, 1, // retainAvailable 39, 0, 0, 0, 100, // maximumPacketSize 18, 0, 4, 116, 101, 115, 116, // assignedClientIdentifier 34, 1, 200, // topicAliasMaximum 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 40, 1, // wildcardSubscriptionAvailable 41, 1, // subscriptionIdentifiersAvailable 42, 0, // sharedSubscriptionAvailable 19, 4, 210, // serverKeepAlive 26, 0, 4, 116, 101, 115, 116, // responseInformation 28, 0, 4, 116, 101, 115, 116, // serverReference 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 22, 0, 4, 1, 2, 3, 4 // authenticationData ]), { protocolVersion: 5 }) testParseGenerate('connack MQTT 5 with properties and doubled user properties', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 100, sessionPresent: false, reasonCode: 0, properties: { sessionExpiryInterval: 1234, receiveMaximum: 432, maximumQoS: 2, retainAvailable: true, maximumPacketSize: 100, assignedClientIdentifier: 'test', topicAliasMaximum: 456, reasonString: 'test', userProperties: { test: ['test', 'test'] }, wildcardSubscriptionAvailable: true, subscriptionIdentifiersAvailable: true, sharedSubscriptionAvailable: false, serverKeepAlive: 1234, responseInformation: 'test', serverReference: 'test', authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) } }, Buffer.from([ 32, 100, 0, 0, 97, // properties length 17, 0, 0, 4, 210, // sessionExpiryInterval 33, 1, 176, // receiveMaximum 36, 2, // Maximum qos 37, 1, // retainAvailable 39, 0, 0, 0, 100, // maximumPacketSize 18, 0, 4, 116, 101, 115, 116, // assignedClientIdentifier 34, 1, 200, // topicAliasMaximum 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 40, 1, // wildcardSubscriptionAvailable 41, 1, // subscriptionIdentifiersAvailable 42, 0, // sharedSubscriptionAvailable 19, 4, 210, // serverKeepAlive 26, 0, 4, 116, 101, 115, 116, // responseInformation 28, 0, 4, 116, 101, 115, 116, // serverReference 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 22, 0, 4, 1, 2, 3, 4 // authenticationData ]), { protocolVersion: 5 }) testParseGenerate('connack with return code 0 session present bit set', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 2, sessionPresent: true, returnCode: 0 }, Buffer.from([ 32, 2, 1, 0 ])) testParseGenerate('connack with return code 5', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 2, sessionPresent: false, returnCode: 5 }, Buffer.from([ 32, 2, 0, 5 ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for connack packet', Buffer.from([ 33, 2, // header 0, // flags 5 // return code ])) // Byte 1 is the "Connect Acknowledge Flags". Bits 7-1 are reserved and MUST be set to 0 [MQTT-3.2.2-1]. testParseError('Invalid connack flags, bits 7-1 must be set to 0', Buffer.from([ 32, 2, // header 2, // flags 5 // return code ])) testGenerateError('Invalid return code', { cmd: 'connack', retain: false, qos: 0, dup: false, length: 2, sessionPresent: false, returnCode: '5' // returncode must be a number }) testParseGenerate('minimal publish', { cmd: 'publish', retain: false, qos: 0, dup: false, length: 10, topic: 'test', payload: Buffer.from('test') }, Buffer.from([ 48, 10, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 116, 101, 115, 116 // Payload (test) ])) testParseGenerate('publish MQTT 5 properties', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 86, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: true, messageExpiryInterval: 4321, topicAlias: 100, responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: ['test', 'test', 'test'] }, subscriptionIdentifier: 120, contentType: 'test' } }, Buffer.from([ 61, 86, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 0, 10, // Message ID 73, // properties length 1, 1, // payloadFormatIndicator 2, 0, 0, 16, 225, // message expiry interval 35, 0, 100, // topicAlias 8, 0, 5, 116, 111, 112, 105, 99, // response topic 9, 0, 4, 1, 2, 3, 4, // correlationData 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 11, 120, // subscriptionIdentifier 3, 0, 4, 116, 101, 115, 116, // content type 116, 101, 115, 116 // Payload (test) ]), { protocolVersion: 5 }) testParseGenerate('publish MQTT 5 with multiple same properties', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 64, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: true, messageExpiryInterval: 4321, topicAlias: 100, responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' }, subscriptionIdentifier: [120, 121, 122], contentType: 'test' } }, Buffer.from([ 61, 64, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 0, 10, // Message ID 51, // properties length 1, 1, // payloadFormatIndicator 2, 0, 0, 16, 225, // message expiry interval 35, 0, 100, // topicAlias 8, 0, 5, 116, 111, 112, 105, 99, // response topic 9, 0, 4, 1, 2, 3, 4, // correlationData 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 11, 120, // subscriptionIdentifier 11, 121, // subscriptionIdentifier 11, 122, // subscriptionIdentifier 3, 0, 4, 116, 101, 115, 116, // content type 116, 101, 115, 116 // Payload (test) ]), { protocolVersion: 5 }) testParseGenerate('publish MQTT 5 properties with 0-4 byte varbyte', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 27, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: false, subscriptionIdentifier: [128, 16384, 2097152] // this tests the varbyte handling } }, Buffer.from([ 61, 27, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 0, 10, // Message ID 14, // properties length 1, 0, // payloadFormatIndicator 11, 128, 1, // subscriptionIdentifier 11, 128, 128, 1, // subscriptionIdentifier 11, 128, 128, 128, 1, // subscriptionIdentifier 116, 101, 115, 116 // Payload (test) ]), { protocolVersion: 5 }) testParseGenerate('publish MQTT 5 properties with max value varbyte', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 22, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: false, subscriptionIdentifier: [1, 268435455] } }, Buffer.from([ 61, 22, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 0, 10, // Message ID 9, // properties length 1, 0, // payloadFormatIndicator 11, 1, // subscriptionIdentifier 11, 255, 255, 255, 127, // subscriptionIdentifier (max value) 116, 101, 115, 116 // Payload (test) ]), { protocolVersion: 5 }) ; (() => { const buffer = Buffer.alloc(2048) testParseGenerate('2KB publish packet', { cmd: 'publish', retain: false, qos: 0, dup: false, length: 2054, topic: 'test', payload: buffer }, Buffer.concat([Buffer.from([ 48, 134, 16, // Header 0, 4, // Topic length 116, 101, 115, 116 // Topic (test) ]), buffer])) })() ; (() => { const maxLength = 268435455 const buffer = Buffer.alloc(maxLength - 6) testParseGenerate('Max payload publish packet', { cmd: 'publish', retain: false, qos: 0, dup: false, length: maxLength, topic: 'test', payload: buffer }, Buffer.concat([Buffer.from([ 48, 255, 255, 255, 127, // Header 0, 4, // Topic length 116, 101, 115, 116 // Topic (test) ]), buffer])) })() testParseGenerate('maximal publish', { cmd: 'publish', retain: true, qos: 2, length: 12, dup: true, topic: 'test', messageId: 10, payload: Buffer.from('test') }, Buffer.from([ 61, 12, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic 0, 10, // Message ID 116, 101, 115, 116 // Payload ])) test('publish all strings generate', t => { const message = { cmd: 'publish', retain: true, qos: 2, length: 12, dup: true, topic: 'test', messageId: 10, payload: Buffer.from('test') } const expected = Buffer.from([ 61, 12, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic 0, 10, // Message ID 116, 101, 115, 116 // Payload ]) t.equal(mqtt.generate(message).toString('hex'), expected.toString('hex')) t.end() }) testParseGenerate('empty publish', { cmd: 'publish', retain: false, qos: 0, dup: false, length: 6, topic: 'test', payload: Buffer.alloc(0) }, Buffer.from([ 48, 6, // Header 0, 4, // Topic length 116, 101, 115, 116 // Topic // Empty payload ])) // A PUBLISH Packet MUST NOT have both QoS bits set to 1. If a Server or Client receives a PUBLISH Packet which has both QoS bits set to 1 it MUST close the Network Connection [MQTT-3.3.1-4]. testParseError('Packet must not have both QoS bits set to 1', Buffer.from([ 0x36, 6, // Header 0, 4, // Topic length 116, 101, 115, 116 // Topic // Empty payload ])) test('splitted publish parse', t => { t.plan(3) const parser = mqtt.parser() const expected = { cmd: 'publish', retain: false, qos: 0, dup: false, length: 10, topic: 'test', payload: Buffer.from('test') } parser.on('packet', packet => { t.deepLooseEqual(packet, expected, 'expected packet') }) t.equal(parser.parse(Buffer.from([ 48, 10, // Header 0, 4, // Topic length 116, 101, 115, 116 // Topic (test) ])), 6, 'remaining bytes') t.equal(parser.parse(Buffer.from([ 116, 101, 115, 116 // Payload (test) ])), 0, 'remaining bytes') }) test('split publish longer', t => { t.plan(3) const length = 255 const topic = 'test' // Minus two bytes for the topic length specifier const payloadLength = length - topic.length - 2 const parser = mqtt.parser() const expected = { cmd: 'publish', retain: false, qos: 0, dup: false, length, topic, payload: Buffer.from('a'.repeat(payloadLength)) } parser.on('packet', packet => { t.deepLooseEqual(packet, expected, 'expected packet') }) t.equal(parser.parse(Buffer.from([ 48, 255, 1, // Header 0, topic.length, // Topic length 116, 101, 115, 116 // Topic (test) ])), 6, 'remaining bytes') t.equal(parser.parse(Buffer.from(Array(payloadLength).fill(97))), 0, 'remaining bytes') }) test('split length parse', t => { t.plan(4) const length = 255 const topic = 'test' const payloadLength = length - topic.length - 2 const parser = mqtt.parser() const expected = { cmd: 'publish', retain: false, qos: 0, dup: false, length, topic, payload: Buffer.from('a'.repeat(payloadLength)) } parser.on('packet', packet => { t.deepLooseEqual(packet, expected, 'expected packet') }) t.equal(parser.parse(Buffer.from([ 48, 255 // Header (partial length) ])), 1, 'remaining bytes') t.equal(parser.parse(Buffer.from([ 1, // Rest of header length 0, topic.length, // Topic length 116, 101, 115, 116 // Topic (test) ])), 6, 'remaining bytes') t.equal(parser.parse(Buffer.from(Array(payloadLength).fill(97))), 0, 'remaining bytes') }) testGenerateError('Invalid variable byte integer: 268435456', { cmd: 'publish', retain: false, qos: 0, dup: false, length: (268435455 + 1), topic: 'test', payload: Buffer.alloc(268435455 + 1 - 6) }, {}, 'Length var byte integer over max allowed value throws error') testGenerateError('Invalid subscriptionIdentifier: 268435456', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 27, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: false, subscriptionIdentifier: 268435456 } }, { protocolVersion: 5 }, 'MQTT 5.0 var byte integer >24 bits throws error') testParseGenerate('puback', { cmd: 'puback', retain: false, qos: 0, dup: false, length: 2, messageId: 2 }, Buffer.from([ 64, 2, // Header 0, 2 // Message ID ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for puback packet', Buffer.from([ 65, 2, // Header 0, 2 // Message ID ])) testParseGenerate('puback without reason and no MQTT 5 properties', { cmd: 'puback', retain: false, qos: 0, dup: false, length: 2, messageId: 2, reasonCode: 0 }, Buffer.from([ 64, 2, // Header 0, 2 // Message ID ]), { protocolVersion: 5 }) testParseGenerate('puback with reason and no MQTT 5 properties', { cmd: 'puback', retain: false, qos: 0, dup: false, length: 4, messageId: 2, reasonCode: 16 }, Buffer.from([ 64, 4, // Header 0, 2, // Message ID 16, // reason code 0 // no user properties ]), { protocolVersion: 5 }) testParseGenerate('puback MQTT 5 properties', { cmd: 'puback', retain: false, qos: 0, dup: false, length: 24, messageId: 2, reasonCode: 16, properties: { reasonString: 'test', userProperties: { test: 'test' } } }, Buffer.from([ 64, 24, // Header 0, 2, // Message ID 16, // reason code 20, // properties length 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties ]), { protocolVersion: 5 }) testParseError('Invalid puback reason code', Buffer.from([ 64, 4, // Header 0, 2, // Message ID 0x11, // reason code 0 // properties length ]), { protocolVersion: 5 }) testParseGenerate('pubrec', { cmd: 'pubrec', retain: false, qos: 0, dup: false, length: 2, messageId: 2 }, Buffer.from([ 80, 2, // Header 0, 2 // Message ID ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for pubrec packet', Buffer.from([ 81, 2, // Header 0, 2 // Message ID ])) testParseGenerate('pubrec MQTT 5 properties', { cmd: 'pubrec', retain: false, qos: 0, dup: false, length: 24, messageId: 2, reasonCode: 16, properties: { reasonString: 'test', userProperties: { test: 'test' } } }, Buffer.from([ 80, 24, // Header 0, 2, // Message ID 16, // reason code 20, // properties length 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties ]), { protocolVersion: 5 }) testParseGenerate('pubrel', { cmd: 'pubrel', retain: false, qos: 1, dup: false, length: 2, messageId: 2 }, Buffer.from([ 98, 2, // Header 0, 2 // Message ID ])) testParseError('Invalid pubrel reason code', Buffer.from([ 98, 4, // Header 0, 2, // Message ID 0x11, // Reason code 0 // Properties length ]), { protocolVersion: 5 }) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x2 for pubrel packet', Buffer.from([ 96, 2, // Header 0, 2 // Message ID ])) testParseGenerate('pubrel MQTT5 properties', { cmd: 'pubrel', retain: false, qos: 1, dup: false, length: 24, messageId: 2, reasonCode: 0x92, properties: { reasonString: 'test', userProperties: { test: 'test' } } }, Buffer.from([ 98, 24, // Header 0, 2, // Message ID 0x92, // reason code 20, // properties length 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties ]), { protocolVersion: 5 }) testParseError('Invalid pubrel reason code', Buffer.from([ 98, 4, // Header 0, 2, // Message ID 16, // reason code 0 // properties length ]), { protocolVersion: 5 }) testParseGenerate('pubcomp', { cmd: 'pubcomp', retain: false, qos: 0, dup: false, length: 2, messageId: 2 }, Buffer.from([ 112, 2, // Header 0, 2 // Message ID ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for pubcomp packet', Buffer.from([ 113, 2, // Header 0, 2 // Message ID ])) testParseGenerate('pubcomp MQTT 5 properties', { cmd: 'pubcomp', retain: false, qos: 0, dup: false, length: 24, messageId: 2, reasonCode: 0x92, properties: { reasonString: 'test', userProperties: { test: 'test' } } }, Buffer.from([ 112, 24, // Header 0, 2, // Message ID 0x92, // reason code 20, // properties length 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties ]), { protocolVersion: 5 }) testParseError('Invalid pubcomp reason code', Buffer.from([ 112, 4, // Header 0, 2, // Message ID 16, // reason code 0 // properties length ]), { protocolVersion: 5 }) testParseError('Invalid header flag bits, must be 0x2 for subscribe packet', Buffer.from([ 128, 9, // Header (subscribeqos=0length=9) 0, 6, // Message ID (6) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0 // Qos (0) ])) testParseGenerate('subscribe to one topic', { cmd: 'subscribe', retain: false, qos: 1, dup: false, length: 9, subscriptions: [ { topic: 'test', qos: 0 } ], messageId: 6 }, Buffer.from([ 130, 9, // Header (subscribeqos=1length=9) 0, 6, // Message ID (6) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0 // Qos (0) ])) testParseError('Invalid subscribe QoS, must be <= 2', Buffer.from([ 130, 9, // Header (subscribeqos=0length=9) 0, 6, // Message ID (6) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 3 // Qos ])) testParseError('Invalid subscribe topic flag bits, bits 7-6 must be 0', Buffer.from([ 130, 10, // Header (subscribeqos=0length=9) 0, 6, // Message ID (6) 0, // Property length (0) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0x80 // Flags ]), { protocolVersion: 5 }) testParseError('Invalid retain handling, must be <= 2', Buffer.from([ 130, 10, // Header (subscribeqos=0length=9) 0, 6, // Message ID (6) 0, // Property length (0) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0x30 // Flags ]), { protocolVersion: 5 }) testParseError('Invalid subscribe topic flag bits, bits 7-2 must be 0', Buffer.from([ 130, 9, // Header (subscribeqos=0length=9) 0, 6, // Message ID (6) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0x08 // Flags ])) testParseGenerate('subscribe to one topic by MQTT 5', { cmd: 'subscribe', retain: false, qos: 1, dup: false, length: 26, subscriptions: [ { topic: 'test', qos: 0, nl: false, rap: true, rh: 1 } ], messageId: 6, properties: { subscriptionIdentifier: 145, userProperties: { test: 'test' } } }, Buffer.from([ 130, 26, // Header (subscribeqos=1length=9) 0, 6, // Message ID (6) 16, // properties length 11, 145, 1, // subscriptionIdentifier 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 24 // settings(qos: 0, noLocal: false, Retain as Published: true, retain handling: 1) ]), { protocolVersion: 5 }) testParseGenerate('subscribe to three topics', { cmd: 'subscribe', retain: false, qos: 1, dup: false, length: 23, subscriptions: [ { topic: 'test', qos: 0 }, { topic: 'uest', qos: 1 }, { topic: 'tfst', qos: 2 } ], messageId: 6 }, Buffer.from([ 130, 23, // Header (publishqos=1length=9) 0, 6, // Message ID (6) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0, // Qos (0) 0, 4, // Topic length 117, 101, 115, 116, // Topic (uest) 1, // Qos (1) 0, 4, // Topic length 116, 102, 115, 116, // Topic (tfst) 2 // Qos (2) ])) testParseGenerate('subscribe to 3 topics by MQTT 5', { cmd: 'subscribe', retain: false, qos: 1, dup: false, length: 40, subscriptions: [ { topic: 'test', qos: 0, nl: false, rap: true, rh: 1 }, { topic: 'uest', qos: 1, nl: false, rap: false, rh: 0 }, { topic: 'tfst', qos: 2, nl: true, rap: false, rh: 0 } ], messageId: 6, properties: { subscriptionIdentifier: 145, userProperties: { test: 'test' } } }, Buffer.from([ 130, 40, // Header (subscribeqos=1length=9) 0, 6, // Message ID (6) 16, // properties length 11, 145, 1, // subscriptionIdentifier 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 24, // settings(qos: 0, noLocal: false, Retain as Published: true, retain handling: 1) 0, 4, // Topic length 117, 101, 115, 116, // Topic (uest) 1, // Qos (1) 0, 4, // Topic length 116, 102, 115, 116, // Topic (tfst) 6 // Qos (2), No Local: true ]), { protocolVersion: 5 }) testParseGenerate('suback', { cmd: 'suback', retain: false, qos: 0, dup: false, length: 5, granted: [0, 1, 2], messageId: 6 }, Buffer.from([ 144, 5, // Header 0, 6, // Message ID 0, 1, 2 ])) testParseGenerate('suback', { cmd: 'suback', retain: false, qos: 0, dup: false, length: 7, granted: [0, 1, 2, 128], messageId: 6 }, Buffer.from([ 144, 7, // Header 0, 6, // Message ID 0, // Property length 0, 1, 2, 128 // Granted qos (0, 1, 2) and a rejected being 0x80 ]), { protocolVersion: 5 }) testParseError('Invalid suback QoS, must be 0, 1, 2 or 128', Buffer.from([ 144, 6, // Header 0, 6, // Message ID 0, 1, 2, 3 // Granted qos (0, 1, 2) ])) testParseError('Invalid suback code', Buffer.from([ 144, 6, // Header 0, 6, // Message ID 0, 1, 2, 0x79 // Granted qos (0, 1, 2) and an invalid code ]), { protocolVersion: 5 }) testParseGenerate('suback MQTT 5', { cmd: 'suback', retain: false, qos: 0, dup: false, length: 27, granted: [0, 1, 2, 128], messageId: 6, properties: { reasonString: 'test', userProperties: { test: 'test' } } }, Buffer.from([ 144, 27, // Header 0, 6, // Message ID 20, // properties length 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 0, 1, 2, 128 // Granted qos (0, 1, 2) and a rejected being 0x80 ]), { protocolVersion: 5 }) testParseGenerate('unsubscribe', { cmd: 'unsubscribe', retain: false, qos: 1, dup: false, length: 14, unsubscriptions: [ 'tfst', 'test' ], messageId: 7 }, Buffer.from([ 162, 14, 0, 7, // Message ID (7) 0, 4, // Topic length 116, 102, 115, 116, // Topic (tfst) 0, 4, // Topic length, 116, 101, 115, 116 // Topic (test) ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x2 for unsubscribe packet', Buffer.from([ 160, 14, 0, 7, // Message ID (7) 0, 4, // Topic length 116, 102, 115, 116, // Topic (tfst) 0, 4, // Topic length, 116, 101, 115, 116 // Topic (test) ])) testGenerateError('Invalid unsubscriptions', { cmd: 'unsubscribe', retain: false, qos: 1, dup: true, length: 5, unsubscriptions: 5, messageId: 7 }, {}, 'unsubscribe with unsubscriptions not an array') testGenerateError('Invalid unsubscriptions', { cmd: 'unsubscribe', retain: false, qos: 1, dup: true, length: 5, unsubscriptions: [1, 2], messageId: 7 }, {}, 'unsubscribe with unsubscriptions as an object') testParseGenerate('unsubscribe MQTT 5', { cmd: 'unsubscribe', retain: false, qos: 1, dup: false, length: 28, unsubscriptions: [ 'tfst', 'test' ], messageId: 7, properties: { userProperties: { test: 'test' } } }, Buffer.from([ 162, 28, 0, 7, // Message ID (7) 13, // properties length 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 0, 4, // Topic length 116, 102, 115, 116, // Topic (tfst) 0, 4, // Topic length, 116, 101, 115, 116 // Topic (test) ]), { protocolVersion: 5 }) testParseGenerate('unsuback', { cmd: 'unsuback', retain: false, qos: 0, dup: false, length: 2, messageId: 8 }, Buffer.from([ 176, 2, // Header 0, 8 // Message ID ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for unsuback packet', Buffer.from([ 177, 2, // Header 0, 8 // Message ID ])) testParseGenerate('unsuback MQTT 5', { cmd: 'unsuback', retain: false, qos: 0, dup: false, length: 25, messageId: 8, properties: { reasonString: 'test', userProperties: { test: 'test' } }, granted: [0, 128] }, Buffer.from([ 176, 25, // Header 0, 8, // Message ID 20, // properties length 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 0, 128 // success and error ]), { protocolVersion: 5 }) testParseError('Invalid unsuback code', Buffer.from([ 176, 4, // Header 0, 8, // Message ID 0, // properties length 0x84 // reason codes ]), { protocolVersion: 5 }) testParseGenerate('pingreq', { cmd: 'pingreq', retain: false, qos: 0, dup: false, length: 0 }, Buffer.from([ 192, 0 // Header ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for pingreq packet', Buffer.from([ 193, 0 // Header ])) testParseGenerate('pingresp', { cmd: 'pingresp', retain: false, qos: 0, dup: false, length: 0 }, Buffer.from([ 208, 0 // Header ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for pingresp packet', Buffer.from([ 209, 0 // Header ])) testParseGenerate('disconnect', { cmd: 'disconnect', retain: false, qos: 0, dup: false, length: 0 }, Buffer.from([ 224, 0 // Header ])) // Where a flag bit is marked as “Reserved” in Table 2.2 - Flag Bits, it is reserved for future use and MUST be set to the value listed in that table [MQTT-2.2.2-1]. If invalid flags are received, the receiver MUST close the Network Connection [MQTT-2.2.2-2] testParseError('Invalid header flag bits, must be 0x0 for disconnect packet', Buffer.from([ 225, 0 // Header ])) testParseGenerate('disconnect MQTT 5', { cmd: 'disconnect', retain: false, qos: 0, dup: false, length: 34, reasonCode: 0, properties: { sessionExpiryInterval: 145, reasonString: 'test', userProperties: { test: 'test' }, serverReference: 'test' } }, Buffer.from([ 224, 34, // Header 0, // reason code 32, // properties length 17, 0, 0, 0, 145, // sessionExpiryInterval 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 28, 0, 4, 116, 101, 115, 116// serverReference ]), { protocolVersion: 5 }) testParseGenerate('disconnect MQTT 5 with no properties', { cmd: 'disconnect', retain: false, qos: 0, dup: false, length: 2, reasonCode: 0 }, Buffer.from([ 224, 2, // Fixed Header (DISCONNECT, Remaining Length) 0, // Reason Code (Normal Disconnection) 0 // Property Length (0 => No Properties) ]), { protocolVersion: 5 }) testParseError('Invalid disconnect reason code', Buffer.from([ 224, 2, // Fixed Header (DISCONNECT, Remaining Length) 0x05, // Reason Code (Normal Disconnection) 0 // Property Length (0 => No Properties) ]), { protocolVersion: 5 }) testParseGenerate('auth MQTT 5', { cmd: 'auth', retain: false, qos: 0, dup: false, length: 36, reasonCode: 0, properties: { authenticationMethod: 'test', authenticationData: Buffer.from([0, 1, 2, 3]), reasonString: 'test', userProperties: { test: 'test' } } }, Buffer.from([ 240, 36, // Header 0, // reason code 34, // properties length 21, 0, 4, 116, 101, 115, 116, // auth method 22, 0, 4, 0, 1, 2, 3, // auth data 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties ]), { protocolVersion: 5 }) testParseError('Invalid auth reason code', Buffer.from([ 240, 2, // Fixed Header (DISCONNECT, Remaining Length) 0x17, // Reason Code 0 // Property Length (0 => No Properties) ]), { protocolVersion: 5 }) testGenerateError('Invalid protocolId', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 42, protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: 'password' }) testGenerateError('Invalid protocol version', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 1, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: 'password' }) testGenerateError('clientId must be supplied before 3.1.1', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 30, username: 'username', password: 'password' }) testGenerateError('clientId must be given if cleanSession set to 0', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQTT', protocolVersion: 4, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: false, keepalive: 30, username: 'username', password: 'password' }) testGenerateError('Invalid keepalive', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 'hello', clientId: 'test', username: 'username', password: 'password' }) testGenerateError('Invalid keepalive', { cmd: 'connect', keepalive: 3.1416 }) testGenerateError('Invalid will', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: 42, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: 'password' }) testGenerateError('Invalid will topic', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, payload: 'payload' }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: 'password' }) testGenerateError('Invalid will payload', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 42 }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: 'password' }) testGenerateError('Invalid username', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 30, clientId: 'test', username: 42, password: 'password' }) testGenerateError('Invalid password', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 30, clientId: 'test', username: 'username', password: 42 }) testGenerateError('Username is required to use password', { cmd: 'connect', retain: false, qos: 0, dup: false, length: 54, protocolId: 'MQIsdp', protocolVersion: 3, will: { retain: true, qos: 2, topic: 'topic', payload: 'payload' }, clean: true, keepalive: 30, clientId: 'test', password: 'password' }) testGenerateError('Invalid messageExpiryInterval: -4321', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 60, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: true, messageExpiryInterval: -4321, topicAlias: 100, responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' }, subscriptionIdentifier: 120, contentType: 'test' } }, { protocolVersion: 5 }) testGenerateError('Invalid topicAlias: -100', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 60, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: true, messageExpiryInterval: 4321, topicAlias: -100, responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' }, subscriptionIdentifier: 120, contentType: 'test' } }, { protocolVersion: 5 }) testGenerateError('Invalid subscriptionIdentifier: -120', { cmd: 'publish', retain: true, qos: 2, dup: true, length: 60, topic: 'test', payload: Buffer.from('test'), messageId: 10, properties: { payloadFormatIndicator: true, messageExpiryInterval: 4321, topicAlias: 100, responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' }, subscriptionIdentifier: -120, contentType: 'test' } }, { protocolVersion: 5 }) test('support cork', t => { t.plan(9) const dest = WS() dest._write = (chunk, enc, cb) => { t.pass('_write called') cb() } mqtt.writeToStream({ cmd: 'connect', retain: false, qos: 0, dup: false, length: 18, protocolId: 'MQIsdp', protocolVersion: 3, clean: false, keepalive: 30, clientId: 'test' }, dest) dest.end() }) // The following test case was designed after experiencing errors // when trying to connect with tls on a non tls mqtt port // the specific behaviour is: // - first byte suggests this is a connect message // - second byte suggests message length to be smaller than buffer length // thus payload processing starts // - the first two bytes suggest a protocol identifier string length // that leads the parser pointer close to the end of the buffer // - when trying to read further connect flags the buffer produces // a "out of range" Error // testParseError('Packet too short', Buffer.from([ 16, 9, 0, 6, 77, 81, 73, 115, 100, 112, 3 ])) // CONNECT Packets that show other protocol IDs than // the valid values MQTT and MQIsdp should cause an error // those packets are a hint that this is not a mqtt connection testParseError('Invalid protocolId', Buffer.from([ 16, 18, 0, 6, 65, 65, 65, 65, 65, 65, // AAAAAA 3, // Protocol version 0, // Connect flags 0, 10, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116 // Client ID ])) // CONNECT Packets that contain an unsupported protocol version // Flag (i.e. not `3` or `4` or '5') should cause an error testParseError('Invalid protocol version', Buffer.from([ 16, 18, 0, 6, 77, 81, 73, 115, 100, 112, // Protocol ID 1, // Protocol version 0, // Connect flags 0, 10, // Keepalive 0, 4, // Client ID length 116, 101, 115, 116 // Client ID ])) // When a packet contains a string in the variable header and the // given string length of this exceeds the overall length of the packet that // was specified in the fixed header, parsing must fail. // this case simulates this behavior with the protocol ID string of the // CONNECT packet. The fixed header suggests a remaining length of 8 bytes // which would be exceeded by the string length of 15 // in this case, a protocol ID parse error is expected testParseError('Cannot parse protocolId', Buffer.from([ 16, 8, // Fixed header 0, 15, // string length 15 --> 15 > 8 --> error! 77, 81, 73, 115, 100, 112, 77, 81, 73, 115, 100, 112, 77, 81, 73, 115, 100, 112, 77, 81, 73, 115, 100, 112, 77, 81, 73, 115, 100, 112, 77, 81, 73, 115, 100, 112, 77, 81, 73, 115, 100, 112, 77, 81, 73, 115, 100, 112 ])) testParseError('Unknown property', Buffer.from([ 61, 60, // Header 0, 4, // Topic length 116, 101, 115, 116, // Topic (test) 0, 10, // Message ID 47, // properties length 126, 1, // unknown property 2, 0, 0, 16, 225, // message expiry interval 35, 0, 100, // topicAlias 8, 0, 5, 116, 111, 112, 105, 99, // response topic 9, 0, 4, 1, 2, 3, 4, // correlationData 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 11, 120, // subscriptionIdentifier 3, 0, 4, 116, 101, 115, 116, // content type 116, 101, 115, 116 // Payload (test) ]), { protocolVersion: 5 }) testParseError('Not supported auth packet for this version MQTT', Buffer.from([ 240, 36, // Header 0, // reason code 34, // properties length 21, 0, 4, 116, 101, 115, 116, // auth method 22, 0, 4, 0, 1, 2, 3, // auth data 31, 0, 4, 116, 101, 115, 116, // reasonString 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties ])) // When a Subscribe packet contains a topic_filter and the given // length is topic_filter.length + 1 then the last byte (requested QoS) is interpreted as topic_filter // reading the requested_qos at the end causes 'Index out of range' read testParseError('Malformed Subscribe Payload', Buffer.from([ 130, 14, // subscribe header and remaining length 0, 123, // packet ID 0, 10, // topic filter length 104, 105, 106, 107, 108, 47, 109, 110, 111, // topic filter with length of 9 bytes 0 // requested QoS ])) test('Cannot parse property code type', t => { const packets = Buffer.from([ 16, 16, 0, 4, 77, 81, 84, 84, 5, 2, 0, 60, 3, 33, 0, 20, 0, 0, 98, 2, 211, 1, 224, 2, 0, 32 ]) t.plan(3) const parser = mqtt.parser() parser.on('error', err => { t.equal(err.message, 'Cannot parse property code type', 'expected error message') t.end() }) parser.on('packet', (packet) => { t.pass('Packet parsed') }) parser.parse(packets) }) testWriteToStreamError('Invalid command', { cmd: 'invalid' }) testWriteToStreamError('Invalid protocolId', { cmd: 'connect', protocolId: {} }) test('userProperties null prototype', t => { t.plan(3) const packet = mqtt.generate({ cmd: 'connect', retain: false, qos: 0, dup: false, length: 125, protocolId: 'MQTT', protocolVersion: 5, will: { retain: true, qos: 2, properties: { willDelayInterval: 1234, payloadFormatIndicator: false, messageExpiryInterval: 4321, contentType: 'test', responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' } }, topic: 'topic', payload: Buffer.from([4, 3, 2, 1]) }, clean: true, keepalive: 30, properties: { sessionExpiryInterval: 1234, receiveMaximum: 432, maximumPacketSize: 100, topicAliasMaximum: 456, requestResponseInformation: true, requestProblemInformation: true, userProperties: { test: 'test' }, authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) }, clientId: 'test' }) const parser = mqtt.parser() parser.on('packet', packet => { t.equal(packet.cmd, 'connect') t.equal(Object.getPrototypeOf(packet.properties.userProperties), null) t.equal(Object.getPrototypeOf(packet.will.properties.userProperties), null) }) parser.parse(packet) }) test('stops parsing after first error', t => { t.plan(4) const parser = mqtt.parser() let packetCount = 0 let errorCount = 0 let expectedPackets = 1 let expectedErrors = 1 parser.on('packet', packet => { t.ok(++packetCount <= expectedPackets, `expected <= ${expectedPackets} packets`) }) parser.on('error', erroneous => { t.ok(++errorCount <= expectedErrors, `expected <= ${expectedErrors} errors`) }) parser.parse(Buffer.from([ // First, a valid connect packet: 16, 12, // Header 0, 4, // Protocol ID length 77, 81, 84, 84, // Protocol ID 4, // Protocol version 2, // Connect flags 0, 30, // Keepalive 0, 0, // Client ID length // Then an invalid subscribe packet: 128, 9, // Header (subscribeqos=0length=9) 0, 6, // Message ID (6) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0, // Qos (0) // And another invalid subscribe packet: 128, 9, // Header (subscribeqos=0length=9) 0, 6, // Message ID (6) 0, 4, // Topic length, 116, 101, 115, 116, // Topic (test) 0, // Qos (0) // Finally, a valid disconnect packet: 224, 0 // Header ])) // Calling parse again clears the error and continues parsing packetCount = 0 errorCount = 0 expectedPackets = 2 expectedErrors = 0 parser.parse(Buffer.from([ // Connect: 16, 12, // Header 0, 4, // Protocol ID length 77, 81, 84, 84, // Protocol ID 4, // Protocol version 2, // Connect flags 0, 30, // Keepalive 0, 0, // Client ID length // Disconnect: 224, 0 // Header ])) }) test('undefined properties', t => { t.plan(2) const packet = mqtt.generate({ cmd: 'connect', retain: false, qos: 0, dup: false, length: 125, protocolId: 'MQTT', protocolVersion: 5, will: { retain: true, qos: 2, properties: { willDelayInterval: 1234, payloadFormatIndicator: false, messageExpiryInterval: 4321, contentType: 'test', responseTopic: 'topic', correlationData: Buffer.from([1, 2, 3, 4]), userProperties: { test: 'test' } }, topic: 'topic', payload: Buffer.from([4, 3, 2, 1]) }, clean: true, keepalive: 30, properties: { sessionExpiryInterval: 1234, receiveMaximum: 432, maximumPacketSize: 100, topicAliasMaximum: 456, requestResponseInformation: true, requestProblemInformation: true, correlationData: undefined, userProperties: { test: 'test' }, authenticationMethod: 'test', authenticationData: Buffer.from([1, 2, 3, 4]) }, clientId: 'test' }) const parser = mqtt.parser() parser.on('packet', packet => { t.equal(packet.cmd, 'connect') t.equal(Object.hasOwn(packet.properties, 'correlationData'), false) }) parser.parse(packet) }) testGenerateErrorMultipleCmds([ 'publish', 'puback', 'pubrec', 'pubrel', 'subscribe', 'suback', 'unsubscribe', 'unsuback' ], 'Invalid messageId', { qos: 1, // required for publish topic: 'test', // required for publish messageId: 'a' }, {}) mqtt-packet-9.0.2/testRandom.js000066400000000000000000000050071476155302400164430ustar00rootroot00000000000000const mqtt = require('./') const crypto = require('crypto') const max = 1E5 const start = Date.now() / 1000 let errors = 0 let packets = 0 let randomPacket const firstBytes = [ 16 * 1, // CONNECT 16 * 2, // CONNACK 16 * 3, // PUBLISH, QoS: 0, No Retain, No Dup 16 * 3 + 1, // PUBLISH, QoS: 0, Retain, No Dup 16 * 3 + 8, // PUBLISH, QoS: 0, No Retain, Dup 16 * 3 + 1 + 8, // PUBLISH, QoS: 0, Retain, Dup 16 * 3 + 2, // PUBLISH, QoS: 1, No Retain, No Dup 16 * 3 + 2 + 1, // PUBLISH, QoS: 1, Retain, No Dup 16 * 3 + 2 + 8, // PUBLISH, QoS: 1, No Retain, Dup 16 * 3 + 2 + 1 + 8, // PUBLISH, QoS: 1, Retain, Dup 16 * 3 + 4, // PUBLISH, QoS: 2, No Retain, No Dup 16 * 3 + 4 + 1, // PUBLISH, QoS: 2, Retain, No Dup 16 * 3 + 4 + 8, // PUBLISH, QoS: 2, No Retain, Dup 16 * 3 + 4 + 1 + 8, // PUBLISH, QoS: 2, Retain, Dup 16 * 4, // PUBACK 16 * 5, // PUBREC 16 * 6, // PUBREL 16 * 7, // PUBCOMP 16 * 8, // SUBSCRIBE 16 * 9, // SUBACK 16 * 10, // UNSUBSCRIBE 16 * 11, // UNSUBACK 16 * 12, // PINGREQ 16 * 13, // PINGRESP 16 * 14, // DISCONNECT 16 * 15 // RESERVED ] function doParse () { const parser = mqtt.parser() parser.on('error', onError) parser.on('packet', onPacket) randomPacket = crypto.randomBytes(Math.floor(Math.random() * 512)) // Increase probability to have a valid first byte in order to at least // enter the parser if (Math.random() > 0.2 && randomPacket.length > 0) randomPacket.writeUInt8(firstBytes[Math.floor(Math.random() * firstBytes.length)], 0) parser.parse(randomPacket) } try { console.log('Starting benchmark') for (let i = 0; i < max; i++) { doParse() } } catch (e) { console.log('Exception occurred at packet') console.log(randomPacket) console.log(e.message) console.log(e.stack) } function onError () { errors++ } function onPacket () { packets++ } const delta = Math.abs(max - packets - errors) const time = Date.now() / 1000 - start console.log('Benchmark complete') console.log('==========================') console.log('Sent packets:', max) console.log('Total time:', Math.round(time * 100) / 100, 'seconds', '\r\n') console.log('Valid packets:', packets) console.log('Erroneous packets:', errors) if ((max - packets - errors) < 0) console.log('Excess packets:', delta, '\r\n') else console.log('Missing packets:', delta, '\r\n') console.log('Total packets:', packets + errors) console.log('Total errors:', errors + delta) console.log('Error rate:', `${((errors + delta) / max * 100).toFixed(2)}%`) console.log('==========================') mqtt-packet-9.0.2/types/000077500000000000000000000000001476155302400151275ustar00rootroot00000000000000mqtt-packet-9.0.2/types/index.d.ts000066400000000000000000000132121476155302400170270ustar00rootroot00000000000000import EventEmitter = NodeJS.EventEmitter import WritableStream = NodeJS.WritableStream export declare type QoS = 0 | 1 | 2 export declare type PacketCmd = 'auth' | 'connack' | 'connect' | 'disconnect' | 'pingreq' | 'pingresp' | 'puback' | 'pubcomp' | 'publish' | 'pubrel' | 'pubrec' | 'suback' | 'subscribe' | 'unsuback' | 'unsubscribe' export declare type UserProperties = {[index: string]: string | string[]} export interface IPacket { cmd: PacketCmd messageId?: number length?: number } export interface IAuthPacket extends IPacket { cmd: 'auth' reasonCode: number, properties?: { authenticationMethod?: string, authenticationData?: Buffer, reasonString?: string, userProperties?: UserProperties, } } export interface IConnectPacket extends IPacket { cmd: 'connect' clientId: string protocolVersion?: 4 | 5 | 3 protocolId?: 'MQTT' | 'MQIsdp' clean?: boolean keepalive?: number username?: string password?: Buffer will?: { topic: string payload: Buffer | string qos?: QoS retain?: boolean properties?: { willDelayInterval?: number, payloadFormatIndicator?: boolean, messageExpiryInterval?: number, contentType?: string, responseTopic?: string, correlationData?: Buffer, userProperties?: UserProperties } } properties?: { sessionExpiryInterval?: number, receiveMaximum?: number, maximumPacketSize?: number, topicAliasMaximum?: number, requestResponseInformation?: boolean, requestProblemInformation?: boolean, userProperties?: UserProperties, authenticationMethod?: string, authenticationData?: Buffer } } export interface IPublishPacket extends IPacket { cmd: 'publish' qos: QoS dup: boolean retain: boolean topic: string payload: string | Buffer properties?: { payloadFormatIndicator?: boolean, messageExpiryInterval?: number, topicAlias?: number, responseTopic?: string, correlationData?: Buffer, userProperties?: UserProperties, subscriptionIdentifier?: number | number[], contentType?: string } } export interface IConnackPacket extends IPacket { cmd: 'connack' returnCode?: number, reasonCode?: number, sessionPresent: boolean properties?: { sessionExpiryInterval?: number, receiveMaximum?: number, maximumQoS?: number, retainAvailable?: boolean, maximumPacketSize?: number, assignedClientIdentifier?: string, topicAliasMaximum?: number, reasonString?: string, userProperties?: UserProperties, wildcardSubscriptionAvailable?: boolean, subscriptionIdentifiersAvailable?: boolean, sharedSubscriptionAvailable?: boolean, serverKeepAlive?: number, responseInformation?: string, serverReference?: string, authenticationMethod?: string, authenticationData?: Buffer } } export interface ISubscription { topic: string qos: QoS, nl?: boolean, rap?: boolean, rh?: number } export interface ISubscribePacket extends IPacket { cmd: 'subscribe' subscriptions: ISubscription[], properties?: { reasonString?: string, subscriptionIdentifier?: number, userProperties?: UserProperties } } export interface ISubackPacket extends IPacket { cmd: 'suback', reasonCode?: number, properties?: { reasonString?: string, userProperties?: UserProperties }, granted: number[] | Object[] } export interface IUnsubscribePacket extends IPacket { cmd: 'unsubscribe', properties?: { reasonString?: string, userProperties?: UserProperties }, unsubscriptions: string[] } export interface IUnsubackPacket extends IPacket { cmd: 'unsuback', reasonCode?: number, properties?: { reasonString?: string, userProperties?: UserProperties }, granted: number[] } export interface IPubackPacket extends IPacket { cmd: 'puback', reasonCode?: number, properties?: { reasonString?: string, userProperties?: UserProperties } } export interface IPubcompPacket extends IPacket { cmd: 'pubcomp', reasonCode?: number, properties?: { reasonString?: string, userProperties?: UserProperties } } export interface IPubrelPacket extends IPacket { cmd: 'pubrel', reasonCode?: number, properties?: { reasonString?: string, userProperties?: UserProperties } } export interface IPubrecPacket extends IPacket { cmd: 'pubrec', reasonCode?: number, properties?: { reasonString?: string, userProperties?: UserProperties } } export interface IPingreqPacket extends IPacket { cmd: 'pingreq' } export interface IPingrespPacket extends IPacket { cmd: 'pingresp' } export interface IDisconnectPacket extends IPacket { cmd: 'disconnect', reasonCode?: number, properties?: { sessionExpiryInterval?: number, reasonString?: string, userProperties?: UserProperties, serverReference?: string } } export declare type Packet = IConnectPacket | IPublishPacket | IConnackPacket | ISubscribePacket | ISubackPacket | IUnsubscribePacket | IUnsubackPacket | IPubackPacket | IPubcompPacket | IPubrelPacket | IPingreqPacket | IPingrespPacket | IDisconnectPacket | IPubrecPacket | IAuthPacket export interface Parser extends EventEmitter { on(event: 'packet', callback: (packet: Packet) => void): this on(event: 'error', callback: (error: any) => void): this parse(buffer: Buffer, opts?: Object): number } export declare function parser(opts?: Object): Parser export declare function generate(packet: Packet, opts?: Object): Buffer export declare function writeToStream(object: Packet, stream: WritableStream, opts?: Object): boolean export declare namespace writeToStream { let cacheNumbers: boolean } mqtt-packet-9.0.2/writeToStream.js000066400000000000000000000751271476155302400171460ustar00rootroot00000000000000const protocol = require('./constants') const { Buffer } = require('buffer') const empty = Buffer.allocUnsafe(0) const zeroBuf = Buffer.from([0]) const numbers = require('./numbers') const nextTick = require('process-nextick-args').nextTick const debug = require('debug')('mqtt-packet:writeToStream') const numCache = numbers.cache const generateNumber = numbers.generateNumber const generateCache = numbers.generateCache const genBufVariableByteInt = numbers.genBufVariableByteInt const generate4ByteBuffer = numbers.generate4ByteBuffer let writeNumber = writeNumberCached let toGenerate = true function generate (packet, stream, opts) { debug('generate called') if (stream.cork) { stream.cork() nextTick(uncork, stream) } if (toGenerate) { toGenerate = false generateCache() } debug('generate: packet.cmd: %s', packet.cmd) 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.destroy(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 () { return writeNumber === writeNumberCached }, set (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) { const settings = packet || {} const protocolId = settings.protocolId || 'MQTT' let protocolVersion = settings.protocolVersion || 4 const will = settings.will let clean = settings.clean const keepalive = settings.keepalive || 0 const clientId = settings.clientId || '' const username = settings.username const password = settings.password /* mqtt5 new oprions */ const properties = settings.properties if (clean === undefined) clean = true let length = 0 // Must be a string and non-falsy if (!protocolId || (typeof protocolId !== 'string' && !Buffer.isBuffer(protocolId))) { stream.destroy(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.destroy(new Error('Invalid protocol version')) return false } else length += 1 // ClientId might be omitted in 3.1.1 and 5, but only if cleanSession is set to 1 if ((typeof clientId === 'string' || Buffer.isBuffer(clientId)) && (clientId || protocolVersion >= 4) && (clientId || clean)) { length += Buffer.byteLength(clientId) + 2 } else { if (protocolVersion < 4) { stream.destroy(new Error('clientId must be supplied before 3.1.1')) return false } if ((clean * 1) === 0) { stream.destroy(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.destroy(new Error('Invalid keepalive')) return false } else length += 2 // Connect flags length += 1 let propertiesData let willProperties // Properties if (protocolVersion === 5) { propertiesData = getProperties(stream, properties) if (!propertiesData) { return false } length += propertiesData.length } // If will exists... if (will) { // It must be an object if (typeof will !== 'object') { stream.destroy(new Error('Invalid will')) return false } // It must have topic typeof string if (!will.topic || typeof will.topic !== 'string') { stream.destroy(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.destroy(new Error('Invalid will payload')) return false } } // will properties willProperties = {} if (protocolVersion === 5) { willProperties = getProperties(stream, will.properties) if (!willProperties) { return false } length += willProperties.length } } // Username let providedUsername = false if (username != null) { if (isStringOrBuffer(username)) { providedUsername = true length += Buffer.byteLength(username) + 2 } else { stream.destroy(new Error('Invalid username')) return false } } // Password if (password != null) { if (!providedUsername) { stream.destroy(new Error('Username is required to use password')) return false } if (isStringOrBuffer(password)) { length += byteLength(password) + 2 } else { stream.destroy(new Error('Invalid password')) return false } } // Generate header stream.write(protocol.CONNECT_HEADER) // Generate length writeVarByteInt(stream, length) // Generate protocol ID writeStringOrBuffer(stream, protocolId) if (settings.bridgeMode) { protocolVersion += 128 } stream.write( protocolVersion === 131 ? protocol.VERSION131 : protocolVersion === 132 ? protocol.VERSION132 : protocolVersion === 4 ? protocol.VERSION4 : protocolVersion === 5 ? protocol.VERSION5 : protocol.VERSION3 ) // Connect flags let 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) { const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const rc = version === 5 ? settings.reasonCode : settings.returnCode const properties = settings.properties let length = 2 // length of rc and sessionHeader // Check return code if (typeof rc !== 'number') { stream.destroy(new Error('Invalid return code')) return false } // mqtt5 properties let propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) if (!propertiesData) { return false } 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) { debug('publish: packet: %o', packet) const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const qos = settings.qos || 0 const retain = settings.retain ? protocol.RETAIN_MASK : 0 const topic = settings.topic const payload = settings.payload || empty const id = settings.messageId const properties = settings.properties let 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.destroy(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.destroy(new Error('Invalid messageId')) return false } else if (qos) length += 2 // mqtt5 properties let propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) if (!propertiesData) { return false } 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 debug('publish: payload: %o', payload) return stream.write(payload) } /* Puback, pubrec, pubrel and pubcomp */ function confirmation (packet, stream, opts) { const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const type = settings.cmd || 'puback' const id = settings.messageId const dup = (settings.dup && type === 'pubrel') ? protocol.DUP_MASK : 0 let qos = 0 const reasonCode = settings.reasonCode const properties = settings.properties let length = version === 5 ? 3 : 2 if (type === 'pubrel') qos = 1 // Check message ID if (typeof id !== 'number') { stream.destroy(new Error('Invalid messageId')) return false } // properies mqtt 5 let propertiesData = null if (version === 5) { // Confirm should not add empty property length with no properties (rfc 3.4.2.2.1) if (typeof properties === 'object') { propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) if (!propertiesData) { return false } length += propertiesData.length } } // Header stream.write(protocol.ACKS[type][qos][dup][0]) // Length === 3 is only true of version === 5 and no properties; therefore if reasonCode === 0 we are allowed to skip both bytes - but if we write the reason code we also have to write property length [MQTT-3.4.2-1]. if (length === 3) length += reasonCode !== 0 ? 1 : -1 writeVarByteInt(stream, length) // Message ID writeNumber(stream, id) // reason code in header - but only if it couldn't be omitted - indicated by length !== 2. if (version === 5 && length !== 2) { stream.write(Buffer.from([reasonCode])) } // properties mqtt 5 if (propertiesData !== null) { propertiesData.write() } else { if (length === 4) { // we have no properties but have written a reason code - so we need to indicate empty properties by filling in a zero. stream.write(Buffer.from([0])) } } return true } function subscribe (packet, stream, opts) { debug('subscribe: packet: ') const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const dup = settings.dup ? protocol.DUP_MASK : 0 const id = settings.messageId const subs = settings.subscriptions const properties = settings.properties let length = 0 // Check message ID if (typeof id !== 'number') { stream.destroy(new Error('Invalid messageId')) return false } else length += 2 // properies mqtt 5 let propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) if (!propertiesData) { return false } length += propertiesData.length } // Check subscriptions if (typeof subs === 'object' && subs.length) { for (let i = 0; i < subs.length; i += 1) { const itopic = subs[i].topic const iqos = subs[i].qos if (typeof itopic !== 'string') { stream.destroy(new Error('Invalid subscriptions - invalid topic')) return false } if (typeof iqos !== 'number') { stream.destroy(new Error('Invalid subscriptions - invalid qos')) return false } if (version === 5) { const nl = subs[i].nl || false if (typeof nl !== 'boolean') { stream.destroy(new Error('Invalid subscriptions - invalid No Local')) return false } const rap = subs[i].rap || false if (typeof rap !== 'boolean') { stream.destroy(new Error('Invalid subscriptions - invalid Retain as Published')) return false } const rh = subs[i].rh || 0 if (typeof rh !== 'number' || rh > 2) { stream.destroy(new Error('Invalid subscriptions - invalid Retain Handling')) return false } } length += Buffer.byteLength(itopic) + 2 + 1 } } else { stream.destroy(new Error('Invalid subscriptions')) return false } // Generate header debug('subscribe: writing to stream: %o', protocol.SUBSCRIBE_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() } let result = true // Generate subs for (const sub of subs) { const jtopic = sub.topic const jqos = sub.qos const jnl = +sub.nl const jrap = +sub.rap const jrh = sub.rh let 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) { const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const id = settings.messageId const granted = settings.granted const properties = settings.properties let length = 0 // Check message ID if (typeof id !== 'number') { stream.destroy(new Error('Invalid messageId')) return false } else length += 2 // Check granted qos vector if (typeof granted === 'object' && granted.length) { for (let i = 0; i < granted.length; i += 1) { if (typeof granted[i] !== 'number') { stream.destroy(new Error('Invalid qos vector')) return false } length += 1 } } else { stream.destroy(new Error('Invalid qos vector')) return false } // properies mqtt 5 let 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) { const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const id = settings.messageId const dup = settings.dup ? protocol.DUP_MASK : 0 const unsubs = settings.unsubscriptions const properties = settings.properties let length = 0 // Check message ID if (typeof id !== 'number') { stream.destroy(new Error('Invalid messageId')) return false } else { length += 2 } // Check unsubs if (typeof unsubs === 'object' && unsubs.length) { for (let i = 0; i < unsubs.length; i += 1) { if (typeof unsubs[i] !== 'string') { stream.destroy(new Error('Invalid unsubscriptions')) return false } length += Buffer.byteLength(unsubs[i]) + 2 } } else { stream.destroy(new Error('Invalid unsubscriptions')) return false } // properies mqtt 5 let propertiesData = null if (version === 5) { propertiesData = getProperties(stream, properties) if (!propertiesData) { return false } 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 let result = true for (let j = 0; j < unsubs.length; j++) { result = writeString(stream, unsubs[j]) } return result } function unsuback (packet, stream, opts) { const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const id = settings.messageId const dup = settings.dup ? protocol.DUP_MASK : 0 const granted = settings.granted const properties = settings.properties const type = settings.cmd const qos = 0 let length = 2 // Check message ID if (typeof id !== 'number') { stream.destroy(new Error('Invalid messageId')) return false } // Check granted if (version === 5) { if (typeof granted === 'object' && granted.length) { for (let i = 0; i < granted.length; i += 1) { if (typeof granted[i] !== 'number') { stream.destroy(new Error('Invalid qos vector')) return false } length += 1 } } else { stream.destroy(new Error('Invalid qos vector')) return false } } // properies mqtt 5 let 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) { const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const reasonCode = settings.reasonCode const properties = settings.properties let length = version === 5 ? 1 : 0 // properies mqtt 5 let 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) { const version = opts ? opts.protocolVersion : 4 const settings = packet || {} const reasonCode = settings.reasonCode const properties = settings.properties let length = version === 5 ? 1 : 0 if (version !== 5) stream.destroy(new Error('Invalid mqtt version for auth packet')) // properies mqtt 5 const 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 */ const varByteIntCache = {} function writeVarByteInt (stream, num) { if (num > protocol.VARBYTEINT_MAX) { stream.destroy(new Error(`Invalid variable byte integer: ${num}`)) return false } let buffer = varByteIntCache[num] if (!buffer) { buffer = genBufVariableByteInt(num) if (num < 16384) varByteIntCache[num] = buffer } debug('writeVarByteInt: writing to stream: %o', buffer) return 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) { const strlen = Buffer.byteLength(string) writeNumber(stream, strlen) debug('writeString: %s', string) return 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) { debug('writeNumberCached: number: %d', number) debug('writeNumberCached: %o', numCache[number]) return stream.write(numCache[number]) } function writeNumberGenerated (stream, number) { const generatedNumber = generateNumber(number) debug('writeNumberGenerated: %o', generatedNumber) return stream.write(generatedNumber) } function write4ByteNumber (stream, number) { const generated4ByteBuffer = generate4ByteBuffer(number) debug('write4ByteNumber: %o', generated4ByteBuffer) return stream.write(generated4ByteBuffer) } /** * 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 () { writeProperties(stream, {}, 0) } } } let propertiesLength = 0 function getLengthProperty (name, value) { const type = protocol.propertiesTypes[name] let length = 0 switch (type) { case 'byte': { if (typeof value !== 'boolean') { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += 1 + 1 break } case 'int8': { if (typeof value !== 'number' || value < 0 || value > 0xff) { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += 1 + 1 break } case 'binary': { if (value && value === null) { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += 1 + Buffer.byteLength(value) + 2 break } case 'int16': { if (typeof value !== 'number' || value < 0 || value > 0xffff) { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += 1 + 2 break } case 'int32': { if (typeof value !== 'number' || value < 0 || value > 0xffffffff) { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += 1 + 4 break } case 'var': { // var byte integer is max 24 bits packed in 32 bits if (typeof value !== 'number' || value < 0 || value > 0x0fffffff) { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += 1 + Buffer.byteLength(genBufVariableByteInt(value)) break } case 'string': { if (typeof value !== 'string') { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += 1 + 2 + Buffer.byteLength(value.toString()) break } case 'pair': { if (typeof value !== 'object') { stream.destroy(new Error(`Invalid ${name}: ${value}`)) return false } length += Object.getOwnPropertyNames(value).reduce((result, name) => { const currentValue = value[name] if (Array.isArray(currentValue)) { result += currentValue.reduce((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.destroy(new Error(`Invalid property ${name}: ${value}`)) return false } } return length } if (properties) { for (const propName in properties) { let propLength = 0 let propValueLength = 0 const propValue = properties[propName] if (propValue === undefined) { continue } else if (Array.isArray(propValue)) { for (let valueIndex = 0; valueIndex < propValue.length; valueIndex++) { propValueLength = getLengthProperty(propName, propValue[valueIndex]) if (!propValueLength) { return false } propLength += propValueLength } } else { propValueLength = getLengthProperty(propName, propValue) if (!propValueLength) { return false } propLength = propValueLength } if (!propLength) return false propertiesLength += propLength } } const propertiesLengthLength = Buffer.byteLength(genBufVariableByteInt(propertiesLength)) return { length: propertiesLengthLength + propertiesLength, write () { writeProperties(stream, properties, propertiesLength) } } } function getPropertiesByMaximumPacketSize (stream, properties, opts, length) { const mayEmptyProps = ['reasonString', 'userProperties'] const maximumPacketSize = opts && opts.properties && opts.properties.maximumPacketSize ? opts.properties.maximumPacketSize : 0 let propertiesData = getProperties(stream, properties) if (maximumPacketSize) { while (length + propertiesData.length > maximumPacketSize) { const currentMayEmptyProp = mayEmptyProps.shift() if (currentMayEmptyProp && properties[currentMayEmptyProp]) { delete properties[currentMayEmptyProp] propertiesData = getProperties(stream, properties) } else { return false } } } return propertiesData } function writeProperty (stream, propName, value) { const 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(name => { const currentValue = value[name] if (Array.isArray(currentValue)) { currentValue.forEach(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.destroy(new Error(`Invalid property ${propName} value: ${value}`)) return false } } } function writeProperties (stream, properties, propertiesLength) { /* write properties to stream */ writeVarByteInt(stream, propertiesLength) for (const propName in properties) { if (Object.prototype.hasOwnProperty.call(properties, propName) && properties[propName] != null) { const value = properties[propName] if (Array.isArray(value)) { for (let valueIndex = 0; valueIndex < value.length; valueIndex++) { writeProperty(stream, propName, value[valueIndex]) } } else { writeProperty(stream, propName, value) } } } } 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