├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── benchmarks ├── generate.js ├── generateNet.js ├── parse.js └── writeToStream.js ├── constants.js ├── generate.js ├── mqtt.js ├── numbers.js ├── package.json ├── packet.js ├── parser.js ├── test.js ├── testRandom.js ├── types └── index.d.ts └── writeToStream.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [16.x, 18.x, 20.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Install 22 | run: | 23 | npm install 24 | 25 | - name: Run tests 26 | run: | 27 | npm run ci 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # mqtt-packet is an OPEN Open Source Project 2 | 3 | ----------------------------------------- 4 | 5 | ## What? 6 | 7 | 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. 8 | 9 | ## Rules 10 | 11 | There are a few basic ground-rules for contributors: 12 | 13 | 1. **No `--force` pushes** or modifying the Git history in any way. 14 | 1. **Non-master branches** ought to be used for ongoing work. 15 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 16 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 17 | 1. Contributors should attempt to adhere to the prevailing code-style. 18 | 19 | ## Releases 20 | 21 | Declaring formal releases remains the prerogative of the project maintainer. 22 | 23 | ## Changes to this arrangement 24 | 25 | 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. 26 | 27 | ----------------------------------------- 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2014-2017 mqtt-packet contributors 5 | --------------------------------------- 6 | 7 | *mqtt-packet contributors listed at * 8 | 9 | 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: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | 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. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mqtt-packet 2 | =========== 3 | 4 | Encode and Decode MQTT 3.1.1, 5.0 packets the node way. 5 | 6 | [![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 7 | 8 | * Installation 9 | * Examples 10 | * Packets 11 | * API 12 | * Contributing 13 | * License & copyright 14 | 15 | This library is tested with node v6, v8, v10, v12 and v14. The last version to support 16 | older versions of node was mqtt-packet@4.1.2. 17 | 18 | Installation 19 | ------------ 20 | 21 | ```bash 22 | npm install mqtt-packet --save 23 | ``` 24 | 25 | Examples 26 | -------- 27 | 28 | ### Generating 29 | 30 | ```js 31 | const mqtt = require('mqtt-packet'); 32 | const object = { 33 | cmd: 'publish', 34 | retain: false, 35 | qos: 0, 36 | dup: false, 37 | length: 10, 38 | topic: 'test', 39 | payload: 'test' // Can also be a Buffer 40 | }; 41 | const opts = { protocolVersion: 4 }; // default is 4. Usually, opts is a connect packet 42 | 43 | console.log(mqtt.generate(object)) 44 | // Prints: 45 | // 46 | // 47 | // 48 | // Which is the same as: 49 | // 50 | // Buffer.from([ 51 | // 48, 10, // Header (publish) 52 | // 0, 4, // Topic length 53 | // 116, 101, 115, 116, // Topic (test) 54 | // 116, 101, 115, 116 // Payload (test) 55 | // ]) 56 | ``` 57 | 58 | ### Parsing 59 | 60 | ```js 61 | const mqtt = require('mqtt-packet'); 62 | const opts = { protocolVersion: 4 }; // default is 4. Usually, opts is a connect packet 63 | const parser = mqtt.parser(opts); 64 | 65 | // Synchronously emits all the parsed packets 66 | parser.on('packet', packet => { 67 | console.log(packet) 68 | // Prints: 69 | // 70 | // { 71 | // cmd: 'publish', 72 | // retain: false, 73 | // qos: 0, 74 | // dup: false, 75 | // length: 10, 76 | // topic: 'test', 77 | // payload: 78 | // } 79 | }) 80 | 81 | parser.parse(Buffer.from([ 82 | 48, 10, // Header (publish) 83 | 0, 4, // Topic length 84 | 116, 101, 115, 116, // Topic (test) 85 | 116, 101, 115, 116 // Payload (test) 86 | ])) 87 | // Returns the number of bytes left in the parser 88 | ``` 89 | 90 | API 91 | --- 92 | 93 | * mqtt#generate() 94 | * mqtt#writeToStream() 95 | * mqtt#parser() 96 | 97 | 98 | 99 | ### mqtt.generate(object, [opts]) 100 | 101 | Generates a `Buffer` containing an MQTT packet. 102 | The object must be one of the ones specified by the [packets](#packets) 103 | section. Throws an `Error` if a packet cannot be generated. 104 | 105 | 106 | 107 | ### mqtt.writeToStream(object, stream, [opts]) 108 | 109 | Writes the mqtt packet defined by `object` to the given stream. 110 | The object must be one of the ones specified by the [packets](#packets) 111 | section. Emits an `Error` on the stream if a packet cannot be generated. 112 | On node >= 0.12, this function automatically calls `cork()` on your stream, 113 | and then it calls `uncork()` on the next tick. 114 | By default cache for number buffers is enabled. 115 | It creates a list of buffers for faster write. To disable cache set `mqtt.writeToStream.cacheNumbers = false`. 116 | Should be set before any `writeToStream` calls. 117 | 118 | 119 | 120 | ### mqtt.parser([opts]) 121 | 122 | Returns a new `Parser` object. `Parser` inherits from `EventEmitter` and 123 | will emit: 124 | 125 | * `packet`, when a new packet is parsed, according to 126 | [packets](#packets) 127 | * `error`, if an error happens 128 | 129 | 130 | 131 | #### Parser.parse(buffer) 132 | 133 | Parses a given `Buffer` and emits synchronously all the MQTT packets that 134 | are included. Returns the number of bytes left to parse. 135 | 136 | If an error happens, an `error` event will be emitted, but no `packet` events 137 | will be emitted after that. Calling `parse()` again clears the error and 138 | previous buffer, as if you created a new `Parser`. 139 | 140 | Packets 141 | ------- 142 | 143 | This section describes the format of all packets emitted by the `Parser` 144 | and that you can input to `generate`. 145 | 146 | ### Connect 147 | 148 | ```js 149 | { 150 | cmd: 'connect', 151 | protocolId: 'MQTT', // Or 'MQIsdp' in MQTT 3.1 and 5.0 152 | protocolVersion: 4, // Or 3 in MQTT 3.1, or 5 in MQTT 5.0 153 | clean: true, // Can also be false 154 | clientId: 'my-device', 155 | keepalive: 0, // Seconds which can be any positive number, with 0 as the default setting 156 | username: 'matteo', 157 | password: Buffer.from('collina'), // Passwords are buffers 158 | will: { 159 | topic: 'mydevice/status', 160 | payload: Buffer.from('dead'), // Payloads are buffers 161 | properties: { // MQTT 5.0 162 | willDelayInterval: 1234, 163 | payloadFormatIndicator: false, 164 | messageExpiryInterval: 4321, 165 | contentType: 'test', 166 | responseTopic: 'topic', 167 | correlationData: Buffer.from([1, 2, 3, 4]), 168 | userProperties: { 169 | 'test': 'test' 170 | } 171 | } 172 | }, 173 | properties: { // MQTT 5.0 properties 174 | sessionExpiryInterval: 1234, 175 | receiveMaximum: 432, 176 | maximumPacketSize: 100, 177 | topicAliasMaximum: 456, 178 | requestResponseInformation: true, 179 | requestProblemInformation: true, 180 | userProperties: { 181 | 'test': 'test' 182 | }, 183 | authenticationMethod: 'test', 184 | authenticationData: Buffer.from([1, 2, 3, 4]) 185 | } 186 | } 187 | ``` 188 | 189 | If `protocolVersion` is 3, `clientId` is mandatory and `generate` will throw if 190 | missing. 191 | 192 | If `password` or `will.payload` are passed as strings, they will 193 | automatically be converted into a `Buffer`. 194 | 195 | ### Connack 196 | 197 | ```js 198 | { 199 | cmd: 'connack', 200 | returnCode: 0, // Or whatever else you see fit MQTT < 5.0 201 | sessionPresent: false, // Can also be true. 202 | reasonCode: 0, // reason code MQTT 5.0 203 | properties: { // MQTT 5.0 properties 204 | sessionExpiryInterval: 1234, 205 | receiveMaximum: 432, 206 | maximumQoS: 1, 207 | retainAvailable: true, 208 | maximumPacketSize: 100, 209 | assignedClientIdentifier: 'test', 210 | topicAliasMaximum: 456, 211 | reasonString: 'test', 212 | userProperties: { 213 | 'test': 'test' 214 | }, 215 | wildcardSubscriptionAvailable: true, 216 | subscriptionIdentifiersAvailable: true, 217 | sharedSubscriptionAvailable: false, 218 | serverKeepAlive: 1234, 219 | responseInformation: 'test', 220 | serverReference: 'test', 221 | authenticationMethod: 'test', 222 | authenticationData: Buffer.from([1, 2, 3, 4]) 223 | } 224 | } 225 | ``` 226 | 227 | The only mandatory argument is `returnCode`, as `generate` will throw if 228 | missing. 229 | 230 | ### Subscribe 231 | 232 | ```js 233 | { 234 | cmd: 'subscribe', 235 | messageId: 42, 236 | properties: { // MQTT 5.0 properties 237 | subscriptionIdentifier: 145, 238 | userProperties: { 239 | test: 'test' 240 | } 241 | } 242 | subscriptions: [{ 243 | topic: 'test', 244 | qos: 0, 245 | nl: false, // no Local MQTT 5.0 flag 246 | rap: true, // Retain as Published MQTT 5.0 flag 247 | rh: 1 // Retain Handling MQTT 5.0 248 | }] 249 | } 250 | ``` 251 | 252 | All properties are mandatory. 253 | 254 | ### Suback 255 | 256 | ```js 257 | { 258 | cmd: 'suback', 259 | messageId: 42, 260 | properties: { // MQTT 5.0 properties 261 | reasonString: 'test', 262 | userProperties: { 263 | 'test': 'test' 264 | } 265 | } 266 | granted: [0, 1, 2, 128] 267 | } 268 | ``` 269 | 270 | All the granted qos __must__ be < 256, as they are encoded as UInt8. 271 | All properties are mandatory. 272 | 273 | ### Unsubscribe 274 | 275 | ```js 276 | { 277 | cmd: 'unsubscribe', 278 | messageId: 42, 279 | properties: { // MQTT 5.0 properties 280 | userProperties: { 281 | 'test': 'test' 282 | } 283 | } 284 | unsubscriptions: [ 285 | 'test', 286 | 'a/topic' 287 | ] 288 | } 289 | ``` 290 | 291 | All properties are mandatory. 292 | 293 | ### Unsuback 294 | 295 | ```js 296 | { 297 | cmd: 'unsuback', 298 | messageId: 42, 299 | properties: { // MQTT 5.0 properties 300 | reasonString: 'test', 301 | userProperties: { 302 | 'test': 'test' 303 | } 304 | } 305 | } 306 | ``` 307 | 308 | All properties are mandatory. 309 | 310 | ### Publish 311 | 312 | ```js 313 | { 314 | cmd: 'publish', 315 | messageId: 42, 316 | qos: 2, 317 | dup: false, 318 | topic: 'test', 319 | payload: Buffer.from('test'), 320 | retain: false, 321 | properties: { // optional properties MQTT 5.0 322 | payloadFormatIndicator: true, 323 | messageExpiryInterval: 4321, 324 | topicAlias: 100, 325 | responseTopic: 'topic', 326 | correlationData: Buffer.from([1, 2, 3, 4]), 327 | userProperties: { 328 | 'test': 'test' 329 | }, 330 | subscriptionIdentifier: 120, // can be an Array in message from broker, if message included in few another subscriptions 331 | contentType: 'test' 332 | } 333 | } 334 | ``` 335 | 336 | Only the `topic` property is mandatory. 337 | Both `topic` and `payload` can be `Buffer` objects instead of strings. 338 | `messageId` is mandatory for `qos > 0`. 339 | 340 | ### Puback 341 | 342 | ```js 343 | { 344 | cmd: 'puback', 345 | messageId: 42, 346 | reasonCode: 16, // only for MQTT 5.0 347 | properties: { // MQTT 5.0 properties 348 | reasonString: 'test', 349 | userProperties: { 350 | 'test': 'test' 351 | } 352 | } 353 | } 354 | ``` 355 | 356 | The only mandatory property is `messageId`, as `generate` will throw if 357 | missing. 358 | 359 | ### Pubrec 360 | 361 | ```js 362 | { 363 | cmd: 'pubrec', 364 | messageId: 42, 365 | reasonCode: 16, // only for MQTT 5.0 366 | properties: { // properties MQTT 5.0 367 | reasonString: 'test', 368 | userProperties: { 369 | 'test': 'test' 370 | } 371 | } 372 | } 373 | ``` 374 | 375 | The only mandatory property is `messageId`, as `generate` will throw if 376 | missing. 377 | 378 | ### Pubrel 379 | 380 | ```js 381 | { 382 | cmd: 'pubrel', 383 | messageId: 42, 384 | reasonCode: 16, // only for MQTT 5.0 385 | properties: { // properties MQTT 5.0 386 | reasonString: 'test', 387 | userProperties: { 388 | 'test': 'test' 389 | } 390 | } 391 | } 392 | ``` 393 | 394 | The only mandatory property is `messageId`, as `generate` will throw if 395 | missing. 396 | 397 | ### Pubcomp 398 | 399 | ```js 400 | { 401 | cmd: 'pubcomp', 402 | messageId: 42, 403 | reasonCode: 16, // only for MQTT 5.0 404 | properties: { // properties MQTT 5.0 405 | reasonString: 'test', 406 | userProperties: { 407 | 'test': 'test' 408 | } 409 | } 410 | } 411 | ``` 412 | 413 | The only mandatory property is `messageId`, as `generate` will throw if 414 | missing. 415 | 416 | ### Pingreq 417 | 418 | ```js 419 | { 420 | cmd: 'pingreq' 421 | } 422 | ``` 423 | 424 | ### Pingresp 425 | 426 | ```js 427 | { 428 | cmd: 'pingresp' 429 | } 430 | ``` 431 | 432 | ### Disconnect 433 | 434 | ```js 435 | { 436 | cmd: 'disconnect', 437 | reasonCode: 0, // MQTT 5.0 code 438 | properties: { // properties MQTT 5.0 439 | sessionExpiryInterval: 145, 440 | reasonString: 'test', 441 | userProperties: { 442 | 'test': 'test' 443 | }, 444 | serverReference: 'test' 445 | } 446 | } 447 | ``` 448 | 449 | ### Auth 450 | 451 | ```js 452 | { 453 | cmd: 'auth', 454 | reasonCode: 0, // MQTT 5.0 code 455 | properties: { // properties MQTT 5.0 456 | authenticationMethod: 'test', 457 | authenticationData: Buffer.from([0, 1, 2, 3]), 458 | reasonString: 'test', 459 | userProperties: { 460 | 'test': 'test' 461 | } 462 | } 463 | } 464 | ``` 465 | 466 | 467 | 468 | Contributing 469 | ------------ 470 | 471 | mqtt-packet is an **OPEN Open Source Project**. This means that: 472 | 473 | > 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. 474 | 475 | See the [CONTRIBUTING.md](https://github.com/mqttjs/mqtt-packet/blob/master/CONTRIBUTING.md) file for more details. 476 | 477 | ### Contributors 478 | 479 | mqtt-packet is only possible due to the excellent work of the following contributors: 480 | 481 | 482 | 483 | 484 | 485 | 486 |
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
Adam RuddGitHub/adamvrTwitter/@adam_vr
Peter SorowkaGitHub/psorowkaTwitter/@psorowka
Siarhei BuntsevichGitHub/scarry1992
487 | 488 | License 489 | ------- 490 | 491 | MIT 492 | -------------------------------------------------------------------------------- /benchmarks/generate.js: -------------------------------------------------------------------------------- 1 | const mqtt = require('../') 2 | const max = 100000 3 | let i 4 | const buf = Buffer.from('test') 5 | 6 | // initialize it 7 | mqtt.generate({ 8 | cmd: 'publish', 9 | topic: 'test', 10 | payload: buf 11 | }) 12 | 13 | const start = Date.now() 14 | 15 | for (i = 0; i < max; i++) { 16 | mqtt.generate({ 17 | cmd: 'publish', 18 | topic: 'test', 19 | payload: buf 20 | }) 21 | } 22 | 23 | const time = Date.now() - start 24 | console.log('Total time', time) 25 | console.log('Total packets', max) 26 | console.log('Packet/s', max / time * 1000) 27 | -------------------------------------------------------------------------------- /benchmarks/generateNet.js: -------------------------------------------------------------------------------- 1 | const mqtt = require('../') 2 | const max = 1000000 3 | let i = 0 4 | const start = Date.now() 5 | let time 6 | const buf = Buffer.allocUnsafe(10) 7 | const net = require('net') 8 | const server = net.createServer(handle) 9 | let dest 10 | 11 | buf.fill('test') 12 | 13 | function handle (sock) { 14 | sock.resume() 15 | } 16 | 17 | server.listen(0, () => { 18 | dest = net.connect(server.address()) 19 | 20 | dest.on('connect', tickWait) 21 | dest.on('drain', tickWait) 22 | dest.on('finish', () => { 23 | time = Date.now() - start 24 | console.log('Total time', time) 25 | console.log('Total packets', max) 26 | console.log('Packet/s', max / time * 1000) 27 | server.close() 28 | }) 29 | }) 30 | 31 | function tickWait () { 32 | // console.log('tickWait', i) 33 | let res = true 34 | // var toSend = new Buffer(5 + buf.length) 35 | 36 | for (; i < max && res; i++) { 37 | res = dest.write(mqtt.generate({ 38 | cmd: 'publish', 39 | topic: 'test', 40 | payload: buf 41 | })) 42 | // buf.copy(toSend, 5) 43 | // res = dest.write(toSend, 'buffer') 44 | // console.log(res) 45 | } 46 | 47 | if (i >= max) { 48 | dest.end() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /benchmarks/parse.js: -------------------------------------------------------------------------------- 1 | const mqtt = require('../') 2 | const parser = mqtt.parser() 3 | const max = 10000000 4 | let i 5 | const start = Date.now() / 1000 6 | 7 | for (i = 0; i < max; i++) { 8 | parser.parse(Buffer.from([ 9 | 48, 10, // Header (publish) 10 | 0, 4, // Topic length 11 | 116, 101, 115, 116, // Topic (test) 12 | 116, 101, 115, 116 // Payload (test) 13 | ])) 14 | } 15 | 16 | const time = Date.now() / 1000 - start 17 | console.log('Total packets', max) 18 | console.log('Total time', Math.round(time * 100) / 100) 19 | console.log('Packet/s', max / time) 20 | -------------------------------------------------------------------------------- /benchmarks/writeToStream.js: -------------------------------------------------------------------------------- 1 | const mqtt = require('../') 2 | const max = 1000000 3 | let i = 0 4 | const start = Date.now() 5 | let time 6 | const buf = Buffer.allocUnsafe(10) 7 | const net = require('net') 8 | const server = net.createServer(handle) 9 | let dest 10 | 11 | function handle (sock) { 12 | sock.resume() 13 | } 14 | 15 | buf.fill('test') 16 | 17 | server.listen(0, () => { 18 | dest = net.connect(server.address()) 19 | 20 | dest.on('connect', tickWait) 21 | dest.on('drain', tickWait) 22 | dest.on('finish', () => { 23 | time = Date.now() - start 24 | console.log('Total time', time) 25 | console.log('Total packets', max) 26 | console.log('Packet/s', max / time * 1000) 27 | server.close() 28 | }) 29 | }) 30 | 31 | function tickWait () { 32 | let res = true 33 | // var toSend = new Buffer(5) 34 | 35 | for (; i < max && res; i++) { 36 | res = mqtt.writeToStream({ 37 | cmd: 'publish', 38 | topic: 'test', 39 | payload: buf 40 | }, dest) 41 | // dest.write(toSend, 'buffer') 42 | // res = dest.write(buf, 'buffer') 43 | } 44 | 45 | if (i >= max) { 46 | dest.end() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | /* Protocol - protocol constants */ 2 | const protocol = module.exports 3 | const { Buffer } = require('buffer') 4 | 5 | /* Command code => mnemonic */ 6 | protocol.types = { 7 | 0: 'reserved', 8 | 1: 'connect', 9 | 2: 'connack', 10 | 3: 'publish', 11 | 4: 'puback', 12 | 5: 'pubrec', 13 | 6: 'pubrel', 14 | 7: 'pubcomp', 15 | 8: 'subscribe', 16 | 9: 'suback', 17 | 10: 'unsubscribe', 18 | 11: 'unsuback', 19 | 12: 'pingreq', 20 | 13: 'pingresp', 21 | 14: 'disconnect', 22 | 15: 'auth' 23 | } 24 | 25 | protocol.requiredHeaderFlags = { 26 | 1: 0, // 'connect' 27 | 2: 0, // 'connack' 28 | 4: 0, // 'puback' 29 | 5: 0, // 'pubrec' 30 | 6: 2, // 'pubrel' 31 | 7: 0, // 'pubcomp' 32 | 8: 2, // 'subscribe' 33 | 9: 0, // 'suback' 34 | 10: 2, // 'unsubscribe' 35 | 11: 0, // 'unsuback' 36 | 12: 0, // 'pingreq' 37 | 13: 0, // 'pingresp' 38 | 14: 0, // 'disconnect' 39 | 15: 0 // 'auth' 40 | } 41 | 42 | protocol.requiredHeaderFlagsErrors = {} 43 | for (const k in protocol.requiredHeaderFlags) { 44 | const v = protocol.requiredHeaderFlags[k] 45 | protocol.requiredHeaderFlagsErrors[k] = 'Invalid header flag bits, must be 0x' + v.toString(16) + ' for ' + protocol.types[k] + ' packet' 46 | } 47 | 48 | /* Mnemonic => Command code */ 49 | protocol.codes = {} 50 | for (const k in protocol.types) { 51 | const v = protocol.types[k] 52 | protocol.codes[v] = k 53 | } 54 | 55 | /* Header */ 56 | protocol.CMD_SHIFT = 4 57 | protocol.CMD_MASK = 0xF0 58 | protocol.DUP_MASK = 0x08 59 | protocol.QOS_MASK = 0x03 60 | protocol.QOS_SHIFT = 1 61 | protocol.RETAIN_MASK = 0x01 62 | 63 | /* Length */ 64 | protocol.VARBYTEINT_MASK = 0x7F 65 | protocol.VARBYTEINT_FIN_MASK = 0x80 66 | protocol.VARBYTEINT_MAX = 268435455 67 | 68 | /* Connack */ 69 | protocol.SESSIONPRESENT_MASK = 0x01 70 | protocol.SESSIONPRESENT_HEADER = Buffer.from([protocol.SESSIONPRESENT_MASK]) 71 | protocol.CONNACK_HEADER = Buffer.from([protocol.codes.connack << protocol.CMD_SHIFT]) 72 | 73 | /* Connect */ 74 | protocol.USERNAME_MASK = 0x80 75 | protocol.PASSWORD_MASK = 0x40 76 | protocol.WILL_RETAIN_MASK = 0x20 77 | protocol.WILL_QOS_MASK = 0x18 78 | protocol.WILL_QOS_SHIFT = 3 79 | protocol.WILL_FLAG_MASK = 0x04 80 | protocol.CLEAN_SESSION_MASK = 0x02 81 | protocol.CONNECT_HEADER = Buffer.from([protocol.codes.connect << protocol.CMD_SHIFT]) 82 | 83 | /* Properties */ 84 | protocol.properties = { 85 | sessionExpiryInterval: 17, 86 | willDelayInterval: 24, 87 | receiveMaximum: 33, 88 | maximumPacketSize: 39, 89 | topicAliasMaximum: 34, 90 | requestResponseInformation: 25, 91 | requestProblemInformation: 23, 92 | userProperties: 38, 93 | authenticationMethod: 21, 94 | authenticationData: 22, 95 | payloadFormatIndicator: 1, 96 | messageExpiryInterval: 2, 97 | contentType: 3, 98 | responseTopic: 8, 99 | correlationData: 9, 100 | maximumQoS: 36, 101 | retainAvailable: 37, 102 | assignedClientIdentifier: 18, 103 | reasonString: 31, 104 | wildcardSubscriptionAvailable: 40, 105 | subscriptionIdentifiersAvailable: 41, 106 | sharedSubscriptionAvailable: 42, 107 | serverKeepAlive: 19, 108 | responseInformation: 26, 109 | serverReference: 28, 110 | topicAlias: 35, 111 | subscriptionIdentifier: 11 112 | } 113 | protocol.propertiesCodes = {} 114 | for (const prop in protocol.properties) { 115 | const id = protocol.properties[prop] 116 | protocol.propertiesCodes[id] = prop 117 | } 118 | protocol.propertiesTypes = { 119 | sessionExpiryInterval: 'int32', 120 | willDelayInterval: 'int32', 121 | receiveMaximum: 'int16', 122 | maximumPacketSize: 'int32', 123 | topicAliasMaximum: 'int16', 124 | requestResponseInformation: 'byte', 125 | requestProblemInformation: 'byte', 126 | userProperties: 'pair', 127 | authenticationMethod: 'string', 128 | authenticationData: 'binary', 129 | payloadFormatIndicator: 'byte', 130 | messageExpiryInterval: 'int32', 131 | contentType: 'string', 132 | responseTopic: 'string', 133 | correlationData: 'binary', 134 | maximumQoS: 'int8', 135 | retainAvailable: 'byte', 136 | assignedClientIdentifier: 'string', 137 | reasonString: 'string', 138 | wildcardSubscriptionAvailable: 'byte', 139 | subscriptionIdentifiersAvailable: 'byte', 140 | sharedSubscriptionAvailable: 'byte', 141 | serverKeepAlive: 'int16', 142 | responseInformation: 'string', 143 | serverReference: 'string', 144 | topicAlias: 'int16', 145 | subscriptionIdentifier: 'var' 146 | } 147 | 148 | function genHeader (type) { 149 | return [0, 1, 2].map(qos => { 150 | return [0, 1].map(dup => { 151 | return [0, 1].map(retain => { 152 | const buf = Buffer.alloc(1) 153 | buf.writeUInt8( 154 | protocol.codes[type] << protocol.CMD_SHIFT | 155 | (dup ? protocol.DUP_MASK : 0) | 156 | qos << protocol.QOS_SHIFT | retain, 0, true) 157 | return buf 158 | }) 159 | }) 160 | }) 161 | } 162 | 163 | /* Publish */ 164 | protocol.PUBLISH_HEADER = genHeader('publish') 165 | 166 | /* Subscribe */ 167 | protocol.SUBSCRIBE_HEADER = genHeader('subscribe') 168 | protocol.SUBSCRIBE_OPTIONS_QOS_MASK = 0x03 169 | protocol.SUBSCRIBE_OPTIONS_NL_MASK = 0x01 170 | protocol.SUBSCRIBE_OPTIONS_NL_SHIFT = 2 171 | protocol.SUBSCRIBE_OPTIONS_RAP_MASK = 0x01 172 | protocol.SUBSCRIBE_OPTIONS_RAP_SHIFT = 3 173 | protocol.SUBSCRIBE_OPTIONS_RH_MASK = 0x03 174 | protocol.SUBSCRIBE_OPTIONS_RH_SHIFT = 4 175 | protocol.SUBSCRIBE_OPTIONS_RH = [0x00, 0x10, 0x20] 176 | protocol.SUBSCRIBE_OPTIONS_NL = 0x04 177 | protocol.SUBSCRIBE_OPTIONS_RAP = 0x08 178 | protocol.SUBSCRIBE_OPTIONS_QOS = [0x00, 0x01, 0x02] 179 | 180 | /* Unsubscribe */ 181 | protocol.UNSUBSCRIBE_HEADER = genHeader('unsubscribe') 182 | 183 | /* Confirmations */ 184 | protocol.ACKS = { 185 | unsuback: genHeader('unsuback'), 186 | puback: genHeader('puback'), 187 | pubcomp: genHeader('pubcomp'), 188 | pubrel: genHeader('pubrel'), 189 | pubrec: genHeader('pubrec') 190 | } 191 | 192 | protocol.SUBACK_HEADER = Buffer.from([protocol.codes.suback << protocol.CMD_SHIFT]) 193 | 194 | /* Protocol versions */ 195 | protocol.VERSION3 = Buffer.from([3]) 196 | protocol.VERSION4 = Buffer.from([4]) 197 | protocol.VERSION5 = Buffer.from([5]) 198 | protocol.VERSION131 = Buffer.from([131]) 199 | protocol.VERSION132 = Buffer.from([132]) 200 | 201 | /* QoS */ 202 | protocol.QOS = [0, 1, 2].map(qos => { 203 | return Buffer.from([qos]) 204 | }) 205 | 206 | /* Empty packets */ 207 | protocol.EMPTY = { 208 | pingreq: Buffer.from([protocol.codes.pingreq << 4, 0]), 209 | pingresp: Buffer.from([protocol.codes.pingresp << 4, 0]), 210 | disconnect: Buffer.from([protocol.codes.disconnect << 4, 0]) 211 | } 212 | 213 | protocol.MQTT5_PUBACK_PUBREC_CODES = { 214 | 0x00: 'Success', 215 | 0x10: 'No matching subscribers', 216 | 0x80: 'Unspecified error', 217 | 0x83: 'Implementation specific error', 218 | 0x87: 'Not authorized', 219 | 0x90: 'Topic Name invalid', 220 | 0x91: 'Packet identifier in use', 221 | 0x97: 'Quota exceeded', 222 | 0x99: 'Payload format invalid' 223 | } 224 | 225 | protocol.MQTT5_PUBREL_PUBCOMP_CODES = { 226 | 0x00: 'Success', 227 | 0x92: 'Packet Identifier not found' 228 | } 229 | 230 | protocol.MQTT5_SUBACK_CODES = { 231 | 0x00: 'Granted QoS 0', 232 | 0x01: 'Granted QoS 1', 233 | 0x02: 'Granted QoS 2', 234 | 0x80: 'Unspecified error', 235 | 0x83: 'Implementation specific error', 236 | 0x87: 'Not authorized', 237 | 0x8F: 'Topic Filter invalid', 238 | 0x91: 'Packet Identifier in use', 239 | 0x97: 'Quota exceeded', 240 | 0x9E: 'Shared Subscriptions not supported', 241 | 0xA1: 'Subscription Identifiers not supported', 242 | 0xA2: 'Wildcard Subscriptions not supported' 243 | } 244 | 245 | protocol.MQTT5_UNSUBACK_CODES = { 246 | 0x00: 'Success', 247 | 0x11: 'No subscription existed', 248 | 0x80: 'Unspecified error', 249 | 0x83: 'Implementation specific error', 250 | 0x87: 'Not authorized', 251 | 0x8F: 'Topic Filter invalid', 252 | 0x91: 'Packet Identifier in use' 253 | } 254 | 255 | protocol.MQTT5_DISCONNECT_CODES = { 256 | 0x00: 'Normal disconnection', 257 | 0x04: 'Disconnect with Will Message', 258 | 0x80: 'Unspecified error', 259 | 0x81: 'Malformed Packet', 260 | 0x82: 'Protocol Error', 261 | 0x83: 'Implementation specific error', 262 | 0x87: 'Not authorized', 263 | 0x89: 'Server busy', 264 | 0x8B: 'Server shutting down', 265 | 0x8D: 'Keep Alive timeout', 266 | 0x8E: 'Session taken over', 267 | 0x8F: 'Topic Filter invalid', 268 | 0x90: 'Topic Name invalid', 269 | 0x93: 'Receive Maximum exceeded', 270 | 0x94: 'Topic Alias invalid', 271 | 0x95: 'Packet too large', 272 | 0x96: 'Message rate too high', 273 | 0x97: 'Quota exceeded', 274 | 0x98: 'Administrative action', 275 | 0x99: 'Payload format invalid', 276 | 0x9A: 'Retain not supported', 277 | 0x9B: 'QoS not supported', 278 | 0x9C: 'Use another server', 279 | 0x9D: 'Server moved', 280 | 0x9E: 'Shared Subscriptions not supported', 281 | 0x9F: 'Connection rate exceeded', 282 | 0xA0: 'Maximum connect time', 283 | 0xA1: 'Subscription Identifiers not supported', 284 | 0xA2: 'Wildcard Subscriptions not supported' 285 | } 286 | 287 | protocol.MQTT5_AUTH_CODES = { 288 | 0x00: 'Success', 289 | 0x18: 'Continue authentication', 290 | 0x19: 'Re-authenticate' 291 | } 292 | -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | const writeToStream = require('./writeToStream') 2 | const { EventEmitter } = require('events') 3 | const { Buffer } = require('buffer') 4 | 5 | function generate (packet, opts) { 6 | const stream = new Accumulator() 7 | writeToStream(packet, stream, opts) 8 | return stream.concat() 9 | } 10 | 11 | class Accumulator extends EventEmitter { 12 | constructor () { 13 | super() 14 | this._array = new Array(20) 15 | this._i = 0 16 | } 17 | 18 | write (chunk) { 19 | this._array[this._i++] = chunk 20 | return true 21 | } 22 | 23 | concat () { 24 | let length = 0 25 | const lengths = new Array(this._array.length) 26 | const list = this._array 27 | let pos = 0 28 | let i 29 | 30 | for (i = 0; i < list.length && list[i] !== undefined; i++) { 31 | if (typeof list[i] !== 'string') lengths[i] = list[i].length 32 | else lengths[i] = Buffer.byteLength(list[i]) 33 | 34 | length += lengths[i] 35 | } 36 | 37 | const result = Buffer.allocUnsafe(length) 38 | 39 | for (i = 0; i < list.length && list[i] !== undefined; i++) { 40 | if (typeof list[i] !== 'string') { 41 | list[i].copy(result, pos) 42 | pos += lengths[i] 43 | } else { 44 | result.write(list[i], pos) 45 | pos += lengths[i] 46 | } 47 | } 48 | 49 | return result 50 | } 51 | 52 | destroy (err) { 53 | if (err) this.emit('error', err) 54 | } 55 | } 56 | 57 | module.exports = generate 58 | -------------------------------------------------------------------------------- /mqtt.js: -------------------------------------------------------------------------------- 1 | exports.parser = require('./parser').parser 2 | exports.generate = require('./generate') 3 | exports.writeToStream = require('./writeToStream') 4 | -------------------------------------------------------------------------------- /numbers.js: -------------------------------------------------------------------------------- 1 | const { Buffer } = require('buffer') 2 | const max = 65536 3 | const cache = {} 4 | 5 | // in node 6 Buffer.subarray returns a Uint8Array instead of a Buffer 6 | // later versions return a Buffer 7 | // alternative is Buffer.slice but that creates a new buffer 8 | // creating new buffers takes time 9 | // SubOk is only false on node < 8 10 | const SubOk = Buffer.isBuffer(Buffer.from([1, 2]).subarray(0, 1)) 11 | 12 | function generateBuffer (i) { 13 | const buffer = Buffer.allocUnsafe(2) 14 | buffer.writeUInt8(i >> 8, 0) 15 | buffer.writeUInt8(i & 0x00FF, 0 + 1) 16 | 17 | return buffer 18 | } 19 | 20 | function generateCache () { 21 | for (let i = 0; i < max; i++) { 22 | cache[i] = generateBuffer(i) 23 | } 24 | } 25 | 26 | function genBufVariableByteInt (num) { 27 | const maxLength = 4 // max 4 bytes 28 | let digit = 0 29 | let pos = 0 30 | const buffer = Buffer.allocUnsafe(maxLength) 31 | 32 | do { 33 | digit = num % 128 | 0 34 | num = num / 128 | 0 35 | if (num > 0) digit = digit | 0x80 36 | 37 | buffer.writeUInt8(digit, pos++) 38 | } while (num > 0 && pos < maxLength) 39 | 40 | if (num > 0) { 41 | pos = 0 42 | } 43 | 44 | return SubOk ? buffer.subarray(0, pos) : buffer.slice(0, pos) 45 | } 46 | 47 | function generate4ByteBuffer (num) { 48 | const buffer = Buffer.allocUnsafe(4) 49 | buffer.writeUInt32BE(num, 0) 50 | return buffer 51 | } 52 | 53 | module.exports = { 54 | cache, 55 | generateCache, 56 | generateNumber: generateBuffer, 57 | genBufVariableByteInt, 58 | generate4ByteBuffer 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-packet", 3 | "version": "9.0.2", 4 | "description": "Parse and generate MQTT packets like a breeze", 5 | "main": "mqtt.js", 6 | "types": "types/index.d.ts", 7 | "contributors": [ 8 | "Matteo Collina (https://github.com/mcollina)", 9 | "Adam Rudd ", 10 | "Peter Sorowka (https://github.com/psorowka)", 11 | "Wouter Klijn (https://github.com/wuhkuh)", 12 | "Siarhei Buntsevich (https://github.com/scarry1992)" 13 | ], 14 | "scripts": { 15 | "test": "tape test.js | tap-spec && standard", 16 | "ci": "tape test.js && node testRandom && standard" 17 | }, 18 | "pre-commit": "test", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/mqttjs/mqtt-packet.git" 22 | }, 23 | "keywords": [ 24 | "MQTT", 25 | "packet", 26 | "parse", 27 | "publish", 28 | "subscribe", 29 | "pubsub" 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/mqttjs/mqtt-packet/issues" 34 | }, 35 | "homepage": "https://github.com/mqttjs/mqtt-packet", 36 | "devDependencies": { 37 | "pre-commit": "^1.2.2", 38 | "readable-stream": "^4.4.2", 39 | "standard": "^17.1.0", 40 | "tap-spec": "^5.0.0", 41 | "tape": "^5.7.2" 42 | }, 43 | "dependencies": { 44 | "bl": "^6.0.8", 45 | "debug": "^4.3.4", 46 | "process-nextick-args": "^2.0.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packet.js: -------------------------------------------------------------------------------- 1 | class Packet { 2 | constructor () { 3 | this.cmd = null 4 | this.retain = false 5 | this.qos = 0 6 | this.dup = false 7 | this.length = -1 8 | this.topic = null 9 | this.payload = null 10 | } 11 | } 12 | 13 | module.exports = Packet 14 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | const bl = require('bl') 2 | const { EventEmitter } = require('events') 3 | const Packet = require('./packet') 4 | const constants = require('./constants') 5 | const debug = require('debug')('mqtt-packet:parser') 6 | 7 | class Parser extends EventEmitter { 8 | constructor () { 9 | super() 10 | this.parser = this.constructor.parser 11 | } 12 | 13 | static parser (opt) { 14 | if (!(this instanceof Parser)) return (new Parser()).parser(opt) 15 | 16 | this.settings = opt || {} 17 | 18 | this._states = [ 19 | '_parseHeader', 20 | '_parseLength', 21 | '_parsePayload', 22 | '_newPacket' 23 | ] 24 | 25 | this._resetState() 26 | return this 27 | } 28 | 29 | _resetState () { 30 | debug('_resetState: resetting packet, error, _list, and _stateCounter') 31 | this.packet = new Packet() 32 | this.error = null 33 | this._list = bl() 34 | this._stateCounter = 0 35 | } 36 | 37 | parse (buf) { 38 | if (this.error) this._resetState() 39 | 40 | this._list.append(buf) 41 | debug('parse: current state: %s', this._states[this._stateCounter]) 42 | while ((this.packet.length !== -1 || this._list.length > 0) && 43 | this[this._states[this._stateCounter]]() && 44 | !this.error) { 45 | this._stateCounter++ 46 | debug('parse: state complete. _stateCounter is now: %d', this._stateCounter) 47 | debug('parse: packet.length: %d, buffer list length: %d', this.packet.length, this._list.length) 48 | if (this._stateCounter >= this._states.length) this._stateCounter = 0 49 | } 50 | debug('parse: exited while loop. packet: %d, buffer list length: %d', this.packet.length, this._list.length) 51 | return this._list.length 52 | } 53 | 54 | _parseHeader () { 55 | // There is at least one byte in the buffer 56 | const zero = this._list.readUInt8(0) 57 | const cmdIndex = zero >> constants.CMD_SHIFT 58 | this.packet.cmd = constants.types[cmdIndex] 59 | const headerFlags = zero & 0xf 60 | const requiredHeaderFlags = constants.requiredHeaderFlags[cmdIndex] 61 | if (requiredHeaderFlags != null && headerFlags !== requiredHeaderFlags) { 62 | // 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] 63 | return this._emitError(new Error(constants.requiredHeaderFlagsErrors[cmdIndex])) 64 | } 65 | this.packet.retain = (zero & constants.RETAIN_MASK) !== 0 66 | this.packet.qos = (zero >> constants.QOS_SHIFT) & constants.QOS_MASK 67 | if (this.packet.qos > 2) { 68 | return this._emitError(new Error('Packet must not have both QoS bits set to 1')) 69 | } 70 | this.packet.dup = (zero & constants.DUP_MASK) !== 0 71 | debug('_parseHeader: packet: %o', this.packet) 72 | 73 | this._list.consume(1) 74 | 75 | return true 76 | } 77 | 78 | _parseLength () { 79 | // There is at least one byte in the list 80 | const result = this._parseVarByteNum(true) 81 | 82 | if (result) { 83 | this.packet.length = result.value 84 | this._list.consume(result.bytes) 85 | } 86 | debug('_parseLength %d', result.value) 87 | return !!result 88 | } 89 | 90 | _parsePayload () { 91 | debug('_parsePayload: payload %O', this._list) 92 | let result = false 93 | 94 | // Do we have a payload? Do we have enough data to complete the payload? 95 | // PINGs have no payload 96 | if (this.packet.length === 0 || this._list.length >= this.packet.length) { 97 | this._pos = 0 98 | 99 | switch (this.packet.cmd) { 100 | case 'connect': 101 | this._parseConnect() 102 | break 103 | case 'connack': 104 | this._parseConnack() 105 | break 106 | case 'publish': 107 | this._parsePublish() 108 | break 109 | case 'puback': 110 | case 'pubrec': 111 | case 'pubrel': 112 | case 'pubcomp': 113 | this._parseConfirmation() 114 | break 115 | case 'subscribe': 116 | this._parseSubscribe() 117 | break 118 | case 'suback': 119 | this._parseSuback() 120 | break 121 | case 'unsubscribe': 122 | this._parseUnsubscribe() 123 | break 124 | case 'unsuback': 125 | this._parseUnsuback() 126 | break 127 | case 'pingreq': 128 | case 'pingresp': 129 | // These are empty, nothing to do 130 | break 131 | case 'disconnect': 132 | this._parseDisconnect() 133 | break 134 | case 'auth': 135 | this._parseAuth() 136 | break 137 | default: 138 | this._emitError(new Error('Not supported')) 139 | } 140 | 141 | result = true 142 | } 143 | debug('_parsePayload complete result: %s', result) 144 | return result 145 | } 146 | 147 | _parseConnect () { 148 | debug('_parseConnect') 149 | let topic // Will topic 150 | let payload // Will payload 151 | let password // Password 152 | let username // Username 153 | const flags = {} 154 | const packet = this.packet 155 | 156 | // Parse protocolId 157 | const protocolId = this._parseString() 158 | 159 | if (protocolId === null) return this._emitError(new Error('Cannot parse protocolId')) 160 | if (protocolId !== 'MQTT' && protocolId !== 'MQIsdp') { 161 | return this._emitError(new Error('Invalid protocolId')) 162 | } 163 | 164 | packet.protocolId = protocolId 165 | 166 | // Parse constants version number 167 | if (this._pos >= this._list.length) return this._emitError(new Error('Packet too short')) 168 | 169 | packet.protocolVersion = this._list.readUInt8(this._pos) 170 | 171 | if (packet.protocolVersion >= 128) { 172 | packet.bridgeMode = true 173 | packet.protocolVersion = packet.protocolVersion - 128 174 | } 175 | 176 | if (packet.protocolVersion !== 3 && packet.protocolVersion !== 4 && packet.protocolVersion !== 5) { 177 | return this._emitError(new Error('Invalid protocol version')) 178 | } 179 | 180 | this._pos++ 181 | 182 | if (this._pos >= this._list.length) { 183 | return this._emitError(new Error('Packet too short')) 184 | } 185 | 186 | if (this._list.readUInt8(this._pos) & 0x1) { 187 | // 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] 188 | return this._emitError(new Error('Connect flag bit 0 must be 0, but got 1')) 189 | } 190 | // Parse connect flags 191 | flags.username = (this._list.readUInt8(this._pos) & constants.USERNAME_MASK) 192 | flags.password = (this._list.readUInt8(this._pos) & constants.PASSWORD_MASK) 193 | flags.will = (this._list.readUInt8(this._pos) & constants.WILL_FLAG_MASK) 194 | 195 | const willRetain = !!(this._list.readUInt8(this._pos) & constants.WILL_RETAIN_MASK) 196 | const willQos = (this._list.readUInt8(this._pos) & 197 | constants.WILL_QOS_MASK) >> constants.WILL_QOS_SHIFT 198 | 199 | if (flags.will) { 200 | packet.will = {} 201 | packet.will.retain = willRetain 202 | packet.will.qos = willQos 203 | } else { 204 | if (willRetain) { 205 | return this._emitError(new Error('Will Retain Flag must be set to zero when Will Flag is set to 0')) 206 | } 207 | if (willQos) { 208 | return this._emitError(new Error('Will QoS must be set to zero when Will Flag is set to 0')) 209 | } 210 | } 211 | 212 | packet.clean = (this._list.readUInt8(this._pos) & constants.CLEAN_SESSION_MASK) !== 0 213 | this._pos++ 214 | 215 | // Parse keepalive 216 | packet.keepalive = this._parseNum() 217 | if (packet.keepalive === -1) return this._emitError(new Error('Packet too short')) 218 | 219 | // parse properties 220 | if (packet.protocolVersion === 5) { 221 | const properties = this._parseProperties() 222 | if (Object.getOwnPropertyNames(properties).length) { 223 | packet.properties = properties 224 | } 225 | } 226 | // Parse clientId 227 | const clientId = this._parseString() 228 | if (clientId === null) return this._emitError(new Error('Packet too short')) 229 | packet.clientId = clientId 230 | debug('_parseConnect: packet.clientId: %s', packet.clientId) 231 | 232 | if (flags.will) { 233 | if (packet.protocolVersion === 5) { 234 | const willProperties = this._parseProperties() 235 | if (Object.getOwnPropertyNames(willProperties).length) { 236 | packet.will.properties = willProperties 237 | } 238 | } 239 | // Parse will topic 240 | topic = this._parseString() 241 | if (topic === null) return this._emitError(new Error('Cannot parse will topic')) 242 | packet.will.topic = topic 243 | debug('_parseConnect: packet.will.topic: %s', packet.will.topic) 244 | 245 | // Parse will payload 246 | payload = this._parseBuffer() 247 | if (payload === null) return this._emitError(new Error('Cannot parse will payload')) 248 | packet.will.payload = payload 249 | debug('_parseConnect: packet.will.paylaod: %s', packet.will.payload) 250 | } 251 | 252 | // Parse username 253 | if (flags.username) { 254 | username = this._parseString() 255 | if (username === null) return this._emitError(new Error('Cannot parse username')) 256 | packet.username = username 257 | debug('_parseConnect: packet.username: %s', packet.username) 258 | } 259 | 260 | // Parse password 261 | if (flags.password) { 262 | password = this._parseBuffer() 263 | if (password === null) return this._emitError(new Error('Cannot parse password')) 264 | packet.password = password 265 | } 266 | // need for right parse auth packet and self set up 267 | this.settings = packet 268 | debug('_parseConnect: complete') 269 | return packet 270 | } 271 | 272 | _parseConnack () { 273 | debug('_parseConnack') 274 | const packet = this.packet 275 | 276 | if (this._list.length < 1) return null 277 | const flags = this._list.readUInt8(this._pos++) 278 | if (flags > 1) { 279 | return this._emitError(new Error('Invalid connack flags, bits 7-1 must be set to 0')) 280 | } 281 | packet.sessionPresent = !!(flags & constants.SESSIONPRESENT_MASK) 282 | 283 | if (this.settings.protocolVersion === 5) { 284 | if (this._list.length >= 2) { 285 | packet.reasonCode = this._list.readUInt8(this._pos++) 286 | } else { 287 | packet.reasonCode = 0 288 | } 289 | } else { 290 | if (this._list.length < 2) return null 291 | packet.returnCode = this._list.readUInt8(this._pos++) 292 | } 293 | 294 | if (packet.returnCode === -1 || packet.reasonCode === -1) return this._emitError(new Error('Cannot parse return code')) 295 | // mqtt 5 properties 296 | if (this.settings.protocolVersion === 5) { 297 | const properties = this._parseProperties() 298 | if (Object.getOwnPropertyNames(properties).length) { 299 | packet.properties = properties 300 | } 301 | } 302 | debug('_parseConnack: complete') 303 | } 304 | 305 | _parsePublish () { 306 | debug('_parsePublish') 307 | const packet = this.packet 308 | packet.topic = this._parseString() 309 | 310 | if (packet.topic === null) return this._emitError(new Error('Cannot parse topic')) 311 | 312 | // Parse messageId 313 | if (packet.qos > 0) if (!this._parseMessageId()) { return } 314 | 315 | // Properties mqtt 5 316 | if (this.settings.protocolVersion === 5) { 317 | const properties = this._parseProperties() 318 | if (Object.getOwnPropertyNames(properties).length) { 319 | packet.properties = properties 320 | } 321 | } 322 | 323 | packet.payload = this._list.slice(this._pos, packet.length) 324 | debug('_parsePublish: payload from buffer list: %o', packet.payload) 325 | } 326 | 327 | _parseSubscribe () { 328 | debug('_parseSubscribe') 329 | const packet = this.packet 330 | let topic 331 | let options 332 | let qos 333 | let rh 334 | let rap 335 | let nl 336 | let subscription 337 | 338 | packet.subscriptions = [] 339 | 340 | if (!this._parseMessageId()) { return } 341 | 342 | // Properties mqtt 5 343 | if (this.settings.protocolVersion === 5) { 344 | const properties = this._parseProperties() 345 | if (Object.getOwnPropertyNames(properties).length) { 346 | packet.properties = properties 347 | } 348 | } 349 | 350 | if (packet.length <= 0) { return this._emitError(new Error('Malformed subscribe, no payload specified')) } 351 | 352 | while (this._pos < packet.length) { 353 | // Parse topic 354 | topic = this._parseString() 355 | if (topic === null) return this._emitError(new Error('Cannot parse topic')) 356 | if (this._pos >= packet.length) return this._emitError(new Error('Malformed Subscribe Payload')) 357 | 358 | options = this._parseByte() 359 | 360 | if (this.settings.protocolVersion === 5) { 361 | if (options & 0xc0) { 362 | return this._emitError(new Error('Invalid subscribe topic flag bits, bits 7-6 must be 0')) 363 | } 364 | } else { 365 | if (options & 0xfc) { 366 | return this._emitError(new Error('Invalid subscribe topic flag bits, bits 7-2 must be 0')) 367 | } 368 | } 369 | 370 | qos = options & constants.SUBSCRIBE_OPTIONS_QOS_MASK 371 | if (qos > 2) { 372 | return this._emitError(new Error('Invalid subscribe QoS, must be <= 2')) 373 | } 374 | nl = ((options >> constants.SUBSCRIBE_OPTIONS_NL_SHIFT) & constants.SUBSCRIBE_OPTIONS_NL_MASK) !== 0 375 | rap = ((options >> constants.SUBSCRIBE_OPTIONS_RAP_SHIFT) & constants.SUBSCRIBE_OPTIONS_RAP_MASK) !== 0 376 | rh = (options >> constants.SUBSCRIBE_OPTIONS_RH_SHIFT) & constants.SUBSCRIBE_OPTIONS_RH_MASK 377 | 378 | if (rh > 2) { 379 | return this._emitError(new Error('Invalid retain handling, must be <= 2')) 380 | } 381 | 382 | subscription = { topic, qos } 383 | 384 | // mqtt 5 options 385 | if (this.settings.protocolVersion === 5) { 386 | subscription.nl = nl 387 | subscription.rap = rap 388 | subscription.rh = rh 389 | } else if (this.settings.bridgeMode) { 390 | subscription.rh = 0 391 | subscription.rap = true 392 | subscription.nl = true 393 | } 394 | 395 | // Push pair to subscriptions 396 | debug('_parseSubscribe: push subscription `%s` to subscription', subscription) 397 | packet.subscriptions.push(subscription) 398 | } 399 | } 400 | 401 | _parseSuback () { 402 | debug('_parseSuback') 403 | const packet = this.packet 404 | this.packet.granted = [] 405 | 406 | if (!this._parseMessageId()) { return } 407 | 408 | // Properties mqtt 5 409 | if (this.settings.protocolVersion === 5) { 410 | const properties = this._parseProperties() 411 | if (Object.getOwnPropertyNames(properties).length) { 412 | packet.properties = properties 413 | } 414 | } 415 | 416 | if (packet.length <= 0) { return this._emitError(new Error('Malformed suback, no payload specified')) } 417 | 418 | // Parse granted QoSes 419 | while (this._pos < this.packet.length) { 420 | const code = this._list.readUInt8(this._pos++) 421 | if (this.settings.protocolVersion === 5) { 422 | if (!constants.MQTT5_SUBACK_CODES[code]) { 423 | return this._emitError(new Error('Invalid suback code')) 424 | } 425 | } else { 426 | if (code > 2 && code !== 0x80) { 427 | return this._emitError(new Error('Invalid suback QoS, must be 0, 1, 2 or 128')) 428 | } 429 | } 430 | this.packet.granted.push(code) 431 | } 432 | } 433 | 434 | _parseUnsubscribe () { 435 | debug('_parseUnsubscribe') 436 | const packet = this.packet 437 | 438 | packet.unsubscriptions = [] 439 | 440 | // Parse messageId 441 | if (!this._parseMessageId()) { return } 442 | 443 | // Properties mqtt 5 444 | if (this.settings.protocolVersion === 5) { 445 | const properties = this._parseProperties() 446 | if (Object.getOwnPropertyNames(properties).length) { 447 | packet.properties = properties 448 | } 449 | } 450 | 451 | if (packet.length <= 0) { return this._emitError(new Error('Malformed unsubscribe, no payload specified')) } 452 | 453 | while (this._pos < packet.length) { 454 | // Parse topic 455 | const topic = this._parseString() 456 | if (topic === null) return this._emitError(new Error('Cannot parse topic')) 457 | 458 | // Push topic to unsubscriptions 459 | debug('_parseUnsubscribe: push topic `%s` to unsubscriptions', topic) 460 | packet.unsubscriptions.push(topic) 461 | } 462 | } 463 | 464 | _parseUnsuback () { 465 | debug('_parseUnsuback') 466 | const packet = this.packet 467 | if (!this._parseMessageId()) return this._emitError(new Error('Cannot parse messageId')) 468 | 469 | if ((this.settings.protocolVersion === 3 || 470 | this.settings.protocolVersion === 4) && packet.length !== 2) { 471 | return this._emitError(new Error('Malformed unsuback, payload length must be 2')) 472 | } 473 | if (packet.length <= 0) { return this._emitError(new Error('Malformed unsuback, no payload specified')) } 474 | 475 | // Properties mqtt 5 476 | if (this.settings.protocolVersion === 5) { 477 | const properties = this._parseProperties() 478 | if (Object.getOwnPropertyNames(properties).length) { 479 | packet.properties = properties 480 | } 481 | // Parse granted QoSes 482 | packet.granted = [] 483 | 484 | while (this._pos < this.packet.length) { 485 | const code = this._list.readUInt8(this._pos++) 486 | if (!constants.MQTT5_UNSUBACK_CODES[code]) { 487 | return this._emitError(new Error('Invalid unsuback code')) 488 | } 489 | this.packet.granted.push(code) 490 | } 491 | } 492 | } 493 | 494 | // parse packets like puback, pubrec, pubrel, pubcomp 495 | _parseConfirmation () { 496 | debug('_parseConfirmation: packet.cmd: `%s`', this.packet.cmd) 497 | const packet = this.packet 498 | 499 | this._parseMessageId() 500 | 501 | if (this.settings.protocolVersion === 5) { 502 | if (packet.length > 2) { 503 | // response code 504 | packet.reasonCode = this._parseByte() 505 | switch (this.packet.cmd) { 506 | case 'puback': 507 | case 'pubrec': 508 | if (!constants.MQTT5_PUBACK_PUBREC_CODES[packet.reasonCode]) { 509 | return this._emitError(new Error('Invalid ' + this.packet.cmd + ' reason code')) 510 | } 511 | break 512 | case 'pubrel': 513 | case 'pubcomp': 514 | if (!constants.MQTT5_PUBREL_PUBCOMP_CODES[packet.reasonCode]) { 515 | return this._emitError(new Error('Invalid ' + this.packet.cmd + ' reason code')) 516 | } 517 | break 518 | } 519 | debug('_parseConfirmation: packet.reasonCode `%d`', packet.reasonCode) 520 | } else { 521 | packet.reasonCode = 0 522 | } 523 | 524 | if (packet.length > 3) { 525 | // properies mqtt 5 526 | const properties = this._parseProperties() 527 | if (Object.getOwnPropertyNames(properties).length) { 528 | packet.properties = properties 529 | } 530 | } 531 | } 532 | 533 | return true 534 | } 535 | 536 | // parse disconnect packet 537 | _parseDisconnect () { 538 | const packet = this.packet 539 | debug('_parseDisconnect') 540 | 541 | if (this.settings.protocolVersion === 5) { 542 | // response code 543 | if (this._list.length > 0) { 544 | packet.reasonCode = this._parseByte() 545 | if (!constants.MQTT5_DISCONNECT_CODES[packet.reasonCode]) { 546 | this._emitError(new Error('Invalid disconnect reason code')) 547 | } 548 | } else { 549 | packet.reasonCode = 0 550 | } 551 | // properies mqtt 5 552 | const properties = this._parseProperties() 553 | if (Object.getOwnPropertyNames(properties).length) { 554 | packet.properties = properties 555 | } 556 | } 557 | 558 | debug('_parseDisconnect result: true') 559 | return true 560 | } 561 | 562 | // parse auth packet 563 | _parseAuth () { 564 | debug('_parseAuth') 565 | const packet = this.packet 566 | 567 | if (this.settings.protocolVersion !== 5) { 568 | return this._emitError(new Error('Not supported auth packet for this version MQTT')) 569 | } 570 | 571 | // response code 572 | packet.reasonCode = this._parseByte() 573 | if (!constants.MQTT5_AUTH_CODES[packet.reasonCode]) { 574 | return this._emitError(new Error('Invalid auth reason code')) 575 | } 576 | // properies mqtt 5 577 | const properties = this._parseProperties() 578 | if (Object.getOwnPropertyNames(properties).length) { 579 | packet.properties = properties 580 | } 581 | 582 | debug('_parseAuth: result: true') 583 | return true 584 | } 585 | 586 | _parseMessageId () { 587 | const packet = this.packet 588 | 589 | packet.messageId = this._parseNum() 590 | 591 | if (packet.messageId === null) { 592 | this._emitError(new Error('Cannot parse messageId')) 593 | return false 594 | } 595 | 596 | debug('_parseMessageId: packet.messageId %d', packet.messageId) 597 | return true 598 | } 599 | 600 | _parseString (maybeBuffer) { 601 | const length = this._parseNum() 602 | const end = length + this._pos 603 | 604 | if (length === -1 || end > this._list.length || end > this.packet.length) return null 605 | 606 | const result = this._list.toString('utf8', this._pos, end) 607 | this._pos += length 608 | debug('_parseString: result: %s', result) 609 | return result 610 | } 611 | 612 | _parseStringPair () { 613 | debug('_parseStringPair') 614 | return { 615 | name: this._parseString(), 616 | value: this._parseString() 617 | } 618 | } 619 | 620 | _parseBuffer () { 621 | const length = this._parseNum() 622 | const end = length + this._pos 623 | 624 | if (length === -1 || end > this._list.length || end > this.packet.length) return null 625 | 626 | const result = this._list.slice(this._pos, end) 627 | 628 | this._pos += length 629 | debug('_parseBuffer: result: %o', result) 630 | return result 631 | } 632 | 633 | _parseNum () { 634 | if (this._list.length - this._pos < 2) return -1 635 | 636 | const result = this._list.readUInt16BE(this._pos) 637 | this._pos += 2 638 | debug('_parseNum: result: %s', result) 639 | return result 640 | } 641 | 642 | _parse4ByteNum () { 643 | if (this._list.length - this._pos < 4) return -1 644 | 645 | const result = this._list.readUInt32BE(this._pos) 646 | this._pos += 4 647 | debug('_parse4ByteNum: result: %s', result) 648 | return result 649 | } 650 | 651 | _parseVarByteNum (fullInfoFlag) { 652 | debug('_parseVarByteNum') 653 | const maxBytes = 4 654 | let bytes = 0 655 | let mul = 1 656 | let value = 0 657 | let result = false 658 | let current 659 | const padding = this._pos ? this._pos : 0 660 | 661 | while (bytes < maxBytes && (padding + bytes) < this._list.length) { 662 | current = this._list.readUInt8(padding + bytes++) 663 | value += mul * (current & constants.VARBYTEINT_MASK) 664 | mul *= 0x80 665 | 666 | if ((current & constants.VARBYTEINT_FIN_MASK) === 0) { 667 | result = true 668 | break 669 | } 670 | if (this._list.length <= bytes) { 671 | break 672 | } 673 | } 674 | 675 | if (!result && bytes === maxBytes && this._list.length >= bytes) { 676 | this._emitError(new Error('Invalid variable byte integer')) 677 | } 678 | 679 | if (padding) { 680 | this._pos += bytes 681 | } 682 | 683 | if (result) { 684 | if (fullInfoFlag) { 685 | result = { bytes, value } 686 | } else { 687 | result = value 688 | } 689 | } else { 690 | result = false 691 | } 692 | 693 | debug('_parseVarByteNum: result: %o', result) 694 | return result 695 | } 696 | 697 | _parseByte () { 698 | let result 699 | if (this._pos < this._list.length) { 700 | result = this._list.readUInt8(this._pos) 701 | this._pos++ 702 | } 703 | debug('_parseByte: result: %o', result) 704 | return result 705 | } 706 | 707 | _parseByType (type) { 708 | debug('_parseByType: type: %s', type) 709 | switch (type) { 710 | case 'byte': { 711 | return this._parseByte() !== 0 712 | } 713 | case 'int8': { 714 | return this._parseByte() 715 | } 716 | case 'int16': { 717 | return this._parseNum() 718 | } 719 | case 'int32': { 720 | return this._parse4ByteNum() 721 | } 722 | case 'var': { 723 | return this._parseVarByteNum() 724 | } 725 | case 'string': { 726 | return this._parseString() 727 | } 728 | case 'pair': { 729 | return this._parseStringPair() 730 | } 731 | case 'binary': { 732 | return this._parseBuffer() 733 | } 734 | } 735 | } 736 | 737 | _parseProperties () { 738 | debug('_parseProperties') 739 | const length = this._parseVarByteNum() 740 | const start = this._pos 741 | const end = start + length 742 | const result = {} 743 | while (this._pos < end) { 744 | const type = this._parseByte() 745 | if (!type) { 746 | this._emitError(new Error('Cannot parse property code type')) 747 | return false 748 | } 749 | const name = constants.propertiesCodes[type] 750 | if (!name) { 751 | this._emitError(new Error('Unknown property')) 752 | return false 753 | } 754 | // user properties process 755 | if (name === 'userProperties') { 756 | if (!result[name]) { 757 | result[name] = Object.create(null) 758 | } 759 | const currentUserProperty = this._parseByType(constants.propertiesTypes[name]) 760 | if (result[name][currentUserProperty.name]) { 761 | if (Array.isArray(result[name][currentUserProperty.name])) { 762 | result[name][currentUserProperty.name].push(currentUserProperty.value) 763 | } else { 764 | const currentValue = result[name][currentUserProperty.name] 765 | result[name][currentUserProperty.name] = [currentValue] 766 | result[name][currentUserProperty.name].push(currentUserProperty.value) 767 | } 768 | } else { 769 | result[name][currentUserProperty.name] = currentUserProperty.value 770 | } 771 | continue 772 | } 773 | if (result[name]) { 774 | if (Array.isArray(result[name])) { 775 | result[name].push(this._parseByType(constants.propertiesTypes[name])) 776 | } else { 777 | result[name] = [result[name]] 778 | result[name].push(this._parseByType(constants.propertiesTypes[name])) 779 | } 780 | } else { 781 | result[name] = this._parseByType(constants.propertiesTypes[name]) 782 | } 783 | } 784 | return result 785 | } 786 | 787 | _newPacket () { 788 | debug('_newPacket') 789 | if (this.packet) { 790 | this._list.consume(this.packet.length) 791 | debug('_newPacket: parser emit packet: packet.cmd: %s, packet.payload: %s, packet.length: %d', this.packet.cmd, this.packet.payload, this.packet.length) 792 | this.emit('packet', this.packet) 793 | } 794 | debug('_newPacket: new packet') 795 | this.packet = new Packet() 796 | 797 | this._pos = 0 798 | 799 | return true 800 | } 801 | 802 | _emitError (err) { 803 | debug('_emitError', err) 804 | this.error = err 805 | this.emit('error', err) 806 | } 807 | } 808 | 809 | module.exports = Parser 810 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | 3 | const test = require('tape') 4 | const mqtt = require('./') 5 | const WS = require('readable-stream').Writable 6 | 7 | function normalExpectedObject (object) { 8 | if (object.username != null) object.username = object.username.toString() 9 | if (object.password != null) object.password = Buffer.from(object.password) 10 | return object 11 | } 12 | 13 | function testParseGenerate (name, object, buffer, opts) { 14 | test(`${name} parse`, t => { 15 | t.plan(2) 16 | 17 | const parser = mqtt.parser(opts) 18 | const expected = object 19 | const fixture = buffer 20 | 21 | parser.on('packet', packet => { 22 | if (packet.cmd !== 'publish') { 23 | delete packet.topic 24 | delete packet.payload 25 | } 26 | t.deepLooseEqual(packet, normalExpectedObject(expected), 'expected packet') 27 | }) 28 | 29 | parser.on('error', err => { 30 | t.fail(err) 31 | }) 32 | 33 | t.equal(parser.parse(fixture), 0, 'remaining bytes') 34 | }) 35 | 36 | test(`${name} generate`, t => { 37 | // For really large buffers, the expanded hex string can be so long as to 38 | // generate an error in nodejs 14.x, so only do the test with extra output 39 | // for relatively small buffers. 40 | const bigLength = 10000 41 | const generatedBuffer = mqtt.generate(object, opts) 42 | if (generatedBuffer.length < bigLength && buffer.length < bigLength) { 43 | t.equal(generatedBuffer.toString('hex'), buffer.toString('hex')) 44 | } else { 45 | const bufferOkay = generatedBuffer.equals(buffer) 46 | if (bufferOkay) { 47 | t.pass() 48 | } else { 49 | // Output abbreviated representations of the buffers. 50 | t.comment('Expected:\n' + util.inspect(buffer)) 51 | t.comment('Got:\n' + util.inspect(generatedBuffer)) 52 | t.fail('Large buffers not equal') 53 | } 54 | } 55 | t.end() 56 | }) 57 | 58 | test(`${name} mirror`, t => { 59 | t.plan(2) 60 | 61 | const parser = mqtt.parser(opts) 62 | const expected = object 63 | const fixture = mqtt.generate(object, opts) 64 | 65 | parser.on('packet', packet => { 66 | if (packet.cmd !== 'publish') { 67 | delete packet.topic 68 | delete packet.payload 69 | } 70 | t.deepLooseEqual(packet, normalExpectedObject(expected), 'expected packet') 71 | }) 72 | 73 | parser.on('error', err => { 74 | t.fail(err) 75 | }) 76 | 77 | t.equal(parser.parse(fixture), 0, 'remaining bytes') 78 | }) 79 | 80 | test(`${name} writeToStream`, t => { 81 | const stream = WS() 82 | stream.write = () => true 83 | stream.on('error', (err) => t.fail(err)) 84 | 85 | const result = mqtt.writeToStream(object, stream, opts) 86 | t.equal(result, true, 'should return true') 87 | t.end() 88 | }) 89 | } 90 | 91 | // the API allows to pass strings as buffers to writeToStream and generate 92 | // parsing them back will result in a string so only generate and compare to buffer 93 | function testGenerateOnly (name, object, buffer, opts) { 94 | test(name, t => { 95 | t.equal(mqtt.generate(object, opts).toString('hex'), buffer.toString('hex')) 96 | t.end() 97 | }) 98 | } 99 | 100 | function testParseOnly (name, object, buffer, opts) { 101 | test(name, t => { 102 | const parser = mqtt.parser(opts) 103 | // const expected = object 104 | // const fixture = buffer 105 | 106 | t.plan(2 + Object.keys(object).length) 107 | 108 | parser.on('packet', packet => { 109 | t.equal(Object.keys(object).length, Object.keys(packet).length, 'key count') 110 | Object.keys(object).forEach(key => { 111 | t.deepEqual(packet[key], object[key], `expected packet property ${key}`) 112 | }) 113 | }) 114 | 115 | t.equal(parser.parse(buffer), 0, 'remaining bytes') 116 | t.end() 117 | }) 118 | } 119 | 120 | function testParseError (expected, fixture, opts) { 121 | test(expected, t => { 122 | t.plan(1) 123 | 124 | const parser = mqtt.parser(opts) 125 | 126 | parser.on('error', err => { 127 | t.equal(err.message, expected, 'expected error message') 128 | }) 129 | 130 | parser.on('packet', () => { 131 | t.fail('parse errors should not be followed by packet events') 132 | }) 133 | 134 | parser.parse(fixture) 135 | t.end() 136 | }) 137 | } 138 | 139 | function testGenerateError (expected, fixture, opts, name) { 140 | test(name || expected, t => { 141 | t.plan(1) 142 | 143 | try { 144 | mqtt.generate(fixture, opts) 145 | } catch (err) { 146 | t.equal(expected, err.message) 147 | } 148 | t.end() 149 | }) 150 | } 151 | 152 | function testGenerateErrorMultipleCmds (cmds, expected, fixture, opts) { 153 | cmds.forEach(cmd => { 154 | const obj = Object.assign({}, fixture) 155 | obj.cmd = cmd 156 | testGenerateError(expected, obj, opts, `${expected} on ${cmd}`) 157 | } 158 | ) 159 | } 160 | 161 | function testParseGenerateDefaults (name, object, buffer, generated, opts) { 162 | testParseOnly(`${name} parse`, generated, buffer, opts) 163 | testGenerateOnly(`${name} generate`, object, buffer, opts) 164 | } 165 | 166 | function testParseAndGenerate (name, object, buffer, opts) { 167 | testParseOnly(`${name} parse`, object, buffer, opts) 168 | testGenerateOnly(`${name} generate`, object, buffer, opts) 169 | } 170 | 171 | function testWriteToStreamError (expected, fixture) { 172 | test(`writeToStream ${expected} error`, t => { 173 | t.plan(2) 174 | 175 | const stream = WS() 176 | 177 | stream.write = () => t.fail('should not have called write') 178 | stream.on('error', () => t.pass('error emitted')) 179 | 180 | const result = mqtt.writeToStream(fixture, stream) 181 | 182 | t.false(result, 'result should be false') 183 | }) 184 | } 185 | 186 | test('cacheNumbers get/set/unset', t => { 187 | t.true(mqtt.writeToStream.cacheNumbers, 'initial state of cacheNumbers is enabled') 188 | mqtt.writeToStream.cacheNumbers = false 189 | t.false(mqtt.writeToStream.cacheNumbers, 'cacheNumbers can be disabled') 190 | mqtt.writeToStream.cacheNumbers = true 191 | t.true(mqtt.writeToStream.cacheNumbers, 'cacheNumbers can be enabled') 192 | t.end() 193 | }) 194 | 195 | test('disabled numbers cache', t => { 196 | const stream = WS() 197 | const message = { 198 | cmd: 'publish', 199 | retain: false, 200 | qos: 0, 201 | dup: false, 202 | length: 10, 203 | topic: Buffer.from('test'), 204 | payload: Buffer.from('test') 205 | } 206 | const expected = Buffer.from([ 207 | 48, 10, // Header 208 | 0, 4, // Topic length 209 | 116, 101, 115, 116, // Topic (test) 210 | 116, 101, 115, 116 // Payload (test) 211 | ]) 212 | let written = Buffer.alloc(0) 213 | 214 | stream.write = (chunk) => { 215 | written = Buffer.concat([written, chunk]) 216 | } 217 | mqtt.writeToStream.cacheNumbers = false 218 | 219 | mqtt.writeToStream(message, stream) 220 | 221 | t.deepEqual(written, expected, 'written buffer is expected') 222 | 223 | mqtt.writeToStream.cacheNumbers = true 224 | 225 | stream.end() 226 | t.end() 227 | }) 228 | 229 | testGenerateError('Unknown command', {}) 230 | 231 | testParseError('Not supported', Buffer.from([0, 1, 0]), {}) 232 | 233 | // Length header field 234 | testParseError('Invalid variable byte integer', Buffer.from( 235 | [16, 255, 255, 255, 255] 236 | ), {}) 237 | testParseError('Invalid variable byte integer', Buffer.from( 238 | [16, 255, 255, 255, 128] 239 | ), {}) 240 | testParseError('Invalid variable byte integer', Buffer.from( 241 | [16, 255, 255, 255, 255, 1] 242 | ), {}) 243 | testParseError('Invalid variable byte integer', Buffer.from( 244 | [16, 255, 255, 255, 255, 127] 245 | ), {}) 246 | testParseError('Invalid variable byte integer', Buffer.from( 247 | [16, 255, 255, 255, 255, 128] 248 | ), {}) 249 | testParseError('Invalid variable byte integer', Buffer.from( 250 | [16, 255, 255, 255, 255, 255, 1] 251 | ), {}) 252 | 253 | testParseGenerate('minimal connect', { 254 | cmd: 'connect', 255 | retain: false, 256 | qos: 0, 257 | dup: false, 258 | length: 18, 259 | protocolId: 'MQIsdp', 260 | protocolVersion: 3, 261 | clean: false, 262 | keepalive: 30, 263 | clientId: 'test' 264 | }, Buffer.from([ 265 | 16, 18, // Header 266 | 0, 6, // Protocol ID length 267 | 77, 81, 73, 115, 100, 112, // Protocol ID 268 | 3, // Protocol version 269 | 0, // Connect flags 270 | 0, 30, // Keepalive 271 | 0, 4, // Client ID length 272 | 116, 101, 115, 116 // Client ID 273 | ])) 274 | 275 | testGenerateOnly('minimal connect with clientId as Buffer', { 276 | cmd: 'connect', 277 | retain: false, 278 | qos: 0, 279 | dup: false, 280 | length: 18, 281 | protocolId: 'MQIsdp', 282 | protocolVersion: 3, 283 | clean: false, 284 | keepalive: 30, 285 | clientId: Buffer.from('test') 286 | }, Buffer.from([ 287 | 16, 18, // Header 288 | 0, 6, // Protocol ID length 289 | 77, 81, 73, 115, 100, 112, // Protocol ID 290 | 3, // Protocol version 291 | 0, // Connect flags 292 | 0, 30, // Keepalive 293 | 0, 4, // Client ID length 294 | 116, 101, 115, 116 // Client ID 295 | ])) 296 | 297 | testParseGenerate('connect MQTT bridge 131', { 298 | cmd: 'connect', 299 | retain: false, 300 | qos: 0, 301 | dup: false, 302 | length: 18, 303 | protocolId: 'MQIsdp', 304 | protocolVersion: 3, 305 | bridgeMode: true, 306 | clean: false, 307 | keepalive: 30, 308 | clientId: 'test' 309 | }, Buffer.from([ 310 | 16, 18, // Header 311 | 0, 6, // Protocol ID length 312 | 77, 81, 73, 115, 100, 112, // Protocol ID 313 | 131, // Protocol version 314 | 0, // Connect flags 315 | 0, 30, // Keepalive 316 | 0, 4, // Client ID length 317 | 116, 101, 115, 116 // Client ID 318 | ])) 319 | 320 | testParseGenerate('connect MQTT bridge 132', { 321 | cmd: 'connect', 322 | retain: false, 323 | qos: 0, 324 | dup: false, 325 | length: 18, 326 | protocolId: 'MQIsdp', 327 | protocolVersion: 4, 328 | bridgeMode: true, 329 | clean: false, 330 | keepalive: 30, 331 | clientId: 'test' 332 | }, Buffer.from([ 333 | 16, 18, // Header 334 | 0, 6, // Protocol ID length 335 | 77, 81, 73, 115, 100, 112, // Protocol ID 336 | 132, // Protocol version 337 | 0, // Connect flags 338 | 0, 30, // Keepalive 339 | 0, 4, // Client ID length 340 | 116, 101, 115, 116 // Client ID 341 | ])) 342 | 343 | testParseGenerate('connect MQTT 5', { 344 | cmd: 'connect', 345 | retain: false, 346 | qos: 0, 347 | dup: false, 348 | length: 125, 349 | protocolId: 'MQTT', 350 | protocolVersion: 5, 351 | will: { 352 | retain: true, 353 | qos: 2, 354 | properties: { 355 | willDelayInterval: 1234, 356 | payloadFormatIndicator: false, 357 | messageExpiryInterval: 4321, 358 | contentType: 'test', 359 | responseTopic: 'topic', 360 | correlationData: Buffer.from([1, 2, 3, 4]), 361 | userProperties: { 362 | test: 'test' 363 | } 364 | }, 365 | topic: 'topic', 366 | payload: Buffer.from([4, 3, 2, 1]) 367 | }, 368 | clean: true, 369 | keepalive: 30, 370 | properties: { 371 | sessionExpiryInterval: 1234, 372 | receiveMaximum: 432, 373 | maximumPacketSize: 100, 374 | topicAliasMaximum: 456, 375 | requestResponseInformation: true, 376 | requestProblemInformation: true, 377 | userProperties: { 378 | test: 'test' 379 | }, 380 | authenticationMethod: 'test', 381 | authenticationData: Buffer.from([1, 2, 3, 4]) 382 | }, 383 | clientId: 'test' 384 | }, Buffer.from([ 385 | 16, 125, // Header 386 | 0, 4, // Protocol ID length 387 | 77, 81, 84, 84, // Protocol ID 388 | 5, // Protocol version 389 | 54, // Connect flags 390 | 0, 30, // Keepalive 391 | 47, // properties length 392 | 17, 0, 0, 4, 210, // sessionExpiryInterval 393 | 33, 1, 176, // receiveMaximum 394 | 39, 0, 0, 0, 100, // maximumPacketSize 395 | 34, 1, 200, // topicAliasMaximum 396 | 25, 1, // requestResponseInformation 397 | 23, 1, // requestProblemInformation, 398 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties, 399 | 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 400 | 22, 0, 4, 1, 2, 3, 4, // authenticationData 401 | 0, 4, // Client ID length 402 | 116, 101, 115, 116, // Client ID 403 | 47, // will properties 404 | 24, 0, 0, 4, 210, // will delay interval 405 | 1, 0, // payload format indicator 406 | 2, 0, 0, 16, 225, // message expiry interval 407 | 3, 0, 4, 116, 101, 115, 116, // content type 408 | 8, 0, 5, 116, 111, 112, 105, 99, // response topic 409 | 9, 0, 4, 1, 2, 3, 4, // corelation data 410 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // user properties 411 | 0, 5, // Will topic length 412 | 116, 111, 112, 105, 99, // Will topic 413 | 0, 4, // Will payload length 414 | 4, 3, 2, 1// Will payload 415 | ])) 416 | 417 | testParseGenerate('connect MQTT 5 with will properties but with empty will payload', { 418 | cmd: 'connect', 419 | retain: false, 420 | qos: 0, 421 | dup: false, 422 | length: 121, 423 | protocolId: 'MQTT', 424 | protocolVersion: 5, 425 | will: { 426 | retain: true, 427 | qos: 2, 428 | properties: { 429 | willDelayInterval: 1234, 430 | payloadFormatIndicator: false, 431 | messageExpiryInterval: 4321, 432 | contentType: 'test', 433 | responseTopic: 'topic', 434 | correlationData: Buffer.from([1, 2, 3, 4]), 435 | userProperties: { 436 | test: 'test' 437 | } 438 | }, 439 | topic: 'topic', 440 | payload: Buffer.from([]) 441 | }, 442 | clean: true, 443 | keepalive: 30, 444 | properties: { 445 | sessionExpiryInterval: 1234, 446 | receiveMaximum: 432, 447 | maximumPacketSize: 100, 448 | topicAliasMaximum: 456, 449 | requestResponseInformation: true, 450 | requestProblemInformation: true, 451 | userProperties: { 452 | test: 'test' 453 | }, 454 | authenticationMethod: 'test', 455 | authenticationData: Buffer.from([1, 2, 3, 4]) 456 | }, 457 | clientId: 'test' 458 | }, Buffer.from([ 459 | 16, 121, // Header 460 | 0, 4, // Protocol ID length 461 | 77, 81, 84, 84, // Protocol ID 462 | 5, // Protocol version 463 | 54, // Connect flags 464 | 0, 30, // Keepalive 465 | 47, // properties length 466 | 17, 0, 0, 4, 210, // sessionExpiryInterval 467 | 33, 1, 176, // receiveMaximum 468 | 39, 0, 0, 0, 100, // maximumPacketSize 469 | 34, 1, 200, // topicAliasMaximum 470 | 25, 1, // requestResponseInformation 471 | 23, 1, // requestProblemInformation, 472 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties, 473 | 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 474 | 22, 0, 4, 1, 2, 3, 4, // authenticationData 475 | 0, 4, // Client ID length 476 | 116, 101, 115, 116, // Client ID 477 | 47, // will properties 478 | 24, 0, 0, 4, 210, // will delay interval 479 | 1, 0, // payload format indicator 480 | 2, 0, 0, 16, 225, // message expiry interval 481 | 3, 0, 4, 116, 101, 115, 116, // content type 482 | 8, 0, 5, 116, 111, 112, 105, 99, // response topic 483 | 9, 0, 4, 1, 2, 3, 4, // corelation data 484 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // user properties 485 | 0, 5, // Will topic length 486 | 116, 111, 112, 105, 99, // Will topic 487 | 0, 0 // Will payload length 488 | ])) 489 | 490 | testParseGenerate('connect MQTT 5 w/o will properties', { 491 | cmd: 'connect', 492 | retain: false, 493 | qos: 0, 494 | dup: false, 495 | length: 78, 496 | protocolId: 'MQTT', 497 | protocolVersion: 5, 498 | will: { 499 | retain: true, 500 | qos: 2, 501 | topic: 'topic', 502 | payload: Buffer.from([4, 3, 2, 1]) 503 | }, 504 | clean: true, 505 | keepalive: 30, 506 | properties: { 507 | sessionExpiryInterval: 1234, 508 | receiveMaximum: 432, 509 | maximumPacketSize: 100, 510 | topicAliasMaximum: 456, 511 | requestResponseInformation: true, 512 | requestProblemInformation: true, 513 | userProperties: { 514 | test: 'test' 515 | }, 516 | authenticationMethod: 'test', 517 | authenticationData: Buffer.from([1, 2, 3, 4]) 518 | }, 519 | clientId: 'test' 520 | }, Buffer.from([ 521 | 16, 78, // Header 522 | 0, 4, // Protocol ID length 523 | 77, 81, 84, 84, // Protocol ID 524 | 5, // Protocol version 525 | 54, // Connect flags 526 | 0, 30, // Keepalive 527 | 47, // properties length 528 | 17, 0, 0, 4, 210, // sessionExpiryInterval 529 | 33, 1, 176, // receiveMaximum 530 | 39, 0, 0, 0, 100, // maximumPacketSize 531 | 34, 1, 200, // topicAliasMaximum 532 | 25, 1, // requestResponseInformation 533 | 23, 1, // requestProblemInformation, 534 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties, 535 | 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 536 | 22, 0, 4, 1, 2, 3, 4, // authenticationData 537 | 0, 4, // Client ID length 538 | 116, 101, 115, 116, // Client ID 539 | 0, // will properties 540 | 0, 5, // Will topic length 541 | 116, 111, 112, 105, 99, // Will topic 542 | 0, 4, // Will payload length 543 | 4, 3, 2, 1// Will payload 544 | ])) 545 | 546 | testParseGenerate('no clientId with 3.1.1', { 547 | cmd: 'connect', 548 | retain: false, 549 | qos: 0, 550 | dup: false, 551 | length: 12, 552 | protocolId: 'MQTT', 553 | protocolVersion: 4, 554 | clean: true, 555 | keepalive: 30, 556 | clientId: '' 557 | }, Buffer.from([ 558 | 16, 12, // Header 559 | 0, 4, // Protocol ID length 560 | 77, 81, 84, 84, // Protocol ID 561 | 4, // Protocol version 562 | 2, // Connect flags 563 | 0, 30, // Keepalive 564 | 0, 0 // Client ID length 565 | ])) 566 | 567 | testParseGenerateDefaults('no clientId with 5.0', { 568 | cmd: 'connect', 569 | protocolId: 'MQTT', 570 | protocolVersion: 5, 571 | clean: true, 572 | keepalive: 60, 573 | properties: 574 | { 575 | receiveMaximum: 20 576 | }, 577 | clientId: '' 578 | }, Buffer.from( 579 | [16, 16, 0, 4, 77, 81, 84, 84, 5, 2, 0, 60, 3, 33, 0, 20, 0, 0] 580 | ), { 581 | cmd: 'connect', 582 | retain: false, 583 | qos: 0, 584 | dup: false, 585 | length: 16, 586 | topic: null, 587 | payload: null, 588 | protocolId: 'MQTT', 589 | protocolVersion: 5, 590 | clean: true, 591 | keepalive: 60, 592 | properties: { 593 | receiveMaximum: 20 594 | }, 595 | clientId: '' 596 | }, { protocolVersion: 5 }) 597 | 598 | testParseGenerateDefaults('utf-8 clientId with 5.0', { 599 | cmd: 'connect', 600 | retain: false, 601 | qos: 0, 602 | dup: false, 603 | length: 23, 604 | protocolId: 'MQTT', 605 | protocolVersion: 4, 606 | clean: true, 607 | keepalive: 30, 608 | clientId: 'Ŧėśt🜄' 609 | }, Buffer.from([ 610 | 16, 23, // Header 611 | 0, 4, // Protocol ID length 612 | 77, 81, 84, 84, // Protocol ID 613 | 4, // Protocol version 614 | 2, // Connect flags 615 | 0, 30, // Keepalive 616 | 0, 11, // Client ID length 617 | 197, 166, // Ŧ (UTF-8: 0xc5a6) 618 | 196, 151, // ė (UTF-8: 0xc497) 619 | 197, 155, // ś (utf-8: 0xc59b) 620 | 116, // t (utf-8: 0x74) 621 | 240, 159, 156, 132 // 🜄 (utf-8: 0xf09f9c84) 622 | ]), { 623 | cmd: 'connect', 624 | retain: false, 625 | qos: 0, 626 | dup: false, 627 | length: 23, 628 | topic: null, 629 | payload: null, 630 | protocolId: 'MQTT', 631 | protocolVersion: 4, 632 | clean: true, 633 | keepalive: 30, 634 | clientId: 'Ŧėśt🜄' 635 | }, { protocol: 5 }) 636 | 637 | testParseGenerateDefaults('default connect', { 638 | cmd: 'connect', 639 | clientId: 'test' 640 | }, Buffer.from([ 641 | 16, 16, 0, 4, 77, 81, 84, 642 | 84, 4, 2, 0, 0, 643 | 0, 4, 116, 101, 115, 116 644 | ]), { 645 | cmd: 'connect', 646 | retain: false, 647 | qos: 0, 648 | dup: false, 649 | length: 16, 650 | topic: null, 651 | payload: null, 652 | protocolId: 'MQTT', 653 | protocolVersion: 4, 654 | clean: true, 655 | keepalive: 0, 656 | clientId: 'test' 657 | }) 658 | 659 | testParseAndGenerate('Version 4 CONACK', { 660 | cmd: 'connack', 661 | retain: false, 662 | qos: 0, 663 | dup: false, 664 | length: 2, 665 | topic: null, 666 | payload: null, 667 | sessionPresent: false, 668 | returnCode: 1 669 | }, Buffer.from([ 670 | 32, 2, // Fixed Header (CONNACK, Remaining Length) 671 | 0, 1 // Variable Header (Session not present, Connection Refused - unacceptable protocol version) 672 | ]), {}) // Default protocolVersion (4) 673 | 674 | testParseAndGenerate('Version 5 CONACK', { 675 | cmd: 'connack', 676 | retain: false, 677 | qos: 0, 678 | dup: false, 679 | length: 3, 680 | topic: null, 681 | payload: null, 682 | sessionPresent: false, 683 | reasonCode: 140 684 | }, Buffer.from([ 685 | 32, 3, // Fixed Header (CONNACK, Remaining Length) 686 | 0, 140, // Variable Header (Session not present, Bad authentication method) 687 | 0 // Property Length Zero 688 | ]), { protocolVersion: 5 }) 689 | 690 | testParseOnly('Version 4 CONACK in Version 5 mode', { 691 | cmd: 'connack', 692 | retain: false, 693 | qos: 0, 694 | dup: false, 695 | length: 2, 696 | topic: null, 697 | payload: null, 698 | sessionPresent: false, 699 | reasonCode: 1 // a version 4 return code stored in the version 5 reasonCode because this client is in version 5 700 | }, Buffer.from([ 701 | 32, 2, // Fixed Header (CONNACK, Remaining Length) 702 | 0, 1 // Variable Header (Session not present, Connection Refused - unacceptable protocol version) 703 | ]), { protocolVersion: 5 }) // message is in version 4 format, but this client is in version 5 mode 704 | 705 | testParseOnly('Version 5 PUBACK test 1', { 706 | cmd: 'puback', 707 | messageId: 42, 708 | retain: false, 709 | qos: 0, 710 | dup: false, 711 | length: 2, 712 | topic: null, 713 | payload: null, 714 | reasonCode: 0 715 | }, Buffer.from([ 716 | 64, 2, // Fixed Header (PUBACK, Remaining Length) 717 | 0, 42 // Variable Header (2 Bytes: Packet Identifier 42, Implied Reason code: Success, Implied no properties) 718 | ]), { protocolVersion: 5 } 719 | ) 720 | 721 | testParseAndGenerate('Version 5 PUBACK test 2', { 722 | cmd: 'puback', 723 | messageId: 42, 724 | retain: false, 725 | qos: 0, 726 | dup: false, 727 | length: 2, 728 | topic: null, 729 | payload: null, 730 | reasonCode: 0 731 | }, Buffer.from([ 732 | 64, 2, // Fixed Header (PUBACK, Remaining Length) 733 | 0, 42 // Variable Header (2 Bytes: Packet Identifier 42, Implied reason code: 0 Success, Implied no properties) 734 | ]), { protocolVersion: 5 } 735 | ) 736 | 737 | testParseOnly('Version 5 PUBACK test 2.1', { 738 | cmd: 'puback', 739 | messageId: 42, 740 | retain: false, 741 | qos: 0, 742 | dup: false, 743 | length: 3, 744 | topic: null, 745 | payload: null, 746 | reasonCode: 0 747 | }, Buffer.from([ 748 | 64, 3, // Fixed Header (PUBACK, Remaining Length) 749 | 0, 42, 0 // Variable Header (2 Bytes: Packet Identifier 42, Reason code: 0 Success, Implied no properties) 750 | ]), { protocolVersion: 5 } 751 | ) 752 | 753 | testParseOnly('Version 5 PUBACK test 3', { 754 | cmd: 'puback', 755 | messageId: 42, 756 | retain: false, 757 | qos: 0, 758 | dup: false, 759 | length: 4, 760 | topic: null, 761 | payload: null, 762 | reasonCode: 0 763 | }, Buffer.from([ 764 | 64, 4, // Fixed Header (PUBACK, Remaining Length) 765 | 0, 42, 0, // Variable Header (2 Bytes: Packet Identifier 42, Reason code: 0 Success) 766 | 0 // no properties 767 | ]), { protocolVersion: 5 } 768 | ) 769 | 770 | testParseOnly('Version 5 CONNACK test 1', { 771 | cmd: 'connack', 772 | retain: false, 773 | qos: 0, 774 | dup: false, 775 | length: 1, 776 | topic: null, 777 | payload: null, 778 | sessionPresent: true, 779 | reasonCode: 0 780 | }, Buffer.from([ 781 | 32, 1, // Fixed Header (CONNACK, Remaining Length) 782 | 1 // Variable Header (Session Present: 1 => true, Implied Reason code: Success, Implied no properties) 783 | ]), { protocolVersion: 5 } 784 | ) 785 | 786 | testParseOnly('Version 5 CONNACK test 2', { 787 | cmd: 'connack', 788 | retain: false, 789 | qos: 0, 790 | dup: false, 791 | length: 2, 792 | topic: null, 793 | payload: null, 794 | sessionPresent: true, 795 | reasonCode: 0 796 | }, Buffer.from([ 797 | 32, 2, // Fixed Header (CONNACK, Remaining Length) 798 | 1, 0 // Variable Header (Session Present: 1 => true, Connect Reason code: Success, Implied no properties) 799 | ]), { protocolVersion: 5 } 800 | ) 801 | 802 | testParseAndGenerate('Version 5 CONNACK test 3', { 803 | cmd: 'connack', 804 | retain: false, 805 | qos: 0, 806 | dup: false, 807 | length: 3, 808 | topic: null, 809 | payload: null, 810 | sessionPresent: true, 811 | reasonCode: 0 812 | }, Buffer.from([ 813 | 32, 3, // Fixed Header (CONNACK, Remaining Length) 814 | 1, 0, // Variable Header (Session Present: 1 => true, Connect Reason code: Success) 815 | 0 // no properties 816 | ]), { protocolVersion: 5 } 817 | ) 818 | 819 | testParseOnly('Version 5 DISCONNECT test 1', { 820 | cmd: 'disconnect', 821 | retain: false, 822 | qos: 0, 823 | dup: false, 824 | length: 0, 825 | topic: null, 826 | payload: null, 827 | reasonCode: 0 828 | }, Buffer.from([ 829 | 224, 0 // Fixed Header (DISCONNECT, Remaining Length), Implied Reason code: Normal Disconnection 830 | ]), { protocolVersion: 5 } 831 | ) 832 | 833 | testParseOnly('Version 5 DISCONNECT test 2', { 834 | cmd: 'disconnect', 835 | retain: false, 836 | qos: 0, 837 | dup: false, 838 | length: 1, 839 | topic: null, 840 | payload: null, 841 | reasonCode: 0 842 | }, Buffer.from([ 843 | 224, 1, // Fixed Header (DISCONNECT, Remaining Length) 844 | 0 // reason Code (Normal disconnection) 845 | ]), { protocolVersion: 5 } 846 | ) 847 | 848 | testParseAndGenerate('Version 5 DISCONNECT test 3', { 849 | cmd: 'disconnect', 850 | retain: false, 851 | qos: 0, 852 | dup: false, 853 | length: 2, 854 | topic: null, 855 | payload: null, 856 | reasonCode: 0 857 | }, Buffer.from([ 858 | 224, 2, // Fixed Header (DISCONNECT, Remaining Length) 859 | 0, // reason Code (Normal disconnection) 860 | 0 // no properties 861 | ]), { protocolVersion: 5 } 862 | ) 863 | 864 | testParseGenerate('empty will payload', { 865 | cmd: 'connect', 866 | retain: false, 867 | qos: 0, 868 | dup: false, 869 | length: 47, 870 | protocolId: 'MQIsdp', 871 | protocolVersion: 3, 872 | will: { 873 | retain: true, 874 | qos: 2, 875 | topic: 'topic', 876 | payload: Buffer.alloc(0) 877 | }, 878 | clean: true, 879 | keepalive: 30, 880 | clientId: 'test', 881 | username: 'username', 882 | password: Buffer.from('password') 883 | }, Buffer.from([ 884 | 16, 47, // Header 885 | 0, 6, // Protocol ID length 886 | 77, 81, 73, 115, 100, 112, // Protocol ID 887 | 3, // Protocol version 888 | 246, // Connect flags 889 | 0, 30, // Keepalive 890 | 0, 4, // Client ID length 891 | 116, 101, 115, 116, // Client ID 892 | 0, 5, // Will topic length 893 | 116, 111, 112, 105, 99, // Will topic 894 | 0, 0, // Will payload length 895 | // Will payload 896 | 0, 8, // Username length 897 | 117, 115, 101, 114, 110, 97, 109, 101, // Username 898 | 0, 8, // Password length 899 | 112, 97, 115, 115, 119, 111, 114, 100 // Password 900 | ])) 901 | 902 | testParseGenerate('empty buffer username payload', { 903 | cmd: 'connect', 904 | retain: false, 905 | qos: 0, 906 | dup: false, 907 | length: 20, 908 | protocolId: 'MQIsdp', 909 | protocolVersion: 3, 910 | clean: true, 911 | keepalive: 30, 912 | clientId: 'test', 913 | username: Buffer.from('') 914 | }, Buffer.from([ 915 | 16, 20, // Header 916 | 0, 6, // Protocol ID length 917 | 77, 81, 73, 115, 100, 112, // Protocol ID 918 | 3, // Protocol version 919 | 130, // Connect flags 920 | 0, 30, // Keepalive 921 | 0, 4, // Client ID length 922 | 116, 101, 115, 116, // Client ID 923 | 0, 0 // Username length 924 | // Empty Username payload 925 | ])) 926 | 927 | testParseGenerate('empty string username payload', { 928 | cmd: 'connect', 929 | retain: false, 930 | qos: 0, 931 | dup: false, 932 | length: 20, 933 | protocolId: 'MQIsdp', 934 | protocolVersion: 3, 935 | clean: true, 936 | keepalive: 30, 937 | clientId: 'test', 938 | username: '' 939 | }, Buffer.from([ 940 | 16, 20, // Header 941 | 0, 6, // Protocol ID length 942 | 77, 81, 73, 115, 100, 112, // Protocol ID 943 | 3, // Protocol version 944 | 130, // Connect flags 945 | 0, 30, // Keepalive 946 | 0, 4, // Client ID length 947 | 116, 101, 115, 116, // Client ID 948 | 0, 0 // Username length 949 | // Empty Username payload 950 | ])) 951 | 952 | testParseGenerate('empty buffer password payload', { 953 | cmd: 'connect', 954 | retain: false, 955 | qos: 0, 956 | dup: false, 957 | length: 30, 958 | protocolId: 'MQIsdp', 959 | protocolVersion: 3, 960 | clean: true, 961 | keepalive: 30, 962 | clientId: 'test', 963 | username: 'username', 964 | password: Buffer.from('') 965 | }, Buffer.from([ 966 | 16, 30, // Header 967 | 0, 6, // Protocol ID length 968 | 77, 81, 73, 115, 100, 112, // Protocol ID 969 | 3, // Protocol version 970 | 194, // Connect flags 971 | 0, 30, // Keepalive 972 | 0, 4, // Client ID length 973 | 116, 101, 115, 116, // Client ID 974 | 0, 8, // Username length 975 | 117, 115, 101, 114, 110, 97, 109, 101, // Username payload 976 | 0, 0 // Password length 977 | // Empty password payload 978 | ])) 979 | 980 | testParseGenerate('empty string password payload', { 981 | cmd: 'connect', 982 | retain: false, 983 | qos: 0, 984 | dup: false, 985 | length: 30, 986 | protocolId: 'MQIsdp', 987 | protocolVersion: 3, 988 | clean: true, 989 | keepalive: 30, 990 | clientId: 'test', 991 | username: 'username', 992 | password: '' 993 | }, Buffer.from([ 994 | 16, 30, // Header 995 | 0, 6, // Protocol ID length 996 | 77, 81, 73, 115, 100, 112, // Protocol ID 997 | 3, // Protocol version 998 | 194, // Connect flags 999 | 0, 30, // Keepalive 1000 | 0, 4, // Client ID length 1001 | 116, 101, 115, 116, // Client ID 1002 | 0, 8, // Username length 1003 | 117, 115, 101, 114, 110, 97, 109, 101, // Username payload 1004 | 0, 0 // Password length 1005 | // Empty password payload 1006 | ])) 1007 | 1008 | testParseGenerate('empty string username and password payload', { 1009 | cmd: 'connect', 1010 | retain: false, 1011 | qos: 0, 1012 | dup: false, 1013 | length: 22, 1014 | protocolId: 'MQIsdp', 1015 | protocolVersion: 3, 1016 | clean: true, 1017 | keepalive: 30, 1018 | clientId: 'test', 1019 | username: '', 1020 | password: Buffer.from('') 1021 | }, Buffer.from([ 1022 | 16, 22, // Header 1023 | 0, 6, // Protocol ID length 1024 | 77, 81, 73, 115, 100, 112, // Protocol ID 1025 | 3, // Protocol version 1026 | 194, // Connect flags 1027 | 0, 30, // Keepalive 1028 | 0, 4, // Client ID length 1029 | 116, 101, 115, 116, // Client ID 1030 | 0, 0, // Username length 1031 | // Empty Username payload 1032 | 0, 0 // Password length 1033 | // Empty password payload 1034 | ])) 1035 | 1036 | testParseGenerate('maximal connect', { 1037 | cmd: 'connect', 1038 | retain: false, 1039 | qos: 0, 1040 | dup: false, 1041 | length: 54, 1042 | protocolId: 'MQIsdp', 1043 | protocolVersion: 3, 1044 | will: { 1045 | retain: true, 1046 | qos: 2, 1047 | topic: 'topic', 1048 | payload: Buffer.from('payload') 1049 | }, 1050 | clean: true, 1051 | keepalive: 30, 1052 | clientId: 'test', 1053 | username: 'username', 1054 | password: Buffer.from('password') 1055 | }, Buffer.from([ 1056 | 16, 54, // Header 1057 | 0, 6, // Protocol ID length 1058 | 77, 81, 73, 115, 100, 112, // Protocol ID 1059 | 3, // Protocol version 1060 | 246, // Connect flags 1061 | 0, 30, // Keepalive 1062 | 0, 4, // Client ID length 1063 | 116, 101, 115, 116, // Client ID 1064 | 0, 5, // Will topic length 1065 | 116, 111, 112, 105, 99, // Will topic 1066 | 0, 7, // Will payload length 1067 | 112, 97, 121, 108, 111, 97, 100, // Will payload 1068 | 0, 8, // Username length 1069 | 117, 115, 101, 114, 110, 97, 109, 101, // Username 1070 | 0, 8, // Password length 1071 | 112, 97, 115, 115, 119, 111, 114, 100 // Password 1072 | ])) 1073 | 1074 | testParseGenerate('max connect with special chars', { 1075 | cmd: 'connect', 1076 | retain: false, 1077 | qos: 0, 1078 | dup: false, 1079 | length: 57, 1080 | protocolId: 'MQIsdp', 1081 | protocolVersion: 3, 1082 | will: { 1083 | retain: true, 1084 | qos: 2, 1085 | topic: 'tòpic', 1086 | payload: Buffer.from('pay£oad') 1087 | }, 1088 | clean: true, 1089 | keepalive: 30, 1090 | clientId: 'te$t', 1091 | username: 'u$ern4me', 1092 | password: Buffer.from('p4$$w0£d') 1093 | }, Buffer.from([ 1094 | 16, 57, // Header 1095 | 0, 6, // Protocol ID length 1096 | 77, 81, 73, 115, 100, 112, // Protocol ID 1097 | 3, // Protocol version 1098 | 246, // Connect flags 1099 | 0, 30, // Keepalive 1100 | 0, 4, // Client ID length 1101 | 116, 101, 36, 116, // Client ID 1102 | 0, 6, // Will topic length 1103 | 116, 195, 178, 112, 105, 99, // Will topic 1104 | 0, 8, // Will payload length 1105 | 112, 97, 121, 194, 163, 111, 97, 100, // Will payload 1106 | 0, 8, // Username length 1107 | 117, 36, 101, 114, 110, 52, 109, 101, // Username 1108 | 0, 9, // Password length 1109 | 112, 52, 36, 36, 119, 48, 194, 163, 100 // Password 1110 | ])) 1111 | 1112 | testGenerateOnly('connect all strings generate', { 1113 | cmd: 'connect', 1114 | retain: false, 1115 | qos: 0, 1116 | dup: false, 1117 | length: 54, 1118 | protocolId: 'MQIsdp', 1119 | protocolVersion: 3, 1120 | will: { 1121 | retain: true, 1122 | qos: 2, 1123 | topic: 'topic', 1124 | payload: 'payload' 1125 | }, 1126 | clean: true, 1127 | keepalive: 30, 1128 | clientId: 'test', 1129 | username: 'username', 1130 | password: 'password' 1131 | }, Buffer.from([ 1132 | 16, 54, // Header 1133 | 0, 6, // Protocol ID length 1134 | 77, 81, 73, 115, 100, 112, // Protocol ID 1135 | 3, // Protocol version 1136 | 246, // Connect flags 1137 | 0, 30, // Keepalive 1138 | 0, 4, // Client ID length 1139 | 116, 101, 115, 116, // Client ID 1140 | 0, 5, // Will topic length 1141 | 116, 111, 112, 105, 99, // Will topic 1142 | 0, 7, // Will payload length 1143 | 112, 97, 121, 108, 111, 97, 100, // Will payload 1144 | 0, 8, // Username length 1145 | 117, 115, 101, 114, 110, 97, 109, 101, // Username 1146 | 0, 8, // Password length 1147 | 112, 97, 115, 115, 119, 111, 114, 100 // Password 1148 | ])) 1149 | 1150 | testParseError('Cannot parse protocolId', Buffer.from([ 1151 | 16, 4, 1152 | 0, 6, 1153 | 77, 81 1154 | ])) 1155 | 1156 | // missing protocol version on connect 1157 | testParseError('Packet too short', Buffer.from([ 1158 | 16, 8, // Header 1159 | 0, 6, // Protocol ID length 1160 | 77, 81, 73, 115, 100, 112 // Protocol ID 1161 | ])) 1162 | 1163 | // missing keepalive on connect 1164 | testParseError('Packet too short', Buffer.from([ 1165 | 16, 10, // Header 1166 | 0, 6, // Protocol ID length 1167 | 77, 81, 73, 115, 100, 112, // Protocol ID 1168 | 3, // Protocol version 1169 | 246 // Connect flags 1170 | ])) 1171 | 1172 | // missing clientid on connect 1173 | testParseError('Packet too short', Buffer.from([ 1174 | 16, 10, // Header 1175 | 0, 6, // Protocol ID length 1176 | 77, 81, 73, 115, 100, 112, // Protocol ID 1177 | 3, // Protocol version 1178 | 246, // Connect flags 1179 | 0, 30 // Keepalive 1180 | ])) 1181 | 1182 | // missing will topic on connect 1183 | testParseError('Cannot parse will topic', Buffer.from([ 1184 | 16, 16, // Header 1185 | 0, 6, // Protocol ID length 1186 | 77, 81, 73, 115, 100, 112, // Protocol ID 1187 | 3, // Protocol version 1188 | 246, // Connect flags 1189 | 0, 30, // Keepalive 1190 | 0, 2, // Will topic length 1191 | 0, 0 // Will topic 1192 | ])) 1193 | 1194 | // missing will payload on connect 1195 | testParseError('Cannot parse will payload', Buffer.from([ 1196 | 16, 23, // Header 1197 | 0, 6, // Protocol ID length 1198 | 77, 81, 73, 115, 100, 112, // Protocol ID 1199 | 3, // Protocol version 1200 | 246, // Connect flags 1201 | 0, 30, // Keepalive 1202 | 0, 5, // Will topic length 1203 | 116, 111, 112, 105, 99, // Will topic 1204 | 0, 2, // Will payload length 1205 | 0, 0 // Will payload 1206 | ])) 1207 | 1208 | // missing username on connect 1209 | testParseError('Cannot parse username', Buffer.from([ 1210 | 16, 32, // Header 1211 | 0, 6, // Protocol ID length 1212 | 77, 81, 73, 115, 100, 112, // Protocol ID 1213 | 3, // Protocol version 1214 | 246, // Connect flags 1215 | 0, 30, // Keepalive 1216 | 0, 5, // Will topic length 1217 | 116, 111, 112, 105, 99, // Will topic 1218 | 0, 7, // Will payload length 1219 | 112, 97, 121, 108, 111, 97, 100, // Will payload 1220 | 0, 2, // Username length 1221 | 0, 0 // Username 1222 | ])) 1223 | 1224 | // missing password on connect 1225 | testParseError('Cannot parse password', Buffer.from([ 1226 | 16, 42, // Header 1227 | 0, 6, // Protocol ID length 1228 | 77, 81, 73, 115, 100, 112, // Protocol ID 1229 | 3, // Protocol version 1230 | 246, // Connect flags 1231 | 0, 30, // Keepalive 1232 | 0, 5, // Will topic length 1233 | 116, 111, 112, 105, 99, // Will topic 1234 | 0, 7, // Will payload length 1235 | 112, 97, 121, 108, 111, 97, 100, // Will payload 1236 | 0, 8, // Username length 1237 | 117, 115, 101, 114, 110, 97, 109, 101, // Username 1238 | 0, 2, // Password length 1239 | 0, 0 // Password 1240 | ])) 1241 | 1242 | // 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] 1243 | testParseError('Invalid header flag bits, must be 0x0 for connect packet', Buffer.from([ 1244 | 18, 10, // Header 1245 | 0, 4, // Protocol ID length 1246 | 0x4d, 0x51, 0x54, 0x54, // Protocol ID 1247 | 3, // Protocol version 1248 | 2, // Connect flags 1249 | 0, 30 // Keepalive 1250 | ])) 1251 | 1252 | // 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] 1253 | testParseError('Connect flag bit 0 must be 0, but got 1', Buffer.from([ 1254 | 16, 10, // Header 1255 | 0, 4, // Protocol ID length 1256 | 0x4d, 0x51, 0x54, 0x54, // Protocol ID 1257 | 3, // Protocol version 1258 | 3, // Connect flags 1259 | 0, 30 // Keepalive 1260 | ])) 1261 | 1262 | // 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]. 1263 | testParseError('Will Retain Flag must be set to zero when Will Flag is set to 0', Buffer.from([ 1264 | 16, 10, // Header 1265 | 0, 4, // Protocol ID length 1266 | 0x4d, 0x51, 0x54, 0x54, // Protocol ID 1267 | 3, // Protocol version 1268 | 0x22, // Connect flags 1269 | 0, 30 // Keepalive 1270 | ])) 1271 | 1272 | // 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]. 1273 | testParseError('Will QoS must be set to zero when Will Flag is set to 0', Buffer.from([ 1274 | 16, 10, // Header 1275 | 0, 4, // Protocol ID length 1276 | 0x4d, 0x51, 0x54, 0x54, // Protocol ID 1277 | 3, // Protocol version 1278 | 0x12, // Connect flags 1279 | 0, 30 // Keepalive 1280 | ])) 1281 | 1282 | // 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]. 1283 | testParseError('Will QoS must be set to zero when Will Flag is set to 0', Buffer.from([ 1284 | 16, 10, // Header 1285 | 0, 4, // Protocol ID length 1286 | 0x4d, 0x51, 0x54, 0x54, // Protocol ID 1287 | 3, // Protocol version 1288 | 0xa, // Connect flags 1289 | 0, 30 // Keepalive 1290 | ])) 1291 | 1292 | // CONNECT, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK (v.5) packets must have payload 1293 | // CONNECT 1294 | testParseError('Packet too short', Buffer.from([ 1295 | 16, // Header 1296 | 8, // Packet length 1297 | 0, 4, // Protocol ID length 1298 | 77, 81, 84, 84, // MQTT 1299 | 5, // Version 1300 | 2, // Clean Start enabled 1301 | 0, 0, // Keep-Alive 1302 | 0, // Property Length 1303 | 0, 0 // Properties 1304 | // No payload 1305 | ]), { protocolVersion: 5 }) 1306 | // SUBSCRIBE 1307 | testParseError('Malformed subscribe, no payload specified', Buffer.from([ 1308 | 130, // Header 1309 | 0 // Packet length 1310 | ]), { protocolVersion: 5 }) 1311 | // SUBACK 1312 | testParseError('Malformed suback, no payload specified', Buffer.from([ 1313 | 144, // Header 1314 | 0 // Packet length 1315 | ]), { protocolVersion: 5 }) 1316 | // UNSUBSCRIBE 1317 | testParseError('Malformed unsubscribe, no payload specified', Buffer.from([ 1318 | 162, // Header 1319 | 0 // Packet length 1320 | ]), { protocolVersion: 5 }) 1321 | // UNSUBACK (v.5) 1322 | testParseError('Malformed unsuback, no payload specified', Buffer.from([ 1323 | 176, // Header 1324 | 0 // Packet length 1325 | ]), { protocolVersion: 5 }) 1326 | // UNSUBACK (v.4) 1327 | testParseError('Malformed unsuback, payload length must be 2', Buffer.from([ 1328 | 176, // Header 1329 | 1, // Packet length 1330 | 1 1331 | ]), { protocolVersion: 4 }) 1332 | // UNSUBACK (v.3) 1333 | testParseError('Malformed unsuback, payload length must be 2', Buffer.from([ 1334 | 176, // Header 1335 | 1, // Packet length 1336 | 1 1337 | ]), { protocolVersion: 3 }) 1338 | 1339 | testParseGenerate('connack with return code 0', { 1340 | cmd: 'connack', 1341 | retain: false, 1342 | qos: 0, 1343 | dup: false, 1344 | length: 2, 1345 | sessionPresent: false, 1346 | returnCode: 0 1347 | }, Buffer.from([ 1348 | 32, 2, 0, 0 1349 | ])) 1350 | 1351 | testParseGenerate('connack MQTT 5 with properties', { 1352 | cmd: 'connack', 1353 | retain: false, 1354 | qos: 0, 1355 | dup: false, 1356 | length: 87, 1357 | sessionPresent: false, 1358 | reasonCode: 0, 1359 | properties: { 1360 | sessionExpiryInterval: 1234, 1361 | receiveMaximum: 432, 1362 | maximumQoS: 2, 1363 | retainAvailable: true, 1364 | maximumPacketSize: 100, 1365 | assignedClientIdentifier: 'test', 1366 | topicAliasMaximum: 456, 1367 | reasonString: 'test', 1368 | userProperties: { 1369 | test: 'test' 1370 | }, 1371 | wildcardSubscriptionAvailable: true, 1372 | subscriptionIdentifiersAvailable: true, 1373 | sharedSubscriptionAvailable: false, 1374 | serverKeepAlive: 1234, 1375 | responseInformation: 'test', 1376 | serverReference: 'test', 1377 | authenticationMethod: 'test', 1378 | authenticationData: Buffer.from([1, 2, 3, 4]) 1379 | } 1380 | }, Buffer.from([ 1381 | 32, 87, 0, 0, 1382 | 84, // properties length 1383 | 17, 0, 0, 4, 210, // sessionExpiryInterval 1384 | 33, 1, 176, // receiveMaximum 1385 | 36, 2, // Maximum qos 1386 | 37, 1, // retainAvailable 1387 | 39, 0, 0, 0, 100, // maximumPacketSize 1388 | 18, 0, 4, 116, 101, 115, 116, // assignedClientIdentifier 1389 | 34, 1, 200, // topicAliasMaximum 1390 | 31, 0, 4, 116, 101, 115, 116, // reasonString 1391 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 1392 | 40, 1, // wildcardSubscriptionAvailable 1393 | 41, 1, // subscriptionIdentifiersAvailable 1394 | 42, 0, // sharedSubscriptionAvailable 1395 | 19, 4, 210, // serverKeepAlive 1396 | 26, 0, 4, 116, 101, 115, 116, // responseInformation 1397 | 28, 0, 4, 116, 101, 115, 116, // serverReference 1398 | 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 1399 | 22, 0, 4, 1, 2, 3, 4 // authenticationData 1400 | ]), { protocolVersion: 5 }) 1401 | 1402 | testParseGenerate('connack MQTT 5 with properties and doubled user properties', { 1403 | cmd: 'connack', 1404 | retain: false, 1405 | qos: 0, 1406 | dup: false, 1407 | length: 100, 1408 | sessionPresent: false, 1409 | reasonCode: 0, 1410 | properties: { 1411 | sessionExpiryInterval: 1234, 1412 | receiveMaximum: 432, 1413 | maximumQoS: 2, 1414 | retainAvailable: true, 1415 | maximumPacketSize: 100, 1416 | assignedClientIdentifier: 'test', 1417 | topicAliasMaximum: 456, 1418 | reasonString: 'test', 1419 | userProperties: { 1420 | test: ['test', 'test'] 1421 | }, 1422 | wildcardSubscriptionAvailable: true, 1423 | subscriptionIdentifiersAvailable: true, 1424 | sharedSubscriptionAvailable: false, 1425 | serverKeepAlive: 1234, 1426 | responseInformation: 'test', 1427 | serverReference: 'test', 1428 | authenticationMethod: 'test', 1429 | authenticationData: Buffer.from([1, 2, 3, 4]) 1430 | } 1431 | }, Buffer.from([ 1432 | 32, 100, 0, 0, 1433 | 97, // properties length 1434 | 17, 0, 0, 4, 210, // sessionExpiryInterval 1435 | 33, 1, 176, // receiveMaximum 1436 | 36, 2, // Maximum qos 1437 | 37, 1, // retainAvailable 1438 | 39, 0, 0, 0, 100, // maximumPacketSize 1439 | 18, 0, 4, 116, 101, 115, 116, // assignedClientIdentifier 1440 | 34, 1, 200, // topicAliasMaximum 1441 | 31, 0, 4, 116, 101, 115, 116, // reasonString 1442 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, 1443 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 1444 | 40, 1, // wildcardSubscriptionAvailable 1445 | 41, 1, // subscriptionIdentifiersAvailable 1446 | 42, 0, // sharedSubscriptionAvailable 1447 | 19, 4, 210, // serverKeepAlive 1448 | 26, 0, 4, 116, 101, 115, 116, // responseInformation 1449 | 28, 0, 4, 116, 101, 115, 116, // serverReference 1450 | 21, 0, 4, 116, 101, 115, 116, // authenticationMethod 1451 | 22, 0, 4, 1, 2, 3, 4 // authenticationData 1452 | ]), { protocolVersion: 5 }) 1453 | 1454 | testParseGenerate('connack with return code 0 session present bit set', { 1455 | cmd: 'connack', 1456 | retain: false, 1457 | qos: 0, 1458 | dup: false, 1459 | length: 2, 1460 | sessionPresent: true, 1461 | returnCode: 0 1462 | }, Buffer.from([ 1463 | 32, 2, 1, 0 1464 | ])) 1465 | 1466 | testParseGenerate('connack with return code 5', { 1467 | cmd: 'connack', 1468 | retain: false, 1469 | qos: 0, 1470 | dup: false, 1471 | length: 2, 1472 | sessionPresent: false, 1473 | returnCode: 5 1474 | }, Buffer.from([ 1475 | 32, 2, 0, 5 1476 | ])) 1477 | 1478 | // 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] 1479 | testParseError('Invalid header flag bits, must be 0x0 for connack packet', Buffer.from([ 1480 | 33, 2, // header 1481 | 0, // flags 1482 | 5 // return code 1483 | ])) 1484 | 1485 | // Byte 1 is the "Connect Acknowledge Flags". Bits 7-1 are reserved and MUST be set to 0 [MQTT-3.2.2-1]. 1486 | testParseError('Invalid connack flags, bits 7-1 must be set to 0', Buffer.from([ 1487 | 32, 2, // header 1488 | 2, // flags 1489 | 5 // return code 1490 | ])) 1491 | 1492 | testGenerateError('Invalid return code', { 1493 | cmd: 'connack', 1494 | retain: false, 1495 | qos: 0, 1496 | dup: false, 1497 | length: 2, 1498 | sessionPresent: false, 1499 | returnCode: '5' // returncode must be a number 1500 | }) 1501 | 1502 | testParseGenerate('minimal publish', { 1503 | cmd: 'publish', 1504 | retain: false, 1505 | qos: 0, 1506 | dup: false, 1507 | length: 10, 1508 | topic: 'test', 1509 | payload: Buffer.from('test') 1510 | }, Buffer.from([ 1511 | 48, 10, // Header 1512 | 0, 4, // Topic length 1513 | 116, 101, 115, 116, // Topic (test) 1514 | 116, 101, 115, 116 // Payload (test) 1515 | ])) 1516 | 1517 | testParseGenerate('publish MQTT 5 properties', { 1518 | cmd: 'publish', 1519 | retain: true, 1520 | qos: 2, 1521 | dup: true, 1522 | length: 86, 1523 | topic: 'test', 1524 | payload: Buffer.from('test'), 1525 | messageId: 10, 1526 | properties: { 1527 | payloadFormatIndicator: true, 1528 | messageExpiryInterval: 4321, 1529 | topicAlias: 100, 1530 | responseTopic: 'topic', 1531 | correlationData: Buffer.from([1, 2, 3, 4]), 1532 | userProperties: { 1533 | test: ['test', 'test', 'test'] 1534 | }, 1535 | subscriptionIdentifier: 120, 1536 | contentType: 'test' 1537 | } 1538 | }, Buffer.from([ 1539 | 61, 86, // Header 1540 | 0, 4, // Topic length 1541 | 116, 101, 115, 116, // Topic (test) 1542 | 0, 10, // Message ID 1543 | 73, // properties length 1544 | 1, 1, // payloadFormatIndicator 1545 | 2, 0, 0, 16, 225, // message expiry interval 1546 | 35, 0, 100, // topicAlias 1547 | 8, 0, 5, 116, 111, 112, 105, 99, // response topic 1548 | 9, 0, 4, 1, 2, 3, 4, // correlationData 1549 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 1550 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 1551 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 1552 | 11, 120, // subscriptionIdentifier 1553 | 3, 0, 4, 116, 101, 115, 116, // content type 1554 | 116, 101, 115, 116 // Payload (test) 1555 | ]), { protocolVersion: 5 }) 1556 | 1557 | testParseGenerate('publish MQTT 5 with multiple same properties', { 1558 | cmd: 'publish', 1559 | retain: true, 1560 | qos: 2, 1561 | dup: true, 1562 | length: 64, 1563 | topic: 'test', 1564 | payload: Buffer.from('test'), 1565 | messageId: 10, 1566 | properties: { 1567 | payloadFormatIndicator: true, 1568 | messageExpiryInterval: 4321, 1569 | topicAlias: 100, 1570 | responseTopic: 'topic', 1571 | correlationData: Buffer.from([1, 2, 3, 4]), 1572 | userProperties: { 1573 | test: 'test' 1574 | }, 1575 | subscriptionIdentifier: [120, 121, 122], 1576 | contentType: 'test' 1577 | } 1578 | }, Buffer.from([ 1579 | 61, 64, // Header 1580 | 0, 4, // Topic length 1581 | 116, 101, 115, 116, // Topic (test) 1582 | 0, 10, // Message ID 1583 | 51, // properties length 1584 | 1, 1, // payloadFormatIndicator 1585 | 2, 0, 0, 16, 225, // message expiry interval 1586 | 35, 0, 100, // topicAlias 1587 | 8, 0, 5, 116, 111, 112, 105, 99, // response topic 1588 | 9, 0, 4, 1, 2, 3, 4, // correlationData 1589 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 1590 | 11, 120, // subscriptionIdentifier 1591 | 11, 121, // subscriptionIdentifier 1592 | 11, 122, // subscriptionIdentifier 1593 | 3, 0, 4, 116, 101, 115, 116, // content type 1594 | 116, 101, 115, 116 // Payload (test) 1595 | ]), { protocolVersion: 5 }) 1596 | 1597 | testParseGenerate('publish MQTT 5 properties with 0-4 byte varbyte', { 1598 | cmd: 'publish', 1599 | retain: true, 1600 | qos: 2, 1601 | dup: true, 1602 | length: 27, 1603 | topic: 'test', 1604 | payload: Buffer.from('test'), 1605 | messageId: 10, 1606 | properties: { 1607 | payloadFormatIndicator: false, 1608 | subscriptionIdentifier: [128, 16384, 2097152] // this tests the varbyte handling 1609 | } 1610 | }, Buffer.from([ 1611 | 61, 27, // Header 1612 | 0, 4, // Topic length 1613 | 116, 101, 115, 116, // Topic (test) 1614 | 0, 10, // Message ID 1615 | 14, // properties length 1616 | 1, 0, // payloadFormatIndicator 1617 | 11, 128, 1, // subscriptionIdentifier 1618 | 11, 128, 128, 1, // subscriptionIdentifier 1619 | 11, 128, 128, 128, 1, // subscriptionIdentifier 1620 | 116, 101, 115, 116 // Payload (test) 1621 | ]), { protocolVersion: 5 }) 1622 | 1623 | testParseGenerate('publish MQTT 5 properties with max value varbyte', { 1624 | cmd: 'publish', 1625 | retain: true, 1626 | qos: 2, 1627 | dup: true, 1628 | length: 22, 1629 | topic: 'test', 1630 | payload: Buffer.from('test'), 1631 | messageId: 10, 1632 | properties: { 1633 | payloadFormatIndicator: false, 1634 | subscriptionIdentifier: [1, 268435455] 1635 | } 1636 | }, Buffer.from([ 1637 | 61, 22, // Header 1638 | 0, 4, // Topic length 1639 | 116, 101, 115, 116, // Topic (test) 1640 | 0, 10, // Message ID 1641 | 9, // properties length 1642 | 1, 0, // payloadFormatIndicator 1643 | 11, 1, // subscriptionIdentifier 1644 | 11, 255, 255, 255, 127, // subscriptionIdentifier (max value) 1645 | 116, 101, 115, 116 // Payload (test) 1646 | ]), { protocolVersion: 5 }) 1647 | 1648 | ; (() => { 1649 | const buffer = Buffer.alloc(2048) 1650 | testParseGenerate('2KB publish packet', { 1651 | cmd: 'publish', 1652 | retain: false, 1653 | qos: 0, 1654 | dup: false, 1655 | length: 2054, 1656 | topic: 'test', 1657 | payload: buffer 1658 | }, Buffer.concat([Buffer.from([ 1659 | 48, 134, 16, // Header 1660 | 0, 4, // Topic length 1661 | 116, 101, 115, 116 // Topic (test) 1662 | ]), buffer])) 1663 | })() 1664 | 1665 | ; (() => { 1666 | const maxLength = 268435455 1667 | const buffer = Buffer.alloc(maxLength - 6) 1668 | testParseGenerate('Max payload publish packet', { 1669 | cmd: 'publish', 1670 | retain: false, 1671 | qos: 0, 1672 | dup: false, 1673 | length: maxLength, 1674 | topic: 'test', 1675 | payload: buffer 1676 | }, Buffer.concat([Buffer.from([ 1677 | 48, 255, 255, 255, 127, // Header 1678 | 0, 4, // Topic length 1679 | 116, 101, 115, 116 // Topic (test) 1680 | ]), buffer])) 1681 | })() 1682 | 1683 | testParseGenerate('maximal publish', { 1684 | cmd: 'publish', 1685 | retain: true, 1686 | qos: 2, 1687 | length: 12, 1688 | dup: true, 1689 | topic: 'test', 1690 | messageId: 10, 1691 | payload: Buffer.from('test') 1692 | }, Buffer.from([ 1693 | 61, 12, // Header 1694 | 0, 4, // Topic length 1695 | 116, 101, 115, 116, // Topic 1696 | 0, 10, // Message ID 1697 | 116, 101, 115, 116 // Payload 1698 | ])) 1699 | 1700 | test('publish all strings generate', t => { 1701 | const message = { 1702 | cmd: 'publish', 1703 | retain: true, 1704 | qos: 2, 1705 | length: 12, 1706 | dup: true, 1707 | topic: 'test', 1708 | messageId: 10, 1709 | payload: Buffer.from('test') 1710 | } 1711 | const expected = Buffer.from([ 1712 | 61, 12, // Header 1713 | 0, 4, // Topic length 1714 | 116, 101, 115, 116, // Topic 1715 | 0, 10, // Message ID 1716 | 116, 101, 115, 116 // Payload 1717 | ]) 1718 | 1719 | t.equal(mqtt.generate(message).toString('hex'), expected.toString('hex')) 1720 | t.end() 1721 | }) 1722 | 1723 | testParseGenerate('empty publish', { 1724 | cmd: 'publish', 1725 | retain: false, 1726 | qos: 0, 1727 | dup: false, 1728 | length: 6, 1729 | topic: 'test', 1730 | payload: Buffer.alloc(0) 1731 | }, Buffer.from([ 1732 | 48, 6, // Header 1733 | 0, 4, // Topic length 1734 | 116, 101, 115, 116 // Topic 1735 | // Empty payload 1736 | ])) 1737 | 1738 | // 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]. 1739 | testParseError('Packet must not have both QoS bits set to 1', Buffer.from([ 1740 | 0x36, 6, // Header 1741 | 0, 4, // Topic length 1742 | 116, 101, 115, 116 // Topic 1743 | // Empty payload 1744 | ])) 1745 | 1746 | test('splitted publish parse', t => { 1747 | t.plan(3) 1748 | 1749 | const parser = mqtt.parser() 1750 | const expected = { 1751 | cmd: 'publish', 1752 | retain: false, 1753 | qos: 0, 1754 | dup: false, 1755 | length: 10, 1756 | topic: 'test', 1757 | payload: Buffer.from('test') 1758 | } 1759 | 1760 | parser.on('packet', packet => { 1761 | t.deepLooseEqual(packet, expected, 'expected packet') 1762 | }) 1763 | 1764 | t.equal(parser.parse(Buffer.from([ 1765 | 48, 10, // Header 1766 | 0, 4, // Topic length 1767 | 116, 101, 115, 116 // Topic (test) 1768 | ])), 6, 'remaining bytes') 1769 | 1770 | t.equal(parser.parse(Buffer.from([ 1771 | 116, 101, 115, 116 // Payload (test) 1772 | ])), 0, 'remaining bytes') 1773 | }) 1774 | 1775 | test('split publish longer', t => { 1776 | t.plan(3) 1777 | 1778 | const length = 255 1779 | const topic = 'test' 1780 | // Minus two bytes for the topic length specifier 1781 | const payloadLength = length - topic.length - 2 1782 | 1783 | const parser = mqtt.parser() 1784 | const expected = { 1785 | cmd: 'publish', 1786 | retain: false, 1787 | qos: 0, 1788 | dup: false, 1789 | length, 1790 | topic, 1791 | payload: Buffer.from('a'.repeat(payloadLength)) 1792 | } 1793 | 1794 | parser.on('packet', packet => { 1795 | t.deepLooseEqual(packet, expected, 'expected packet') 1796 | }) 1797 | 1798 | t.equal(parser.parse(Buffer.from([ 1799 | 48, 255, 1, // Header 1800 | 0, topic.length, // Topic length 1801 | 116, 101, 115, 116 // Topic (test) 1802 | ])), 6, 'remaining bytes') 1803 | 1804 | t.equal(parser.parse(Buffer.from(Array(payloadLength).fill(97))), 1805 | 0, 'remaining bytes') 1806 | }) 1807 | 1808 | test('split length parse', t => { 1809 | t.plan(4) 1810 | 1811 | const length = 255 1812 | const topic = 'test' 1813 | const payloadLength = length - topic.length - 2 1814 | 1815 | const parser = mqtt.parser() 1816 | const expected = { 1817 | cmd: 'publish', 1818 | retain: false, 1819 | qos: 0, 1820 | dup: false, 1821 | length, 1822 | topic, 1823 | payload: Buffer.from('a'.repeat(payloadLength)) 1824 | } 1825 | 1826 | parser.on('packet', packet => { 1827 | t.deepLooseEqual(packet, expected, 'expected packet') 1828 | }) 1829 | 1830 | t.equal(parser.parse(Buffer.from([ 1831 | 48, 255 // Header (partial length) 1832 | ])), 1, 'remaining bytes') 1833 | 1834 | t.equal(parser.parse(Buffer.from([ 1835 | 1, // Rest of header length 1836 | 0, topic.length, // Topic length 1837 | 116, 101, 115, 116 // Topic (test) 1838 | ])), 6, 'remaining bytes') 1839 | 1840 | t.equal(parser.parse(Buffer.from(Array(payloadLength).fill(97))), 1841 | 0, 'remaining bytes') 1842 | }) 1843 | 1844 | testGenerateError('Invalid variable byte integer: 268435456', { 1845 | cmd: 'publish', 1846 | retain: false, 1847 | qos: 0, 1848 | dup: false, 1849 | length: (268435455 + 1), 1850 | topic: 'test', 1851 | payload: Buffer.alloc(268435455 + 1 - 6) 1852 | }, {}, 'Length var byte integer over max allowed value throws error') 1853 | 1854 | testGenerateError('Invalid subscriptionIdentifier: 268435456', { 1855 | cmd: 'publish', 1856 | retain: true, 1857 | qos: 2, 1858 | dup: true, 1859 | length: 27, 1860 | topic: 'test', 1861 | payload: Buffer.from('test'), 1862 | messageId: 10, 1863 | properties: { 1864 | payloadFormatIndicator: false, 1865 | subscriptionIdentifier: 268435456 1866 | } 1867 | }, { protocolVersion: 5 }, 'MQTT 5.0 var byte integer >24 bits throws error') 1868 | 1869 | testParseGenerate('puback', { 1870 | cmd: 'puback', 1871 | retain: false, 1872 | qos: 0, 1873 | dup: false, 1874 | length: 2, 1875 | messageId: 2 1876 | }, Buffer.from([ 1877 | 64, 2, // Header 1878 | 0, 2 // Message ID 1879 | ])) 1880 | 1881 | // 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] 1882 | testParseError('Invalid header flag bits, must be 0x0 for puback packet', Buffer.from([ 1883 | 65, 2, // Header 1884 | 0, 2 // Message ID 1885 | ])) 1886 | 1887 | testParseGenerate('puback without reason and no MQTT 5 properties', { 1888 | cmd: 'puback', 1889 | retain: false, 1890 | qos: 0, 1891 | dup: false, 1892 | length: 2, 1893 | messageId: 2, 1894 | reasonCode: 0 1895 | }, Buffer.from([ 1896 | 64, 2, // Header 1897 | 0, 2 // Message ID 1898 | ]), { protocolVersion: 5 }) 1899 | 1900 | testParseGenerate('puback with reason and no MQTT 5 properties', { 1901 | cmd: 'puback', 1902 | retain: false, 1903 | qos: 0, 1904 | dup: false, 1905 | length: 4, 1906 | messageId: 2, 1907 | reasonCode: 16 1908 | }, Buffer.from([ 1909 | 64, 4, // Header 1910 | 0, 2, // Message ID 1911 | 16, // reason code 1912 | 0 // no user properties 1913 | ]), { protocolVersion: 5 }) 1914 | 1915 | testParseGenerate('puback MQTT 5 properties', { 1916 | cmd: 'puback', 1917 | retain: false, 1918 | qos: 0, 1919 | dup: false, 1920 | length: 24, 1921 | messageId: 2, 1922 | reasonCode: 16, 1923 | properties: { 1924 | reasonString: 'test', 1925 | userProperties: { 1926 | test: 'test' 1927 | } 1928 | } 1929 | }, Buffer.from([ 1930 | 64, 24, // Header 1931 | 0, 2, // Message ID 1932 | 16, // reason code 1933 | 20, // properties length 1934 | 31, 0, 4, 116, 101, 115, 116, // reasonString 1935 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties 1936 | ]), { protocolVersion: 5 }) 1937 | 1938 | testParseError('Invalid puback reason code', Buffer.from([ 1939 | 64, 4, // Header 1940 | 0, 2, // Message ID 1941 | 0x11, // reason code 1942 | 0 // properties length 1943 | ]), { protocolVersion: 5 }) 1944 | 1945 | testParseGenerate('pubrec', { 1946 | cmd: 'pubrec', 1947 | retain: false, 1948 | qos: 0, 1949 | dup: false, 1950 | length: 2, 1951 | messageId: 2 1952 | }, Buffer.from([ 1953 | 80, 2, // Header 1954 | 0, 2 // Message ID 1955 | ])) 1956 | 1957 | // 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] 1958 | testParseError('Invalid header flag bits, must be 0x0 for pubrec packet', Buffer.from([ 1959 | 81, 2, // Header 1960 | 0, 2 // Message ID 1961 | ])) 1962 | 1963 | testParseGenerate('pubrec MQTT 5 properties', { 1964 | cmd: 'pubrec', 1965 | retain: false, 1966 | qos: 0, 1967 | dup: false, 1968 | length: 24, 1969 | messageId: 2, 1970 | reasonCode: 16, 1971 | properties: { 1972 | reasonString: 'test', 1973 | userProperties: { 1974 | test: 'test' 1975 | } 1976 | } 1977 | }, Buffer.from([ 1978 | 80, 24, // Header 1979 | 0, 2, // Message ID 1980 | 16, // reason code 1981 | 20, // properties length 1982 | 31, 0, 4, 116, 101, 115, 116, // reasonString 1983 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties 1984 | ]), { protocolVersion: 5 }) 1985 | 1986 | testParseGenerate('pubrel', { 1987 | cmd: 'pubrel', 1988 | retain: false, 1989 | qos: 1, 1990 | dup: false, 1991 | length: 2, 1992 | messageId: 2 1993 | }, Buffer.from([ 1994 | 98, 2, // Header 1995 | 0, 2 // Message ID 1996 | ])) 1997 | 1998 | testParseError('Invalid pubrel reason code', Buffer.from([ 1999 | 98, 4, // Header 2000 | 0, 2, // Message ID 2001 | 0x11, // Reason code 2002 | 0 // Properties length 2003 | ]), { protocolVersion: 5 }) 2004 | 2005 | // 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] 2006 | testParseError('Invalid header flag bits, must be 0x2 for pubrel packet', Buffer.from([ 2007 | 96, 2, // Header 2008 | 0, 2 // Message ID 2009 | ])) 2010 | 2011 | testParseGenerate('pubrel MQTT5 properties', { 2012 | cmd: 'pubrel', 2013 | retain: false, 2014 | qos: 1, 2015 | dup: false, 2016 | length: 24, 2017 | messageId: 2, 2018 | reasonCode: 0x92, 2019 | properties: { 2020 | reasonString: 'test', 2021 | userProperties: { 2022 | test: 'test' 2023 | } 2024 | } 2025 | }, Buffer.from([ 2026 | 98, 24, // Header 2027 | 0, 2, // Message ID 2028 | 0x92, // reason code 2029 | 20, // properties length 2030 | 31, 0, 4, 116, 101, 115, 116, // reasonString 2031 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties 2032 | ]), { protocolVersion: 5 }) 2033 | 2034 | testParseError('Invalid pubrel reason code', Buffer.from([ 2035 | 98, 4, // Header 2036 | 0, 2, // Message ID 2037 | 16, // reason code 2038 | 0 // properties length 2039 | ]), { protocolVersion: 5 }) 2040 | 2041 | testParseGenerate('pubcomp', { 2042 | cmd: 'pubcomp', 2043 | retain: false, 2044 | qos: 0, 2045 | dup: false, 2046 | length: 2, 2047 | messageId: 2 2048 | }, Buffer.from([ 2049 | 112, 2, // Header 2050 | 0, 2 // Message ID 2051 | ])) 2052 | 2053 | // 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] 2054 | testParseError('Invalid header flag bits, must be 0x0 for pubcomp packet', Buffer.from([ 2055 | 113, 2, // Header 2056 | 0, 2 // Message ID 2057 | ])) 2058 | 2059 | testParseGenerate('pubcomp MQTT 5 properties', { 2060 | cmd: 'pubcomp', 2061 | retain: false, 2062 | qos: 0, 2063 | dup: false, 2064 | length: 24, 2065 | messageId: 2, 2066 | reasonCode: 0x92, 2067 | properties: { 2068 | reasonString: 'test', 2069 | userProperties: { 2070 | test: 'test' 2071 | } 2072 | } 2073 | }, Buffer.from([ 2074 | 112, 24, // Header 2075 | 0, 2, // Message ID 2076 | 0x92, // reason code 2077 | 20, // properties length 2078 | 31, 0, 4, 116, 101, 115, 116, // reasonString 2079 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties 2080 | ]), { protocolVersion: 5 }) 2081 | 2082 | testParseError('Invalid pubcomp reason code', Buffer.from([ 2083 | 112, 4, // Header 2084 | 0, 2, // Message ID 2085 | 16, // reason code 2086 | 0 // properties length 2087 | ]), { protocolVersion: 5 }) 2088 | 2089 | testParseError('Invalid header flag bits, must be 0x2 for subscribe packet', Buffer.from([ 2090 | 128, 9, // Header (subscribeqos=0length=9) 2091 | 0, 6, // Message ID (6) 2092 | 0, 4, // Topic length, 2093 | 116, 101, 115, 116, // Topic (test) 2094 | 0 // Qos (0) 2095 | ])) 2096 | 2097 | testParseGenerate('subscribe to one topic', { 2098 | cmd: 'subscribe', 2099 | retain: false, 2100 | qos: 1, 2101 | dup: false, 2102 | length: 9, 2103 | subscriptions: [ 2104 | { 2105 | topic: 'test', 2106 | qos: 0 2107 | } 2108 | ], 2109 | messageId: 6 2110 | }, Buffer.from([ 2111 | 130, 9, // Header (subscribeqos=1length=9) 2112 | 0, 6, // Message ID (6) 2113 | 0, 4, // Topic length, 2114 | 116, 101, 115, 116, // Topic (test) 2115 | 0 // Qos (0) 2116 | ])) 2117 | 2118 | testParseError('Invalid subscribe QoS, must be <= 2', Buffer.from([ 2119 | 130, 9, // Header (subscribeqos=0length=9) 2120 | 0, 6, // Message ID (6) 2121 | 0, 4, // Topic length, 2122 | 116, 101, 115, 116, // Topic (test) 2123 | 3 // Qos 2124 | ])) 2125 | 2126 | testParseError('Invalid subscribe topic flag bits, bits 7-6 must be 0', Buffer.from([ 2127 | 130, 10, // Header (subscribeqos=0length=9) 2128 | 0, 6, // Message ID (6) 2129 | 0, // Property length (0) 2130 | 0, 4, // Topic length, 2131 | 116, 101, 115, 116, // Topic (test) 2132 | 0x80 // Flags 2133 | ]), { protocolVersion: 5 }) 2134 | 2135 | testParseError('Invalid retain handling, must be <= 2', Buffer.from([ 2136 | 130, 10, // Header (subscribeqos=0length=9) 2137 | 0, 6, // Message ID (6) 2138 | 0, // Property length (0) 2139 | 0, 4, // Topic length, 2140 | 116, 101, 115, 116, // Topic (test) 2141 | 0x30 // Flags 2142 | ]), { protocolVersion: 5 }) 2143 | 2144 | testParseError('Invalid subscribe topic flag bits, bits 7-2 must be 0', Buffer.from([ 2145 | 130, 9, // Header (subscribeqos=0length=9) 2146 | 0, 6, // Message ID (6) 2147 | 0, 4, // Topic length, 2148 | 116, 101, 115, 116, // Topic (test) 2149 | 0x08 // Flags 2150 | ])) 2151 | 2152 | testParseGenerate('subscribe to one topic by MQTT 5', { 2153 | cmd: 'subscribe', 2154 | retain: false, 2155 | qos: 1, 2156 | dup: false, 2157 | length: 26, 2158 | subscriptions: [ 2159 | { 2160 | topic: 'test', 2161 | qos: 0, 2162 | nl: false, 2163 | rap: true, 2164 | rh: 1 2165 | } 2166 | ], 2167 | messageId: 6, 2168 | properties: { 2169 | subscriptionIdentifier: 145, 2170 | userProperties: { 2171 | test: 'test' 2172 | } 2173 | } 2174 | }, Buffer.from([ 2175 | 130, 26, // Header (subscribeqos=1length=9) 2176 | 0, 6, // Message ID (6) 2177 | 16, // properties length 2178 | 11, 145, 1, // subscriptionIdentifier 2179 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 2180 | 0, 4, // Topic length, 2181 | 116, 101, 115, 116, // Topic (test) 2182 | 24 // settings(qos: 0, noLocal: false, Retain as Published: true, retain handling: 1) 2183 | ]), { protocolVersion: 5 }) 2184 | 2185 | testParseGenerate('subscribe to three topics', { 2186 | cmd: 'subscribe', 2187 | retain: false, 2188 | qos: 1, 2189 | dup: false, 2190 | length: 23, 2191 | subscriptions: [ 2192 | { 2193 | topic: 'test', 2194 | qos: 0 2195 | }, { 2196 | topic: 'uest', 2197 | qos: 1 2198 | }, { 2199 | topic: 'tfst', 2200 | qos: 2 2201 | } 2202 | ], 2203 | messageId: 6 2204 | }, Buffer.from([ 2205 | 130, 23, // Header (publishqos=1length=9) 2206 | 0, 6, // Message ID (6) 2207 | 0, 4, // Topic length, 2208 | 116, 101, 115, 116, // Topic (test) 2209 | 0, // Qos (0) 2210 | 0, 4, // Topic length 2211 | 117, 101, 115, 116, // Topic (uest) 2212 | 1, // Qos (1) 2213 | 0, 4, // Topic length 2214 | 116, 102, 115, 116, // Topic (tfst) 2215 | 2 // Qos (2) 2216 | ])) 2217 | 2218 | testParseGenerate('subscribe to 3 topics by MQTT 5', { 2219 | cmd: 'subscribe', 2220 | retain: false, 2221 | qos: 1, 2222 | dup: false, 2223 | length: 40, 2224 | subscriptions: [ 2225 | { 2226 | topic: 'test', 2227 | qos: 0, 2228 | nl: false, 2229 | rap: true, 2230 | rh: 1 2231 | }, 2232 | { 2233 | topic: 'uest', 2234 | qos: 1, 2235 | nl: false, 2236 | rap: false, 2237 | rh: 0 2238 | }, { 2239 | topic: 'tfst', 2240 | qos: 2, 2241 | nl: true, 2242 | rap: false, 2243 | rh: 0 2244 | } 2245 | ], 2246 | messageId: 6, 2247 | properties: { 2248 | subscriptionIdentifier: 145, 2249 | userProperties: { 2250 | test: 'test' 2251 | } 2252 | } 2253 | }, Buffer.from([ 2254 | 130, 40, // Header (subscribeqos=1length=9) 2255 | 0, 6, // Message ID (6) 2256 | 16, // properties length 2257 | 11, 145, 1, // subscriptionIdentifier 2258 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 2259 | 0, 4, // Topic length, 2260 | 116, 101, 115, 116, // Topic (test) 2261 | 24, // settings(qos: 0, noLocal: false, Retain as Published: true, retain handling: 1) 2262 | 0, 4, // Topic length 2263 | 117, 101, 115, 116, // Topic (uest) 2264 | 1, // Qos (1) 2265 | 0, 4, // Topic length 2266 | 116, 102, 115, 116, // Topic (tfst) 2267 | 6 // Qos (2), No Local: true 2268 | ]), { protocolVersion: 5 }) 2269 | 2270 | testParseGenerate('suback', { 2271 | cmd: 'suback', 2272 | retain: false, 2273 | qos: 0, 2274 | dup: false, 2275 | length: 5, 2276 | granted: [0, 1, 2], 2277 | messageId: 6 2278 | }, Buffer.from([ 2279 | 144, 5, // Header 2280 | 0, 6, // Message ID 2281 | 0, 1, 2 2282 | ])) 2283 | 2284 | testParseGenerate('suback', { 2285 | cmd: 'suback', 2286 | retain: false, 2287 | qos: 0, 2288 | dup: false, 2289 | length: 7, 2290 | granted: [0, 1, 2, 128], 2291 | messageId: 6 2292 | }, Buffer.from([ 2293 | 144, 7, // Header 2294 | 0, 6, // Message ID 2295 | 0, // Property length 2296 | 0, 1, 2, 128 // Granted qos (0, 1, 2) and a rejected being 0x80 2297 | ]), { protocolVersion: 5 }) 2298 | 2299 | testParseError('Invalid suback QoS, must be 0, 1, 2 or 128', Buffer.from([ 2300 | 144, 6, // Header 2301 | 0, 6, // Message ID 2302 | 0, 1, 2, 3 // Granted qos (0, 1, 2) 2303 | ])) 2304 | 2305 | testParseError('Invalid suback code', Buffer.from([ 2306 | 144, 6, // Header 2307 | 0, 6, // Message ID 2308 | 0, 1, 2, 0x79 // Granted qos (0, 1, 2) and an invalid code 2309 | ]), { protocolVersion: 5 }) 2310 | 2311 | testParseGenerate('suback MQTT 5', { 2312 | cmd: 'suback', 2313 | retain: false, 2314 | qos: 0, 2315 | dup: false, 2316 | length: 27, 2317 | granted: [0, 1, 2, 128], 2318 | messageId: 6, 2319 | properties: { 2320 | reasonString: 'test', 2321 | userProperties: { 2322 | test: 'test' 2323 | } 2324 | } 2325 | }, Buffer.from([ 2326 | 144, 27, // Header 2327 | 0, 6, // Message ID 2328 | 20, // properties length 2329 | 31, 0, 4, 116, 101, 115, 116, // reasonString 2330 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 2331 | 0, 1, 2, 128 // Granted qos (0, 1, 2) and a rejected being 0x80 2332 | ]), { protocolVersion: 5 }) 2333 | 2334 | testParseGenerate('unsubscribe', { 2335 | cmd: 'unsubscribe', 2336 | retain: false, 2337 | qos: 1, 2338 | dup: false, 2339 | length: 14, 2340 | unsubscriptions: [ 2341 | 'tfst', 2342 | 'test' 2343 | ], 2344 | messageId: 7 2345 | }, Buffer.from([ 2346 | 162, 14, 2347 | 0, 7, // Message ID (7) 2348 | 0, 4, // Topic length 2349 | 116, 102, 115, 116, // Topic (tfst) 2350 | 0, 4, // Topic length, 2351 | 116, 101, 115, 116 // Topic (test) 2352 | ])) 2353 | 2354 | // 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] 2355 | testParseError('Invalid header flag bits, must be 0x2 for unsubscribe packet', Buffer.from([ 2356 | 160, 14, 2357 | 0, 7, // Message ID (7) 2358 | 0, 4, // Topic length 2359 | 116, 102, 115, 116, // Topic (tfst) 2360 | 0, 4, // Topic length, 2361 | 116, 101, 115, 116 // Topic (test) 2362 | ])) 2363 | 2364 | testGenerateError('Invalid unsubscriptions', { 2365 | cmd: 'unsubscribe', 2366 | retain: false, 2367 | qos: 1, 2368 | dup: true, 2369 | length: 5, 2370 | unsubscriptions: 5, 2371 | messageId: 7 2372 | }, {}, 'unsubscribe with unsubscriptions not an array') 2373 | 2374 | testGenerateError('Invalid unsubscriptions', { 2375 | cmd: 'unsubscribe', 2376 | retain: false, 2377 | qos: 1, 2378 | dup: true, 2379 | length: 5, 2380 | unsubscriptions: [1, 2], 2381 | messageId: 7 2382 | }, {}, 'unsubscribe with unsubscriptions as an object') 2383 | 2384 | testParseGenerate('unsubscribe MQTT 5', { 2385 | cmd: 'unsubscribe', 2386 | retain: false, 2387 | qos: 1, 2388 | dup: false, 2389 | length: 28, 2390 | unsubscriptions: [ 2391 | 'tfst', 2392 | 'test' 2393 | ], 2394 | messageId: 7, 2395 | properties: { 2396 | userProperties: { 2397 | test: 'test' 2398 | } 2399 | } 2400 | }, Buffer.from([ 2401 | 162, 28, 2402 | 0, 7, // Message ID (7) 2403 | 13, // properties length 2404 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 2405 | 0, 4, // Topic length 2406 | 116, 102, 115, 116, // Topic (tfst) 2407 | 0, 4, // Topic length, 2408 | 116, 101, 115, 116 // Topic (test) 2409 | ]), { protocolVersion: 5 }) 2410 | 2411 | testParseGenerate('unsuback', { 2412 | cmd: 'unsuback', 2413 | retain: false, 2414 | qos: 0, 2415 | dup: false, 2416 | length: 2, 2417 | messageId: 8 2418 | }, Buffer.from([ 2419 | 176, 2, // Header 2420 | 0, 8 // Message ID 2421 | ])) 2422 | 2423 | // 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] 2424 | testParseError('Invalid header flag bits, must be 0x0 for unsuback packet', Buffer.from([ 2425 | 177, 2, // Header 2426 | 0, 8 // Message ID 2427 | ])) 2428 | 2429 | testParseGenerate('unsuback MQTT 5', { 2430 | cmd: 'unsuback', 2431 | retain: false, 2432 | qos: 0, 2433 | dup: false, 2434 | length: 25, 2435 | messageId: 8, 2436 | properties: { 2437 | reasonString: 'test', 2438 | userProperties: { 2439 | test: 'test' 2440 | } 2441 | }, 2442 | granted: [0, 128] 2443 | }, Buffer.from([ 2444 | 176, 25, // Header 2445 | 0, 8, // Message ID 2446 | 20, // properties length 2447 | 31, 0, 4, 116, 101, 115, 116, // reasonString 2448 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 2449 | 0, 128 // success and error 2450 | ]), { protocolVersion: 5 }) 2451 | 2452 | testParseError('Invalid unsuback code', Buffer.from([ 2453 | 176, 4, // Header 2454 | 0, 8, // Message ID 2455 | 0, // properties length 2456 | 0x84 // reason codes 2457 | ]), { protocolVersion: 5 }) 2458 | 2459 | testParseGenerate('pingreq', { 2460 | cmd: 'pingreq', 2461 | retain: false, 2462 | qos: 0, 2463 | dup: false, 2464 | length: 0 2465 | }, Buffer.from([ 2466 | 192, 0 // Header 2467 | ])) 2468 | 2469 | // 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] 2470 | testParseError('Invalid header flag bits, must be 0x0 for pingreq packet', Buffer.from([ 2471 | 193, 0 // Header 2472 | ])) 2473 | 2474 | testParseGenerate('pingresp', { 2475 | cmd: 'pingresp', 2476 | retain: false, 2477 | qos: 0, 2478 | dup: false, 2479 | length: 0 2480 | }, Buffer.from([ 2481 | 208, 0 // Header 2482 | ])) 2483 | 2484 | // 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] 2485 | testParseError('Invalid header flag bits, must be 0x0 for pingresp packet', Buffer.from([ 2486 | 209, 0 // Header 2487 | ])) 2488 | 2489 | testParseGenerate('disconnect', { 2490 | cmd: 'disconnect', 2491 | retain: false, 2492 | qos: 0, 2493 | dup: false, 2494 | length: 0 2495 | }, Buffer.from([ 2496 | 224, 0 // Header 2497 | ])) 2498 | 2499 | // 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] 2500 | testParseError('Invalid header flag bits, must be 0x0 for disconnect packet', Buffer.from([ 2501 | 225, 0 // Header 2502 | ])) 2503 | 2504 | testParseGenerate('disconnect MQTT 5', { 2505 | cmd: 'disconnect', 2506 | retain: false, 2507 | qos: 0, 2508 | dup: false, 2509 | length: 34, 2510 | reasonCode: 0, 2511 | properties: { 2512 | sessionExpiryInterval: 145, 2513 | reasonString: 'test', 2514 | userProperties: { 2515 | test: 'test' 2516 | }, 2517 | serverReference: 'test' 2518 | } 2519 | }, Buffer.from([ 2520 | 224, 34, // Header 2521 | 0, // reason code 2522 | 32, // properties length 2523 | 17, 0, 0, 0, 145, // sessionExpiryInterval 2524 | 31, 0, 4, 116, 101, 115, 116, // reasonString 2525 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 2526 | 28, 0, 4, 116, 101, 115, 116// serverReference 2527 | ]), { protocolVersion: 5 }) 2528 | 2529 | testParseGenerate('disconnect MQTT 5 with no properties', { 2530 | cmd: 'disconnect', 2531 | retain: false, 2532 | qos: 0, 2533 | dup: false, 2534 | length: 2, 2535 | reasonCode: 0 2536 | }, Buffer.from([ 2537 | 224, 2, // Fixed Header (DISCONNECT, Remaining Length) 2538 | 0, // Reason Code (Normal Disconnection) 2539 | 0 // Property Length (0 => No Properties) 2540 | ]), { protocolVersion: 5 }) 2541 | 2542 | testParseError('Invalid disconnect reason code', Buffer.from([ 2543 | 224, 2, // Fixed Header (DISCONNECT, Remaining Length) 2544 | 0x05, // Reason Code (Normal Disconnection) 2545 | 0 // Property Length (0 => No Properties) 2546 | ]), { protocolVersion: 5 }) 2547 | 2548 | testParseGenerate('auth MQTT 5', { 2549 | cmd: 'auth', 2550 | retain: false, 2551 | qos: 0, 2552 | dup: false, 2553 | length: 36, 2554 | reasonCode: 0, 2555 | properties: { 2556 | authenticationMethod: 'test', 2557 | authenticationData: Buffer.from([0, 1, 2, 3]), 2558 | reasonString: 'test', 2559 | userProperties: { 2560 | test: 'test' 2561 | } 2562 | } 2563 | }, Buffer.from([ 2564 | 240, 36, // Header 2565 | 0, // reason code 2566 | 34, // properties length 2567 | 21, 0, 4, 116, 101, 115, 116, // auth method 2568 | 22, 0, 4, 0, 1, 2, 3, // auth data 2569 | 31, 0, 4, 116, 101, 115, 116, // reasonString 2570 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties 2571 | ]), { protocolVersion: 5 }) 2572 | 2573 | testParseError('Invalid auth reason code', Buffer.from([ 2574 | 240, 2, // Fixed Header (DISCONNECT, Remaining Length) 2575 | 0x17, // Reason Code 2576 | 0 // Property Length (0 => No Properties) 2577 | ]), { protocolVersion: 5 }) 2578 | 2579 | testGenerateError('Invalid protocolId', { 2580 | cmd: 'connect', 2581 | retain: false, 2582 | qos: 0, 2583 | dup: false, 2584 | length: 54, 2585 | protocolId: 42, 2586 | protocolVersion: 3, 2587 | will: { 2588 | retain: true, 2589 | qos: 2, 2590 | topic: 'topic', 2591 | payload: 'payload' 2592 | }, 2593 | clean: true, 2594 | keepalive: 30, 2595 | clientId: 'test', 2596 | username: 'username', 2597 | password: 'password' 2598 | }) 2599 | 2600 | testGenerateError('Invalid protocol version', { 2601 | cmd: 'connect', 2602 | retain: false, 2603 | qos: 0, 2604 | dup: false, 2605 | length: 54, 2606 | protocolId: 'MQIsdp', 2607 | protocolVersion: 1, 2608 | will: { 2609 | retain: true, 2610 | qos: 2, 2611 | topic: 'topic', 2612 | payload: 'payload' 2613 | }, 2614 | clean: true, 2615 | keepalive: 30, 2616 | clientId: 'test', 2617 | username: 'username', 2618 | password: 'password' 2619 | }) 2620 | 2621 | testGenerateError('clientId must be supplied before 3.1.1', { 2622 | cmd: 'connect', 2623 | retain: false, 2624 | qos: 0, 2625 | dup: false, 2626 | length: 54, 2627 | protocolId: 'MQIsdp', 2628 | protocolVersion: 3, 2629 | will: { 2630 | retain: true, 2631 | qos: 2, 2632 | topic: 'topic', 2633 | payload: 'payload' 2634 | }, 2635 | clean: true, 2636 | keepalive: 30, 2637 | username: 'username', 2638 | password: 'password' 2639 | }) 2640 | 2641 | testGenerateError('clientId must be given if cleanSession set to 0', { 2642 | cmd: 'connect', 2643 | retain: false, 2644 | qos: 0, 2645 | dup: false, 2646 | length: 54, 2647 | protocolId: 'MQTT', 2648 | protocolVersion: 4, 2649 | will: { 2650 | retain: true, 2651 | qos: 2, 2652 | topic: 'topic', 2653 | payload: 'payload' 2654 | }, 2655 | clean: false, 2656 | keepalive: 30, 2657 | username: 'username', 2658 | password: 'password' 2659 | }) 2660 | 2661 | testGenerateError('Invalid keepalive', { 2662 | cmd: 'connect', 2663 | retain: false, 2664 | qos: 0, 2665 | dup: false, 2666 | length: 54, 2667 | protocolId: 'MQIsdp', 2668 | protocolVersion: 3, 2669 | will: { 2670 | retain: true, 2671 | qos: 2, 2672 | topic: 'topic', 2673 | payload: 'payload' 2674 | }, 2675 | clean: true, 2676 | keepalive: 'hello', 2677 | clientId: 'test', 2678 | username: 'username', 2679 | password: 'password' 2680 | }) 2681 | 2682 | testGenerateError('Invalid keepalive', { 2683 | cmd: 'connect', 2684 | keepalive: 3.1416 2685 | }) 2686 | 2687 | testGenerateError('Invalid will', { 2688 | cmd: 'connect', 2689 | retain: false, 2690 | qos: 0, 2691 | dup: false, 2692 | length: 54, 2693 | protocolId: 'MQIsdp', 2694 | protocolVersion: 3, 2695 | will: 42, 2696 | clean: true, 2697 | keepalive: 30, 2698 | clientId: 'test', 2699 | username: 'username', 2700 | password: 'password' 2701 | }) 2702 | 2703 | testGenerateError('Invalid will topic', { 2704 | cmd: 'connect', 2705 | retain: false, 2706 | qos: 0, 2707 | dup: false, 2708 | length: 54, 2709 | protocolId: 'MQIsdp', 2710 | protocolVersion: 3, 2711 | will: { 2712 | retain: true, 2713 | qos: 2, 2714 | payload: 'payload' 2715 | }, 2716 | clean: true, 2717 | keepalive: 30, 2718 | clientId: 'test', 2719 | username: 'username', 2720 | password: 'password' 2721 | }) 2722 | 2723 | testGenerateError('Invalid will payload', { 2724 | cmd: 'connect', 2725 | retain: false, 2726 | qos: 0, 2727 | dup: false, 2728 | length: 54, 2729 | protocolId: 'MQIsdp', 2730 | protocolVersion: 3, 2731 | will: { 2732 | retain: true, 2733 | qos: 2, 2734 | topic: 'topic', 2735 | payload: 42 2736 | }, 2737 | clean: true, 2738 | keepalive: 30, 2739 | clientId: 'test', 2740 | username: 'username', 2741 | password: 'password' 2742 | }) 2743 | 2744 | testGenerateError('Invalid username', { 2745 | cmd: 'connect', 2746 | retain: false, 2747 | qos: 0, 2748 | dup: false, 2749 | length: 54, 2750 | protocolId: 'MQIsdp', 2751 | protocolVersion: 3, 2752 | will: { 2753 | retain: true, 2754 | qos: 2, 2755 | topic: 'topic', 2756 | payload: 'payload' 2757 | }, 2758 | clean: true, 2759 | keepalive: 30, 2760 | clientId: 'test', 2761 | username: 42, 2762 | password: 'password' 2763 | }) 2764 | 2765 | testGenerateError('Invalid password', { 2766 | cmd: 'connect', 2767 | retain: false, 2768 | qos: 0, 2769 | dup: false, 2770 | length: 54, 2771 | protocolId: 'MQIsdp', 2772 | protocolVersion: 3, 2773 | will: { 2774 | retain: true, 2775 | qos: 2, 2776 | topic: 'topic', 2777 | payload: 'payload' 2778 | }, 2779 | clean: true, 2780 | keepalive: 30, 2781 | clientId: 'test', 2782 | username: 'username', 2783 | password: 42 2784 | }) 2785 | 2786 | testGenerateError('Username is required to use password', { 2787 | cmd: 'connect', 2788 | retain: false, 2789 | qos: 0, 2790 | dup: false, 2791 | length: 54, 2792 | protocolId: 'MQIsdp', 2793 | protocolVersion: 3, 2794 | will: { 2795 | retain: true, 2796 | qos: 2, 2797 | topic: 'topic', 2798 | payload: 'payload' 2799 | }, 2800 | clean: true, 2801 | keepalive: 30, 2802 | clientId: 'test', 2803 | password: 'password' 2804 | }) 2805 | 2806 | testGenerateError('Invalid messageExpiryInterval: -4321', { 2807 | cmd: 'publish', 2808 | retain: true, 2809 | qos: 2, 2810 | dup: true, 2811 | length: 60, 2812 | topic: 'test', 2813 | payload: Buffer.from('test'), 2814 | messageId: 10, 2815 | properties: { 2816 | payloadFormatIndicator: true, 2817 | messageExpiryInterval: -4321, 2818 | topicAlias: 100, 2819 | responseTopic: 'topic', 2820 | correlationData: Buffer.from([1, 2, 3, 4]), 2821 | userProperties: { 2822 | test: 'test' 2823 | }, 2824 | subscriptionIdentifier: 120, 2825 | contentType: 'test' 2826 | } 2827 | }, { protocolVersion: 5 }) 2828 | 2829 | testGenerateError('Invalid topicAlias: -100', { 2830 | cmd: 'publish', 2831 | retain: true, 2832 | qos: 2, 2833 | dup: true, 2834 | length: 60, 2835 | topic: 'test', 2836 | payload: Buffer.from('test'), 2837 | messageId: 10, 2838 | properties: { 2839 | payloadFormatIndicator: true, 2840 | messageExpiryInterval: 4321, 2841 | topicAlias: -100, 2842 | responseTopic: 'topic', 2843 | correlationData: Buffer.from([1, 2, 3, 4]), 2844 | userProperties: { 2845 | test: 'test' 2846 | }, 2847 | subscriptionIdentifier: 120, 2848 | contentType: 'test' 2849 | } 2850 | }, { protocolVersion: 5 }) 2851 | 2852 | testGenerateError('Invalid subscriptionIdentifier: -120', { 2853 | cmd: 'publish', 2854 | retain: true, 2855 | qos: 2, 2856 | dup: true, 2857 | length: 60, 2858 | topic: 'test', 2859 | payload: Buffer.from('test'), 2860 | messageId: 10, 2861 | properties: { 2862 | payloadFormatIndicator: true, 2863 | messageExpiryInterval: 4321, 2864 | topicAlias: 100, 2865 | responseTopic: 'topic', 2866 | correlationData: Buffer.from([1, 2, 3, 4]), 2867 | userProperties: { 2868 | test: 'test' 2869 | }, 2870 | subscriptionIdentifier: -120, 2871 | contentType: 'test' 2872 | } 2873 | }, { protocolVersion: 5 }) 2874 | 2875 | test('support cork', t => { 2876 | t.plan(9) 2877 | 2878 | const dest = WS() 2879 | 2880 | dest._write = (chunk, enc, cb) => { 2881 | t.pass('_write called') 2882 | cb() 2883 | } 2884 | 2885 | mqtt.writeToStream({ 2886 | cmd: 'connect', 2887 | retain: false, 2888 | qos: 0, 2889 | dup: false, 2890 | length: 18, 2891 | protocolId: 'MQIsdp', 2892 | protocolVersion: 3, 2893 | clean: false, 2894 | keepalive: 30, 2895 | clientId: 'test' 2896 | }, dest) 2897 | 2898 | dest.end() 2899 | }) 2900 | 2901 | // The following test case was designed after experiencing errors 2902 | // when trying to connect with tls on a non tls mqtt port 2903 | // the specific behaviour is: 2904 | // - first byte suggests this is a connect message 2905 | // - second byte suggests message length to be smaller than buffer length 2906 | // thus payload processing starts 2907 | // - the first two bytes suggest a protocol identifier string length 2908 | // that leads the parser pointer close to the end of the buffer 2909 | // - when trying to read further connect flags the buffer produces 2910 | // a "out of range" Error 2911 | // 2912 | testParseError('Packet too short', Buffer.from([ 2913 | 16, 9, 2914 | 0, 6, 2915 | 77, 81, 73, 115, 100, 112, 2916 | 3 2917 | ])) 2918 | 2919 | // CONNECT Packets that show other protocol IDs than 2920 | // the valid values MQTT and MQIsdp should cause an error 2921 | // those packets are a hint that this is not a mqtt connection 2922 | testParseError('Invalid protocolId', Buffer.from([ 2923 | 16, 18, 2924 | 0, 6, 2925 | 65, 65, 65, 65, 65, 65, // AAAAAA 2926 | 3, // Protocol version 2927 | 0, // Connect flags 2928 | 0, 10, // Keepalive 2929 | 0, 4, // Client ID length 2930 | 116, 101, 115, 116 // Client ID 2931 | ])) 2932 | 2933 | // CONNECT Packets that contain an unsupported protocol version 2934 | // Flag (i.e. not `3` or `4` or '5') should cause an error 2935 | testParseError('Invalid protocol version', Buffer.from([ 2936 | 16, 18, 2937 | 0, 6, 2938 | 77, 81, 73, 115, 100, 112, // Protocol ID 2939 | 1, // Protocol version 2940 | 0, // Connect flags 2941 | 0, 10, // Keepalive 2942 | 0, 4, // Client ID length 2943 | 116, 101, 115, 116 // Client ID 2944 | ])) 2945 | 2946 | // When a packet contains a string in the variable header and the 2947 | // given string length of this exceeds the overall length of the packet that 2948 | // was specified in the fixed header, parsing must fail. 2949 | // this case simulates this behavior with the protocol ID string of the 2950 | // CONNECT packet. The fixed header suggests a remaining length of 8 bytes 2951 | // which would be exceeded by the string length of 15 2952 | // in this case, a protocol ID parse error is expected 2953 | testParseError('Cannot parse protocolId', Buffer.from([ 2954 | 16, 8, // Fixed header 2955 | 0, 15, // string length 15 --> 15 > 8 --> error! 2956 | 77, 81, 73, 115, 100, 112, 2957 | 77, 81, 73, 115, 100, 112, 2958 | 77, 81, 73, 115, 100, 112, 2959 | 77, 81, 73, 115, 100, 112, 2960 | 77, 81, 73, 115, 100, 112, 2961 | 77, 81, 73, 115, 100, 112, 2962 | 77, 81, 73, 115, 100, 112, 2963 | 77, 81, 73, 115, 100, 112 2964 | ])) 2965 | 2966 | testParseError('Unknown property', Buffer.from([ 2967 | 61, 60, // Header 2968 | 0, 4, // Topic length 2969 | 116, 101, 115, 116, // Topic (test) 2970 | 0, 10, // Message ID 2971 | 47, // properties length 2972 | 126, 1, // unknown property 2973 | 2, 0, 0, 16, 225, // message expiry interval 2974 | 35, 0, 100, // topicAlias 2975 | 8, 0, 5, 116, 111, 112, 105, 99, // response topic 2976 | 9, 0, 4, 1, 2, 3, 4, // correlationData 2977 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116, // userProperties 2978 | 11, 120, // subscriptionIdentifier 2979 | 3, 0, 4, 116, 101, 115, 116, // content type 2980 | 116, 101, 115, 116 // Payload (test) 2981 | ]), { protocolVersion: 5 }) 2982 | 2983 | testParseError('Not supported auth packet for this version MQTT', Buffer.from([ 2984 | 240, 36, // Header 2985 | 0, // reason code 2986 | 34, // properties length 2987 | 21, 0, 4, 116, 101, 115, 116, // auth method 2988 | 22, 0, 4, 0, 1, 2, 3, // auth data 2989 | 31, 0, 4, 116, 101, 115, 116, // reasonString 2990 | 38, 0, 4, 116, 101, 115, 116, 0, 4, 116, 101, 115, 116 // userProperties 2991 | ])) 2992 | 2993 | // When a Subscribe packet contains a topic_filter and the given 2994 | // length is topic_filter.length + 1 then the last byte (requested QoS) is interpreted as topic_filter 2995 | // reading the requested_qos at the end causes 'Index out of range' read 2996 | testParseError('Malformed Subscribe Payload', Buffer.from([ 2997 | 130, 14, // subscribe header and remaining length 2998 | 0, 123, // packet ID 2999 | 0, 10, // topic filter length 3000 | 104, 105, 106, 107, 108, 47, 109, 110, 111, // topic filter with length of 9 bytes 3001 | 0 // requested QoS 3002 | ])) 3003 | 3004 | test('Cannot parse property code type', t => { 3005 | const packets = Buffer.from([ 3006 | 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 3007 | ]) 3008 | 3009 | t.plan(3) 3010 | 3011 | const parser = mqtt.parser() 3012 | 3013 | parser.on('error', err => { 3014 | t.equal(err.message, 'Cannot parse property code type', 'expected error message') 3015 | t.end() 3016 | }) 3017 | 3018 | parser.on('packet', (packet) => { 3019 | t.pass('Packet parsed') 3020 | }) 3021 | 3022 | parser.parse(packets) 3023 | }) 3024 | 3025 | testWriteToStreamError('Invalid command', { 3026 | cmd: 'invalid' 3027 | }) 3028 | 3029 | testWriteToStreamError('Invalid protocolId', { 3030 | cmd: 'connect', 3031 | protocolId: {} 3032 | }) 3033 | 3034 | test('userProperties null prototype', t => { 3035 | t.plan(3) 3036 | 3037 | const packet = mqtt.generate({ 3038 | cmd: 'connect', 3039 | retain: false, 3040 | qos: 0, 3041 | dup: false, 3042 | length: 125, 3043 | protocolId: 'MQTT', 3044 | protocolVersion: 5, 3045 | will: { 3046 | retain: true, 3047 | qos: 2, 3048 | properties: { 3049 | willDelayInterval: 1234, 3050 | payloadFormatIndicator: false, 3051 | messageExpiryInterval: 4321, 3052 | contentType: 'test', 3053 | responseTopic: 'topic', 3054 | correlationData: Buffer.from([1, 2, 3, 4]), 3055 | userProperties: { 3056 | test: 'test' 3057 | } 3058 | }, 3059 | topic: 'topic', 3060 | payload: Buffer.from([4, 3, 2, 1]) 3061 | }, 3062 | clean: true, 3063 | keepalive: 30, 3064 | properties: { 3065 | sessionExpiryInterval: 1234, 3066 | receiveMaximum: 432, 3067 | maximumPacketSize: 100, 3068 | topicAliasMaximum: 456, 3069 | requestResponseInformation: true, 3070 | requestProblemInformation: true, 3071 | userProperties: { 3072 | test: 'test' 3073 | }, 3074 | authenticationMethod: 'test', 3075 | authenticationData: Buffer.from([1, 2, 3, 4]) 3076 | }, 3077 | clientId: 'test' 3078 | }) 3079 | 3080 | const parser = mqtt.parser() 3081 | 3082 | parser.on('packet', packet => { 3083 | t.equal(packet.cmd, 'connect') 3084 | t.equal(Object.getPrototypeOf(packet.properties.userProperties), null) 3085 | t.equal(Object.getPrototypeOf(packet.will.properties.userProperties), null) 3086 | }) 3087 | 3088 | parser.parse(packet) 3089 | }) 3090 | 3091 | test('stops parsing after first error', t => { 3092 | t.plan(4) 3093 | 3094 | const parser = mqtt.parser() 3095 | 3096 | let packetCount = 0 3097 | let errorCount = 0 3098 | let expectedPackets = 1 3099 | let expectedErrors = 1 3100 | 3101 | parser.on('packet', packet => { 3102 | t.ok(++packetCount <= expectedPackets, `expected <= ${expectedPackets} packets`) 3103 | }) 3104 | 3105 | parser.on('error', erroneous => { 3106 | t.ok(++errorCount <= expectedErrors, `expected <= ${expectedErrors} errors`) 3107 | }) 3108 | 3109 | parser.parse(Buffer.from([ 3110 | // First, a valid connect packet: 3111 | 3112 | 16, 12, // Header 3113 | 0, 4, // Protocol ID length 3114 | 77, 81, 84, 84, // Protocol ID 3115 | 4, // Protocol version 3116 | 2, // Connect flags 3117 | 0, 30, // Keepalive 3118 | 0, 0, // Client ID length 3119 | 3120 | // Then an invalid subscribe packet: 3121 | 3122 | 128, 9, // Header (subscribeqos=0length=9) 3123 | 0, 6, // Message ID (6) 3124 | 0, 4, // Topic length, 3125 | 116, 101, 115, 116, // Topic (test) 3126 | 0, // Qos (0) 3127 | 3128 | // And another invalid subscribe packet: 3129 | 3130 | 128, 9, // Header (subscribeqos=0length=9) 3131 | 0, 6, // Message ID (6) 3132 | 0, 4, // Topic length, 3133 | 116, 101, 115, 116, // Topic (test) 3134 | 0, // Qos (0) 3135 | 3136 | // Finally, a valid disconnect packet: 3137 | 3138 | 224, 0 // Header 3139 | ])) 3140 | 3141 | // Calling parse again clears the error and continues parsing 3142 | packetCount = 0 3143 | errorCount = 0 3144 | expectedPackets = 2 3145 | expectedErrors = 0 3146 | 3147 | parser.parse(Buffer.from([ 3148 | // Connect: 3149 | 3150 | 16, 12, // Header 3151 | 0, 4, // Protocol ID length 3152 | 77, 81, 84, 84, // Protocol ID 3153 | 4, // Protocol version 3154 | 2, // Connect flags 3155 | 0, 30, // Keepalive 3156 | 0, 0, // Client ID length 3157 | 3158 | // Disconnect: 3159 | 3160 | 224, 0 // Header 3161 | ])) 3162 | }) 3163 | 3164 | test('undefined properties', t => { 3165 | t.plan(2) 3166 | 3167 | const packet = mqtt.generate({ 3168 | cmd: 'connect', 3169 | retain: false, 3170 | qos: 0, 3171 | dup: false, 3172 | length: 125, 3173 | protocolId: 'MQTT', 3174 | protocolVersion: 5, 3175 | will: { 3176 | retain: true, 3177 | qos: 2, 3178 | properties: { 3179 | willDelayInterval: 1234, 3180 | payloadFormatIndicator: false, 3181 | messageExpiryInterval: 4321, 3182 | contentType: 'test', 3183 | responseTopic: 'topic', 3184 | correlationData: Buffer.from([1, 2, 3, 4]), 3185 | userProperties: { 3186 | test: 'test' 3187 | } 3188 | }, 3189 | topic: 'topic', 3190 | payload: Buffer.from([4, 3, 2, 1]) 3191 | }, 3192 | clean: true, 3193 | keepalive: 30, 3194 | properties: { 3195 | sessionExpiryInterval: 1234, 3196 | receiveMaximum: 432, 3197 | maximumPacketSize: 100, 3198 | topicAliasMaximum: 456, 3199 | requestResponseInformation: true, 3200 | requestProblemInformation: true, 3201 | correlationData: undefined, 3202 | userProperties: { 3203 | test: 'test' 3204 | }, 3205 | authenticationMethod: 'test', 3206 | authenticationData: Buffer.from([1, 2, 3, 4]) 3207 | }, 3208 | clientId: 'test' 3209 | }) 3210 | 3211 | const parser = mqtt.parser() 3212 | 3213 | parser.on('packet', packet => { 3214 | t.equal(packet.cmd, 'connect') 3215 | t.equal(Object.hasOwn(packet.properties, 'correlationData'), false) 3216 | }) 3217 | 3218 | parser.parse(packet) 3219 | }) 3220 | 3221 | testGenerateErrorMultipleCmds([ 3222 | 'publish', 3223 | 'puback', 3224 | 'pubrec', 3225 | 'pubrel', 3226 | 'subscribe', 3227 | 'suback', 3228 | 'unsubscribe', 3229 | 'unsuback' 3230 | ], 'Invalid messageId', { 3231 | qos: 1, // required for publish 3232 | topic: 'test', // required for publish 3233 | messageId: 'a' 3234 | }, {}) 3235 | -------------------------------------------------------------------------------- /testRandom.js: -------------------------------------------------------------------------------- 1 | const mqtt = require('./') 2 | const crypto = require('crypto') 3 | const max = 1E5 4 | const start = Date.now() / 1000 5 | let errors = 0 6 | let packets = 0 7 | let randomPacket 8 | const firstBytes = [ 9 | 16 * 1, // CONNECT 10 | 16 * 2, // CONNACK 11 | 16 * 3, // PUBLISH, QoS: 0, No Retain, No Dup 12 | 16 * 3 + 1, // PUBLISH, QoS: 0, Retain, No Dup 13 | 16 * 3 + 8, // PUBLISH, QoS: 0, No Retain, Dup 14 | 16 * 3 + 1 + 8, // PUBLISH, QoS: 0, Retain, Dup 15 | 16 * 3 + 2, // PUBLISH, QoS: 1, No Retain, No Dup 16 | 16 * 3 + 2 + 1, // PUBLISH, QoS: 1, Retain, No Dup 17 | 16 * 3 + 2 + 8, // PUBLISH, QoS: 1, No Retain, Dup 18 | 16 * 3 + 2 + 1 + 8, // PUBLISH, QoS: 1, Retain, Dup 19 | 16 * 3 + 4, // PUBLISH, QoS: 2, No Retain, No Dup 20 | 16 * 3 + 4 + 1, // PUBLISH, QoS: 2, Retain, No Dup 21 | 16 * 3 + 4 + 8, // PUBLISH, QoS: 2, No Retain, Dup 22 | 16 * 3 + 4 + 1 + 8, // PUBLISH, QoS: 2, Retain, Dup 23 | 16 * 4, // PUBACK 24 | 16 * 5, // PUBREC 25 | 16 * 6, // PUBREL 26 | 16 * 7, // PUBCOMP 27 | 16 * 8, // SUBSCRIBE 28 | 16 * 9, // SUBACK 29 | 16 * 10, // UNSUBSCRIBE 30 | 16 * 11, // UNSUBACK 31 | 16 * 12, // PINGREQ 32 | 16 * 13, // PINGRESP 33 | 16 * 14, // DISCONNECT 34 | 16 * 15 // RESERVED 35 | ] 36 | 37 | function doParse () { 38 | const parser = mqtt.parser() 39 | 40 | parser.on('error', onError) 41 | parser.on('packet', onPacket) 42 | randomPacket = crypto.randomBytes(Math.floor(Math.random() * 512)) 43 | 44 | // Increase probability to have a valid first byte in order to at least 45 | // enter the parser 46 | if (Math.random() > 0.2 && randomPacket.length > 0) randomPacket.writeUInt8(firstBytes[Math.floor(Math.random() * firstBytes.length)], 0) 47 | parser.parse(randomPacket) 48 | } 49 | 50 | try { 51 | console.log('Starting benchmark') 52 | for (let i = 0; i < max; i++) { 53 | doParse() 54 | } 55 | } catch (e) { 56 | console.log('Exception occurred at packet') 57 | console.log(randomPacket) 58 | console.log(e.message) 59 | console.log(e.stack) 60 | } 61 | 62 | function onError () { 63 | errors++ 64 | } 65 | 66 | function onPacket () { 67 | packets++ 68 | } 69 | 70 | const delta = Math.abs(max - packets - errors) 71 | const time = Date.now() / 1000 - start 72 | console.log('Benchmark complete') 73 | console.log('==========================') 74 | console.log('Sent packets:', max) 75 | console.log('Total time:', Math.round(time * 100) / 100, 'seconds', '\r\n') 76 | 77 | console.log('Valid packets:', packets) 78 | console.log('Erroneous packets:', errors) 79 | 80 | if ((max - packets - errors) < 0) console.log('Excess packets:', delta, '\r\n') 81 | else console.log('Missing packets:', delta, '\r\n') 82 | 83 | console.log('Total packets:', packets + errors) 84 | console.log('Total errors:', errors + delta) 85 | console.log('Error rate:', `${((errors + delta) / max * 100).toFixed(2)}%`) 86 | console.log('==========================') 87 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter = NodeJS.EventEmitter 2 | import WritableStream = NodeJS.WritableStream 3 | 4 | export declare type QoS = 0 | 1 | 2 5 | 6 | export declare type PacketCmd = 'auth' | 7 | 'connack' | 8 | 'connect' | 9 | 'disconnect' | 10 | 'pingreq' | 11 | 'pingresp' | 12 | 'puback' | 13 | 'pubcomp' | 14 | 'publish' | 15 | 'pubrel' | 16 | 'pubrec' | 17 | 'suback' | 18 | 'subscribe' | 19 | 'unsuback' | 20 | 'unsubscribe' 21 | 22 | export declare type UserProperties = {[index: string]: string | string[]} 23 | 24 | export interface IPacket { 25 | cmd: PacketCmd 26 | messageId?: number 27 | length?: number 28 | } 29 | 30 | export interface IAuthPacket extends IPacket { 31 | cmd: 'auth' 32 | reasonCode: number, 33 | properties?: { 34 | authenticationMethod?: string, 35 | authenticationData?: Buffer, 36 | reasonString?: string, 37 | userProperties?: UserProperties, 38 | } 39 | } 40 | 41 | export interface IConnectPacket extends IPacket { 42 | cmd: 'connect' 43 | clientId: string 44 | protocolVersion?: 4 | 5 | 3 45 | protocolId?: 'MQTT' | 'MQIsdp' 46 | clean?: boolean 47 | keepalive?: number 48 | username?: string 49 | password?: Buffer 50 | will?: { 51 | topic: string 52 | payload: Buffer | string 53 | qos?: QoS 54 | retain?: boolean 55 | properties?: { 56 | willDelayInterval?: number, 57 | payloadFormatIndicator?: boolean, 58 | messageExpiryInterval?: number, 59 | contentType?: string, 60 | responseTopic?: string, 61 | correlationData?: Buffer, 62 | userProperties?: UserProperties 63 | } 64 | } 65 | properties?: { 66 | sessionExpiryInterval?: number, 67 | receiveMaximum?: number, 68 | maximumPacketSize?: number, 69 | topicAliasMaximum?: number, 70 | requestResponseInformation?: boolean, 71 | requestProblemInformation?: boolean, 72 | userProperties?: UserProperties, 73 | authenticationMethod?: string, 74 | authenticationData?: Buffer 75 | } 76 | } 77 | 78 | export interface IPublishPacket extends IPacket { 79 | cmd: 'publish' 80 | qos: QoS 81 | dup: boolean 82 | retain: boolean 83 | topic: string 84 | payload: string | Buffer 85 | properties?: { 86 | payloadFormatIndicator?: boolean, 87 | messageExpiryInterval?: number, 88 | topicAlias?: number, 89 | responseTopic?: string, 90 | correlationData?: Buffer, 91 | userProperties?: UserProperties, 92 | subscriptionIdentifier?: number | number[], 93 | contentType?: string 94 | } 95 | } 96 | 97 | export interface IConnackPacket extends IPacket { 98 | cmd: 'connack' 99 | returnCode?: number, 100 | reasonCode?: number, 101 | sessionPresent: boolean 102 | properties?: { 103 | sessionExpiryInterval?: number, 104 | receiveMaximum?: number, 105 | maximumQoS?: number, 106 | retainAvailable?: boolean, 107 | maximumPacketSize?: number, 108 | assignedClientIdentifier?: string, 109 | topicAliasMaximum?: number, 110 | reasonString?: string, 111 | userProperties?: UserProperties, 112 | wildcardSubscriptionAvailable?: boolean, 113 | subscriptionIdentifiersAvailable?: boolean, 114 | sharedSubscriptionAvailable?: boolean, 115 | serverKeepAlive?: number, 116 | responseInformation?: string, 117 | serverReference?: string, 118 | authenticationMethod?: string, 119 | authenticationData?: Buffer 120 | } 121 | } 122 | 123 | export interface ISubscription { 124 | topic: string 125 | qos: QoS, 126 | nl?: boolean, 127 | rap?: boolean, 128 | rh?: number 129 | } 130 | 131 | export interface ISubscribePacket extends IPacket { 132 | cmd: 'subscribe' 133 | subscriptions: ISubscription[], 134 | properties?: { 135 | reasonString?: string, 136 | subscriptionIdentifier?: number, 137 | userProperties?: UserProperties 138 | } 139 | } 140 | 141 | export interface ISubackPacket extends IPacket { 142 | cmd: 'suback', 143 | reasonCode?: number, 144 | properties?: { 145 | reasonString?: string, 146 | userProperties?: UserProperties 147 | }, 148 | granted: number[] | Object[] 149 | } 150 | 151 | export interface IUnsubscribePacket extends IPacket { 152 | cmd: 'unsubscribe', 153 | properties?: { 154 | reasonString?: string, 155 | userProperties?: UserProperties 156 | }, 157 | unsubscriptions: string[] 158 | } 159 | 160 | export interface IUnsubackPacket extends IPacket { 161 | cmd: 'unsuback', 162 | reasonCode?: number, 163 | properties?: { 164 | reasonString?: string, 165 | userProperties?: UserProperties 166 | }, 167 | granted: number[] 168 | } 169 | 170 | export interface IPubackPacket extends IPacket { 171 | cmd: 'puback', 172 | reasonCode?: number, 173 | properties?: { 174 | reasonString?: string, 175 | userProperties?: UserProperties 176 | } 177 | } 178 | 179 | export interface IPubcompPacket extends IPacket { 180 | cmd: 'pubcomp', 181 | reasonCode?: number, 182 | properties?: { 183 | reasonString?: string, 184 | userProperties?: UserProperties 185 | } 186 | } 187 | 188 | export interface IPubrelPacket extends IPacket { 189 | cmd: 'pubrel', 190 | reasonCode?: number, 191 | properties?: { 192 | reasonString?: string, 193 | userProperties?: UserProperties 194 | } 195 | } 196 | 197 | export interface IPubrecPacket extends IPacket { 198 | cmd: 'pubrec', 199 | reasonCode?: number, 200 | properties?: { 201 | reasonString?: string, 202 | userProperties?: UserProperties 203 | } 204 | } 205 | 206 | export interface IPingreqPacket extends IPacket { 207 | cmd: 'pingreq' 208 | } 209 | 210 | export interface IPingrespPacket extends IPacket { 211 | cmd: 'pingresp' 212 | } 213 | 214 | export interface IDisconnectPacket extends IPacket { 215 | cmd: 'disconnect', 216 | reasonCode?: number, 217 | properties?: { 218 | sessionExpiryInterval?: number, 219 | reasonString?: string, 220 | userProperties?: UserProperties, 221 | serverReference?: string 222 | } 223 | } 224 | 225 | export declare type Packet = IConnectPacket | 226 | IPublishPacket | 227 | IConnackPacket | 228 | ISubscribePacket | 229 | ISubackPacket | 230 | IUnsubscribePacket | 231 | IUnsubackPacket | 232 | IPubackPacket | 233 | IPubcompPacket | 234 | IPubrelPacket | 235 | IPingreqPacket | 236 | IPingrespPacket | 237 | IDisconnectPacket | 238 | IPubrecPacket | 239 | IAuthPacket 240 | 241 | export interface Parser extends EventEmitter { 242 | on(event: 'packet', callback: (packet: Packet) => void): this 243 | 244 | on(event: 'error', callback: (error: any) => void): this 245 | 246 | parse(buffer: Buffer, opts?: Object): number 247 | } 248 | 249 | export declare function parser(opts?: Object): Parser 250 | 251 | export declare function generate(packet: Packet, opts?: Object): Buffer 252 | 253 | export declare function writeToStream(object: Packet, stream: WritableStream, opts?: Object): boolean 254 | 255 | export declare namespace writeToStream { 256 | let cacheNumbers: boolean 257 | } 258 | -------------------------------------------------------------------------------- /writeToStream.js: -------------------------------------------------------------------------------- 1 | const protocol = require('./constants') 2 | const { Buffer } = require('buffer') 3 | const empty = Buffer.allocUnsafe(0) 4 | const zeroBuf = Buffer.from([0]) 5 | const numbers = require('./numbers') 6 | const nextTick = require('process-nextick-args').nextTick 7 | const debug = require('debug')('mqtt-packet:writeToStream') 8 | 9 | const numCache = numbers.cache 10 | const generateNumber = numbers.generateNumber 11 | const generateCache = numbers.generateCache 12 | const genBufVariableByteInt = numbers.genBufVariableByteInt 13 | const generate4ByteBuffer = numbers.generate4ByteBuffer 14 | let writeNumber = writeNumberCached 15 | let toGenerate = true 16 | 17 | function generate (packet, stream, opts) { 18 | debug('generate called') 19 | if (stream.cork) { 20 | stream.cork() 21 | nextTick(uncork, stream) 22 | } 23 | 24 | if (toGenerate) { 25 | toGenerate = false 26 | generateCache() 27 | } 28 | debug('generate: packet.cmd: %s', packet.cmd) 29 | switch (packet.cmd) { 30 | case 'connect': 31 | return connect(packet, stream, opts) 32 | case 'connack': 33 | return connack(packet, stream, opts) 34 | case 'publish': 35 | return publish(packet, stream, opts) 36 | case 'puback': 37 | case 'pubrec': 38 | case 'pubrel': 39 | case 'pubcomp': 40 | return confirmation(packet, stream, opts) 41 | case 'subscribe': 42 | return subscribe(packet, stream, opts) 43 | case 'suback': 44 | return suback(packet, stream, opts) 45 | case 'unsubscribe': 46 | return unsubscribe(packet, stream, opts) 47 | case 'unsuback': 48 | return unsuback(packet, stream, opts) 49 | case 'pingreq': 50 | case 'pingresp': 51 | return emptyPacket(packet, stream, opts) 52 | case 'disconnect': 53 | return disconnect(packet, stream, opts) 54 | case 'auth': 55 | return auth(packet, stream, opts) 56 | default: 57 | stream.destroy(new Error('Unknown command')) 58 | return false 59 | } 60 | } 61 | /** 62 | * Controls numbers cache. 63 | * Set to "false" to allocate buffers on-the-flight instead of pre-generated cache 64 | */ 65 | Object.defineProperty(generate, 'cacheNumbers', { 66 | get () { 67 | return writeNumber === writeNumberCached 68 | }, 69 | set (value) { 70 | if (value) { 71 | if (!numCache || Object.keys(numCache).length === 0) toGenerate = true 72 | writeNumber = writeNumberCached 73 | } else { 74 | toGenerate = false 75 | writeNumber = writeNumberGenerated 76 | } 77 | } 78 | }) 79 | 80 | function uncork (stream) { 81 | stream.uncork() 82 | } 83 | 84 | function connect (packet, stream, opts) { 85 | const settings = packet || {} 86 | const protocolId = settings.protocolId || 'MQTT' 87 | let protocolVersion = settings.protocolVersion || 4 88 | const will = settings.will 89 | let clean = settings.clean 90 | const keepalive = settings.keepalive || 0 91 | const clientId = settings.clientId || '' 92 | const username = settings.username 93 | const password = settings.password 94 | /* mqtt5 new oprions */ 95 | const properties = settings.properties 96 | 97 | if (clean === undefined) clean = true 98 | 99 | let length = 0 100 | 101 | // Must be a string and non-falsy 102 | if (!protocolId || 103 | (typeof protocolId !== 'string' && !Buffer.isBuffer(protocolId))) { 104 | stream.destroy(new Error('Invalid protocolId')) 105 | return false 106 | } else length += protocolId.length + 2 107 | 108 | // Must be 3 or 4 or 5 109 | if (protocolVersion !== 3 && protocolVersion !== 4 && protocolVersion !== 5) { 110 | stream.destroy(new Error('Invalid protocol version')) 111 | return false 112 | } else length += 1 113 | 114 | // ClientId might be omitted in 3.1.1 and 5, but only if cleanSession is set to 1 115 | if ((typeof clientId === 'string' || Buffer.isBuffer(clientId)) && 116 | (clientId || protocolVersion >= 4) && (clientId || clean)) { 117 | length += Buffer.byteLength(clientId) + 2 118 | } else { 119 | if (protocolVersion < 4) { 120 | stream.destroy(new Error('clientId must be supplied before 3.1.1')) 121 | return false 122 | } 123 | if ((clean * 1) === 0) { 124 | stream.destroy(new Error('clientId must be given if cleanSession set to 0')) 125 | return false 126 | } 127 | } 128 | 129 | // Must be a two byte number 130 | if (typeof keepalive !== 'number' || 131 | keepalive < 0 || 132 | keepalive > 65535 || 133 | keepalive % 1 !== 0) { 134 | stream.destroy(new Error('Invalid keepalive')) 135 | return false 136 | } else length += 2 137 | 138 | // Connect flags 139 | length += 1 140 | 141 | let propertiesData 142 | let willProperties 143 | 144 | // Properties 145 | if (protocolVersion === 5) { 146 | propertiesData = getProperties(stream, properties) 147 | if (!propertiesData) { return false } 148 | length += propertiesData.length 149 | } 150 | 151 | // If will exists... 152 | if (will) { 153 | // It must be an object 154 | if (typeof will !== 'object') { 155 | stream.destroy(new Error('Invalid will')) 156 | return false 157 | } 158 | // It must have topic typeof string 159 | if (!will.topic || typeof will.topic !== 'string') { 160 | stream.destroy(new Error('Invalid will topic')) 161 | return false 162 | } else { 163 | length += Buffer.byteLength(will.topic) + 2 164 | } 165 | 166 | // Payload 167 | length += 2 // payload length 168 | if (will.payload) { 169 | if (will.payload.length >= 0) { 170 | if (typeof will.payload === 'string') { 171 | length += Buffer.byteLength(will.payload) 172 | } else { 173 | length += will.payload.length 174 | } 175 | } else { 176 | stream.destroy(new Error('Invalid will payload')) 177 | return false 178 | } 179 | } 180 | // will properties 181 | willProperties = {} 182 | if (protocolVersion === 5) { 183 | willProperties = getProperties(stream, will.properties) 184 | if (!willProperties) { return false } 185 | length += willProperties.length 186 | } 187 | } 188 | 189 | // Username 190 | let providedUsername = false 191 | if (username != null) { 192 | if (isStringOrBuffer(username)) { 193 | providedUsername = true 194 | length += Buffer.byteLength(username) + 2 195 | } else { 196 | stream.destroy(new Error('Invalid username')) 197 | return false 198 | } 199 | } 200 | 201 | // Password 202 | if (password != null) { 203 | if (!providedUsername) { 204 | stream.destroy(new Error('Username is required to use password')) 205 | return false 206 | } 207 | 208 | if (isStringOrBuffer(password)) { 209 | length += byteLength(password) + 2 210 | } else { 211 | stream.destroy(new Error('Invalid password')) 212 | return false 213 | } 214 | } 215 | 216 | // Generate header 217 | stream.write(protocol.CONNECT_HEADER) 218 | 219 | // Generate length 220 | writeVarByteInt(stream, length) 221 | 222 | // Generate protocol ID 223 | writeStringOrBuffer(stream, protocolId) 224 | 225 | if (settings.bridgeMode) { 226 | protocolVersion += 128 227 | } 228 | 229 | stream.write( 230 | protocolVersion === 131 231 | ? protocol.VERSION131 232 | : protocolVersion === 132 233 | ? protocol.VERSION132 234 | : protocolVersion === 4 235 | ? protocol.VERSION4 236 | : protocolVersion === 5 237 | ? protocol.VERSION5 238 | : protocol.VERSION3 239 | ) 240 | 241 | // Connect flags 242 | let flags = 0 243 | flags |= (username != null) ? protocol.USERNAME_MASK : 0 244 | flags |= (password != null) ? protocol.PASSWORD_MASK : 0 245 | flags |= (will && will.retain) ? protocol.WILL_RETAIN_MASK : 0 246 | flags |= (will && will.qos) ? will.qos << protocol.WILL_QOS_SHIFT : 0 247 | flags |= will ? protocol.WILL_FLAG_MASK : 0 248 | flags |= clean ? protocol.CLEAN_SESSION_MASK : 0 249 | 250 | stream.write(Buffer.from([flags])) 251 | 252 | // Keepalive 253 | writeNumber(stream, keepalive) 254 | 255 | // Properties 256 | if (protocolVersion === 5) { 257 | propertiesData.write() 258 | } 259 | 260 | // Client ID 261 | writeStringOrBuffer(stream, clientId) 262 | 263 | // Will 264 | if (will) { 265 | if (protocolVersion === 5) { 266 | willProperties.write() 267 | } 268 | writeString(stream, will.topic) 269 | writeStringOrBuffer(stream, will.payload) 270 | } 271 | 272 | // Username and password 273 | if (username != null) { 274 | writeStringOrBuffer(stream, username) 275 | } 276 | if (password != null) { 277 | writeStringOrBuffer(stream, password) 278 | } 279 | // This is a small packet that happens only once on a stream 280 | // We assume the stream is always free to receive more data after this 281 | return true 282 | } 283 | 284 | function connack (packet, stream, opts) { 285 | const version = opts ? opts.protocolVersion : 4 286 | const settings = packet || {} 287 | const rc = version === 5 ? settings.reasonCode : settings.returnCode 288 | const properties = settings.properties 289 | let length = 2 // length of rc and sessionHeader 290 | 291 | // Check return code 292 | if (typeof rc !== 'number') { 293 | stream.destroy(new Error('Invalid return code')) 294 | return false 295 | } 296 | // mqtt5 properties 297 | let propertiesData = null 298 | if (version === 5) { 299 | propertiesData = getProperties(stream, properties) 300 | if (!propertiesData) { return false } 301 | length += propertiesData.length 302 | } 303 | 304 | stream.write(protocol.CONNACK_HEADER) 305 | // length 306 | writeVarByteInt(stream, length) 307 | stream.write(settings.sessionPresent ? protocol.SESSIONPRESENT_HEADER : zeroBuf) 308 | 309 | stream.write(Buffer.from([rc])) 310 | if (propertiesData != null) { 311 | propertiesData.write() 312 | } 313 | return true 314 | } 315 | 316 | function publish (packet, stream, opts) { 317 | debug('publish: packet: %o', packet) 318 | const version = opts ? opts.protocolVersion : 4 319 | const settings = packet || {} 320 | const qos = settings.qos || 0 321 | const retain = settings.retain ? protocol.RETAIN_MASK : 0 322 | const topic = settings.topic 323 | const payload = settings.payload || empty 324 | const id = settings.messageId 325 | const properties = settings.properties 326 | 327 | let length = 0 328 | 329 | // Topic must be a non-empty string or Buffer 330 | if (typeof topic === 'string') length += Buffer.byteLength(topic) + 2 331 | else if (Buffer.isBuffer(topic)) length += topic.length + 2 332 | else { 333 | stream.destroy(new Error('Invalid topic')) 334 | return false 335 | } 336 | 337 | // Get the payload length 338 | if (!Buffer.isBuffer(payload)) length += Buffer.byteLength(payload) 339 | else length += payload.length 340 | 341 | // Message ID must a number if qos > 0 342 | if (qos && typeof id !== 'number') { 343 | stream.destroy(new Error('Invalid messageId')) 344 | return false 345 | } else if (qos) length += 2 346 | 347 | // mqtt5 properties 348 | let propertiesData = null 349 | if (version === 5) { 350 | propertiesData = getProperties(stream, properties) 351 | if (!propertiesData) { return false } 352 | length += propertiesData.length 353 | } 354 | 355 | // Header 356 | stream.write(protocol.PUBLISH_HEADER[qos][settings.dup ? 1 : 0][retain ? 1 : 0]) 357 | 358 | // Remaining length 359 | writeVarByteInt(stream, length) 360 | 361 | // Topic 362 | writeNumber(stream, byteLength(topic)) 363 | stream.write(topic) 364 | 365 | // Message ID 366 | if (qos > 0) writeNumber(stream, id) 367 | 368 | // Properties 369 | if (propertiesData != null) { 370 | propertiesData.write() 371 | } 372 | 373 | // Payload 374 | debug('publish: payload: %o', payload) 375 | return stream.write(payload) 376 | } 377 | 378 | /* Puback, pubrec, pubrel and pubcomp */ 379 | function confirmation (packet, stream, opts) { 380 | const version = opts ? opts.protocolVersion : 4 381 | const settings = packet || {} 382 | const type = settings.cmd || 'puback' 383 | const id = settings.messageId 384 | const dup = (settings.dup && type === 'pubrel') ? protocol.DUP_MASK : 0 385 | let qos = 0 386 | const reasonCode = settings.reasonCode 387 | const properties = settings.properties 388 | let length = version === 5 ? 3 : 2 389 | 390 | if (type === 'pubrel') qos = 1 391 | 392 | // Check message ID 393 | if (typeof id !== 'number') { 394 | stream.destroy(new Error('Invalid messageId')) 395 | return false 396 | } 397 | 398 | // properies mqtt 5 399 | let propertiesData = null 400 | if (version === 5) { 401 | // Confirm should not add empty property length with no properties (rfc 3.4.2.2.1) 402 | if (typeof properties === 'object') { 403 | propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) 404 | if (!propertiesData) { return false } 405 | length += propertiesData.length 406 | } 407 | } 408 | 409 | // Header 410 | stream.write(protocol.ACKS[type][qos][dup][0]) 411 | 412 | // 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]. 413 | if (length === 3) length += reasonCode !== 0 ? 1 : -1 414 | writeVarByteInt(stream, length) 415 | 416 | // Message ID 417 | writeNumber(stream, id) 418 | 419 | // reason code in header - but only if it couldn't be omitted - indicated by length !== 2. 420 | if (version === 5 && length !== 2) { 421 | stream.write(Buffer.from([reasonCode])) 422 | } 423 | 424 | // properties mqtt 5 425 | if (propertiesData !== null) { 426 | propertiesData.write() 427 | } else { 428 | if (length === 4) { 429 | // we have no properties but have written a reason code - so we need to indicate empty properties by filling in a zero. 430 | stream.write(Buffer.from([0])) 431 | } 432 | } 433 | return true 434 | } 435 | 436 | function subscribe (packet, stream, opts) { 437 | debug('subscribe: packet: ') 438 | const version = opts ? opts.protocolVersion : 4 439 | const settings = packet || {} 440 | const dup = settings.dup ? protocol.DUP_MASK : 0 441 | const id = settings.messageId 442 | const subs = settings.subscriptions 443 | const properties = settings.properties 444 | 445 | let length = 0 446 | 447 | // Check message ID 448 | if (typeof id !== 'number') { 449 | stream.destroy(new Error('Invalid messageId')) 450 | return false 451 | } else length += 2 452 | 453 | // properies mqtt 5 454 | let propertiesData = null 455 | if (version === 5) { 456 | propertiesData = getProperties(stream, properties) 457 | if (!propertiesData) { return false } 458 | length += propertiesData.length 459 | } 460 | 461 | // Check subscriptions 462 | if (typeof subs === 'object' && subs.length) { 463 | for (let i = 0; i < subs.length; i += 1) { 464 | const itopic = subs[i].topic 465 | const iqos = subs[i].qos 466 | 467 | if (typeof itopic !== 'string') { 468 | stream.destroy(new Error('Invalid subscriptions - invalid topic')) 469 | return false 470 | } 471 | if (typeof iqos !== 'number') { 472 | stream.destroy(new Error('Invalid subscriptions - invalid qos')) 473 | return false 474 | } 475 | 476 | if (version === 5) { 477 | const nl = subs[i].nl || false 478 | if (typeof nl !== 'boolean') { 479 | stream.destroy(new Error('Invalid subscriptions - invalid No Local')) 480 | return false 481 | } 482 | const rap = subs[i].rap || false 483 | if (typeof rap !== 'boolean') { 484 | stream.destroy(new Error('Invalid subscriptions - invalid Retain as Published')) 485 | return false 486 | } 487 | const rh = subs[i].rh || 0 488 | if (typeof rh !== 'number' || rh > 2) { 489 | stream.destroy(new Error('Invalid subscriptions - invalid Retain Handling')) 490 | return false 491 | } 492 | } 493 | 494 | length += Buffer.byteLength(itopic) + 2 + 1 495 | } 496 | } else { 497 | stream.destroy(new Error('Invalid subscriptions')) 498 | return false 499 | } 500 | 501 | // Generate header 502 | debug('subscribe: writing to stream: %o', protocol.SUBSCRIBE_HEADER) 503 | stream.write(protocol.SUBSCRIBE_HEADER[1][dup ? 1 : 0][0]) 504 | 505 | // Generate length 506 | writeVarByteInt(stream, length) 507 | 508 | // Generate message ID 509 | writeNumber(stream, id) 510 | 511 | // properies mqtt 5 512 | if (propertiesData !== null) { 513 | propertiesData.write() 514 | } 515 | 516 | let result = true 517 | 518 | // Generate subs 519 | for (const sub of subs) { 520 | const jtopic = sub.topic 521 | const jqos = sub.qos 522 | const jnl = +sub.nl 523 | const jrap = +sub.rap 524 | const jrh = sub.rh 525 | let joptions 526 | 527 | // Write topic string 528 | writeString(stream, jtopic) 529 | 530 | // options process 531 | joptions = protocol.SUBSCRIBE_OPTIONS_QOS[jqos] 532 | if (version === 5) { 533 | joptions |= jnl ? protocol.SUBSCRIBE_OPTIONS_NL : 0 534 | joptions |= jrap ? protocol.SUBSCRIBE_OPTIONS_RAP : 0 535 | joptions |= jrh ? protocol.SUBSCRIBE_OPTIONS_RH[jrh] : 0 536 | } 537 | // Write options 538 | result = stream.write(Buffer.from([joptions])) 539 | } 540 | 541 | return result 542 | } 543 | 544 | function suback (packet, stream, opts) { 545 | const version = opts ? opts.protocolVersion : 4 546 | const settings = packet || {} 547 | const id = settings.messageId 548 | const granted = settings.granted 549 | const properties = settings.properties 550 | let length = 0 551 | 552 | // Check message ID 553 | if (typeof id !== 'number') { 554 | stream.destroy(new Error('Invalid messageId')) 555 | return false 556 | } else length += 2 557 | 558 | // Check granted qos vector 559 | if (typeof granted === 'object' && granted.length) { 560 | for (let i = 0; i < granted.length; i += 1) { 561 | if (typeof granted[i] !== 'number') { 562 | stream.destroy(new Error('Invalid qos vector')) 563 | return false 564 | } 565 | length += 1 566 | } 567 | } else { 568 | stream.destroy(new Error('Invalid qos vector')) 569 | return false 570 | } 571 | 572 | // properies mqtt 5 573 | let propertiesData = null 574 | if (version === 5) { 575 | propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) 576 | if (!propertiesData) { return false } 577 | length += propertiesData.length 578 | } 579 | 580 | // header 581 | stream.write(protocol.SUBACK_HEADER) 582 | 583 | // Length 584 | writeVarByteInt(stream, length) 585 | 586 | // Message ID 587 | writeNumber(stream, id) 588 | 589 | // properies mqtt 5 590 | if (propertiesData !== null) { 591 | propertiesData.write() 592 | } 593 | 594 | return stream.write(Buffer.from(granted)) 595 | } 596 | 597 | function unsubscribe (packet, stream, opts) { 598 | const version = opts ? opts.protocolVersion : 4 599 | const settings = packet || {} 600 | const id = settings.messageId 601 | const dup = settings.dup ? protocol.DUP_MASK : 0 602 | const unsubs = settings.unsubscriptions 603 | const properties = settings.properties 604 | 605 | let length = 0 606 | 607 | // Check message ID 608 | if (typeof id !== 'number') { 609 | stream.destroy(new Error('Invalid messageId')) 610 | return false 611 | } else { 612 | length += 2 613 | } 614 | // Check unsubs 615 | if (typeof unsubs === 'object' && unsubs.length) { 616 | for (let i = 0; i < unsubs.length; i += 1) { 617 | if (typeof unsubs[i] !== 'string') { 618 | stream.destroy(new Error('Invalid unsubscriptions')) 619 | return false 620 | } 621 | length += Buffer.byteLength(unsubs[i]) + 2 622 | } 623 | } else { 624 | stream.destroy(new Error('Invalid unsubscriptions')) 625 | return false 626 | } 627 | // properies mqtt 5 628 | let propertiesData = null 629 | if (version === 5) { 630 | propertiesData = getProperties(stream, properties) 631 | if (!propertiesData) { return false } 632 | length += propertiesData.length 633 | } 634 | 635 | // Header 636 | stream.write(protocol.UNSUBSCRIBE_HEADER[1][dup ? 1 : 0][0]) 637 | 638 | // Length 639 | writeVarByteInt(stream, length) 640 | 641 | // Message ID 642 | writeNumber(stream, id) 643 | 644 | // properies mqtt 5 645 | if (propertiesData !== null) { 646 | propertiesData.write() 647 | } 648 | 649 | // Unsubs 650 | let result = true 651 | for (let j = 0; j < unsubs.length; j++) { 652 | result = writeString(stream, unsubs[j]) 653 | } 654 | 655 | return result 656 | } 657 | 658 | function unsuback (packet, stream, opts) { 659 | const version = opts ? opts.protocolVersion : 4 660 | const settings = packet || {} 661 | const id = settings.messageId 662 | const dup = settings.dup ? protocol.DUP_MASK : 0 663 | const granted = settings.granted 664 | const properties = settings.properties 665 | const type = settings.cmd 666 | const qos = 0 667 | 668 | let length = 2 669 | 670 | // Check message ID 671 | if (typeof id !== 'number') { 672 | stream.destroy(new Error('Invalid messageId')) 673 | return false 674 | } 675 | 676 | // Check granted 677 | if (version === 5) { 678 | if (typeof granted === 'object' && granted.length) { 679 | for (let i = 0; i < granted.length; i += 1) { 680 | if (typeof granted[i] !== 'number') { 681 | stream.destroy(new Error('Invalid qos vector')) 682 | return false 683 | } 684 | length += 1 685 | } 686 | } else { 687 | stream.destroy(new Error('Invalid qos vector')) 688 | return false 689 | } 690 | } 691 | 692 | // properies mqtt 5 693 | let propertiesData = null 694 | if (version === 5) { 695 | propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) 696 | if (!propertiesData) { return false } 697 | length += propertiesData.length 698 | } 699 | 700 | // Header 701 | stream.write(protocol.ACKS[type][qos][dup][0]) 702 | 703 | // Length 704 | writeVarByteInt(stream, length) 705 | 706 | // Message ID 707 | writeNumber(stream, id) 708 | 709 | // properies mqtt 5 710 | if (propertiesData !== null) { 711 | propertiesData.write() 712 | } 713 | 714 | // payload 715 | if (version === 5) { 716 | stream.write(Buffer.from(granted)) 717 | } 718 | return true 719 | } 720 | 721 | function emptyPacket (packet, stream, opts) { 722 | return stream.write(protocol.EMPTY[packet.cmd]) 723 | } 724 | 725 | function disconnect (packet, stream, opts) { 726 | const version = opts ? opts.protocolVersion : 4 727 | const settings = packet || {} 728 | const reasonCode = settings.reasonCode 729 | const properties = settings.properties 730 | let length = version === 5 ? 1 : 0 731 | 732 | // properies mqtt 5 733 | let propertiesData = null 734 | if (version === 5) { 735 | propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) 736 | if (!propertiesData) { return false } 737 | length += propertiesData.length 738 | } 739 | 740 | // Header 741 | stream.write(Buffer.from([protocol.codes.disconnect << 4])) 742 | 743 | // Length 744 | writeVarByteInt(stream, length) 745 | 746 | // reason code in header 747 | if (version === 5) { 748 | stream.write(Buffer.from([reasonCode])) 749 | } 750 | 751 | // properies mqtt 5 752 | if (propertiesData !== null) { 753 | propertiesData.write() 754 | } 755 | 756 | return true 757 | } 758 | 759 | function auth (packet, stream, opts) { 760 | const version = opts ? opts.protocolVersion : 4 761 | const settings = packet || {} 762 | const reasonCode = settings.reasonCode 763 | const properties = settings.properties 764 | let length = version === 5 ? 1 : 0 765 | 766 | if (version !== 5) stream.destroy(new Error('Invalid mqtt version for auth packet')) 767 | 768 | // properies mqtt 5 769 | const propertiesData = getPropertiesByMaximumPacketSize(stream, properties, opts, length) 770 | if (!propertiesData) { return false } 771 | length += propertiesData.length 772 | 773 | // Header 774 | stream.write(Buffer.from([protocol.codes.auth << 4])) 775 | 776 | // Length 777 | writeVarByteInt(stream, length) 778 | 779 | // reason code in header 780 | stream.write(Buffer.from([reasonCode])) 781 | 782 | // properies mqtt 5 783 | if (propertiesData !== null) { 784 | propertiesData.write() 785 | } 786 | return true 787 | } 788 | 789 | /** 790 | * writeVarByteInt - write an MQTT style variable byte integer to the buffer 791 | * 792 | * @param buffer - destination 793 | * @param pos - offset 794 | * @param length - length (>0) 795 | * @returns number of bytes written 796 | * 797 | * @api private 798 | */ 799 | 800 | const varByteIntCache = {} 801 | function writeVarByteInt (stream, num) { 802 | if (num > protocol.VARBYTEINT_MAX) { 803 | stream.destroy(new Error(`Invalid variable byte integer: ${num}`)) 804 | return false 805 | } 806 | 807 | let buffer = varByteIntCache[num] 808 | 809 | if (!buffer) { 810 | buffer = genBufVariableByteInt(num) 811 | if (num < 16384) varByteIntCache[num] = buffer 812 | } 813 | debug('writeVarByteInt: writing to stream: %o', buffer) 814 | return stream.write(buffer) 815 | } 816 | 817 | /** 818 | * writeString - write a utf8 string to the buffer 819 | * 820 | * @param buffer - destination 821 | * @param pos - offset 822 | * @param string - string to write 823 | * @return number of bytes written 824 | * 825 | * @api private 826 | */ 827 | 828 | function writeString (stream, string) { 829 | const strlen = Buffer.byteLength(string) 830 | writeNumber(stream, strlen) 831 | 832 | debug('writeString: %s', string) 833 | return stream.write(string, 'utf8') 834 | } 835 | 836 | /** 837 | * writeStringPair - write a utf8 string pairs to the buffer 838 | * 839 | * @param buffer - destination 840 | * @param name - string name to write 841 | * @param value - string value to write 842 | * @return number of bytes written 843 | * 844 | * @api private 845 | */ 846 | function writeStringPair (stream, name, value) { 847 | writeString(stream, name) 848 | writeString(stream, value) 849 | } 850 | 851 | /** 852 | * writeNumber - write a two byte number to the buffer 853 | * 854 | * @param buffer - destination 855 | * @param pos - offset 856 | * @param number - number to write 857 | * @return number of bytes written 858 | * 859 | * @api private 860 | */ 861 | function writeNumberCached (stream, number) { 862 | debug('writeNumberCached: number: %d', number) 863 | debug('writeNumberCached: %o', numCache[number]) 864 | return stream.write(numCache[number]) 865 | } 866 | function writeNumberGenerated (stream, number) { 867 | const generatedNumber = generateNumber(number) 868 | debug('writeNumberGenerated: %o', generatedNumber) 869 | return stream.write(generatedNumber) 870 | } 871 | function write4ByteNumber (stream, number) { 872 | const generated4ByteBuffer = generate4ByteBuffer(number) 873 | debug('write4ByteNumber: %o', generated4ByteBuffer) 874 | return stream.write(generated4ByteBuffer) 875 | } 876 | /** 877 | * writeStringOrBuffer - write a String or Buffer with the its length prefix 878 | * 879 | * @param buffer - destination 880 | * @param pos - offset 881 | * @param toWrite - String or Buffer 882 | * @return number of bytes written 883 | */ 884 | function writeStringOrBuffer (stream, toWrite) { 885 | if (typeof toWrite === 'string') { 886 | writeString(stream, toWrite) 887 | } else if (toWrite) { 888 | writeNumber(stream, toWrite.length) 889 | stream.write(toWrite) 890 | } else writeNumber(stream, 0) 891 | } 892 | 893 | function getProperties (stream, properties) { 894 | /* connect properties */ 895 | if (typeof properties !== 'object' || properties.length != null) { 896 | return { 897 | length: 1, 898 | write () { 899 | writeProperties(stream, {}, 0) 900 | } 901 | } 902 | } 903 | let propertiesLength = 0 904 | function getLengthProperty (name, value) { 905 | const type = protocol.propertiesTypes[name] 906 | let length = 0 907 | switch (type) { 908 | case 'byte': { 909 | if (typeof value !== 'boolean') { 910 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 911 | return false 912 | } 913 | length += 1 + 1 914 | break 915 | } 916 | case 'int8': { 917 | if (typeof value !== 'number' || value < 0 || value > 0xff) { 918 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 919 | return false 920 | } 921 | length += 1 + 1 922 | break 923 | } 924 | case 'binary': { 925 | if (value && value === null) { 926 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 927 | return false 928 | } 929 | length += 1 + Buffer.byteLength(value) + 2 930 | break 931 | } 932 | case 'int16': { 933 | if (typeof value !== 'number' || value < 0 || value > 0xffff) { 934 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 935 | return false 936 | } 937 | length += 1 + 2 938 | break 939 | } 940 | case 'int32': { 941 | if (typeof value !== 'number' || value < 0 || value > 0xffffffff) { 942 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 943 | return false 944 | } 945 | length += 1 + 4 946 | break 947 | } 948 | case 'var': { 949 | // var byte integer is max 24 bits packed in 32 bits 950 | if (typeof value !== 'number' || value < 0 || value > 0x0fffffff) { 951 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 952 | return false 953 | } 954 | length += 1 + Buffer.byteLength(genBufVariableByteInt(value)) 955 | break 956 | } 957 | case 'string': { 958 | if (typeof value !== 'string') { 959 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 960 | return false 961 | } 962 | length += 1 + 2 + Buffer.byteLength(value.toString()) 963 | break 964 | } 965 | case 'pair': { 966 | if (typeof value !== 'object') { 967 | stream.destroy(new Error(`Invalid ${name}: ${value}`)) 968 | return false 969 | } 970 | length += Object.getOwnPropertyNames(value).reduce((result, name) => { 971 | const currentValue = value[name] 972 | if (Array.isArray(currentValue)) { 973 | result += currentValue.reduce((currentLength, value) => { 974 | currentLength += 1 + 2 + Buffer.byteLength(name.toString()) + 2 + Buffer.byteLength(value.toString()) 975 | return currentLength 976 | }, 0) 977 | } else { 978 | result += 1 + 2 + Buffer.byteLength(name.toString()) + 2 + Buffer.byteLength(value[name].toString()) 979 | } 980 | return result 981 | }, 0) 982 | break 983 | } 984 | default: { 985 | stream.destroy(new Error(`Invalid property ${name}: ${value}`)) 986 | return false 987 | } 988 | } 989 | return length 990 | } 991 | if (properties) { 992 | for (const propName in properties) { 993 | let propLength = 0 994 | let propValueLength = 0 995 | const propValue = properties[propName] 996 | if (propValue === undefined) { 997 | continue 998 | } else if (Array.isArray(propValue)) { 999 | for (let valueIndex = 0; valueIndex < propValue.length; valueIndex++) { 1000 | propValueLength = getLengthProperty(propName, propValue[valueIndex]) 1001 | if (!propValueLength) { return false } 1002 | propLength += propValueLength 1003 | } 1004 | } else { 1005 | propValueLength = getLengthProperty(propName, propValue) 1006 | if (!propValueLength) { return false } 1007 | propLength = propValueLength 1008 | } 1009 | if (!propLength) return false 1010 | propertiesLength += propLength 1011 | } 1012 | } 1013 | const propertiesLengthLength = Buffer.byteLength(genBufVariableByteInt(propertiesLength)) 1014 | 1015 | return { 1016 | length: propertiesLengthLength + propertiesLength, 1017 | write () { 1018 | writeProperties(stream, properties, propertiesLength) 1019 | } 1020 | } 1021 | } 1022 | 1023 | function getPropertiesByMaximumPacketSize (stream, properties, opts, length) { 1024 | const mayEmptyProps = ['reasonString', 'userProperties'] 1025 | const maximumPacketSize = opts && opts.properties && opts.properties.maximumPacketSize ? opts.properties.maximumPacketSize : 0 1026 | 1027 | let propertiesData = getProperties(stream, properties) 1028 | if (maximumPacketSize) { 1029 | while (length + propertiesData.length > maximumPacketSize) { 1030 | const currentMayEmptyProp = mayEmptyProps.shift() 1031 | if (currentMayEmptyProp && properties[currentMayEmptyProp]) { 1032 | delete properties[currentMayEmptyProp] 1033 | propertiesData = getProperties(stream, properties) 1034 | } else { 1035 | return false 1036 | } 1037 | } 1038 | } 1039 | return propertiesData 1040 | } 1041 | 1042 | function writeProperty (stream, propName, value) { 1043 | const type = protocol.propertiesTypes[propName] 1044 | switch (type) { 1045 | case 'byte': { 1046 | stream.write(Buffer.from([protocol.properties[propName]])) 1047 | stream.write(Buffer.from([+value])) 1048 | break 1049 | } 1050 | case 'int8': { 1051 | stream.write(Buffer.from([protocol.properties[propName]])) 1052 | stream.write(Buffer.from([value])) 1053 | break 1054 | } 1055 | case 'binary': { 1056 | stream.write(Buffer.from([protocol.properties[propName]])) 1057 | writeStringOrBuffer(stream, value) 1058 | break 1059 | } 1060 | case 'int16': { 1061 | stream.write(Buffer.from([protocol.properties[propName]])) 1062 | writeNumber(stream, value) 1063 | break 1064 | } 1065 | case 'int32': { 1066 | stream.write(Buffer.from([protocol.properties[propName]])) 1067 | write4ByteNumber(stream, value) 1068 | break 1069 | } 1070 | case 'var': { 1071 | stream.write(Buffer.from([protocol.properties[propName]])) 1072 | writeVarByteInt(stream, value) 1073 | break 1074 | } 1075 | case 'string': { 1076 | stream.write(Buffer.from([protocol.properties[propName]])) 1077 | writeString(stream, value) 1078 | break 1079 | } 1080 | case 'pair': { 1081 | Object.getOwnPropertyNames(value).forEach(name => { 1082 | const currentValue = value[name] 1083 | if (Array.isArray(currentValue)) { 1084 | currentValue.forEach(value => { 1085 | stream.write(Buffer.from([protocol.properties[propName]])) 1086 | writeStringPair(stream, name.toString(), value.toString()) 1087 | }) 1088 | } else { 1089 | stream.write(Buffer.from([protocol.properties[propName]])) 1090 | writeStringPair(stream, name.toString(), currentValue.toString()) 1091 | } 1092 | }) 1093 | break 1094 | } 1095 | default: { 1096 | stream.destroy(new Error(`Invalid property ${propName} value: ${value}`)) 1097 | return false 1098 | } 1099 | } 1100 | } 1101 | 1102 | function writeProperties (stream, properties, propertiesLength) { 1103 | /* write properties to stream */ 1104 | writeVarByteInt(stream, propertiesLength) 1105 | for (const propName in properties) { 1106 | if (Object.prototype.hasOwnProperty.call(properties, propName) && properties[propName] != null) { 1107 | const value = properties[propName] 1108 | if (Array.isArray(value)) { 1109 | for (let valueIndex = 0; valueIndex < value.length; valueIndex++) { 1110 | writeProperty(stream, propName, value[valueIndex]) 1111 | } 1112 | } else { 1113 | writeProperty(stream, propName, value) 1114 | } 1115 | } 1116 | } 1117 | } 1118 | 1119 | function byteLength (bufOrString) { 1120 | if (!bufOrString) return 0 1121 | else if (bufOrString instanceof Buffer) return bufOrString.length 1122 | else return Buffer.byteLength(bufOrString) 1123 | } 1124 | 1125 | function isStringOrBuffer (field) { 1126 | return typeof field === 'string' || field instanceof Buffer 1127 | } 1128 | 1129 | module.exports = generate 1130 | --------------------------------------------------------------------------------