From 5c87b1b835d2197eb587795e52b2b4c0a670367d Mon Sep 17 00:00:00 2001 From: Evan Summers Date: Tue, 22 Oct 2019 10:11:06 +0200 Subject: [PATCH 001/110] Correct "nedbb" typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 389ad6e2c..677562b48 100644 --- a/README.md +++ b/README.md @@ -485,7 +485,7 @@ Other implementations of `mqtt.Store`: * [mqtt-level-store](http://npm.im/mqtt-level-store) which uses [Level-browserify](http://npm.im/level-browserify) to store the inflight data, making it usable both in Node and the Browser. -* [mqtt-nedbb-store](https://github.com/behrad/mqtt-nedb-store) which +* [mqtt-nedb-store](https://github.com/behrad/mqtt-nedb-store) which uses [nedb](https://www.npmjs.com/package/nedb) to store the inflight data. * [mqtt-localforage-store](http://npm.im/mqtt-localforage-store) which uses From ea439b1e36b6fceadfed4ff58e8018cafaa08bbb Mon Sep 17 00:00:00 2001 From: Behnam Mohammadi Date: Fri, 31 Jan 2020 03:45:56 +0330 Subject: [PATCH 002/110] docs: minor style improvements * Update README.md * Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 677562b48..8d153f6d3 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ the `connect` event. Typically a `net.Socket`. * `queueQoSZero`: if connection is broken, queue outgoing QoS zero messages (default `true`) * `customHandleAcks`: MQTT 5 feature of custom handling puback and pubrec packets. Its callback: ```js - customHandleAcks: function(topic, message, packet, done) {*some logic wit colling done(error, reasonCode)*} + customHandleAcks: function(topic, message, packet, done) {/*some logic wit colling done(error, reasonCode)*/} ``` * `properties`: properties MQTT 5.0. `object` that supports the following properties: @@ -383,7 +383,7 @@ Subscribe to a topic or topics keys the topic name and as value the QoS, like `{'test1': {qos: 0}, 'test2': {qos: 1}}`. MQTT `topic` wildcard characters are supported (`+` - for single level and `#` - for multi level) * `options` is the options to subscribe with, including: - * `qos` qos subscription level, default 0 + * `qos` QoS subscription level, default 0 * `nl` No Local MQTT 5.0 flag (If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the ClientID of the publishing connection) * `rap` Retain as Published MQTT 5.0 flag (If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If false, Application Messages forwarded using this subscription have the RETAIN flag set to 0.) * `rh` Retain Handling MQTT 5.0 (This option specifies whether retained messages are sent when the subscription is established.) @@ -395,7 +395,7 @@ Subscribe to a topic or topics * `err` a subscription error or an error that occurs when client is disconnecting * `granted` is an array of `{topic, qos}` where: * `topic` is a subscribed to topic - * `qos` is the granted qos level on it + * `qos` is the granted QoS level on it ------------------------------------------------------- From 98e9a464ac47e1d80e0499ca5c72747eb9bb193c Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 10 Feb 2020 11:05:43 -0800 Subject: [PATCH 003/110] refactor: zuul to airtap for browser testing (#1045) --- .zuul.yml => .airtaprc.yml | 3 +++ .travis.yml | 1 + package.json | 13 ++++++------- test/browser/server.js | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) rename .zuul.yml => .airtaprc.yml (74%) diff --git a/.zuul.yml b/.airtaprc.yml similarity index 74% rename from .zuul.yml rename to .airtaprc.yml index 184ddd659..77d341d98 100644 --- a/.zuul.yml +++ b/.airtaprc.yml @@ -1,3 +1,4 @@ +sauce_connect: true ui: mocha-bdd browsers: - name: chrome @@ -8,3 +9,5 @@ browsers: version: latest - name: internet explorer version: latest + - name: microsoftedge + version: latest diff --git a/.travis.yml b/.travis.yml index a675fbb2d..d4f4c603d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ node_js: env: # For compiling optional extensions addons: + sauce_connect: true apt: sources: - ubuntu-toolchain-r-test diff --git a/package.json b/package.json index c4bfbc66f..53bd031d2 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", "typescript-compile-execute": "node test/typescript/*.js", "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", - "prepare": "npm run browser-build", "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js -s mqtt > dist/mqtt.js && uglifyjs < dist/mqtt.js > dist/mqtt.min.js", - "browser-test": "zuul --server test/browser/server.js --local --open test/browser/test.js", - "sauce-test": "zuul --server test/browser/server.js --tunnel ngrok -- test/browser/test.js", + "prepare": "npm run browser-build", + "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", + "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" }, "pre-commit": [ @@ -81,6 +81,7 @@ }, "devDependencies": { "@types/node": "^10.0.0", + "airtap": "^3.0.0", "browserify": "^16.2.2", "codecov": "^3.0.4", "global": "^4.3.2", @@ -89,7 +90,7 @@ "mocha": "^4.1.0", "mqtt-connection": "^4.0.0", "pre-commit": "^1.2.2", - "rimraf": "^2.6.2", + "rimraf": "^3.0.2", "safe-buffer": "^5.1.2", "should": "^13.2.1", "sinon": "~1.17.7", @@ -100,9 +101,7 @@ "tslint-config-standard": "^8.0.1", "typescript": "^3.2.2", "uglify-js": "^3.4.5", - "ws": "^3.3.3", - "zuul": "^3.12.0", - "zuul-ngrok": "^4.0.0" + "ws": "^3.3.3" }, "standard": { "env": [ diff --git a/test/browser/server.js b/test/browser/server.js index e5fab8c73..0b5e96516 100644 --- a/test/browser/server.js +++ b/test/browser/server.js @@ -122,11 +122,11 @@ function start (startPort, done) { } if (require.main === module) { - start(process.env.PORT || process.env.ZUUL_PORT, function (err) { + start(process.env.PORT || process.env.AIRTAP_PORT, function (err) { if (err) { console.error(err) return } - console.log('tunnelled server started on port', process.env.PORT || process.env.ZUUL_PORT) + console.log('tunnelled server started on port', process.env.PORT || process.env.AIRTAP_PORT) }) } From b77241c6d84c3d7f98db487eb003a2dffd24c430 Mon Sep 17 00:00:00 2001 From: Vincent Trumpff Date: Tue, 11 Feb 2020 16:44:34 +0100 Subject: [PATCH 004/110] fix various options definition for ts usage (#1043) * fix connect options will payload type Looing at mqtt-packet, 'will' payload should be a buffer * PR feedback * qos should be optional * add buffer/string + optional qos tests * try to stick to the file's coding style --- test/typescript/broker-connect-subscribe-and-publish.ts | 9 ++++++--- types/lib/client-options.d.ts | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/typescript/broker-connect-subscribe-and-publish.ts b/test/typescript/broker-connect-subscribe-and-publish.ts index e22610eb8..ecdb363cc 100644 --- a/test/typescript/broker-connect-subscribe-and-publish.ts +++ b/test/typescript/broker-connect-subscribe-and-publish.ts @@ -2,9 +2,11 @@ import {IClientOptions, Client, connect, IConnackPacket} from '../..' const BROKER = 'test.mosquitto.org' -const PAYLOAD = 'hello from TS' +const PAYLOAD_WILL = Buffer.from('bye from TS') +const PAYLOAD_QOS = Buffer.from('hello from TS (with qos=2)') +const PAYLOAD_RETAIN = 'hello from TS (with retain=true)' const TOPIC = 'typescript-test-' + Math.random().toString(16).substr(2) -const opts: IClientOptions = {} +const opts: IClientOptions = {will: {topic: TOPIC, payload: PAYLOAD_WILL, qos: 0, retain: false}} console.log(`connect(${JSON.stringify(BROKER)})`) const client:Client = connect(`mqtt://${BROKER}`, opts) @@ -13,7 +15,8 @@ client.subscribe({[TOPIC]: {qos: 2}}, (err, granted) => { granted.forEach(({topic, qos}) => { console.log(`subscribed to ${topic} with qos=${qos}`) }) - client.publish(TOPIC, PAYLOAD, {qos: 2}) + client.publish(TOPIC, PAYLOAD_QOS, {qos: 2}) + client.publish(TOPIC, PAYLOAD_RETAIN, {retain: true}) }).on('message', (topic: string, payload: Buffer) => { console.log(`message from ${topic}: ${payload}`) client.end() diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index 8c34abcfb..69bacaed6 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -80,7 +80,7 @@ export interface IClientOptions extends ISecureClientOptions { /** * the message to publish */ - payload: string + payload: Buffer | string /** * the QoS */ @@ -134,7 +134,7 @@ export interface IClientPublishOptions { /** * the QoS */ - qos: QoS + qos?: QoS /** * the retain flag */ From d764f02552639e9fed0f2b2039ca86d9941ba0cb Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Fri, 21 Feb 2020 15:49:04 -0800 Subject: [PATCH 005/110] chore: tidy up debug logs (#1052) removes concats and other tidying up --- README.md | 10 ++++++ lib/client.js | 80 ++++++++++++++++++++++++++++++++++++++++++----- lib/connect/ws.js | 13 +++++--- 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8d153f6d3..601f73c5c 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,16 @@ mqtt pub -t 'hello' -h 'test.mosquitto.org' -m 'from MQTT.js' See `mqtt help ` for the command help. + +## Debug Logs + +MQTT.js uses the [debug](https://www.npmjs.com/package/debug#cmd) package for debugging purposes. To enable debug logs, add the following environment variable on runtime : +```ps +# (example using PowerShell, the VS Code default) +$env:DEBUG='mqttjs*' + +``` + ## API diff --git a/lib/client.js b/lib/client.js index ca003bb6b..f3006de95 100644 --- a/lib/client.js +++ b/lib/client.js @@ -11,6 +11,7 @@ var inherits = require('inherits') var reInterval = require('reinterval') var validations = require('./validations') var xtend = require('xtend') +var debug = require('debug')('mqttjs:client') var setImmediate = global.setImmediate || function (callback) { // works in node v0.8 process.nextTick(callback) @@ -76,19 +77,25 @@ function defaultId () { } function sendPacket (client, packet, cb) { + debug('sendPacket: packet: %O', packet) + debug('sendPacket: emitting `packetsend`') client.emit('packetsend', packet) + debug('sendPacket: writing to stream') var result = mqttPacket.writeToStream(packet, client.stream, client.options) - + debug('sendPacket: writeToStream result %s', result) if (!result && cb) { + debug('sendPacket: handle events on `drain` once through callback.') client.stream.once('drain', cb) } else if (cb) { + debug('sendPacket: invoking cb') cb() } } function flush (queue) { if (queue) { + debug('flush: queue exists? %b', !!(queue)) Object.keys(queue).forEach(function (messageId) { if (typeof queue[messageId].cb === 'function') { queue[messageId].cb(new Error('Connection closed')) @@ -100,6 +107,7 @@ function flush (queue) { function flushVolatile (queue) { if (queue) { + debug('flushVolatile: queue exists? %s', !!(queue)) Object.keys(queue).forEach(function (messageId) { if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { queue[messageId].cb(new Error('Connection closed')) @@ -110,6 +118,7 @@ function flushVolatile (queue) { } function storeAndSend (client, packet, cb, cbStorePut) { + debug('storeAndSend: store packet with cmd: %s to outgoingStore', packet.cmd) client.outgoingStore.put(packet, function storedPacket (err) { if (err) { return cb && cb(err) @@ -119,7 +128,9 @@ function storeAndSend (client, packet, cb, cbStorePut) { }) } -function nop () {} +function nop (error) { + debug('nop hit: %o', error) +} /** * MqttClient constructor @@ -137,6 +148,7 @@ function MqttClient (streamBuilder, options) { } this.options = options || {} + debug('MqttClient: options: %o', options) // Defaults for (k in defaultConnectOptions) { @@ -196,16 +208,19 @@ function MqttClient (streamBuilder, options) { // Mark disconnected on stream close this.on('close', function () { + debug('MqttClient: close event. Mark disconnected.') this.connected = false clearTimeout(this.connackTimer) }) // Send queued packets this.on('connect', function () { + debug('MqttClient:connect') var queue = this.queue function deliver () { var entry = queue.shift() + debug('MqttClient:deliver: entry %o', entry) var packet = null if (!entry) { @@ -213,7 +228,7 @@ function MqttClient (streamBuilder, options) { } packet = entry.packet - + debug('MqttClient:deliver: call _sendPacket for %o', packet) that._sendPacket( packet, function (err) { @@ -228,12 +243,15 @@ function MqttClient (streamBuilder, options) { deliver() }) - // Clear ping timer this.on('close', function () { + debug('MqttClient:close: clear ping timer') if (that.pingTimer !== null) { that.pingTimer.clear() that.pingTimer = null } + + debug('MqttClient:close: call _setupReconnect') + this._setupReconnect() }) // Setup reconnect timer on disconnect @@ -241,6 +259,7 @@ function MqttClient (streamBuilder, options) { events.EventEmitter.call(this) + debug('MqttClient: call _setupStream') this._setupStream() } inherits(MqttClient, events.EventEmitter) @@ -258,11 +277,14 @@ MqttClient.prototype._setupStream = function () { var completeParse = null var packets = [] + debug('_setupStream: calling method to clear reconnect') this._clearReconnect() + debug('_setupStream: setting stream builder') this.stream = this.streamBuilder(this) parser.on('packet', function (packet) { + debug('parser: on packet push to packets array.') packets.push(packet) }) @@ -277,23 +299,28 @@ MqttClient.prototype._setupStream = function () { } function work () { + debug('stream:work: called') var packet = packets.shift() if (packet) { + debug('stream:work: calling _handlePacket') that._handlePacket(packet, nextTickWork) } else { var done = completeParse completeParse = null + debug('stream:work: done is %s', !!(done)) if (done) done() } } writable._write = function (buf, enc, done) { completeParse = done + debug('stream:writable:_write: parsing buffer') parser.parse(buf) work() } + debug('_setupStream: piping stream to writable') this.stream.pipe(writable) // Suppress connection errors @@ -301,11 +328,14 @@ MqttClient.prototype._setupStream = function () { // Echo stream close this.stream.on('close', function () { + debug('stream: on close') flushVolatile(that.outgoing) + debug('stream: emit close to MqttClient') that.emit('close') }) // Send a connect packet + debug('_setupStream: sending packet `connect`') connectPacket = Object.create(this.options) connectPacket.cmd = 'connect' // avoid message queue @@ -336,6 +366,7 @@ MqttClient.prototype._setupStream = function () { } MqttClient.prototype._handlePacket = function (packet, done) { + debug('_handlePacket') var options = this.options if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { @@ -343,7 +374,7 @@ MqttClient.prototype._handlePacket = function (packet, done) { this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) return this } - + debug('_handlePacket: emitting packetreceive') this.emit('packetreceive', packet) switch (packet.cmd) { @@ -413,6 +444,7 @@ MqttClient.prototype._checkDisconnecting = function (callback) { * @example client.publish('topic', 'message', console.log); */ MqttClient.prototype.publish = function (topic, message, opts, callback) { + debug('MqttClient:publish `%s` to topic `%s`', message, topic) var packet var options = this.options @@ -464,16 +496,20 @@ MqttClient.prototype.publish = function (topic, message, opts, callback) { cb: callback || nop } if (this._storeProcessing) { + debug('_storeProcessing enabled') this._packetIdsDuringStoreProcessing[packet.messageId] = false this._storePacket(packet, undefined, opts.cbStorePut) } else { + debug('MqttClient:publish: packet cmd: %s', packet.cmd) this._sendPacket(packet, undefined, opts.cbStorePut) } break default: if (this._storeProcessing) { + debug('_storeProcessing enabled') this._storePacket(packet, callback, opts.cbStorePut) } else { + debug('MqttClient:publish: packet cmd: %s', packet.cmd) this._sendPacket(packet, callback, opts.cbStorePut) } break @@ -531,6 +567,7 @@ MqttClient.prototype.subscribe = function () { } if (this._checkDisconnecting(callback)) { + debug('subscribe: discconecting true') return this } @@ -546,6 +583,7 @@ MqttClient.prototype.subscribe = function () { if (Array.isArray(obj)) { obj.forEach(function (topic) { + debug('subscribe: array topic %s', topic) if (!that._resubscribeTopics.hasOwnProperty(topic) || that._resubscribeTopics[topic].qos < opts.qos || resubscribe) { @@ -559,6 +597,7 @@ MqttClient.prototype.subscribe = function () { currentOpts.rh = opts.rh currentOpts.properties = opts.properties } + debug('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) subs.push(currentOpts) } }) @@ -566,6 +605,7 @@ MqttClient.prototype.subscribe = function () { Object .keys(obj) .forEach(function (k) { + debug('subscribe: object topic %s', k) if (!that._resubscribeTopics.hasOwnProperty(k) || that._resubscribeTopics[k].qos < obj[k].qos || resubscribe) { @@ -579,6 +619,7 @@ MqttClient.prototype.subscribe = function () { currentOpts.rh = obj[k].rh currentOpts.properties = opts.properties } + debug('subscribe: pushing `%s` to subs list', currentOpts) subs.push(currentOpts) } }) @@ -604,6 +645,7 @@ MqttClient.prototype.subscribe = function () { // subscriptions to resubscribe to in case of disconnect if (this.options.resubscribe) { + debug('subscribe: resubscribe true') var topics = [] subs.forEach(function (sub) { if (that.options.reconnectPeriod > 0) { @@ -634,7 +676,7 @@ MqttClient.prototype.subscribe = function () { callback(err, subs) } } - + debug('subscribe: calling _sendPacket') this._sendPacket(packet) return this @@ -701,6 +743,7 @@ MqttClient.prototype.unsubscribe = function () { cb: callback } + debug('unsubscribe: send packet') this._sendPacket(packet) return this @@ -716,6 +759,7 @@ MqttClient.prototype.unsubscribe = function () { * @api public */ MqttClient.prototype.end = function () { + debug('client end - close connection') var that = this var force = arguments[0] @@ -813,6 +857,7 @@ MqttClient.prototype.removeOutgoingMessage = function (mid) { * @api public */ MqttClient.prototype.reconnect = function (opts) { + debug('client reconnect') var that = this var f = function () { if (opts) { @@ -843,7 +888,9 @@ MqttClient.prototype.reconnect = function (opts) { * @api privateish */ MqttClient.prototype._reconnect = function () { + debug('_reconnect: emitting reconnect to client') this.emit('reconnect') + debug('_reconnect: calling _setupStream') this._setupStream() } @@ -851,14 +898,17 @@ MqttClient.prototype._reconnect = function () { * _setupReconnect - setup reconnect timer */ MqttClient.prototype._setupReconnect = function () { + debug('_setupReconnect') var that = this if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { if (!this.reconnecting) { + debug('_setupReconnect: emitting offline state') this.emit('offline') this.reconnecting = true } that.reconnectTimer = setInterval(function () { + debug('reconnectTimer calling _reconnect') that._reconnect() }, that.options.reconnectPeriod) } @@ -881,9 +931,11 @@ MqttClient.prototype._clearReconnect = function () { MqttClient.prototype._cleanUp = function (forced, done) { var opts = arguments[2] if (done) { + debug('_cleanUp: done callback provided for on stream close') this.stream.on('close', done) } + debug('_cleanUp: forced? %s', forced) if (forced) { if ((this.options.reconnectPeriod === 0) && this.options.clean) { flush(this.outgoing) @@ -891,6 +943,7 @@ MqttClient.prototype._cleanUp = function (forced, done) { this.stream.destroy() } else { var packet = xtend({ cmd: 'disconnect' }, opts) + debug('_cleanUp: sending disconnect packet') this._sendPacket( packet, setImmediate.bind( @@ -901,11 +954,13 @@ MqttClient.prototype._cleanUp = function (forced, done) { } if (!this.disconnecting) { + debug('_cleanUp: client not disconnecting. Clearing and resetting reconnect.') this._clearReconnect() this._setupReconnect() } if (this.pingTimer !== null) { + debug('_cleanUp: clearing pingTimer') this.pingTimer.clear() this.pingTimer = null } @@ -925,9 +980,11 @@ MqttClient.prototype._cleanUp = function (forced, done) { * @api private */ MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { + debug('_sendPacket') cbStorePut = cbStorePut || nop if (!this.connected) { + debug('_sendPacket: client not connected. Storing packet offline.') this._storePacket(packet, cb, cbStorePut) return } @@ -1046,8 +1103,8 @@ MqttClient.prototype._handlePingresp = function () { * @param {Object} packet * @api private */ - MqttClient.prototype._handleConnack = function (packet) { + debug('_handleConnack') var options = this.options var version = options.protocolVersion var rc = version === 5 ? packet.reasonCode : packet.returnCode @@ -1110,6 +1167,7 @@ default: for now i just suppressed the warnings */ MqttClient.prototype._handlePublish = function (packet, done) { + debug('_handlePublish: packet %o', packet) done = typeof done !== 'undefined' ? done : nop var topic = packet.topic.toString() var message = packet.payload @@ -1118,7 +1176,7 @@ MqttClient.prototype._handlePublish = function (packet, done) { var that = this var options = this.options var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] - + debug('_handlePublish: qos %d', qos) switch (qos) { case 2: { options.customHandleAcks(topic, message, packet, function (error, code) { @@ -1189,6 +1247,7 @@ MqttClient.prototype.handleMessage = function (packet, callback) { */ MqttClient.prototype._handleAck = function (packet) { + debug('handling ack packet') /* eslint no-fallthrough: "off" */ var mid = packet.messageId var type = packet.cmd @@ -1198,11 +1257,13 @@ MqttClient.prototype._handleAck = function (packet) { var err if (!cb) { + debug('Server sent an ack in error. Ignoring.') // Server sent an ack in error, ignore it. return } // Process + debug('ack packet of type: %s', type) switch (type) { case 'pubcomp': // same thing as puback for QoS 2 @@ -1269,6 +1330,7 @@ MqttClient.prototype._handleAck = function (packet) { * @api private */ MqttClient.prototype._handlePubrel = function (packet, callback) { + debug('handling pubrel packet') callback = typeof callback !== 'undefined' ? callback : nop var mid = packet.messageId var that = this @@ -1328,12 +1390,14 @@ MqttClient.prototype.getLastMessageId = function () { * @api private */ MqttClient.prototype._resubscribe = function (connack) { + debug('_resubscribe') var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) if (!this._firstConnection && (this.options.clean || (this.options.protocolVersion === 5 && !connack.sessionPresent)) && _resubscribeTopicsKeys.length > 0) { if (this.options.resubscribe) { if (this.options.protocolVersion === 5) { + debug('_resubscribe: protocolVersion 5') for (var topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { var resubscribeTopic = {} resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 65e25e3a3..583078efb 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,5 +1,6 @@ 'use strict' +var debug = require('debug')('mqttjs:connect:ws') var websocket = require('websocket-stream') var urlModule = require('url') var WSS_OPTIONS = [ @@ -49,6 +50,8 @@ function setDefaultOpts (opts) { } function createWebSocket (client, opts) { + debug('createWebSocket') + debug('opts: %o', opts) var websocketSubProtocol = (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) ? 'mqttv3.1' @@ -56,14 +59,16 @@ function createWebSocket (client, opts) { setDefaultOpts(opts) var url = buildUrl(opts, client) + debug('creating new Websocket for url: %s and protocol: %s', url, websocketSubProtocol) return websocket(url, [websocketSubProtocol], opts.wsOptions) } -function buildBuilder (client, opts) { +function streamBuilder (client, opts) { return createWebSocket(client, opts) } -function buildBuilderBrowser (client, opts) { +function browserStreamBuilder (client, opts) { + debug('browserStreamBuilder') if (!opts.hostname) { opts.hostname = opts.host } @@ -86,7 +91,7 @@ function buildBuilderBrowser (client, opts) { } if (IS_BROWSER) { - module.exports = buildBuilderBrowser + module.exports = browserStreamBuilder } else { - module.exports = buildBuilder + module.exports = streamBuilder } From f6534c2d8348afadc91c4d6c636447430be4642b Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 24 Feb 2020 11:15:40 -0800 Subject: [PATCH 006/110] feat: support SNI on TLS (#1055) Co-authored-by: ewan-chalmers --- lib/connect/tls.js | 1 + test/secure_client.js | 29 +++++++++++++++++++++++++++++ test/server.js | 4 +++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/connect/tls.js b/lib/connect/tls.js index eda78be51..419cedde9 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -5,6 +5,7 @@ function buildBuilder (mqttClient, opts) { var connection opts.port = opts.port || 8883 opts.host = opts.hostname || opts.host || 'localhost' + opts.servername = opts.host opts.rejectUnauthorized = opts.rejectUnauthorized !== false diff --git a/test/secure_client.js b/test/secure_client.js index 378924861..a2a6d1e7b 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -9,6 +9,7 @@ var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') var Server = require('./server') +var assert = require('chai').assert var server = new Server.SecureServer({ key: fs.readFileSync(KEY), @@ -153,5 +154,33 @@ describe('MqttSecureClient', function () { done() }) }) + + it.only('should support SNI on the TLS connection', function (done) { + var hostname, client + server.removeAllListeners('secureConnection') // clear eventHandler + server.once('secureConnection', function (tlsSocket) { // one time eventHandler + assert.equal(tlsSocket.servername, hostname) // validate SNI set + server.setupConnection(tlsSocket) + }) + + + hostname = 'localhost' + client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + host: hostname + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + server.on('secureConnection', server.setupConnection) // reset eventHandler + done() + }) + }) }) }) diff --git a/test/server.js b/test/server.js index 3baf7f16b..0c68bd766 100644 --- a/test/server.js +++ b/test/server.js @@ -8,7 +8,7 @@ var MqttServer var FastMqttServer var MqttSecureServer -function setupConnection (duplex) { +var setupConnection = function (duplex) { var that = this var connection = new Connection(duplex, function () { that.emit('client', connection) @@ -91,3 +91,5 @@ MqttSecureServer = module.exports.SecureServer = return this } inherits(MqttSecureServer, tls.Server) +MqttSecureServer.prototype.setupConnection = setupConnection + From c8ee0e2c2380b87cab4a31a0fcabaab9100d62c7 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 24 Feb 2020 11:41:57 -0800 Subject: [PATCH 007/110] fix: remove only (#1058) --- test/secure_client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/secure_client.js b/test/secure_client.js index a2a6d1e7b..9a4fd4675 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -155,7 +155,7 @@ describe('MqttSecureClient', function () { }) }) - it.only('should support SNI on the TLS connection', function (done) { + it('should support SNI on the TLS connection', function (done) { var hostname, client server.removeAllListeners('secureConnection') // clear eventHandler server.once('secureConnection', function (tlsSocket) { // one time eventHandler From 3cea393e2608e4c091f6bccdcf2d7bfd703bb98b Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 24 Feb 2020 11:45:15 -0800 Subject: [PATCH 008/110] feat: connection error handler (#1053) --- lib/client.js | 18 ++++++++++++++---- lib/connect/tcp.js | 4 ++-- package.json | 8 +++++--- test/abstract_client.js | 20 +++++++++++++++++--- test/secure_client.js | 3 +-- test/server.js | 1 - 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/client.js b/lib/client.js index f3006de95..15e645947 100644 --- a/lib/client.js +++ b/lib/client.js @@ -3,7 +3,7 @@ /** * Module dependencies */ -var events = require('events') +var EventEmitter = require('events').EventEmitter var Store = require('./store') var mqttPacket = require('mqtt-packet') var Writable = require('readable-stream').Writable @@ -257,12 +257,12 @@ function MqttClient (streamBuilder, options) { // Setup reconnect timer on disconnect this.on('close', this._setupReconnect) - events.EventEmitter.call(this) + EventEmitter.call(this) debug('MqttClient: call _setupStream') this._setupStream() } -inherits(MqttClient, events.EventEmitter) +inherits(MqttClient, EventEmitter) /** * setup the event handlers in the inner stream. @@ -320,11 +320,21 @@ MqttClient.prototype._setupStream = function () { work() } + function streamErrorHandler (error) { + debug('stream error') + if (error.code === 'ECONNREFUSED') { + // handle error + that.emit('error', error) + } else { + nop(error) + } + } + debug('_setupStream: piping stream to writable') this.stream.pipe(writable) // Suppress connection errors - this.stream.on('error', nop) + this.stream.on('error', streamErrorHandler) // Echo stream close this.stream.on('close', function () { diff --git a/lib/connect/tcp.js b/lib/connect/tcp.js index b47770db9..ac6537dca 100644 --- a/lib/connect/tcp.js +++ b/lib/connect/tcp.js @@ -5,7 +5,7 @@ var net = require('net') variables port and host can be removed since you have all required information in opts object */ -function buildBuilder (client, opts) { +function streamBuilder (client, opts) { var port, host opts.port = opts.port || 1883 opts.hostname = opts.hostname || opts.host || 'localhost' @@ -16,4 +16,4 @@ function buildBuilder (client, opts) { return net.createConnection(port, host) } -module.exports = buildBuilder +module.exports = streamBuilder diff --git a/package.json b/package.json index 53bd031d2..86dbfa76d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", - "Siarhei Buntsevich (https://github.com/scarry1992)" + "Siarhei Buntsevich (https://github.com/scarry1992)", + "Yoseph Maguire (https://github.com/YoDaMa)" ], "keywords": [ "mqtt", @@ -83,6 +84,7 @@ "@types/node": "^10.0.0", "airtap": "^3.0.0", "browserify": "^16.2.2", + "chai": "^4.2.0", "codecov": "^3.0.4", "global": "^4.3.2", "istanbul": "^0.4.5", @@ -93,14 +95,14 @@ "rimraf": "^3.0.2", "safe-buffer": "^5.1.2", "should": "^13.2.1", - "sinon": "~1.17.7", + "sinon": "^9.0.0", "snazzy": "^8.0.0", "standard": "^11.0.1", "through2": "^3.0.0", "tslint": "^5.11.0", "tslint-config-standard": "^8.0.1", "typescript": "^3.2.2", - "uglify-js": "^3.4.5", + "uglify-js": "^3.8.0", "ws": "^3.3.3" }, "standard": { diff --git a/test/abstract_client.js b/test/abstract_client.js index 017a2e377..00dcae928 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -9,6 +9,7 @@ var mqtt = require('../') var xtend = require('xtend') var Server = require('./server') var Store = require('./../lib/store') +var assert = require('chai').assert var port = 9876 module.exports = function (server, config) { @@ -336,7 +337,7 @@ module.exports = function (server, config) { }) }) - it('should emit error', function (done) { + it('should emit error on invalid clientId', function (done) { var client = connect({clientId: 'invalid'}) client.once('connect', function () { done(new Error('Should not emit connect')) @@ -349,6 +350,17 @@ module.exports = function (server, config) { }) }) + it('should emit error event if the socket refuses the connection', function (done) { + // fake a port + var client = connect({ port: 4557 }) + + client.on('error', function (e) { + assert.equal(e.code, 'ECONNREFUSED') + client.end() + done() + }) + }) + it('should have different client ids', function (done) { var client1 = connect() var client2 = connect() @@ -361,7 +373,7 @@ module.exports = function (server, config) { }) describe('handling offline states', function () { - it('should emit offline events once when the client transitions from connected states to disconnected ones', function (done) { + it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { var client = connect({reconnectPeriod: 20}) client.on('connect', function () { @@ -373,10 +385,12 @@ module.exports = function (server, config) { }) }) - it('should emit offline events once when the client (at first) can NOT connect to servers', function (done) { + it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { // fake a port var client = connect({ reconnectPeriod: 20, port: 4557 }) + client.on('error', function () {}) + client.on('offline', function () { client.end(true, done) }) diff --git a/test/secure_client.js b/test/secure_client.js index 9a4fd4675..a3a77868d 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -163,7 +163,6 @@ describe('MqttSecureClient', function () { server.setupConnection(tlsSocket) }) - hostname = 'localhost' client = mqtt.connect({ protocol: 'mqtts', @@ -172,7 +171,7 @@ describe('MqttSecureClient', function () { rejectUnauthorized: true, host: hostname }) - + client.on('error', function (err) { done(err) }) diff --git a/test/server.js b/test/server.js index 0c68bd766..c92b5800e 100644 --- a/test/server.js +++ b/test/server.js @@ -92,4 +92,3 @@ MqttSecureServer = module.exports.SecureServer = } inherits(MqttSecureServer, tls.Server) MqttSecureServer.prototype.setupConnection = setupConnection - From d8b6827a0117421ef1d2504442b314f3f3e92257 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Wed, 26 Feb 2020 10:20:33 -0800 Subject: [PATCH 009/110] chore: github actions (#1059) --- .github/workflows/nodejs.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/nodejs.yml diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 000000000..1969046d4 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,25 @@ +name: Node.js CI + +on: [pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [8.x, 10.x, 12.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + if: always() + - run: npm test + env: + CI: true \ No newline at end of file From 54316ba1a448a9016de99f206605f9bf408663cd Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Wed, 26 Feb 2020 14:50:41 -0800 Subject: [PATCH 010/110] chore: istanbul to nyc and uglify-js to uglify-es (#1061) --- .github/workflows/nodejs.yml | 4 ++-- package.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 1969046d4..410c4bc92 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -10,6 +10,7 @@ jobs: strategy: matrix: node-version: [8.x, 10.x, 12.x] + fail-fast: false steps: - uses: actions/checkout@v2 @@ -19,7 +20,6 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm install - run: npm run build --if-present - if: always() - run: npm test env: - CI: true \ No newline at end of file + CI: true diff --git a/package.json b/package.json index 86dbfa76d..0dc997aef 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "main": "mqtt.js", "types": "types/index.d.ts", "scripts": { - "test": "node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly --", + "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", "pretest": "standard | snazzy", "tslint": "if [[ \"`node -v`\" != \"v4.3.2\" ]]; then tslint types/**/*.d.ts; fi", "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", "typescript-compile-execute": "node test/typescript/*.js", "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", - "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js -s mqtt > dist/mqtt.js && uglifyjs < dist/mqtt.js > dist/mqtt.min.js", + "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && uglifyjs dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", "prepare": "npm run browser-build", "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", @@ -83,14 +83,14 @@ "devDependencies": { "@types/node": "^10.0.0", "airtap": "^3.0.0", - "browserify": "^16.2.2", + "browserify": "^16.5.0", "chai": "^4.2.0", "codecov": "^3.0.4", "global": "^4.3.2", - "istanbul": "^0.4.5", "mkdirp": "^0.5.1", "mocha": "^4.1.0", "mqtt-connection": "^4.0.0", + "nyc": "^15.0.0", "pre-commit": "^1.2.2", "rimraf": "^3.0.2", "safe-buffer": "^5.1.2", @@ -102,7 +102,7 @@ "tslint": "^5.11.0", "tslint-config-standard": "^8.0.1", "typescript": "^3.2.2", - "uglify-js": "^3.8.0", + "uglify-es": "^3.3.9", "ws": "^3.3.3" }, "standard": { From 66e295ada333eb47e0881c22c2ef6e65e852a72c Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 27 Feb 2020 11:17:48 -0800 Subject: [PATCH 011/110] chore: add master branch action (#1062) --- .github/workflows/nodejs.yml | 8 +++++++- .gitignore | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 410c4bc92..358d27aef 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,6 +1,12 @@ name: Node.js CI -on: [pull_request] +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: build: diff --git a/.gitignore b/.gitignore index 805ce7faa..5c315db7f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ npm-debug.log dist/ yarn.lock coverage +.nyc_output .idea/* test/typescript/.idea/* test/typescript/*.js From 2e46e08396f7a854ff53454bd0fa1f1d96b1dd27 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Wed, 15 Apr 2020 10:22:25 -0700 Subject: [PATCH 012/110] feat(client): error handling and test resilience (#1076) * feat: error handling and test fixes * fix: addressing tony feedback --- lib/client.js | 119 ++- lib/connect/index.js | 5 +- test/abstract_client.js | 1041 ++++++++++++----------- test/client.js | 935 ++++---------------- test/client_mqtt5.js | 538 ++++++++++++ test/helpers/port_list.js | 45 + test/helpers/server.js | 7 +- test/helpers/server_process.js | 4 +- test/mocha.opts | 2 +- test/secure_client.js | 15 +- test/server.js | 112 +-- test/server_helpers_for_client_tests.js | 100 +++ test/websocket_client.js | 14 +- 13 files changed, 1555 insertions(+), 1382 deletions(-) create mode 100644 test/client_mqtt5.js create mode 100644 test/helpers/port_list.js create mode 100644 test/server_helpers_for_client_tests.js diff --git a/lib/client.js b/lib/client.js index 15e645947..d61c732ba 100644 --- a/lib/client.js +++ b/lib/client.js @@ -26,6 +26,16 @@ var defaultConnectOptions = { clean: true, resubscribe: true } + +var socketErrors = [ + 'ECONNREFUSED', + 'EADDRINUSE', + 'ECONNRESET', + 'ENOTFOUND' +] + +// Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND. + var errors = { 0: '', 1: 'Unacceptable protocol version', @@ -148,7 +158,7 @@ function MqttClient (streamBuilder, options) { } this.options = options || {} - debug('MqttClient: options: %o', options) + debug('MqttClient :: options: %o', options) // Defaults for (k in defaultConnectOptions) { @@ -161,6 +171,8 @@ function MqttClient (streamBuilder, options) { this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() + debug('MqttClient: clientId', this.options.clientId) + this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } this.streamBuilder = streamBuilder @@ -206,13 +218,6 @@ function MqttClient (streamBuilder, options) { // True if connection is first time. this._firstConnection = true - // Mark disconnected on stream close - this.on('close', function () { - debug('MqttClient: close event. Mark disconnected.') - this.connected = false - clearTimeout(this.connackTimer) - }) - // Send queued packets this.on('connect', function () { debug('MqttClient:connect') @@ -244,6 +249,10 @@ function MqttClient (streamBuilder, options) { }) this.on('close', function () { + debug('MqttClient: close event. Mark disconnected.') + this.connected = false + clearTimeout(this.connackTimer) + debug('MqttClient:close: clear ping timer') if (that.pingTimer !== null) { that.pingTimer.clear() @@ -253,10 +262,6 @@ function MqttClient (streamBuilder, options) { debug('MqttClient:close: call _setupReconnect') this._setupReconnect() }) - - // Setup reconnect timer on disconnect - this.on('close', this._setupReconnect) - EventEmitter.call(this) debug('MqttClient: call _setupStream') @@ -322,8 +327,9 @@ MqttClient.prototype._setupStream = function () { function streamErrorHandler (error) { debug('stream error') - if (error.code === 'ECONNREFUSED') { + if (socketErrors.includes(error.code)) { // handle error + debug('streamErrorHandler :: emitting error') that.emit('error', error) } else { nop(error) @@ -338,7 +344,7 @@ MqttClient.prototype._setupStream = function () { // Echo stream close this.stream.on('close', function () { - debug('stream: on close') + debug('(%s)stream :: on close', that.options.clientId) flushVolatile(that.outgoing) debug('stream: emit close to MqttClient') that.emit('close') @@ -357,7 +363,9 @@ MqttClient.prototype._setupStream = function () { // auth if (this.options.properties) { if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { - this.emit('error', new Error('Packet has no Authentication Method')) + that.end(() => + this.emit('error', new Error('Packet has no Authentication Method') + )) return this } if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { @@ -371,6 +379,7 @@ MqttClient.prototype._setupStream = function () { clearTimeout(this.connackTimer) this.connackTimer = setTimeout(function () { + debug('connectTimeout hit! Calling _cleanUp with force `true`') that._cleanUp(true) }, this.options.connectTimeout) } @@ -769,7 +778,6 @@ MqttClient.prototype.unsubscribe = function () { * @api public */ MqttClient.prototype.end = function () { - debug('client end - close connection') var that = this var force = arguments[0] @@ -794,16 +802,20 @@ MqttClient.prototype.end = function () { opts = null } + debug('end :: cb? %s', !!cb) cb = cb || nop function closeStores () { + debug('end :: (%s) :: closeStores: closing incoming and outgoing stores', that.options.clientId) that.disconnected = true that.incomingStore.close(function () { that.outgoingStore.close(function () { + debug('end :: (%s) :: closeStores: emitting end', that.options.clientId) + that.emit('end') if (cb) { + debug('end :: (%s) :: closeStores: invoking callback with args', that.options.clientId) cb.apply(null, arguments) } - that.emit('end') }) }) if (that._deferredReconnect) { @@ -815,7 +827,12 @@ MqttClient.prototype.end = function () { // defer closesStores of an I/O cycle, // just to make sure things are // ok for websockets - that._cleanUp(force, setImmediate.bind(null, closeStores), opts) + debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) + that._cleanUp(force, () => { + debug('end :: finish :: calling process.nextTick on closeStores') + // var boundProcess = process.nextTick.bind(null, closeStores) + process.nextTick(closeStores.bind(that)) + }, opts) } if (this.disconnecting) { @@ -828,8 +845,10 @@ MqttClient.prototype.end = function () { if (!force && Object.keys(this.outgoing).length > 0) { // wait 10ms, just to be sure we received all of it + debug('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) } else { + debug('end :: (%s) :: immediately calling finish', that.options.clientId) finish() } @@ -840,16 +859,16 @@ MqttClient.prototype.end = function () { * removeOutgoingMessage - remove a message in outgoing store * the outgoing callback will be called withe Error('Message removed') if the message is removed * - * @param {Number} mid - messageId to remove message + * @param {Number} messageId - messageId to remove message * @returns {MqttClient} this - for chaining * @api public * * @example client.removeOutgoingMessage(client.getLastMessageId()); */ -MqttClient.prototype.removeOutgoingMessage = function (mid) { - var cb = this.outgoing[mid] ? this.outgoing[mid].cb : null - delete this.outgoing[mid] - this.outgoingStore.del({messageId: mid}, function () { +MqttClient.prototype.removeOutgoingMessage = function (messageId) { + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null + delete this.outgoing[messageId] + this.outgoingStore.del({messageId: messageId}, function () { cb(new Error('Message removed')) }) return this @@ -913,14 +932,17 @@ MqttClient.prototype._setupReconnect = function () { if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { if (!this.reconnecting) { - debug('_setupReconnect: emitting offline state') + debug('_setupReconnect :: emitting offline state') this.emit('offline') this.reconnecting = true } + debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) that.reconnectTimer = setInterval(function () { - debug('reconnectTimer calling _reconnect') + debug('reconnectTimer calling _reconnect()') that._reconnect() }, that.options.reconnectPeriod) + } else { + debug('_setupReconnect :: doing nothing...') } } @@ -928,6 +950,7 @@ MqttClient.prototype._setupReconnect = function () { * _clearReconnect - clear the reconnect timer */ MqttClient.prototype._clearReconnect = function () { + debug('_clearReconnect called. clearing reconnectTimer') if (this.reconnectTimer) { clearInterval(this.reconnectTimer) this.reconnectTimer = null @@ -950,10 +973,11 @@ MqttClient.prototype._cleanUp = function (forced, done) { if ((this.options.reconnectPeriod === 0) && this.options.clean) { flush(this.outgoing) } + debug('(%s)_cleanUp: destroying stream', this.options.clientId) this.stream.destroy() } else { var packet = xtend({ cmd: 'disconnect' }, opts) - debug('_cleanUp: sending disconnect packet') + debug('(%s)_cleanUp: sending disconnect packet', this.options.clientId) this._sendPacket( packet, setImmediate.bind( @@ -976,6 +1000,7 @@ MqttClient.prototype._cleanUp = function (forced, done) { } if (done && !this.connected) { + debug('(%s)_cleanUp: removing stream `done` callback `close` listener', this.options.clientId) this.stream.removeListener('close', done) done() } @@ -983,18 +1008,17 @@ MqttClient.prototype._cleanUp = function (forced, done) { /** * _sendPacket - send or queue a packet - * @param {String} type - packet type (see `protocol`) * @param {Object} packet - packet options * @param {Function} cb - callback when the packet is sent * @param {Function} cbStorePut - called when message is put into outgoingStore * @api private */ MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { - debug('_sendPacket') + debug('_sendPacket :: (%s) :: start', this.options.clientId) cbStorePut = cbStorePut || nop if (!this.connected) { - debug('_sendPacket: client not connected. Storing packet offline.') + debug('_sendPacket :: client not connected. Storing packet offline.') this._storePacket(packet, cb, cbStorePut) return } @@ -1029,19 +1053,22 @@ MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { sendPacket(this, packet, cb) break } + debug('_sendPacket :: (%s) :: end', this.options.clientId) } /** * _storePacket - queue a packet - * @param {String} type - packet type (see `protocol`) * @param {Object} packet - packet options * @param {Function} cb - callback when the packet is sent * @param {Function} cbStorePut - called when message is put into outgoingStore * @api private */ MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { + debug('_storePacket :: packet: %o', packet) + debug('_storePacket :: cb? %s', !!cb) cbStorePut = cbStorePut || nop + // check that the packet is not a qos of 0, or that the command is not a publish if (((packet.qos || 0) === 0 && this.queueQoSZero) || packet.cmd !== 'publish') { this.queue.push({ packet: packet, cb: cb }) } else if (packet.qos > 0) { @@ -1089,11 +1116,14 @@ MqttClient.prototype._shiftPingInterval = function () { * @api private */ MqttClient.prototype._checkPing = function () { + debug('_checkPing :: checking ping...') if (this.pingResp) { + debug('_checkPing :: ping response received. Clearing flag and sending `pingreq`') this.pingResp = false this._sendPacket({ cmd: 'pingreq' }) } else { // do a forced cleanup since socket will be in bad shape + debug('_checkPing :: calling _cleanUp with force true') this._cleanUp(true) } } @@ -1162,7 +1192,7 @@ case 0: if (1 === qos) { this._sendPacket({ cmd: 'puback', - messageId: mid + messageId: messageId }); } // emit the message event for both qos 1 and 0 @@ -1182,7 +1212,7 @@ MqttClient.prototype._handlePublish = function (packet, done) { var topic = packet.topic.toString() var message = packet.payload var qos = packet.qos - var mid = packet.messageId + var messageId = packet.messageId var that = this var options = this.options var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] @@ -1197,10 +1227,10 @@ MqttClient.prototype._handlePublish = function (packet, done) { if (error) { return that.emit('error', error) } if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for pubrec')) } if (code) { - that._sendPacket({cmd: 'pubrec', messageId: mid, reasonCode: code}, done) + that._sendPacket({cmd: 'pubrec', messageId: messageId, reasonCode: code}, done) } else { that.incomingStore.put(packet, function () { - that._sendPacket({cmd: 'pubrec', messageId: mid}, done) + that._sendPacket({cmd: 'pubrec', messageId: messageId}, done) }) } }) @@ -1220,7 +1250,7 @@ MqttClient.prototype._handlePublish = function (packet, done) { if (err) { return done && done(err) } - that._sendPacket({cmd: 'puback', messageId: mid, reasonCode: code}, done) + that._sendPacket({cmd: 'puback', messageId: messageId, reasonCode: code}, done) }) }) break @@ -1232,6 +1262,7 @@ MqttClient.prototype._handlePublish = function (packet, done) { break default: // do nothing + debug('_handlePublish: unknown QoS. Doing nothing.') // log or throw an error about unknown qos break } @@ -1259,10 +1290,10 @@ MqttClient.prototype.handleMessage = function (packet, callback) { MqttClient.prototype._handleAck = function (packet) { debug('handling ack packet') /* eslint no-fallthrough: "off" */ - var mid = packet.messageId + var messageId = packet.messageId var type = packet.cmd var response = null - var cb = this.outgoing[mid] ? this.outgoing[mid].cb : null + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null var that = this var err @@ -1285,14 +1316,14 @@ MqttClient.prototype._handleAck = function (packet) { err.code = pubackRC cb(err, packet) } - delete this.outgoing[mid] + delete this.outgoing[messageId] this.outgoingStore.del(packet, cb) break case 'pubrec': response = { cmd: 'pubrel', qos: 2, - messageId: mid + messageId: messageId } var pubrecRC = packet.reasonCode @@ -1305,11 +1336,11 @@ MqttClient.prototype._handleAck = function (packet) { } break case 'suback': - delete this.outgoing[mid] + delete this.outgoing[messageId] for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { if ((packet.granted[grantedI] & 0x80) !== 0) { // suback with Failure status - var topics = this.messageIdToTopic[mid] + var topics = this.messageIdToTopic[messageId] if (topics) { topics.forEach(function (topic) { delete that._resubscribeTopics[topic] @@ -1320,7 +1351,7 @@ MqttClient.prototype._handleAck = function (packet) { cb(null, packet) break case 'unsuback': - delete this.outgoing[mid] + delete this.outgoing[messageId] cb(null) break default: @@ -1342,10 +1373,10 @@ MqttClient.prototype._handleAck = function (packet) { MqttClient.prototype._handlePubrel = function (packet, callback) { debug('handling pubrel packet') callback = typeof callback !== 'undefined' ? callback : nop - var mid = packet.messageId + var messageId = packet.messageId var that = this - var comp = {cmd: 'pubcomp', messageId: mid} + var comp = {cmd: 'pubcomp', messageId: messageId} that.incomingStore.get(packet, function (err, pub) { if (!err) { diff --git a/lib/connect/index.js b/lib/connect/index.js index a9896187e..45dcde819 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -147,8 +147,9 @@ function connect (brokerUrl, opts) { return protocols[opts.protocol](client, opts) } - - return new MqttClient(wrapper, opts) + var client = new MqttClient(wrapper, opts) + client.on('error', function () { /* Automatically set up client error handling */ }) + return client } module.exports = connect diff --git a/test/abstract_client.js b/test/abstract_client.js index 00dcae928..f66f57d38 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -3,17 +3,18 @@ /** * Testing dependencies */ -var should = require('should') +var should = require('chai').should var sinon = require('sinon') var mqtt = require('../') var xtend = require('xtend') -var Server = require('./server') +var MqttServer = require('./server').MqttServer var Store = require('./../lib/store') var assert = require('chai').assert -var port = 9876 +var ports = require('./helpers/port_list') module.exports = function (server, config) { var version = config.protocolVersion || 4 + function connect (opts) { opts = xtend(config, opts) return mqtt.connect(opts) @@ -52,13 +53,12 @@ module.exports = function (server, config) { var client = connect() client.once('close', function () { - should.not.exist(client.pingTimer) - client.end() - done() + assert.notExists(client.pingTimer) + client.end(true, done) }) client.once('connect', function () { - should.exist(client.pingTimer) + assert.exists(client.pingTimer) client.stream.end() }) }) @@ -147,10 +147,11 @@ module.exports = function (server, config) { var client = connect() client.once('connect', function () { - should.exist(client.pingTimer) - client.end() - should.not.exist(client.pingTimer) - done() + assert.exists(client.pingTimer) + client.end(() => { + assert.notExists(client.pingTimer) + done() + }) }) }) @@ -181,21 +182,30 @@ module.exports = function (server, config) { done() }) - setTimeout(client.end.bind(client), 200) + // after 200ms manually invoke client.end + setTimeout(() => { + var boundEnd = client.end.bind(client) + boundEnd() + }, 200) }) - it('should emit end only once for a reconnecting client', function (done) { - var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 10}) - - client.once('end', function () { - var timeout = setTimeout(done.bind(null)) - client.once('end', function () { - clearTimeout(timeout) - done(new Error('end emitted twice')) - }) - }) + it.skip('should emit end only once for a reconnecting client', function (done) { + // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. + // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code + // there will be gists showing the difference between a successful test here and a failed test. For now we + // will add the retries syntax because of the flakiness. + var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20}) + setTimeout(done.bind(null), 1000) + var endCallback = function () { + assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') + } - setTimeout(client.end.bind(client), 300) + var spy = sinon.spy(endCallback) + client.on('end', spy) + setTimeout(() => { + client.end.bind(client) + client.end() + }, 300) }) }) @@ -205,8 +215,8 @@ module.exports = function (server, config) { client.on('error', done) server.once('client', function () { - client.end() done() + client.end() }) }) @@ -216,9 +226,9 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('connect', function (packet) { - packet.clientId.should.match(/mqttjs.*/) + assert.include(packet.clientId, 'mqttjs') + client.end(done) serverClient.disconnect() - done() }) }) }) @@ -229,7 +239,7 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('connect', function (packet) { - packet.clean.should.be.true() + assert.strictEqual(packet.clean, true) serverClient.disconnect() done() }) @@ -244,9 +254,11 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('connect', function (packet) { - packet.clientId.should.match(/testclient/) + assert.include(packet.clientId, 'testclient') serverClient.disconnect() - done() + client.end(function (err) { + done(err) + }) }) }) }) @@ -259,10 +271,10 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('connect', function (packet) { - packet.clientId.should.match(/testclient/) - packet.clean.should.be.false() + assert.include(packet.clientId, 'testclient') + assert.isFalse(packet.clean) serverClient.disconnect() - done() + client.end(true, done) }) }) }) @@ -272,9 +284,9 @@ module.exports = function (server, config) { var client = connect({ clean: false }) client.on('error', function (err) { done(err) - // done(new Error('should have thrown')); }) } catch (err) { + assert.strictEqual(err.message, 'Missing clientId for unclean clients') done() } }) @@ -287,7 +299,7 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('connect', function (packet) { - packet.clientId.should.match(/testclient/) + assert.include(packet.clientId, 'testclient') serverClient.disconnect() done() }) @@ -297,8 +309,7 @@ module.exports = function (server, config) { it('should emit connect', function (done) { var client = connect() client.once('connect', function () { - client.end() - done() + client.end(true, done) }) client.once('error', done) }) @@ -316,9 +327,9 @@ module.exports = function (server, config) { var client = connect() client.once('connect', function (packet) { - should(packet.sessionPresent).be.equal(true) + assert.strictEqual(packet.sessionPresent, true) client.once('connect', function (packet) { - should(packet.sessionPresent).be.equal(false) + assert.strictEqual(packet.sessionPresent, false) client.end() done() }) @@ -344,7 +355,7 @@ module.exports = function (server, config) { }) client.once('error', function (error) { var value = version === 5 ? 128 : 2 - should(error.code).be.equal(value) // code for clientID identifer rejected + assert.strictEqual(error.code, value) // code for clientID identifer rejected client.end() done() }) @@ -362,13 +373,19 @@ module.exports = function (server, config) { }) it('should have different client ids', function (done) { + // bug identified in this test: the client.end callback is invoked twice, once when the `end` + // method completes closing the stores and invokes the callback, and another time when the + // stream is closed. When the stream is closed, for some reason the closeStores method is called + // a second time. var client1 = connect() var client2 = connect() - client1.options.clientId.should.not.equal(client2.options.clientId) - client1.end(true) - client2.end(true) - setImmediate(done) + assert.notStrictEqual(client1.options.clientId, client2.options.clientId) + client1.end(true, () => { + client2.end(true, () => { + done() + }) + }) }) }) @@ -439,8 +456,8 @@ module.exports = function (server, config) { if (err) { return done(err) } - granted2.should.Array() - granted2.should.be.empty() + assert.isArray(granted2) + assert.isEmpty(granted2) done() }) }) @@ -513,10 +530,10 @@ module.exports = function (server, config) { client.publish('test', 'test') client.subscribe('test') client.unsubscribe('test') - client.queue.length.should.equal(3) + assert.strictEqual(client.queue.length, 3) client.once('connect', function () { - client.queue.length.should.equal(0) + assert.strictEqual(client.queue.length, 0) setTimeout(function () { client.end(true, done) }, 10) @@ -527,7 +544,7 @@ module.exports = function (server, config) { var client = connect({queueQoSZero: false}) client.publish('test', 'test', {qos: 0}) - client.queue.length.should.equal(0) + assert.strictEqual(client.queue.length, 0) client.on('connect', function () { setTimeout(function () { client.end(true, done) @@ -542,7 +559,7 @@ module.exports = function (server, config) { client.publish('test', 'test', {qos: 2}) client.subscribe('test') client.unsubscribe('test') - client.queue.length.should.equal(2) + assert.strictEqual(client.queue.length, 2) client.on('connect', function () { setTimeout(function () { client.end(true, done) @@ -555,26 +572,26 @@ module.exports = function (server, config) { var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) var publishCount = 0 - var server2 = new Server(function (c) { - c.on('connect', function () { - c.connack({returnCode: 0}) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function () { + serverClient.connack({returnCode: 0}) }) - c.on('publish', function (packet) { + serverClient.on('publish', function (packet) { if (packet.qos !== 0) { - c.puback({messageId: packet.messageId}) + serverClient.puback({messageId: packet.messageId}) } switch (publishCount++) { case 0: - packet.payload.toString().should.equal('payload1') + assert.strictEqual(packet.payload.toString(), 'payload1') break case 1: - packet.payload.toString().should.equal('payload2') + assert.strictEqual(packet.payload.toString(), 'payload2') break case 2: - packet.payload.toString().should.equal('payload3') + assert.strictEqual(packet.payload.toString(), 'payload3') break case 3: - packet.payload.toString().should.equal('payload4') + assert.strictEqual(packet.payload.toString(), 'payload4') server2.close() done() break @@ -582,9 +599,9 @@ module.exports = function (server, config) { }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: false, clientId: 'cid1', @@ -617,7 +634,7 @@ module.exports = function (server, config) { }) client.on('connect', function () { - called.should.equal(true) + assert.isTrue(called) setTimeout(function () { client.end(true, done) }, 10) @@ -634,7 +651,7 @@ module.exports = function (server, config) { }) client.publish('test', 'test', function () { client.end(false, function () { - subscribeCalled.should.be.equal(true) + assert.strictEqual(subscribeCalled, true) done() }) }) @@ -649,7 +666,7 @@ module.exports = function (server, config) { client.subscribe('test') client.publish('test', 'test', { qos: 1 }, function () { client.end(false, function () { - messageReceived.should.equal(true) + assert.strictEqual(messageReceived, true) done() }) }) @@ -693,7 +710,7 @@ module.exports = function (server, config) { var client = connect() var payload = 'test' var topic = 'test' - + // don't wait on connect to send publish client.publish(topic, payload) server.on('client', onClient) @@ -704,10 +721,10 @@ module.exports = function (server, config) { }) serverClient.once('publish', function (packet) { - packet.topic.should.equal(topic) - packet.payload.toString().should.equal(payload) - packet.qos.should.equal(0) - packet.retain.should.equal(false) + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) client.end(true, done) }) } @@ -717,21 +734,26 @@ module.exports = function (server, config) { var client = connect() var payload = 'test' var topic = 'test' - + // block on connect before sending publish client.on('connect', function () { client.publish(topic, payload) }) - server.once('client', function (serverClient) { + server.on('client', onClient) + + function onClient (serverClient) { + serverClient.once('connect', function () { + server.removeListener('client', onClient) + }) + serverClient.once('publish', function (packet) { - packet.topic.should.equal(topic) - packet.payload.toString().should.equal(payload) - packet.qos.should.equal(0) - packet.retain.should.equal(false) - client.end() - done() + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) }) - }) + } }) it('should publish a message (retain, offline)', function (done) { @@ -746,13 +768,12 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('publish', function (packet) { - packet.topic.should.equal(topic) - packet.payload.toString().should.equal(payload) - packet.qos.should.equal(0) - packet.retain.should.equal(true) - called.should.equal(true) - client.end() - done() + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, true) + assert.strictEqual(called, true) + client.end(true, done) }) }) }) @@ -760,20 +781,21 @@ module.exports = function (server, config) { it('should emit a packetsend event', function (done) { var client = connect() var payload = 'test_payload' - var testTopic = 'testTopic' + var topic = 'testTopic' client.on('packetsend', function (packet) { if (packet.cmd === 'publish') { - packet.qos.should.equal(0) - packet.topic.should.equal(testTopic) - packet.payload.should.equal(payload) - packet.retain.should.equal(false) - client.end() - done() + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + } else { + done(new Error('packet.cmd was not publish!')) } }) - client.publish(testTopic, payload) + client.publish(topic, payload) }) it('should accept options', function (done) { @@ -791,13 +813,12 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('publish', function (packet) { - packet.topic.should.equal(topic) - packet.payload.toString().should.equal(payload) - packet.qos.should.equal(opts.qos, 'incorrect qos') - packet.retain.should.equal(opts.retain, 'incorrect ret') - packet.dup.should.equal(false, 'incorrect dup') - client.end() - done() + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, false, 'incorrect dup') + client.end(done) }) }) }) @@ -814,13 +835,12 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('publish', function (packet) { - packet.topic.should.equal(topic) - packet.payload.toString().should.equal(payload) - packet.qos.should.equal(defaultOpts.qos, 'incorrect qos') - packet.retain.should.equal(defaultOpts.retain, 'incorrect ret') - packet.dup.should.equal(defaultOpts.dup, 'incorrect dup') - client.end() - done() + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') + client.end(true, done) }) }) }) @@ -841,11 +861,12 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('publish', function (packet) { - packet.topic.should.equal(topic) - packet.payload.toString().should.equal(payload) - packet.dup.should.equal(opts.dup, 'incorrect dup') - client.end() - done() + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') + client.end(done) }) }) }) @@ -953,11 +974,10 @@ module.exports = function (server, config) { setTimeout(function () { handleMessageCount++ // next message event should not emit until handleMessage completes - handleMessageCount.should.equal(messageEventCount) + assert.strictEqual(handleMessageCount, messageEventCount) if (handleMessageCount === 10) { setTimeout(function () { - client.end() - done() + client.end(true, done) }) } callback() @@ -974,8 +994,9 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.on('offline', function () { - client.end() - done('error went offline... didnt see this happen') + client.end(true, function () { + done('error went offline... didnt see this happen') + }) }) serverClient.on('subscribe', function () { @@ -991,16 +1012,11 @@ module.exports = function (server, config) { }) } - it('should publish 10 QoS 0 and receive them only when `handleMessage` finishes', function (done) { - testQosHandleMessage(0, done) - }) - - it('should publish 10 QoS 1 and receive them only when `handleMessage` finishes', function (done) { - testQosHandleMessage(1, done) - }) - - it('should publish 10 QoS 2 and receive them only when `handleMessage` finishes', function (done) { - testQosHandleMessage(2, done) + var qosTests = [ 0, 1, 2 ] + qosTests.forEach(function (QoS) { + it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { + testQosHandleMessage(QoS, done) + }) }) it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { @@ -1018,10 +1034,10 @@ module.exports = function (server, config) { payload: 'test', qos: 1 }, function (err) { - should.exist(err) + assert.exists(err) }) - client._sendPacket.callCount.should.equal(0) + assert.strictEqual(client._sendPacket.callCount, 0) client.end() client.on('connect', function () { done() }) }) @@ -1041,25 +1057,25 @@ module.exports = function (server, config) { payload: 'test', qos: 1 }) - done() + client.end(true, done) } catch (err) { - done(err) - } finally { - client.end() + client.end(true, () => { done(err) }) } }) - it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { - function AsyncStore () { - if (!(this instanceof AsyncStore)) { - return new AsyncStore() + it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() } } - AsyncStore.prototype.put = function (packet, cb) { - process.nextTick(function () { - cb(new Error('Error')) - }) - } + var store = new AsyncStore() var client = connect({incomingStore: store}) @@ -1067,94 +1083,112 @@ module.exports = function (server, config) { messageId: 1, topic: 'test', payload: 'test', - qos: 2 + qos: 1 }, function () { - done() client.end() + done() }) }) - it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { - function AsyncStore () { - if (!(this instanceof AsyncStore)) { - return new AsyncStore() + it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() } } - AsyncStore.prototype.del = function (packet, cb) { - process.nextTick(function () { - cb(new Error('Error')) - }) - } - AsyncStore.prototype.get = function (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } + var store = new AsyncStore() var client = connect({incomingStore: store}) - client._handlePubrel({ + client._handlePublish({ messageId: 1, + topic: 'test', + payload: 'test', qos: 2 }, function () { - done() client.end() + done() }) }) - it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { - var delComplete = false - function AsyncStore () { - if (!(this instanceof AsyncStore)) { - return new AsyncStore() + it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + cb(new Error('Error')) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() } } - AsyncStore.prototype.del = function (packet, cb) { - process.nextTick(function () { - delComplete = true - cb(null) - }) - } - AsyncStore.prototype.get = function (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } + var store = new AsyncStore() - var client = connect({incomingStore: store}) + var client = connect({ incomingStore: store }) client._handlePubrel({ messageId: 1, qos: 2 }, function () { - delComplete.should.equal(true) - done() - client.end() + client.end(true, done) }) }) - it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { - function AsyncStore () { - if (!(this instanceof AsyncStore)) { - return new AsyncStore() + it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { + var delComplete = false + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + delComplete = true + cb(null) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() } } - AsyncStore.prototype.put = function (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } + var store = new AsyncStore() var client = connect({incomingStore: store}) - client._handlePublish({ + client._handlePubrel({ messageId: 1, - topic: 'test', - payload: 'test', - qos: 1 + qos: 2 }, function () { - done() - client.end() + assert.isTrue(delComplete) + client.end(true, done) }) }) @@ -1163,8 +1197,8 @@ module.exports = function (server, config) { var client = connect({incomingStore: store}) var messageId = Math.floor(65535 * Math.random()) - var topic = 'test' - var payload = 'test' + var topic = 'testTopic' + var payload = 'testPayload' var qos = 2 client.handleMessage = function (packet, callback) { @@ -1182,14 +1216,12 @@ module.exports = function (server, config) { cmd: 'publish' }, function () { // cleans up the client - client.end() - client._sendPacket = sinon.spy() client._handlePubrel({cmd: 'pubrel', messageId: messageId}, function (err) { - should.exist(err) + assert.exists(err) + assert.strictEqual(client._sendPacket.callCount, 0) + client.end(true, done) }) - client._sendPacket.callCount.should.equal(0) - done() }) }) }) @@ -1220,11 +1252,9 @@ module.exports = function (server, config) { }, function () { try { client._handlePubrel({cmd: 'pubrel', messageId: messageId}) - done() + client.end(true, done) } catch (err) { - done(err) - } finally { - client.end() + client.end(true, () => { done(err) }) } }) }) @@ -1236,26 +1266,26 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new Server(function (c) { + var server2 = new MqttServer(function (serverClient) { // errors are not interesting for this test // but they might happen on some platforms - c.on('error', function () {}) + serverClient.on('error', function () {}) - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) }) - c.on('publish', function (packet) { - c.puback({messageId: packet.messageId}) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) if (reconnect) { switch (publishCount++) { case 0: - packet.payload.toString().should.equal('payload1') + assert.strictEqual(packet.payload.toString(), 'payload1') break case 1: - packet.payload.toString().should.equal('payload2') + assert.strictEqual(packet.payload.toString(), 'payload2') break case 2: - packet.payload.toString().should.equal('payload3') + assert.strictEqual(packet.payload.toString(), 'payload3') server2.close() done() break @@ -1264,9 +1294,9 @@ module.exports = function (server, config) { }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: false, clientId: 'cid1', @@ -1313,30 +1343,31 @@ module.exports = function (server, config) { client.publish('test', 'test', {qos: qos, cbStorePut: cbStorePut}, function (err) { if (err) done(err) callbacks.push('publish') - should.deepEqual(callbacks, expected) - done() + assert.deepEqual(callbacks, expected) + client.end(true, done) }) - client.end() }) } - it('should not call cbStorePut when publishing message with QoS `0` and clean `true`', function (done) { - testCallbackStorePutByQoS(0, true, ['publish'], done) - }) - it('should not call cbStorePut when publishing message with QoS `0` and clean `false`', function (done) { - testCallbackStorePutByQoS(0, false, ['publish'], done) - }) - it('should call cbStorePut before publish completes when publishing message with QoS `1` and clean `true`', function (done) { - testCallbackStorePutByQoS(1, true, ['storeput', 'publish'], done) - }) - it('should call cbStorePut before publish completes when publishing message with QoS `1` and clean `false`', function (done) { - testCallbackStorePutByQoS(1, false, ['storeput', 'publish'], done) - }) - it('should call cbStorePut before publish completes when publishing message with QoS `2` and clean `true`', function (done) { - testCallbackStorePutByQoS(2, true, ['storeput', 'publish'], done) - }) - it('should call cbStorePut before publish completes when publishing message with QoS `2` and clean `false`', function (done) { - testCallbackStorePutByQoS(2, false, ['storeput', 'publish'], done) + var callbackStorePutByQoSParameters = [ + {args: [0, true], expected: ['publish']}, + {args: [0, false], expected: ['publish']}, + {args: [1, true], expected: ['storeput', 'publish']}, + {args: [1, false], expected: ['storeput', 'publish']}, + {args: [2, true], expected: ['storeput', 'publish']}, + {args: [2, false], expected: ['storeput', 'publish']} + ] + + callbackStorePutByQoSParameters.forEach(function (test) { + if (test.args[0] === 0) { // QoS 0 + it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } else { // QoS 1 and 2 + it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } }) }) @@ -1348,9 +1379,8 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('unsubscribe', function (packet) { - packet.unsubscriptions.should.containEql('test') - client.end() - done() + assert.include(packet.unsubscriptions, 'test') + client.end(done) }) }) }) @@ -1365,9 +1395,8 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('unsubscribe', function (packet) { - packet.unsubscriptions.should.containEql(topic) - client.end() - done() + assert.include(packet.unsubscriptions, topic) + client.end(done) }) }) }) @@ -1382,8 +1411,7 @@ module.exports = function (server, config) { client.on('packetsend', function (packet) { if (packet.cmd === 'subscribe') { - client.end() - done() + client.end(true, done) } }) }) @@ -1398,8 +1426,7 @@ module.exports = function (server, config) { client.on('packetreceive', function (packet) { if (packet.cmd === 'suback') { - client.end() - done() + client.end(true, done) } }) }) @@ -1414,8 +1441,8 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.once('unsubscribe', function (packet) { - packet.unsubscriptions.should.eql(topics) - done() + assert.deepStrictEqual(packet.unsubscriptions, topics) + client.end(done) }) }) }) @@ -1425,13 +1452,14 @@ module.exports = function (server, config) { var topic = 'topic' client.once('connect', function () { - client.unsubscribe(topic, done) + client.unsubscribe(topic, () => { + client.end(true, done) + }) }) server.once('client', function (serverClient) { serverClient.once('unsubscribe', function (packet) { serverClient.unsuback(packet) - client.end() }) }) }) @@ -1441,14 +1469,16 @@ module.exports = function (server, config) { var topic = '中国' client.once('connect', function () { - client.unsubscribe(topic) + client.unsubscribe(topic, () => { + client.end(err => { + done(err) + }) + }) }) server.once('client', function (serverClient) { serverClient.once('unsubscribe', function (packet) { - packet.unsubscriptions.should.containEql(topic) - client.end() - done() + assert.include(packet.unsubscriptions, topic) }) }) }) @@ -1473,16 +1503,15 @@ module.exports = function (server, config) { client.once('connect', function () { clock.tick(interval * 1000) - client._checkPing.callCount.should.equal(1) + assert.strictEqual(client._checkPing.callCount, 1) clock.tick(interval * 1000) - client._checkPing.callCount.should.equal(2) + assert.strictEqual(client._checkPing.callCount, 2) clock.tick(interval * 1000) - client._checkPing.callCount.should.equal(3) + assert.strictEqual(client._checkPing.callCount, 3) - client.end() - done() + client.end(true, done) }) }) @@ -1497,9 +1526,9 @@ module.exports = function (server, config) { clock.tick(intervalMs - 1) client.publish('foo', 'bar') clock.tick(2) - client._checkPing.callCount.should.equal(0) - client.end() - done() + + assert.strictEqual(client._checkPing.callCount, 0) + client.end(true, done) }) }) @@ -1517,9 +1546,9 @@ module.exports = function (server, config) { clock.tick(intervalMs - 1) client.publish('foo', 'bar') clock.tick(2) - client._checkPing.callCount.should.equal(1) - client.end() - done() + + assert.strictEqual(client._checkPing.callCount, 1) + client.end(true, done) }) }) }) @@ -1528,18 +1557,16 @@ module.exports = function (server, config) { it('should set a ping timer', function (done) { var client = connect({keepalive: 3}) client.once('connect', function () { - should.exist(client.pingTimer) - client.end() - done() + assert.exists(client.pingTimer) + client.end(true, done) }) }) it('should not set a ping timer keepalive=0', function (done) { var client = connect({keepalive: 0}) client.on('connect', function () { - should.not.exist(client.pingTimer) - client.end() - done() + assert.notExists(client.pingTimer) + client.end(true, done) }) }) @@ -1551,8 +1578,7 @@ module.exports = function (server, config) { client.once('connect', function () { client.once('connect', function () { - client.end() - done() + client.end(true, done) }) }) }) @@ -1573,15 +1599,15 @@ module.exports = function (server, config) { client.publish('foo', 'bar') setTimeout(function () { - client._checkPing.callCount.should.equal(0) + assert.strictEqual(client._checkPing.callCount, 0) client.publish('foo', 'bar') setTimeout(function () { - client._checkPing.callCount.should.equal(0) + assert.strictEqual(client._checkPing.callCount, 0) client.publish('foo', 'bar') setTimeout(function () { - client._checkPing.callCount.should.equal(0) + assert.strictEqual(client._checkPing.callCount, 0) done() }, 75) }, 75) @@ -1622,7 +1648,7 @@ module.exports = function (server, config) { result.rap = false result.rh = 0 } - packet.subscriptions.should.containEql(result) + assert.include(packet.subscriptions[0], result) done() }) }) @@ -1679,13 +1705,13 @@ module.exports = function (server, config) { return result }) - packet.subscriptions.should.eql(expected) - done() + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) }) }) }) - it('should accept an hash of subscriptions', function (done) { + it('should accept a hash of subscriptions', function (done) { var client = connect() var topics = { test1: {qos: 0}, @@ -1716,8 +1742,8 @@ module.exports = function (server, config) { } } - packet.subscriptions.should.eql(expected) - done() + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) }) }) }) @@ -1744,7 +1770,7 @@ module.exports = function (server, config) { expected[0].rh = 0 } - packet.subscriptions.should.eql(expected) + assert.deepStrictEqual(packet.subscriptions, expected) done() }) }) @@ -1770,8 +1796,9 @@ module.exports = function (server, config) { result.rap = false result.rh = 0 } - packet.subscriptions.should.containEql(result) - done() + + assert.include(packet.subscriptions[0], result) + client.end(err => done(err)) }) }) }) @@ -1785,16 +1812,16 @@ module.exports = function (server, config) { if (err) { done(err) } else { - should.exist(granted, 'granted not given') - var result = {topic: 'test', qos: 2} + assert.exists(granted, 'granted not given') + var expectedResult = {topic: 'test', qos: 2} if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - result.properties = undefined + expectedResult.nl = false + expectedResult.rap = false + expectedResult.rh = 0 + expectedResult.properties = undefined } - granted.should.containEql(result) - done() + assert.include(granted[0], expectedResult) + client.end(err => done(err)) } }) }) @@ -1806,8 +1833,8 @@ module.exports = function (server, config) { client.once('connect', function () { client.end(true, function () { client.subscribe(topic, {qos: 2}, function (err, granted) { - should.not.exist(granted, 'granted given') - should.exist(err, 'no error given') + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') done() }) }) @@ -1821,8 +1848,8 @@ module.exports = function (server, config) { client.once('connect', function () { client.end(true, function () { client.subscribe(topic, function (err, granted) { - should.not.exist(granted, 'granted given') - should.exist(err, 'no error given') + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') done() }) }) @@ -1848,8 +1875,8 @@ module.exports = function (server, config) { result.rap = false result.rh = 0 } - packet.subscriptions.should.containEql(result) - done() + assert.include(packet.subscriptions[0], result) + client.end(done) }) }) }) @@ -1866,13 +1893,13 @@ module.exports = function (server, config) { messageId: 5 } + // client.subscribe(testPacket.topic) client.once('message', function (topic, message, packet) { - topic.should.equal(testPacket.topic) - message.toString().should.equal(testPacket.payload) - packet.should.equal(packet) - client.end() - done() + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) }) server.once('client', function (serverClient) { @@ -1895,12 +1922,11 @@ module.exports = function (server, config) { client.subscribe(testPacket.topic) client.on('packetreceive', function (packet) { if (packet.cmd === 'publish') { - packet.qos.should.equal(1) - packet.topic.should.equal(testPacket.topic) - packet.payload.toString().should.equal(testPacket.payload) - packet.retain.should.equal(true) - client.end() - done() + assert.strictEqual(packet.qos, 1) + assert.strictEqual(packet.topic, testPacket.topic) + assert.strictEqual(packet.payload.toString(), testPacket.payload) + assert.strictEqual(packet.retain, true) + client.end(true, done) } }) @@ -1923,11 +1949,11 @@ module.exports = function (server, config) { client.subscribe(testPacket.topic) client.once('message', function (topic, message, packet) { - topic.should.equal(testPacket.topic) - message.should.be.an.instanceOf(Buffer) - message.toString().should.equal(testPacket.payload) - packet.should.equal(packet) - done() + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) }) server.once('client', function (serverClient) { @@ -1951,10 +1977,11 @@ module.exports = function (server, config) { client.subscribe(testPacket.topic) client.once('message', function (topic, message, packet) { - topic.should.equal(testPacket.topic) - message.toString().should.equal(testPacket.payload) - packet.should.equal(packet) - done() + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) }) server.once('client', function (serverClient) { @@ -1976,13 +2003,20 @@ module.exports = function (server, config) { server.testPublish = testPacket + var messageHandler = function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + + assert.strictEqual(spiedMessageHandler.callCount, 1) + client.end(true, done) + } + + var spiedMessageHandler = sinon.spy(messageHandler) + client.subscribe(testPacket.topic) - client.on('message', function (topic, message, packet) { - topic.should.equal(testPacket.topic) - message.toString().should.equal(testPacket.payload) - packet.should.equal(packet) - done() - }) + client.on('message', spiedMessageHandler) server.once('client', function (serverClient) { serverClient.on('subscribe', function () { @@ -1993,7 +2027,7 @@ module.exports = function (server, config) { }) }) - it('should support chinese topic', function (done) { + it('should support a chinese topic', function (done) { var client = connect({ encoding: 'binary' }) var testPacket = { topic: '国', @@ -2005,11 +2039,12 @@ module.exports = function (server, config) { client.subscribe(testPacket.topic) client.once('message', function (topic, message, packet) { - topic.should.equal(testPacket.topic) - message.should.be.an.instanceOf(Buffer) - message.toString().should.equal(testPacket.payload) - packet.should.equal(packet) - done() + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) }) server.once('client', function (serverClient) { @@ -2027,7 +2062,9 @@ module.exports = function (server, config) { var testMessage = 'message' client.once('connect', function () { - client.subscribe(testTopic, {qos: 0}) + client.subscribe(testTopic, {qos: 0}, () => { + client.end(true, done) + }) }) server.once('client', function (serverClient) { @@ -2038,7 +2075,6 @@ module.exports = function (server, config) { qos: 0, retain: false }) - done() }) }) }) @@ -2064,8 +2100,8 @@ module.exports = function (server, config) { }) serverClient.once('puback', function (packet) { - packet.messageId.should.equal(mid) - done() + assert.strictEqual(packet.messageId, mid) + client.end(done) }) }) }) @@ -2075,9 +2111,9 @@ module.exports = function (server, config) { var testTopic = 'test' var testMessage = 'message' var mid = 253 - var publishReceived = false - var pubrecReceived = false - var pubrelReceived = false + var publishReceived = 0 + var pubrecReceived = 0 + var pubrelReceived = 0 client.once('connect', function () { client.subscribe(testTopic, {qos: 2}) @@ -2090,14 +2126,14 @@ module.exports = function (server, config) { // expected, but not specifically part of QOS 2 semantics break case 'publish': - pubrecReceived.should.be.false() - pubrelReceived.should.be.false() - publishReceived = true + assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') + assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') + publishReceived += 1 break case 'pubrel': - publishReceived.should.be.true() - pubrecReceived.should.be.true() - pubrelReceived = true + assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') + pubrelReceived += 1 break default: should.fail() @@ -2115,18 +2151,18 @@ module.exports = function (server, config) { }) serverClient.on('pubrec', function () { - publishReceived.should.be.true() - pubrelReceived.should.be.false() - pubrecReceived = true + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') + assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') + pubrecReceived += 1 }) serverClient.once('pubcomp', function () { client.removeAllListeners() serverClient.removeAllListeners() - publishReceived.should.be.true() - pubrecReceived.should.be.true() - pubrelReceived.should.be.true() - done() + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') + assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') + client.end(true, done) }) }) }) @@ -2143,7 +2179,7 @@ module.exports = function (server, config) { client.on('packetreceive', (packet) => { if (packet.cmd === 'pubrel') { - should(client.incomingStore._inflights.size).be.equal(1) + assert.strictEqual(client.incomingStore._inflights.size, 1) } }) @@ -2158,9 +2194,9 @@ module.exports = function (server, config) { }) serverClient.once('pubcomp', function () { - should(client.incomingStore._inflights.size).be.equal(0) + assert.strictEqual(client.incomingStore._inflights.size, 0) client.removeAllListeners() - done() + client.end(true, done) }) }) }) @@ -2197,7 +2233,7 @@ module.exports = function (server, config) { case 'subscribe': const suback = {cmd: 'suback', messageId: packet.messageId, granted: [2]} client._handlePacket(suback, function (err) { - should(err).not.be.ok() + assert.isNotOk(err) }) break case 'pubrec': @@ -2207,11 +2243,11 @@ module.exports = function (server, config) { pubcompCount++ if (pubcompCount === 2) { // end the test once the client has gone through two rounds of replying to pubrel messages - pubrelCount.should.be.exactly(2) - handleMessageCount.should.be.exactly(1) - emitMessageCount.should.be.exactly(1) + assert.strictEqual(pubrelCount, 2) + assert.strictEqual(handleMessageCount, 1) + assert.strictEqual(emitMessageCount, 1) client._sendPacket = origSendPacket - done() + client.end(true, done) break } } @@ -2221,9 +2257,10 @@ module.exports = function (server, config) { pubrelCount++ client._handlePacket(pubrel, function (err) { if (shouldSendFail) { - should(err).be.ok() + assert.exists(err) + assert.instanceOf(err, Error) } else { - should(err).not.be.ok() + assert.notExists(err) } }) break @@ -2234,7 +2271,7 @@ module.exports = function (server, config) { client.subscribe(testTopic, {qos: 2}) const publish = {cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid} client._handlePacket(publish, function (err) { - should(err).not.be.ok() + assert.notExists(err) }) }) } @@ -2249,15 +2286,18 @@ module.exports = function (server, config) { }) describe('auto reconnect', function () { - it('should mark the client disconnecting if #end called', function () { + it('should mark the client disconnecting if #end called', function (done) { var client = connect() - client.end() - client.disconnecting.should.eql(true) + client.end(true, err => { + assert.isTrue(client.disconnecting) + done(err) + }) }) it('should reconnect after stream disconnect', function (done) { var client = connect() + var tryReconnect = true client.on('connect', function () { @@ -2265,8 +2305,7 @@ module.exports = function (server, config) { client.stream.end() tryReconnect = false } else { - client.end() - done() + client.end(true, done) } }) }) @@ -2285,15 +2324,15 @@ module.exports = function (server, config) { client.stream.end() tryReconnect = false } else { - reconnectEvent.should.equal(true) - client.end() - done() + assert.isTrue(reconnectEvent) + client.end(true, done) } }) }) it('should emit \'offline\' after going offline', function (done) { var client = connect() + var tryReconnect = true var offlineEvent = false @@ -2306,9 +2345,8 @@ module.exports = function (server, config) { client.stream.end() tryReconnect = false } else { - offlineEvent.should.equal(true) - client.end() - done() + assert.isTrue(offlineEvent) + client.end(true, done) } }) }) @@ -2326,43 +2364,46 @@ module.exports = function (server, config) { var client = connect() client.once('connect', function () { - should.not.exist(client.reconnectTimer) + assert.notExists(client.reconnectTimer) client.stream.end() }) client.once('close', function () { - should.exist(client.reconnectTimer) - client.end() - done() + assert.exists(client.reconnectTimer) + client.end(true, done) }) }) - it('should allow specification of a reconnect period', function (done) { - var end - var period = 200 - var client = connect({reconnectPeriod: period}) - var reconnect = false - var start = Date.now() + var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] + reconnectPeriodTests.forEach((test) => { + it('should allow specification of a reconnect period', function (done) { + var end + var client = connect({reconnectPeriod: test.period}) + var reconnect = false + var start = Date.now() - client.on('connect', function () { - if (!reconnect) { - client.stream.end() - reconnect = true - } else { - client.end() - end = Date.now() - if (end - start >= period) { - // Connected in about 2 seconds, that's good enough - done() + client.on('connect', function () { + if (!reconnect) { + client.stream.end() + reconnect = true } else { - done(new Error('Strange reconnect period')) + end = Date.now() + client.end(() => { + if (end - start >= test.period - 200 && end - start <= test.period + 200) { + // give the connection a 200 ms slush window + done() + } else { + done(new Error('Strange reconnect period')) + } + }) } - } + }) }) }) it('should always cleanup successfully on reconnection', function (done) { var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1}) + // bind client.end so that when it is called it is automatically passed in the done callback setTimeout(client.end.bind(client, done), 50) }) @@ -2393,8 +2434,7 @@ module.exports = function (server, config) { function check () { if (serverPublished && clientCalledBack) { - client.end() - done() + client.end(true, done) } } }) @@ -2407,10 +2447,11 @@ module.exports = function (server, config) { serverClient.on('connect', function () { setImmediate(function () { serverClient.stream.destroy() - client.end() - serverPublished.should.be.false() - clientCalledBack.should.be.false() - done() + client.end(true, err => { + assert.isFalse(serverPublished) + assert.isFalse(clientCalledBack) + done(err) + }) }) }) server.once('client', function (serverClientNew) { @@ -2453,8 +2494,7 @@ module.exports = function (server, config) { function check () { if (serverPublished && clientCalledBack) { - client.end() - done() + client.end(true, done) } } }) @@ -2480,16 +2520,18 @@ module.exports = function (server, config) { client.publish('hello', 'world', { qos: 1 }, function (err) { clientCalledBack = true - should(err.message).be.equal('Message removed') + assert.exists(err, 'error should exist') + assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') }) - should(Object.keys(client.outgoing).length).be.equal(1) - should(client.outgoingStore._inflights.size).be.equal(1) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) client.removeOutgoingMessage(client.getLastMessageId()) - should(Object.keys(client.outgoing).length).be.equal(0) - should(client.outgoingStore._inflights.size).be.equal(0) - clientCalledBack.should.be.true() - client.end() - done() + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, (err) => { + done(err) + }) }) it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { @@ -2513,16 +2555,15 @@ module.exports = function (server, config) { client.publish('hello', 'world', { qos: 2 }, function (err) { clientCalledBack = true - should(err.message).be.equal('Message removed') + assert.strictEqual(err.message, 'Message removed') }) - should(Object.keys(client.outgoing).length).be.equal(1) - should(client.outgoingStore._inflights.size).be.equal(1) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) client.removeOutgoingMessage(client.getLastMessageId()) - should(Object.keys(client.outgoing).length).be.equal(0) - should(client.outgoingStore._inflights.size).be.equal(0) - clientCalledBack.should.be.true() - client.end() - done() + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, done) }) it('should resubscribe when reconnecting', function (done) { @@ -2541,15 +2582,14 @@ module.exports = function (server, config) { server.once('client', function (serverClient) { serverClient.on('subscribe', function () { - client.end() - done() + client.end(done) }) }) }) tryReconnect = false } else { - reconnectEvent.should.equal(true) + assert.isTrue(reconnectEvent) } }) }) @@ -2577,9 +2617,9 @@ module.exports = function (server, config) { tryReconnect = false } else { - reconnectEvent.should.equal(true) - should(Object.keys(client._resubscribeTopics).length).be.equal(0) - done() + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) + client.end(true, done) } }) }) @@ -2587,24 +2627,24 @@ module.exports = function (server, config) { it('should not resubscribe when reconnecting if suback is error', function (done) { var tryReconnect = true var reconnectEvent = false - var server2 = new Server(function (c) { - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) }) - c.on('subscribe', function (packet) { - c.suback({ + serverClient.on('subscribe', function (packet) { + serverClient.suback({ messageId: packet.messageId, granted: packet.subscriptions.map(function (e) { return e.qos | 0x80 }) }) - c.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) }) }) - server2.listen(port + 49, function () { + server2.listen(ports.PORTAND49, function () { var client = mqtt.connect({ - port: port + 49, + port: ports.PORTAND49, host: 'localhost', reconnectPeriod: 100 }) @@ -2626,10 +2666,10 @@ module.exports = function (server, config) { }) tryReconnect = false } else { - reconnectEvent.should.equal(true) - should(Object.keys(client._resubscribeTopics).length).be.equal(0) + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) server2.close() - done() + client.end(true, done) } }) }) @@ -2640,23 +2680,23 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new Server(function (c) { - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) if (reconnect) { - c.pubrel({ messageId: 1 }) + serverClient.pubrel({ messageId: 1 }) } }) - c.on('subscribe', function (packet) { - c.suback({ + serverClient.on('subscribe', function (packet) { + serverClient.suback({ messageId: packet.messageId, granted: packet.subscriptions.map(function (e) { return e.qos }) }) - c.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) + serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) }) - c.on('pubrec', function (packet) { + serverClient.on('pubrec', function (packet) { client.end(false, function () { client.reconnect({ incomingStore: incomingStore, @@ -2664,16 +2704,17 @@ module.exports = function (server, config) { }) }) }) - c.on('pubcomp', function (packet) { - client.end() - server2.close() - done() + serverClient.on('pubcomp', function (packet) { + client.end(true, () => { + server2.close() + done() + }) }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: false, clientId: 'cid1', @@ -2690,8 +2731,8 @@ module.exports = function (server, config) { } }) client.on('message', function (topic, message) { - topic.should.equal('topic') - message.toString().should.equal('payload') + assert.strictEqual(topic, 'topic') + assert.strictEqual(message.toString(), 'payload') }) }) }) @@ -2699,27 +2740,27 @@ module.exports = function (server, config) { it('should clear outgoing if close from server', function (done) { var reconnect = false var client = {} - var server2 = new Server(function (c) { - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) }) - c.on('subscribe', function (packet) { + serverClient.on('subscribe', function (packet) { if (reconnect) { - c.suback({ + serverClient.suback({ messageId: packet.messageId, granted: packet.subscriptions.map(function (e) { return e.qos }) }) } else { - c.destroy() + serverClient.destroy() } }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: true, clientId: 'cid1', @@ -2739,7 +2780,7 @@ module.exports = function (server, config) { server2.close() done() } else { - Object.keys(client.outgoing).length.should.equal(0) + assert.strictEqual(Object.keys(client.outgoing).length, 0) reconnect = true client.reconnect() } @@ -2752,16 +2793,16 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new Server(function (c) { - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) }) - c.on('publish', function (packet) { + serverClient.on('publish', function (packet) { if (reconnect) { server2.close() - done() + client.end(true, done) } else { - client.end(true, function () { + client.end(true, () => { client.reconnect({ incomingStore: incomingStore, outgoingStore: outgoingStore @@ -2772,9 +2813,9 @@ module.exports = function (server, config) { }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: false, clientId: 'cid1', @@ -2797,14 +2838,14 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new Server(function (c) { - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) }) - c.on('publish', function (packet) { + serverClient.on('publish', function (packet) { if (reconnect) { server2.close() - done() + client.end(true, done) } else { client.end(true, function () { client.reconnect({ @@ -2817,9 +2858,9 @@ module.exports = function (server, config) { }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: false, clientId: 'cid1', @@ -2842,19 +2883,19 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new Server(function (c) { - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) }) - c.on('publish', function (packet) { + serverClient.on('publish', function (packet) { if (!reconnect) { - c.pubrec({messageId: packet.messageId}) + serverClient.pubrec({messageId: packet.messageId}) } }) - c.on('pubrel', function () { + serverClient.on('pubrel', function () { if (reconnect) { server2.close() - done() + client.end(true, done) } else { client.end(true, function () { client.reconnect({ @@ -2867,9 +2908,9 @@ module.exports = function (server, config) { }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: false, clientId: 'cid1', @@ -2894,28 +2935,28 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new Server(function (c) { + var server2 = new MqttServer(function (serverClient) { // errors are not interesting for this test // but they might happen on some platforms - c.on('error', function () {}) + serverClient.on('error', function () {}) - c.on('connect', function (packet) { - c.connack({returnCode: 0}) + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) }) - c.on('publish', function (packet) { - c.puback({messageId: packet.messageId}) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) if (reconnect) { switch (publishCount++) { case 0: - packet.payload.toString().should.equal('payload1') + assert.strictEqual(packet.payload.toString(), 'payload1') break case 1: - packet.payload.toString().should.equal('payload2') + assert.strictEqual(packet.payload.toString(), 'payload2') break case 2: - packet.payload.toString().should.equal('payload3') + assert.strictEqual(packet.payload.toString(), 'payload3') server2.close() - done() + client.end(true, done) break } } else { @@ -2933,9 +2974,9 @@ module.exports = function (server, config) { }) }) - server2.listen(port + 50, function () { + server2.listen(ports.PORTAND50, function () { client = mqtt.connect({ - port: port + 50, + port: ports.PORTAND50, host: 'localhost', clean: false, clientId: 'cid1', @@ -2967,7 +3008,7 @@ module.exports = function (server, config) { tryReconnect = false client.reconnect() } else { - reconnectEvent.should.equal(true) + assert.isTrue(reconnectEvent) done() } }) @@ -2999,7 +3040,7 @@ module.exports = function (server, config) { client.reconnect() }, 100) } else { - reconnectEvent.should.equal(true) + assert.isTrue(reconnectEvent) done() } }) @@ -3057,7 +3098,7 @@ module.exports = function (server, config) { // after the second connection, confirm that the only two // subscribes have taken place, then cleanup and exit if (connectCount >= 2) { - subscribeCount.should.equal(2) + assert.strictEqual(subscribeCount, 2) client.end(true, done) } }) diff --git a/test/client.js b/test/client.js index 008d03717..83f0800cd 100644 --- a/test/client.js +++ b/test/client.js @@ -1,8 +1,8 @@ 'use strict' var mqtt = require('..') -var should = require('should') -var fork = require('child_process').fork +var assert = require('chai').assert +const { fork } = require('child_process') var path = require('path') var abstractClientTests = require('./abstract_client') var net = require('net') @@ -11,171 +11,81 @@ var mqttPacket = require('mqtt-packet') var Buffer = require('safe-buffer').Buffer var Duplex = require('readable-stream').Duplex var Connection = require('mqtt-connection') -var Server = require('./server') -var FastServer = require('./server').FastMqttServer -var port = 9876 -var server - -function connOnlyServer () { - return new Server(function (client) { - client.on('connect', function (packet) { - client.connack({returnCode: 0}) - }) - }) -} - -/** - * Test server - */ -function buildServer (fastFlag) { - var handler = function (client) { - client.on('auth', function (packet) { - var rc = 'reasonCode' - var connack = {} - connack[rc] = 0 - client.connack(connack) - }) - client.on('connect', function (packet) { - var rc = 'returnCode' - var connack = {} - if (client.options && client.options.protocolVersion === 5) { - rc = 'reasonCode' - if (packet.clientId === 'invalid') { - connack[rc] = 128 - } else { - connack[rc] = 0 - } - } else { - if (packet.clientId === 'invalid') { - connack[rc] = 2 - } else { - connack[rc] = 0 - } - } - if (packet.properties && packet.properties.authenticationMethod) { - return false - } else { - client.connack(connack) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) +var MqttServer = require('./server').MqttServer +var util = require('util') +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var debug = require('debug')('TEST:client') - client.on('unsubscribe', function (packet) { - packet.granted = packet.unsubscriptions.map(function () { return 0 }) - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) - } - if (fastFlag) { - return new FastServer(handler) - } else { - return new Server(handler) - } -} +describe('MqttClient', function () { + var client + var server = serverBuilder() + var config = {protocol: 'mqtt', port: ports.PORT} + server.listen(ports.PORT) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) -server = buildServer().listen(port) + abstractClientTests(server, config) -describe('MqttClient', function () { describe('creating', function () { it('should allow instantiation of MqttClient without the \'new\' operator', function (done) { - should(function () { - var client - try { - client = mqtt.MqttClient(function () { - throw Error('break') - }, {}) - client.end() - } catch (err) { - if (err.message !== 'break') { - throw err - } - done() - } - }).not.throw('Object # has no method \'_setupStream\'') + try { + client = mqtt.MqttClient(function () { + throw Error('break') + }, {}) + client.end() + } catch (err) { + assert.strictEqual(err.message, 'break') + done() + } }) }) - var config = { protocol: 'mqtt', port: port } - abstractClientTests(server, config) - describe('message ids', function () { it('should increment the message id', function () { - var client = mqtt.connect(config) + client = mqtt.connect(config) var currentId = client._nextId() - client._nextId().should.equal(currentId + 1) + assert.equal(client._nextId(), currentId + 1) client.end() }) it('should return 1 once the internal counter reached limit', function () { - var client = mqtt.connect(config) + client = mqtt.connect(config) client.nextId = 65535 - client._nextId().should.equal(65535) - client._nextId().should.equal(1) + assert.equal(client._nextId(), 65535) + assert.equal(client._nextId(), 1) client.end() }) it('should return 65535 for last message id once the internal counter reached limit', function () { - var client = mqtt.connect(config) + client = mqtt.connect(config) client.nextId = 65535 - client._nextId().should.equal(65535) - client.getLastMessageId().should.equal(65535) - client._nextId().should.equal(1) - client.getLastMessageId().should.equal(1) + assert.equal(client._nextId(), 65535) + assert.equal(client.getLastMessageId(), 65535) + assert.equal(client._nextId(), 1) + assert.equal(client.getLastMessageId(), 1) client.end() }) it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { - var server2 = new Server(function (c) { - c.on('connect', function (packet) { - c.connack({returnCode: 0}) - c.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) }) }) - server2.listen(port + 49, function () { - var client = mqtt.connect({ - port: port + 49, + server2.listen(ports.PORTAND49, function () { + client = mqtt.connect({ + port: ports.PORTAND49, host: 'localhost' }) @@ -200,7 +110,7 @@ describe('MqttClient', function () { cb() // nothing to do } }) - var client = new mqtt.MqttClient(function () { + client = new mqtt.MqttClient(function () { return duplex }, {}) @@ -241,11 +151,16 @@ describe('MqttClient', function () { describe('flushing', function () { it('should attempt to complete pending unsub and send on ping timeout', function (done) { this.timeout(10000) - var server3 = connOnlyServer().listen(port + 72) + var server3 = new MqttServer(function (client) { + client.on('connect', function (packet) { + client.connack({returnCode: 0}) + }) + }).listen(ports.PORTAND72) + var pubCallbackCalled = false var unsubscribeCallbackCalled = false - var client = mqtt.connect({ - port: port + 72, + client = mqtt.connect({ + port: ports.PORTAND72, host: 'localhost', keepalive: 1, connectTimeout: 350, @@ -253,16 +168,16 @@ describe('MqttClient', function () { }) client.once('connect', () => { client.publish('fakeTopic', 'fakeMessage', {qos: 1}, (err, result) => { - should.exist(err) + assert.exists(err) pubCallbackCalled = true }) client.unsubscribe('fakeTopic', (err, result) => { - should.exist(err) + assert.exists(err) unsubscribeCallbackCalled = true }) setTimeout(() => { client.end(() => { - should.equal(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') + assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') server3.close() done() }) @@ -273,47 +188,56 @@ describe('MqttClient', function () { describe('reconnecting', function () { it('should attempt to reconnect once server is down', function (done) { - this.timeout(15000) + this.timeout(30000) - var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js')) - var client = mqtt.connect({ port: 3000, host: 'localhost', keepalive: 1 }) + var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) + innerServer.on('close', (code) => { + if (code) { + done(util.format('child process closed with code %d', code)) + } + }) + innerServer.on('exit', (code) => { + if (code) { + done(util.format('child process exited with code %d', code)) + } + }) + + client = mqtt.connect({ port: 3000, host: 'localhost', keepalive: 1 }) client.once('connect', function () { innerServer.kill('SIGINT') // mocks server shutdown - client.once('close', function () { - should.exist(client.reconnectTimer) - client.end() - done() + assert.exists(client.reconnectTimer) + client.end(true, done) }) }) }) it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { this.timeout(15000) + var actualURL41 = 'wss://localhost:9917/' + var actualURL42 = 'ws://localhost:9918/' + var serverPort41 = serverBuilder(true).listen(ports.PORTAND41) + var serverPort42 = serverBuilder(true).listen(ports.PORTAND42) - var server = buildServer(true).listen(port + 41) - var server2 = buildServer(true).listen(port + 42) - - server2.on('listening', function () { - var client = mqtt.connect({ + serverPort42.on('listening', function () { + client = mqtt.connect({ protocol: 'wss', servers: [ - { port: port + 42, host: 'localhost', protocol: 'ws' }, - { port: port + 41, host: 'localhost' } + { port: ports.PORTAND41, host: 'localhost' }, + { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' } ], keepalive: 50 }) - server2.on('client', function (c) { - should.equal(client.stream.socket.url, 'ws://localhost:9918/', 'Protocol for first connection should use ws.') - c.stream.destroy() - server2.close() + serverPort41.once('client', function () { + assert.equal(client.stream.socket.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') + client.end(true, done) + serverPort41.close() }) - - server.once('client', function () { - should.equal(client.stream.socket.url, 'wss://localhost:9917/', 'Protocol for second client should use the default protocol: wss, on port: port + 42.') - client.end() - done() + serverPort42.on('client', function (c) { + assert.equal(client.stream.socket.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') + c.stream.destroy() + serverPort42.close() }) client.once('connect', function () { @@ -325,7 +249,7 @@ describe('MqttClient', function () { it('should reconnect if a connack is not received in an interval', function (done) { this.timeout(2000) - var server2 = net.createServer().listen(port + 43) + var server2 = net.createServer().listen(ports.PORTAND43) server2.on('connection', function (c) { eos(c, function () { @@ -334,17 +258,18 @@ describe('MqttClient', function () { }) server2.on('listening', function () { - var client = mqtt.connect({ + client = mqtt.connect({ servers: [ - { port: port + 43, host: 'localhost_fake' }, - { port: port, host: 'localhost' } + { port: ports.PORTAND43, host: 'localhost_fake' }, + { port: ports.PORT, host: 'localhost' } ], connectTimeout: 500 }) server.once('client', function () { - client.end() - done() + client.end(true, (err) => { + done(err) + }) }) client.once('connect', function () { @@ -356,7 +281,7 @@ describe('MqttClient', function () { it('should not be cleared by the connack timer', function (done) { this.timeout(4000) - var server2 = net.createServer().listen(port + 44) + var server2 = net.createServer().listen(ports.PORTAND44) server2.on('connection', function (c) { c.destroy() @@ -367,8 +292,8 @@ describe('MqttClient', function () { var connectTimeout = 1000 var reconnectPeriod = 100 var expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) - var client = mqtt.connect({ - port: port + 44, + client = mqtt.connect({ + port: ports.PORTAND44, host: 'localhost', connectTimeout: connectTimeout, reconnectPeriod: reconnectPeriod @@ -377,8 +302,7 @@ describe('MqttClient', function () { client.on('reconnect', function () { reconnects++ if (reconnects >= expectedReconnects) { - client.end() - done() + client.end(true, done) } }) }) @@ -387,27 +311,30 @@ describe('MqttClient', function () { it('should not keep requeueing the first message when offline', function (done) { this.timeout(2500) - var server2 = buildServer().listen(port + 45) - var client = mqtt.connect({ - port: port + 45, + var server2 = serverBuilder().listen(ports.PORTAND45) + client = mqtt.connect({ + port: ports.PORTAND45, host: 'localhost', connectTimeout: 350, reconnectPeriod: 300 }) - server2.on('client', function (c) { + server2.on('client', function (serverClient) { client.publish('hello', 'world', { qos: 1 }, function () { - c.destroy() - server2.close() - client.publish('hello', 'world', { qos: 1 }) + serverClient.destroy() + server2.close(() => { + debug('now publishing message in an offline state') + client.publish('hello', 'world', { qos: 1 }) + }) }) }) setTimeout(function () { if (client.queue.length === 0) { - client.end(true) - done() + debug('calling final client.end()') + client.end(true, (err) => done(err)) } else { + debug('calling client.end()') client.end(true) } }, 2000) @@ -419,37 +346,41 @@ describe('MqttClient', function () { var KILL_COUNT = 4 var killedConnections = 0 var subIds = {} - var client = mqtt.connect({ - port: port + 46, + client = mqtt.connect({ + port: ports.PORTAND46, host: 'localhost', connectTimeout: 350, reconnectPeriod: 300 }) - var server2 = new Server(function (client) { - client.on('error', function () {}) - client.on('connect', function (packet) { + var server2 = new MqttServer(function (serverClient) { + serverClient.on('error', function () {}) + debug('setting serverClient connect callback') + serverClient.on('connect', function (packet) { if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) + debug('connack with returnCode 2') + serverClient.connack({returnCode: 2}) } else { - client.connack({returnCode: 0}) + debug('connack with returnCode 0') + serverClient.connack({returnCode: 0}) } }) - }).listen(port + 46) + }).listen(ports.PORTAND46) - server2.on('client', function (c) { + server2.on('client', function (serverClient) { + debug('client received on server2.') + debug('subscribing to topic `topic`') client.subscribe('topic', function () { - done() - client.end() - c.destroy() - server2.close() + debug('once subscribed to topic, end client, destroy serverClient, and close server.') + serverClient.destroy() + server2.close(() => { client.end(true, done) }) }) - c.on('subscribe', function (packet) { + serverClient.on('subscribe', function (packet) { if (killedConnections < KILL_COUNT) { // Kill the first few sub attempts to simulate a flaky connection killedConnections++ - c.destroy() + serverClient.destroy() } else { // Keep track of acks if (!subIds[packet.messageId]) { @@ -459,11 +390,11 @@ describe('MqttClient', function () { if (subIds[packet.messageId] > 1) { done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) client.end(true) - c.destroy() + serverClient.end() server2.destroy() } - c.suback({ + serverClient.suback({ messageId: packet.messageId, granted: packet.subscriptions.map(function (e) { return e.qos @@ -476,22 +407,19 @@ describe('MqttClient', function () { it('should not fill the queue of subscribes if it cannot connect', function (done) { this.timeout(2500) - - var port2 = port + 48 - var server2 = net.createServer(function (stream) { - var client = new Connection(stream) + var serverClient = new Connection(stream) - client.on('error', function () {}) - client.on('connect', function (packet) { - client.connack({returnCode: 0}) - client.destroy() + serverClient.on('error', function (e) { /* do nothing */ }) + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.destroy() }) }) - server2.listen(port2, function () { - var client = mqtt.connect({ - port: port2, + server2.listen(ports.PORTAND48, function () { + client = mqtt.connect({ + port: ports.PORTAND48, host: 'localhost', connectTimeout: 350, reconnectPeriod: 300 @@ -500,9 +428,10 @@ describe('MqttClient', function () { client.subscribe('hello') setTimeout(function () { - client.queue.length.should.equal(1) - client.end() - done() + assert.equal(client.queue.length, 1) + client.end(true, () => { + done() + }) }, 1000) }) }) @@ -513,43 +442,42 @@ describe('MqttClient', function () { var KILL_COUNT = 4 var killedConnections = 0 var pubIds = {} - var client = mqtt.connect({ - port: port + 47, + client = mqtt.connect({ + port: ports.PORTAND47, host: 'localhost', connectTimeout: 350, reconnectPeriod: 300 }) var server2 = net.createServer(function (stream) { - var client = new Connection(stream) - client.on('error', function () {}) - client.on('connect', function (packet) { + var serverClient = new Connection(stream) + serverClient.on('error', function () {}) + serverClient.on('connect', function (packet) { if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) + serverClient.connack({returnCode: 2}) } else { - client.connack({returnCode: 0}) + serverClient.connack({returnCode: 0}) } }) - this.emit('client', client) - }).listen(port + 47) + this.emit('client', serverClient) + }).listen(ports.PORTAND47) - server2.on('client', function (c) { + server2.on('client', function (serverClient) { client.publish('topic', 'data', { qos: 1 }, function () { - done() - client.end() - c.destroy() - server2.destroy() + serverClient.destroy() + server2.close() + client.end(true, done) }) - c.on('publish', function onPublish (packet) { + serverClient.on('publish', function onPublish (packet) { if (killedConnections < KILL_COUNT) { // Kill the first few pub attempts to simulate a flaky connection killedConnections++ - c.destroy() + serverClient.destroy() // to avoid receiving inflight messages - c.removeListener('publish', onPublish) + serverClient.removeListener('publish', onPublish) } else { // Keep track of acks if (!pubIds[packet.messageId]) { @@ -561,11 +489,11 @@ describe('MqttClient', function () { if (pubIds[packet.messageId] > 1) { done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) client.end(true) - c.destroy() + serverClient.destroy() server2.destroy() } - c.puback(packet) + serverClient.puback(packet) } }) }) @@ -574,7 +502,8 @@ describe('MqttClient', function () { it('check emit error on checkDisconnection w/o callback', function (done) { this.timeout(15000) - var server118 = new Server(function (client) { + + var server118 = new MqttServer(function (client) { client.on('connect', function (packet) { client.connack({ reasonCode: 0 @@ -586,15 +515,18 @@ describe('MqttClient', function () { client.puback(packet) }) }) - }).listen(port + 118) + }).listen(ports.PORTAND118) + var opts = { host: 'localhost', - port: port + 118, + port: ports.PORTAND118, protocolVersion: 5 } - var client = mqtt.connect(opts) + client = mqtt.connect(opts) + + // wait for the client to receive an error... client.on('error', function (error) { - should(error.message).be.equal('client disconnecting') + assert.equal(error.message, 'client disconnecting') server118.close() done() }) @@ -605,525 +537,4 @@ describe('MqttClient', function () { server118.close() }) }) - - describe('MQTT 5.0', function () { - var server = buildServer().listen(port + 115) - var config = { protocol: 'mqtt', port: port + 115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } - abstractClientTests(server, config) - it('should has Auth method with Auth data', function (done) { - this.timeout(5000) - var opts = {host: 'localhost', port: port + 115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} - try { - mqtt.connect(opts) - } catch (error) { - should(error.message).be.equal('Packet has no Authentication Method') - } - done() - }) - it('auth packet', function (done) { - this.timeout(15000) - server.once('client', function (client) { - client.on('auth', function (packet) { - done() - }) - }) - var opts = {host: 'localhost', port: port + 115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} - mqtt.connect(opts) - }) - it('Maximum Packet Size', function (done) { - this.timeout(15000) - var opts = {host: 'localhost', port: port + 115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} - var client = mqtt.connect(opts) - client.on('error', function (error) { - should(error.message).be.equal('exceeding packets size connack') - done() - }) - }) - describe('Topic Alias', function () { - it('topicAlias > topicAliasMaximum', function (done) { - this.timeout(15000) - var maximum = 15 - var current = 22 - server.once('client', function (client) { - client.on('publish', function (packet) { - if (packet.properties && packet.properties.topicAlias) { - done(new Error('Packet should not have topicAlias')) - return false - } - done() - }) - }) - var opts = {host: 'localhost', port: port + 115, protocolVersion: 5, properties: { topicAliasMaximum: maximum }} - var client = mqtt.connect(opts) - client.publish('t/h', 'Message', { properties: { topicAlias: current } }) - }) - it('topicAlias w/o topicAliasMaximum in settings', function (done) { - this.timeout(15000) - server.once('client', function (client) { - client.on('publish', function (packet) { - if (packet.properties && packet.properties.topicAlias) { - done(new Error('Packet should not have topicAlias')) - return false - } - done() - }) - }) - var opts = {host: 'localhost', port: port + 115, protocolVersion: 5} - var client = mqtt.connect(opts) - client.publish('t/h', 'Message', { properties: { topicAlias: 22 } }) - }) - }) - it('Change values of some properties by server response', function (done) { - this.timeout(15000) - var server116 = new Server(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 15, - serverKeepAlive: 16, - maximumPacketSize: 95 - } - }) - }) - }).listen(port + 116) - var opts = { - host: 'localhost', - port: port + 116, - protocolVersion: 5, - properties: { - topicAliasMaximum: 10, - serverKeepAlive: 11, - maximumPacketSize: 100 - } - } - var client = mqtt.connect(opts) - client.on('connect', function () { - should(client.options.keepalive).be.equal(16) - should(client.options.properties.topicAliasMaximum).be.equal(15) - should(client.options.properties.maximumPacketSize).be.equal(95) - server116.close() - done() - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { - this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server316 = new Server(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0, - sessionPresent: false - }) - client.on('subscribe', function () { - if (!tryReconnect) { - client.end() - server316.close() - done() - } - }) - }) - }).listen(port + 316) - var opts = { - host: 'localhost', - port: port + 316, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - should(connack.sessionPresent).be.equal(false) - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - }) - - tryReconnect = false - } else { - reconnectEvent.should.equal(true) - } - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { - this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server326 = new Server(function (client) { - client.on('connect', function (packet) { - client.on('subscribe', function (packet) { - if (!reconnectEvent) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - if (!tryReconnect) { - should(packet.properties.userProperties.test).be.equal('test') - client.end() - server326.close() - done() - } - } - }) - client.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - }).listen(port + 326) - var opts = { - host: 'localhost', - port: port + 326, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - should(connack.sessionPresent).be.equal(false) - if (tryReconnect) { - client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { - client.stream.end() - }) - - tryReconnect = false - } else { - reconnectEvent.should.equal(true) - } - }) - }) - - var serverErr = new Server(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0 - }) - }) - client.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - packet.reasonCode = 142 - delete packet.cmd - client.puback(packet) - break - case 2: - packet.reasonCode = 142 - delete packet.cmd - client.pubrec(packet) - break - } - }) - }) - - client.on('pubrel', function (packet) { - packet.reasonCode = 142 - delete packet.cmd - client.pubcomp(packet) - }) - }) - it('Subscribe properties', function (done) { - this.timeout(15000) - var opts = { - host: 'localhost', - port: port + 119, - protocolVersion: 5 - } - var subOptions = { properties: { subscriptionIdentifier: 1234 } } - var server119 = new Server(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0 - }) - }) - client.on('subscribe', function (packet) { - should(packet.properties.subscriptionIdentifier).be.equal(subOptions.properties.subscriptionIdentifier) - server119.close() - done() - }) - }).listen(port + 119) - - var client = mqtt.connect(opts) - client.on('connect', function () { - client.subscribe('a/b', subOptions) - }) - }) - - it('puback handling errors check', function (done) { - this.timeout(15000) - serverErr.listen(port + 117) - var opts = { - host: 'localhost', - port: port + 117, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 1}, function (err, packet) { - should(err.message).be.equal('Publish error: Session taken over') - should(err.code).be.equal(142) - }) - serverErr.close() - done() - }) - }) - it('pubrec handling errors check', function (done) { - this.timeout(15000) - serverErr.listen(port + 118) - var opts = { - host: 'localhost', - port: port + 118, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 2}, function (err, packet) { - should(err.message).be.equal('Publish error: Session taken over') - should(err.code).be.equal(142) - }) - serverErr.close() - done() - }) - }) - it('puback handling custom reason code', function (done) { - this.timeout(15000) - serverErr.listen(port + 117) - var opts = { - host: 'localhost', - port: port + 117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverErr.once('client', function (c) { - c.once('subscribe', function () { - c.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - - c.on('puback', function (packet) { - should(packet.reasonCode).be.equal(128) - client.end() - c.destroy() - serverErr.close() - done() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - it('server side disconnect', function (done) { - this.timeout(15000) - var server327 = new Server(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0 - }) - client.disconnect({reasonCode: 128}) - server327.close() - }) - }) - server327.listen(port + 327) - var opts = { - host: 'localhost', - port: port + 327, - protocolVersion: 5 - } - - var client = mqtt.connect(opts) - client.once('disconnect', function (disconnectPacket) { - should(disconnectPacket.reasonCode).be.equal(128) - done() - }) - }) - it('pubrec handling custom reason code', function (done) { - this.timeout(15000) - serverErr.listen(port + 117) - var opts = { - host: 'localhost', - port: port + 117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverErr.once('client', function (c) { - c.once('subscribe', function () { - c.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - - c.on('pubrec', function (packet) { - should(packet.reasonCode).be.equal(128) - client.end() - c.destroy() - serverErr.close() - done() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - it('puback handling custom reason code with error', function (done) { - this.timeout(15000) - serverErr.listen(port + 117) - var opts = { - host: 'localhost', - port: port + 117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverErr.once('client', function (c) { - c.once('subscribe', function () { - c.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - should(error.message).be.equal('a/b is not valid') - client.end() - serverErr.close() - done() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - it('pubrec handling custom reason code with error', function (done) { - this.timeout(15000) - serverErr.listen(port + 117) - var opts = { - host: 'localhost', - port: port + 117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverErr.once('client', function (c) { - c.once('subscribe', function () { - c.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - should(error.message).be.equal('a/b is not valid') - client.end() - serverErr.close() - done() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - it('puback handling custom invalid reason code', function (done) { - this.timeout(15000) - serverErr.listen(port + 117) - var opts = { - host: 'localhost', - port: port + 117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 124124 - } - cb(code) - } - } - - serverErr.once('client', function (c) { - c.once('subscribe', function () { - c.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - should(error.message).be.equal('Wrong reason code for puback') - client.end() - serverErr.close() - done() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - it('pubrec handling custom invalid reason code', function (done) { - this.timeout(15000) - serverErr.listen(port + 117) - var opts = { - host: 'localhost', - port: port + 117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 34535 - } - cb(code) - } - } - - serverErr.once('client', function (c) { - c.once('subscribe', function () { - c.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - should(error.message).be.equal('Wrong reason code for pubrec') - client.end() - serverErr.close() - done() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - }) }) diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js new file mode 100644 index 000000000..20de9f4ad --- /dev/null +++ b/test/client_mqtt5.js @@ -0,0 +1,538 @@ +'use strict' + +var mqtt = require('..') +var abstractClientTests = require('./abstract_client') +var Buffer = require('safe-buffer').Buffer +var MqttServer = require('./server').MqttServer +var assert = require('chai').assert +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var ports = require('./helpers/port_list') + +describe('MQTT 5.0', function () { + var server = serverBuilder().listen(ports.PORTAND115) + var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } + + abstractClientTests(server, config) + + // var server = serverBuilder().listen(ports.PORTAND115) + + var topicAliasTests = [ + {properties: {}, name: 'should allow any topicAlias when no topicAliasMaximum provided in settings'}, + {properties: { topicAliasMaximum: 15 }, name: 'should not allow topicAlias > topicAliasMaximum when topicAliasMaximum provided in settings'} + ] + + topicAliasTests.forEach(function (test) { + it(test.name, function (done) { + this.timeout(15000) + server.once('client', function (serverClient) { + serverClient.on('publish', function (packet) { + if (packet.properties && packet.properties.topicAlias) { + done(new Error('Packet should not have topicAlias')) + return false + } else { + serverClient.end(done) + } + }) + }) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: test.properties} + var client = mqtt.connect(opts) + client.publish('t/h', 'Message', { properties: { topicAlias: 22 } }) + }) + }) + + it('should throw an error if there is Auth Data with no Auth Method', function (done) { + this.timeout(5000) + var client + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} + console.log('client connecting') + client = mqtt.connect(opts) + client.on('error', function (error) { + console.log('error hit') + assert.strictEqual(error.message, 'Packet has no Authentication Method') + // client will not be connected, so we will call done. + assert.isTrue(client.disconnected, 'validate client is disconnected') + client.end(true) + done() + }) + }) + + it('auth packet', function (done) { + this.timeout(15000) + server.once('client', function (serverClient) { + console.log('server received client') + serverClient.on('auth', function (packet) { + console.log('serverClient received auth: packet %o', packet) + serverClient.end(done) + }) + }) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} + console.log('calling mqtt connect') + mqtt.connect(opts) + }) + + it('Maximum Packet Size', function (done) { + this.timeout(15000) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'exceeding packets size connack') + client.end(true, done) + }) + }) + + it('Change values of some properties by server response', function (done) { + this.timeout(15000) + var server116 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 15, + serverKeepAlive: 16, + maximumPacketSize: 95 + } + }) + }) + }).listen(ports.PORTAND116) + var opts = { + host: 'localhost', + port: ports.PORTAND116, + protocolVersion: 5, + properties: { + topicAliasMaximum: 10, + serverKeepAlive: 11, + maximumPacketSize: 100 + } + } + var client = mqtt.connect(opts) + client.on('connect', function () { + assert.strictEqual(client.options.keepalive, 16) + assert.strictEqual(client.options.properties.topicAliasMaximum, 15) + assert.strictEqual(client.options.properties.maximumPacketSize, 95) + server116.close() + client.end(true, done) + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { + this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server316 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + serverClient.on('subscribe', function () { + if (!tryReconnect) { + server316.close() + serverClient.end(done) + } + }) + }) + }).listen(ports.PORTAND316) + var opts = { + host: 'localhost', + port: ports.PORTAND316, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { + // this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server326 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + serverClient.on('subscribe', function (packet) { + if (!reconnectEvent) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } else { + if (!tryReconnect) { + assert.strictEqual(packet.properties.userProperties.test, 'test') + serverClient.end(done) + server326.close() + } + } + }) + }).listen(ports.PORTAND326) + + var opts = { + host: 'localhost', + port: ports.PORTAND326, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + var serverThatSendsErrors = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + packet.reasonCode = 142 + delete packet.cmd + serverClient.puback(packet) + break + case 2: + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubcomp(packet) + }) + }) + + it('Subscribe properties', function (done) { + this.timeout(15000) + var opts = { + host: 'localhost', + port: ports.PORTAND119, + protocolVersion: 5 + } + var subOptions = { properties: { subscriptionIdentifier: 1234 } } + var server119 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('subscribe', function (packet) { + assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) + server119.close() + serverClient.end() + done() + }) + }).listen(ports.PORTAND119) + + var client = mqtt.connect(opts) + client.on('connect', function () { + client.subscribe('a/b', subOptions) + }) + }) + + it('puback handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 1}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('pubrec handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND118) + var opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 2}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('puback handling custom reason code', function (done) { + // this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + + serverClient.on('puback', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + serverClient.end(done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('server side disconnect', function (done) { + this.timeout(15000) + var server327 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + serverClient.disconnect({reasonCode: 128}) + server327.close() + }) + }) + server327.listen(ports.PORTAND327) + var opts = { + host: 'localhost', + port: ports.PORTAND327, + protocolVersion: 5 + } + + var client = mqtt.connect(opts) + client.once('disconnect', function (disconnectPacket) { + assert.strictEqual(disconnectPacket.reasonCode, 128) + client.end(true, done) + }) + }) + + it('pubrec handling custom reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + + serverClient.on('pubrec', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + client.end(true, done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 124124 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for puback') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 34535 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for pubrec') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) +}) diff --git a/test/helpers/port_list.js b/test/helpers/port_list.js new file mode 100644 index 000000000..46253bf21 --- /dev/null +++ b/test/helpers/port_list.js @@ -0,0 +1,45 @@ +var PORT = 9876 +var PORTAND41 = PORT + 41 +var PORTAND42 = PORT + 42 +var PORTAND43 = PORT + 43 +var PORTAND44 = PORT + 44 +var PORTAND45 = PORT + 45 +var PORTAND46 = PORT + 46 +var PORTAND47 = PORT + 47 +var PORTAND48 = PORT + 48 +var PORTAND49 = PORT + 49 +var PORTAND50 = PORT + 50 +var PORTAND72 = PORT + 72 +var PORTAND114 = PORT + 114 +var PORTAND115 = PORT + 115 +var PORTAND116 = PORT + 116 +var PORTAND117 = PORT + 117 +var PORTAND118 = PORT + 118 +var PORTAND119 = PORT + 119 +var PORTAND316 = PORT + 316 +var PORTAND326 = PORT + 326 +var PORTAND327 = PORT + 327 + +module.exports = { + PORT, + PORTAND41, + PORTAND42, + PORTAND43, + PORTAND44, + PORTAND45, + PORTAND46, + PORTAND47, + PORTAND48, + PORTAND49, + PORTAND50, + PORTAND72, + PORTAND114, + PORTAND115, + PORTAND116, + PORTAND117, + PORTAND118, + PORTAND119, + PORTAND316, + PORTAND326, + PORTAND327 +} diff --git a/test/helpers/server.js b/test/helpers/server.js index 9750bf1ff..46bd79537 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -1,10 +1,11 @@ 'use strict' -var Server = require('../server') +var MqttServer = require('../server').MqttServer +var MqttSecureServer = require('../server').MqttSecureServer var fs = require('fs') module.exports.init_server = function (PORT) { - var server = new Server(function (client) { + var server = new MqttServer(function (client) { client.on('connect', function () { client.connack(0) }) @@ -39,7 +40,7 @@ module.exports.init_server = function (PORT) { } module.exports.init_secure_server = function (port, key, cert) { - var server = new Server.SecureServer({ + var server = new MqttSecureServer({ key: fs.readFileSync(key), cert: fs.readFileSync(cert) }, function (client) { diff --git a/test/helpers/server_process.js b/test/helpers/server_process.js index 747dc679f..7558bebf6 100644 --- a/test/helpers/server_process.js +++ b/test/helpers/server_process.js @@ -1,8 +1,8 @@ 'use strict' -var Server = require('../server') +var MqttServer = require('../server').MqttServer -new Server(function (client) { +new MqttServer(function (client) { client.on('connect', function () { client.connack({ returnCode: 0 }) }) diff --git a/test/mocha.opts b/test/mocha.opts index 4a099c904..4008c54c1 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,4 @@ --check-leaks ---timeout 5000 +--timeout 10000 --exit diff --git a/test/secure_client.js b/test/secure_client.js index a3a77868d..95b7a6197 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -8,13 +8,11 @@ var port = 9899 var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') -var Server = require('./server') +var MqttSecureServer = require('./server').MqttSecureServer var assert = require('chai').assert -var server = new Server.SecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) -}, function (client) { +var serverListener = function (client) { + // this is the Server's MQTT Client client.on('connect', function (packet) { if (packet.clientId === 'invalid') { client.connack({returnCode: 2}) @@ -70,7 +68,12 @@ var server = new Server.SecureServer({ client.on('pingreq', function () { client.pingresp() }) -}).listen(port) +} + +var server = new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) +}, serverListener).listen(port) describe('MqttSecureClient', function () { var config = { protocol: 'mqtts', port: port, rejectUnauthorized: false } diff --git a/test/server.js b/test/server.js index c92b5800e..ccfe2f4d1 100644 --- a/test/server.js +++ b/test/server.js @@ -2,65 +2,54 @@ var net = require('net') var tls = require('tls') -var inherits = require('inherits') var Connection = require('mqtt-connection') -var MqttServer -var FastMqttServer -var MqttSecureServer -var setupConnection = function (duplex) { - var that = this - var connection = new Connection(duplex, function () { - that.emit('client', connection) - }) -} - -/* +/** * MqttServer * * @param {Function} listener - fired on client connection */ -MqttServer = module.exports = function Server (listener) { - if (!(this instanceof Server)) { - return new Server(listener) - } +class MqttServer extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + var that = this + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) + }) - net.Server.call(this) - - this.on('connection', setupConnection) - - if (listener) { - this.on('client', listener) + if (listener) { + this.on('client', listener) + } } - - return this } -inherits(MqttServer, net.Server) -/* - * FastMqttServer(w/o waiting for initialization) +/** + * MqttServerNoWait (w/o waiting for initialization) * * @param {Function} listener - fired on client connection */ -FastMqttServer = module.exports.FastMqttServer = function Server (listener) { - if (!(this instanceof Server)) { - return new Server(listener) - } - - net.Server.call(this) +class MqttServerNoWait extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex) + // do not wait for connection to return to send it to the client. + this.emit('client', connection) + }) - this.on('connection', function (duplex) { - var connection = new Connection(duplex) - this.emit('client', connection) - }) - - if (listener) { - this.on('client', listener) + if (listener) { + this.on('client', listener) + } } - - return this } -inherits(FastMqttServer, net.Server) /** * MqttSecureServer @@ -68,27 +57,38 @@ inherits(FastMqttServer, net.Server) * @param {Object} opts - server options * @param {Function} listener */ -MqttSecureServer = module.exports.SecureServer = - function SecureServer (opts, listener) { - if (!(this instanceof SecureServer)) { - return new SecureServer(opts, listener) - } - - // new MqttSecureServer(function(){}) +class MqttSecureServer extends tls.Server { + constructor (opts, listener) { if (typeof opts === 'function') { listener = opts opts = {} } - tls.Server.call(this, opts) + // sets a listener for the 'connection' event + super(opts) + this.connectionList = [] + + this.on('secureConnection', function (socket) { + this.connectionList.push(socket) + var that = this + var connection = new Connection(socket, function () { + that.emit('client', connection) + }) + }) if (listener) { this.on('client', listener) } + } - this.on('secureConnection', setupConnection) - - return this + setupConnection (duplex) { + var that = this + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) } -inherits(MqttSecureServer, tls.Server) -MqttSecureServer.prototype.setupConnection = setupConnection +} + +exports.MqttServer = MqttServer +exports.MqttServerNoWait = MqttServerNoWait +exports.MqttSecureServer = MqttSecureServer diff --git a/test/server_helpers_for_client_tests.js b/test/server_helpers_for_client_tests.js new file mode 100644 index 000000000..34f1a8d35 --- /dev/null +++ b/test/server_helpers_for_client_tests.js @@ -0,0 +1,100 @@ +'use strict' + +var MqttServer = require('./server').MqttServer +var MqttServerNoWait = require('./server').MqttServerNoWait +var debug = require('debug')('TEST:server_helpers') + +/** + * This will build the client for the server to use during testing, and set up the + * server side client based on mqtt-connection for handling MQTT messages. + * @param {boolean} fastFlag + */ +function serverBuilder (fastFlag) { + var handler = function (serverClient) { + serverClient.on('auth', function (packet) { + var rc = 'reasonCode' + var connack = {} + connack[rc] = 0 + serverClient.connack(connack) + }) + serverClient.on('connect', function (packet) { + var rc = 'returnCode' + var connack = {} + if (serverClient.options && serverClient.options.protocolVersion === 5) { + rc = 'reasonCode' + if (packet.clientId === 'invalid') { + connack[rc] = 128 + } else { + connack[rc] = 0 + } + } else { + if (packet.clientId === 'invalid') { + connack[rc] = 2 + } else { + connack[rc] = 0 + } + } + if (packet.properties && packet.properties.authenticationMethod) { + return false + } else { + serverClient.connack(connack) + } + }) + + serverClient.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + serverClient.puback(packet) + break + case 2: + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + serverClient.pubcomp(packet) + }) + + serverClient.on('pubrec', function (packet) { + serverClient.pubrel(packet) + }) + + serverClient.on('pubcomp', function () { + // Nothing to be done + }) + + serverClient.on('subscribe', function (packet) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + serverClient.on('unsubscribe', function (packet) { + packet.granted = packet.unsubscriptions.map(function () { return 0 }) + serverClient.unsuback(packet) + }) + + serverClient.on('pingreq', function () { + serverClient.pingresp() + }) + + serverClient.on('end', function () { + debug('disconnected from server') + }) + } + if (fastFlag) { + return new MqttServerNoWait(handler) + } else { + return new MqttServer(handler) + } +} + +exports.serverBuilder = serverBuilder diff --git a/test/websocket_client.js b/test/websocket_client.js index 08c18b147..e9d2d4c79 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -26,9 +26,7 @@ function attachWebsocketServer (wsServer) { return wsServer } -attachWebsocketServer(server) - -server.on('client', function (client) { +function attachClientEventHandlers (client) { client.on('connect', function (packet) { if (packet.clientId === 'invalid') { client.connack({ returnCode: 2 }) @@ -81,7 +79,11 @@ server.on('client', function (client) { client.on('pingreq', function () { client.pingresp() }) -}).listen(port) +} + +attachWebsocketServer(server) + +server.on('client', attachClientEventHandlers).listen(port) describe('Websocket Client', function () { var baseConfig = { protocol: 'ws', port: port } @@ -93,7 +95,7 @@ describe('Websocket Client', function () { it('should use mqtt as the protocol by default', function (done) { server.once('client', function (client) { - client.stream.socket.protocol.should.equal('mqtt') + assert.strictEqual(client.stream.socket.protocol, 'mqtt') }) mqtt.connect(makeOptions()).on('connect', function () { this.end(true, done) @@ -127,7 +129,7 @@ describe('Websocket Client', function () { it('should use mqttv3.1 as the protocol if using v3.1', function (done) { server.once('client', function (client) { - client.stream.socket.protocol.should.equal('mqttv3.1') + assert.strictEqual(client.stream.socket.protocol, 'mqttv3.1') }) var opts = makeOptions({ From d789632740b77ba900f82785794fd08a76d3c561 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Fri, 24 Apr 2020 15:17:33 -0700 Subject: [PATCH 013/110] chore: cleanup readme for v4 release --- .github/workflows/nodejs.yml | 4 ++-- README.md | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 358d27aef..47e6645ef 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,6 +1,6 @@ -name: Node.js CI +name: MQTT.js CI -on: +on: push: branches: - master diff --git a/README.md b/README.md index 601f73c5c..1c6e83835 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,7 @@ ![mqtt.js](https://raw.githubusercontent.com/mqttjs/MQTT.js/137ee0e3940c1f01049a30248c70f24dc6e6f829/MQTT.js.png) ======= -[![Build Status](https://travis-ci.org/mqttjs/MQTT.js.svg)](https://travis-ci.org/mqttjs/MQTT.js) [![codecov](https://codecov.io/gh/mqttjs/MQTT.js/branch/master/graph/badge.svg)](https://codecov.io/gh/mqttjs/MQTT.js) - -[![NPM](https://nodei.co/npm-dl/mqtt.png)](https://nodei.co/npm/mqtt/) [![NPM](https://nodei.co/npm/mqtt.png)](https://nodei.co/npm/mqtt/) - -[![Sauce Test Status](https://saucelabs.com/browser-matrix/mqttjs.svg)](https://saucelabs.com/u/mqttjs) +![Github Test Status](https://github.com/mqttjs/MQTT.js/workflows/MQTT.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/mqttjs/MQTT.js/branch/master/graph/badge.svg)](https://codecov.io/gh/mqttjs/MQTT.js) MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written in JavaScript for node.js and the browser. @@ -31,6 +27,11 @@ Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github. ## Important notes for existing users +v4.0.0 removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to +debug logging, along with some feature additions. + +v3.0.0 adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. + v2.0.0 removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending packets. It also removes all the deprecated functionality in v1.0.0, mainly `mqtt.createConnection` and `mqtt.Server`. From v2.0.0, @@ -48,6 +49,13 @@ performance by a 30% factor, embeds Websocket support support for QoS 1 and 2. The previous API is still supported but deprecated, as such, it is not documented in this README. +For v4.0.0: +As a __breaking change__, by default a error handler is built into the MQTT.js client, so if any +errors are emitted and the user has not created an event handler on the client for errors, the client will +not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been +added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. + +For v2.0.0: As a __breaking change__, the `encoding` option in the old client is removed, and now everything is UTF-8 with the exception of the `password` in the CONNECT message and `payload` in the PUBLISH message, From fa4e06b0ee678757e5d88c827be0b2a6f0b4e970 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Fri, 24 Apr 2020 15:48:05 -0700 Subject: [PATCH 014/110] chore: change workflow to v10-14 and doc fixes (#1079) --- .github/workflows/nodejs.yml | 2 +- README.md | 47 ++++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 47e6645ef..bea543018 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [8.x, 10.x, 12.x] + node-version: [10.x, 12.x, 14.x] fail-fast: false steps: diff --git a/README.md b/README.md index 1c6e83835..71415f0f6 100644 --- a/README.md +++ b/README.md @@ -27,35 +27,25 @@ Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github. ## Important notes for existing users -v4.0.0 removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to +__v4.0.0__ (Released 04/2020) removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to debug logging, along with some feature additions. -v3.0.0 adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. +As a __breaking change__, by default a error handler is built into the MQTT.js client, so if any +errors are emitted and the user has not created an event handler on the client for errors, the client will +not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been +added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. -v2.0.0 removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending +__v3.0.0__ adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. + +__Note:__ MQTT v5 support is experimental as it has not been implemented by brokers yet. + +__v2.0.0__ removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending packets. It also removes all the deprecated functionality in v1.0.0, mainly `mqtt.createConnection` and `mqtt.Server`. From v2.0.0, subscriptions are restored upon reconnection if `clean: true`. v1.x.x is now in *LTS*, and it will keep being supported as long as there are v0.8, v0.10 and v0.12 users. -v1.0.0 improves the overall architecture of the project, which is now -split into three components: MQTT.js keeps the Client, -[mqtt-connection](http://npm.im/mqtt-connection) includes the barebone -Connection code for server-side usage, and [mqtt-packet](http://npm.im/mqtt-packet) -includes the protocol parser and generator. The new Client improves -performance by a 30% factor, embeds Websocket support -([MOWS](http://npm.im/mows) is now deprecated), and it has a better -support for QoS 1 and 2. The previous API is still supported but -deprecated, as such, it is not documented in this README. - -For v4.0.0: -As a __breaking change__, by default a error handler is built into the MQTT.js client, so if any -errors are emitted and the user has not created an event handler on the client for errors, the client will -not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been -added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. - -For v2.0.0: As a __breaking change__, the `encoding` option in the old client is removed, and now everything is UTF-8 with the exception of the `password` in the CONNECT message and `payload` in the PUBLISH message, @@ -64,7 +54,15 @@ which are `Buffer`. Another __breaking change__ is that MQTT.js now defaults to MQTT v3.1.1, so to support old brokers, please read the [client options doc](#client). -MQTT v5 support is experimental as it has not been implemented by brokers yet. +__v1.0.0__ improves the overall architecture of the project, which is now +split into three components: MQTT.js keeps the Client, +[mqtt-connection](http://npm.im/mqtt-connection) includes the barebone +Connection code for server-side usage, and [mqtt-packet](http://npm.im/mqtt-packet) +includes the protocol parser and generator. The new Client improves +performance by a 30% factor, embeds Websocket support +([MOWS](http://npm.im/mows) is now deprecated), and it has a better +support for QoS 1 and 2. The previous API is still supported but +deprecated, as such, it is not documented in this README. ## Installation @@ -328,6 +326,13 @@ Emitted when the client goes offline. Emitted when the client cannot connect (i.e. connack rc != 0) or when a parsing error occurs. +The following TLS errors will be emitted as an `error` event: + +* `ECONNREFUSED` +* `ECONNRESET` +* `EADDRINUSE` +* `ENOTFOUND` + #### Event `'end'` `function () {}` From 231682db6db58eed51334e24a0b7f22a63a42f61 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Sat, 25 Apr 2020 09:35:49 -0700 Subject: [PATCH 015/110] refactor: callbacks on end() (#1080) --- lib/client.js | 9 +++------ test/client_mqtt5.js | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/client.js b/lib/client.js index d61c732ba..cffb40d2e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -777,13 +777,9 @@ MqttClient.prototype.unsubscribe = function () { * * @api public */ -MqttClient.prototype.end = function () { +MqttClient.prototype.end = function (force, opts, cb) { var that = this - var force = arguments[0] - var opts = arguments[1] - var cb = arguments[2] - if (force == null || typeof force !== 'boolean') { cb = opts || nop opts = force @@ -814,7 +810,7 @@ MqttClient.prototype.end = function () { that.emit('end') if (cb) { debug('end :: (%s) :: closeStores: invoking callback with args', that.options.clientId) - cb.apply(null, arguments) + cb() } }) }) @@ -836,6 +832,7 @@ MqttClient.prototype.end = function () { } if (this.disconnecting) { + cb() return this } diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index 20de9f4ad..28809e154 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -51,8 +51,7 @@ describe('MQTT 5.0', function () { assert.strictEqual(error.message, 'Packet has no Authentication Method') // client will not be connected, so we will call done. assert.isTrue(client.disconnected, 'validate client is disconnected') - client.end(true) - done() + client.end(true, done) }) }) From bd4e24a9ec16c31f966801fc7ccc0953236d7830 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 27 Apr 2020 19:06:51 +0200 Subject: [PATCH 016/110] Bumped v4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0dc997aef..5f80324eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "3.0.0", + "version": "4.0.0", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 78d056d74110961eead0c346716fb73693b79315 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 5 May 2020 12:17:14 -0700 Subject: [PATCH 017/110] refactor: better debugging (#1085) --- lib/client.js | 113 ++++++++++++++++++++++------------------ lib/connect/index.js | 6 ++- lib/connect/tcp.js | 2 + lib/connect/tls.js | 4 ++ lib/connect/ws.js | 5 +- test/abstract_client.js | 8 +-- 6 files changed, 80 insertions(+), 58 deletions(-) diff --git a/lib/client.js b/lib/client.js index cffb40d2e..f73d3c67b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -87,18 +87,18 @@ function defaultId () { } function sendPacket (client, packet, cb) { - debug('sendPacket: packet: %O', packet) - debug('sendPacket: emitting `packetsend`') + debug('sendPacket :: packet: %O', packet) + debug('sendPacket :: emitting `packetsend`') client.emit('packetsend', packet) - debug('sendPacket: writing to stream') + debug('sendPacket :: writing to stream') var result = mqttPacket.writeToStream(packet, client.stream, client.options) - debug('sendPacket: writeToStream result %s', result) + debug('sendPacket :: writeToStream result %s', result) if (!result && cb) { - debug('sendPacket: handle events on `drain` once through callback.') + debug('sendPacket :: handle events on `drain` once through callback.') client.stream.once('drain', cb) } else if (cb) { - debug('sendPacket: invoking cb') + debug('sendPacket :: invoking cb') cb() } } @@ -117,7 +117,7 @@ function flush (queue) { function flushVolatile (queue) { if (queue) { - debug('flushVolatile: queue exists? %s', !!(queue)) + debug('flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') Object.keys(queue).forEach(function (messageId) { if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { queue[messageId].cb(new Error('Connection closed')) @@ -128,7 +128,7 @@ function flushVolatile (queue) { } function storeAndSend (client, packet, cb, cbStorePut) { - debug('storeAndSend: store packet with cmd: %s to outgoingStore', packet.cmd) + debug('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) client.outgoingStore.put(packet, function storedPacket (err) { if (err) { return cb && cb(err) @@ -139,7 +139,7 @@ function storeAndSend (client, packet, cb, cbStorePut) { } function nop (error) { - debug('nop hit: %o', error) + debug('nop ::', error) } /** @@ -158,7 +158,6 @@ function MqttClient (streamBuilder, options) { } this.options = options || {} - debug('MqttClient :: options: %o', options) // Defaults for (k in defaultConnectOptions) { @@ -169,9 +168,16 @@ function MqttClient (streamBuilder, options) { } } + debug('MqttClient :: options.protocol', options.protocol) + debug('MqttClient :: options.protocolVersion', options.protocolVersion) + debug('MqttClient :: options.username', options.username) + debug('MqttClient :: options.keepalive', options.keepalive) + debug('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) + debug('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) + this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() - debug('MqttClient: clientId', this.options.clientId) + debug('MqttClient :: clientId', this.options.clientId) this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } @@ -220,12 +226,11 @@ function MqttClient (streamBuilder, options) { // Send queued packets this.on('connect', function () { - debug('MqttClient:connect') var queue = this.queue function deliver () { var entry = queue.shift() - debug('MqttClient:deliver: entry %o', entry) + debug('deliver :: entry %o', entry) var packet = null if (!entry) { @@ -233,7 +238,7 @@ function MqttClient (streamBuilder, options) { } packet = entry.packet - debug('MqttClient:deliver: call _sendPacket for %o', packet) + debug('deliver :: call _sendPacket for %o', packet) that._sendPacket( packet, function (err) { @@ -245,26 +250,29 @@ function MqttClient (streamBuilder, options) { ) } + debug('connect :: sending queued packets') deliver() }) this.on('close', function () { - debug('MqttClient: close event. Mark disconnected.') + debug('close :: connected set to `false`') this.connected = false + + debug('close :: clearing connackTimer') clearTimeout(this.connackTimer) - debug('MqttClient:close: clear ping timer') + debug('close :: clearing ping timer') if (that.pingTimer !== null) { that.pingTimer.clear() that.pingTimer = null } - debug('MqttClient:close: call _setupReconnect') + debug('close :: calling _setupReconnect') this._setupReconnect() }) EventEmitter.call(this) - debug('MqttClient: call _setupStream') + debug('MqttClient :: setting up stream') this._setupStream() } inherits(MqttClient, EventEmitter) @@ -282,14 +290,14 @@ MqttClient.prototype._setupStream = function () { var completeParse = null var packets = [] - debug('_setupStream: calling method to clear reconnect') + debug('_setupStream :: calling method to clear reconnect') this._clearReconnect() - debug('_setupStream: setting stream builder') + debug('_setupStream :: using streamBuilder provided to client to create stream') this.stream = this.streamBuilder(this) parser.on('packet', function (packet) { - debug('parser: on packet push to packets array.') + debug('parser :: on packet push to packets array.') packets.push(packet) }) @@ -304,29 +312,30 @@ MqttClient.prototype._setupStream = function () { } function work () { - debug('stream:work: called') + debug('work :: getting next packet in queue') var packet = packets.shift() if (packet) { - debug('stream:work: calling _handlePacket') + debug('work :: packet pulled from queue') that._handlePacket(packet, nextTickWork) } else { + debug('work :: no packets in queue') var done = completeParse completeParse = null - debug('stream:work: done is %s', !!(done)) + debug('work :: done flag is %s', !!(done)) if (done) done() } } writable._write = function (buf, enc, done) { completeParse = done - debug('stream:writable:_write: parsing buffer') + debug('writable stream :: parsing buffer') parser.parse(buf) work() } function streamErrorHandler (error) { - debug('stream error') + debug('streamErrorHandler :: error', error.message) if (socketErrors.includes(error.code)) { // handle error debug('streamErrorHandler :: emitting error') @@ -336,7 +345,7 @@ MqttClient.prototype._setupStream = function () { } } - debug('_setupStream: piping stream to writable') + debug('_setupStream :: pipe stream to writable stream') this.stream.pipe(writable) // Suppress connection errors @@ -379,13 +388,12 @@ MqttClient.prototype._setupStream = function () { clearTimeout(this.connackTimer) this.connackTimer = setTimeout(function () { - debug('connectTimeout hit! Calling _cleanUp with force `true`') + debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') that._cleanUp(true) }, this.options.connectTimeout) } MqttClient.prototype._handlePacket = function (packet, done) { - debug('_handlePacket') var options = this.options if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { @@ -393,7 +401,7 @@ MqttClient.prototype._handlePacket = function (packet, done) { this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) return this } - debug('_handlePacket: emitting packetreceive') + debug('_handlePacket :: emitting packetreceive') this.emit('packetreceive', packet) switch (packet.cmd) { @@ -463,7 +471,7 @@ MqttClient.prototype._checkDisconnecting = function (callback) { * @example client.publish('topic', 'message', console.log); */ MqttClient.prototype.publish = function (topic, message, opts, callback) { - debug('MqttClient:publish `%s` to topic `%s`', message, topic) + debug('publish :: message `%s` to topic `%s`', message, topic) var packet var options = this.options @@ -506,6 +514,7 @@ MqttClient.prototype.publish = function (topic, message, opts, callback) { } } + debug('publish :: qos', opts.qos) switch (opts.qos) { case 1: case 2: @@ -664,7 +673,7 @@ MqttClient.prototype.subscribe = function () { // subscriptions to resubscribe to in case of disconnect if (this.options.resubscribe) { - debug('subscribe: resubscribe true') + debug('subscribe :: resubscribe true') var topics = [] subs.forEach(function (sub) { if (that.options.reconnectPeriod > 0) { @@ -695,7 +704,7 @@ MqttClient.prototype.subscribe = function () { callback(err, subs) } } - debug('subscribe: calling _sendPacket') + debug('subscribe :: call _sendPacket') this._sendPacket(packet) return this @@ -762,7 +771,7 @@ MqttClient.prototype.unsubscribe = function () { cb: callback } - debug('unsubscribe: send packet') + debug('unsubscribe: call _sendPacket') this._sendPacket(packet) return this @@ -780,6 +789,8 @@ MqttClient.prototype.unsubscribe = function () { MqttClient.prototype.end = function (force, opts, cb) { var that = this + debug('end :: (%s)', this.opts.clientId) + if (force == null || typeof force !== 'boolean') { cb = opts || nop opts = force @@ -802,14 +813,14 @@ MqttClient.prototype.end = function (force, opts, cb) { cb = cb || nop function closeStores () { - debug('end :: (%s) :: closeStores: closing incoming and outgoing stores', that.options.clientId) + debug('end :: closeStores: closing incoming and outgoing stores') that.disconnected = true that.incomingStore.close(function () { that.outgoingStore.close(function () { - debug('end :: (%s) :: closeStores: emitting end', that.options.clientId) + debug('end :: closeStores: emitting end') that.emit('end') if (cb) { - debug('end :: (%s) :: closeStores: invoking callback with args', that.options.clientId) + debug('end :: closeStores: invoking callback with args') cb() } }) @@ -924,18 +935,18 @@ MqttClient.prototype._reconnect = function () { * _setupReconnect - setup reconnect timer */ MqttClient.prototype._setupReconnect = function () { - debug('_setupReconnect') var that = this if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { if (!this.reconnecting) { - debug('_setupReconnect :: emitting offline state') + debug('_setupReconnect :: emit `offline` state') this.emit('offline') + debug('_setupReconnect :: set `reconnecting` to `true`') this.reconnecting = true } debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) that.reconnectTimer = setInterval(function () { - debug('reconnectTimer calling _reconnect()') + debug('reconnectTimer :: reconnect triggered!') that._reconnect() }, that.options.reconnectPeriod) } else { @@ -947,7 +958,7 @@ MqttClient.prototype._setupReconnect = function () { * _clearReconnect - clear the reconnect timer */ MqttClient.prototype._clearReconnect = function () { - debug('_clearReconnect called. clearing reconnectTimer') + debug('_clearReconnect : clearing reconnect timer') if (this.reconnectTimer) { clearInterval(this.reconnectTimer) this.reconnectTimer = null @@ -961,20 +972,20 @@ MqttClient.prototype._clearReconnect = function () { MqttClient.prototype._cleanUp = function (forced, done) { var opts = arguments[2] if (done) { - debug('_cleanUp: done callback provided for on stream close') + debug('_cleanUp :: done callback provided for on stream close') this.stream.on('close', done) } - debug('_cleanUp: forced? %s', forced) + debug('_cleanUp :: forced? %s', forced) if (forced) { if ((this.options.reconnectPeriod === 0) && this.options.clean) { flush(this.outgoing) } - debug('(%s)_cleanUp: destroying stream', this.options.clientId) + debug('_cleanUp :: (%s) :: destroying stream', this.options.clientId) this.stream.destroy() } else { var packet = xtend({ cmd: 'disconnect' }, opts) - debug('(%s)_cleanUp: sending disconnect packet', this.options.clientId) + debug('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) this._sendPacket( packet, setImmediate.bind( @@ -985,19 +996,19 @@ MqttClient.prototype._cleanUp = function (forced, done) { } if (!this.disconnecting) { - debug('_cleanUp: client not disconnecting. Clearing and resetting reconnect.') + debug('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') this._clearReconnect() this._setupReconnect() } if (this.pingTimer !== null) { - debug('_cleanUp: clearing pingTimer') + debug('_cleanUp :: clearing pingTimer') this.pingTimer.clear() this.pingTimer = null } if (done && !this.connected) { - debug('(%s)_cleanUp: removing stream `done` callback `close` listener', this.options.clientId) + debug('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) this.stream.removeListener('close', done) done() } @@ -1087,6 +1098,7 @@ MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { * @api private */ MqttClient.prototype._setupPingTimer = function () { + debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) var that = this if (!this.pingTimer && this.options.keepalive) { @@ -1285,7 +1297,6 @@ MqttClient.prototype.handleMessage = function (packet, callback) { */ MqttClient.prototype._handleAck = function (packet) { - debug('handling ack packet') /* eslint no-fallthrough: "off" */ var messageId = packet.messageId var type = packet.cmd @@ -1295,13 +1306,13 @@ MqttClient.prototype._handleAck = function (packet) { var err if (!cb) { - debug('Server sent an ack in error. Ignoring.') + debug('_handleAck :: Server sent an ack in error. Ignoring.') // Server sent an ack in error, ignore it. return } // Process - debug('ack packet of type: %s', type) + debug('_handleAck :: packet type', type) switch (type) { case 'pubcomp': // same thing as puback for QoS 2 diff --git a/lib/connect/index.js b/lib/connect/index.js index 45dcde819..7496ef352 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -4,6 +4,8 @@ var MqttClient = require('../client') var Store = require('../store') var url = require('url') var xtend = require('xtend') +var debug = require('debug')('mqttjs') + var protocols = {} if (process.title !== 'browser') { @@ -48,6 +50,7 @@ function parseAuthOptions (opts) { * @param {Object} opts - see MqttClient#constructor */ function connect (brokerUrl, opts) { + debug('connecting to an MQTT broker...') if ((typeof brokerUrl === 'object') && !opts) { opts = brokerUrl brokerUrl = null @@ -98,7 +101,7 @@ function connect (brokerUrl, opts) { } } } else { - // don't know what protocol he want to use, mqtts or wss + // A cert and key was provided, however no protocol was specified, so we will throw an error. throw new Error('Missing secure protocol key') } } @@ -145,6 +148,7 @@ function connect (brokerUrl, opts) { client._reconnectCount++ } + debug('calling streambuilder for',opts.protocol) return protocols[opts.protocol](client, opts) } var client = new MqttClient(wrapper, opts) diff --git a/lib/connect/tcp.js b/lib/connect/tcp.js index ac6537dca..9912102eb 100644 --- a/lib/connect/tcp.js +++ b/lib/connect/tcp.js @@ -1,5 +1,6 @@ 'use strict' var net = require('net') +var debug = require('debug')('mqttjs:tcp') /* variables port and host can be removed since @@ -13,6 +14,7 @@ function streamBuilder (client, opts) { port = opts.port host = opts.hostname + debug('port %d and host %s', port, host) return net.createConnection(port, host) } diff --git a/lib/connect/tls.js b/lib/connect/tls.js index 419cedde9..e368b33c8 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -1,5 +1,7 @@ 'use strict' var tls = require('tls') +var debug = require('debug')('mqttjs:tls') + function buildBuilder (mqttClient, opts) { var connection @@ -11,6 +13,8 @@ function buildBuilder (mqttClient, opts) { delete opts.path + debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) + connection = tls.connect(opts) /* eslint no-use-before-define: [2, "nofunc"] */ connection.on('secureConnect', function () { diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 583078efb..958562c79 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,6 +1,6 @@ 'use strict' -var debug = require('debug')('mqttjs:connect:ws') +var debug = require('debug')('mqttjs:ws') var websocket = require('websocket-stream') var urlModule = require('url') var WSS_OPTIONS = [ @@ -51,7 +51,6 @@ function setDefaultOpts (opts) { function createWebSocket (client, opts) { debug('createWebSocket') - debug('opts: %o', opts) var websocketSubProtocol = (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) ? 'mqttv3.1' @@ -59,7 +58,7 @@ function createWebSocket (client, opts) { setDefaultOpts(opts) var url = buildUrl(opts, client) - debug('creating new Websocket for url: %s and protocol: %s', url, websocketSubProtocol) + debug('url %s protocol %s', url, websocketSubProtocol) return websocket(url, [websocketSubProtocol], opts.wsOptions) } diff --git a/test/abstract_client.js b/test/abstract_client.js index f66f57d38..8437f7215 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -2376,8 +2376,9 @@ module.exports = function (server, config) { var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] reconnectPeriodTests.forEach((test) => { - it('should allow specification of a reconnect period', function (done) { + it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { var end + var reconnectSlushTime = 200 var client = connect({reconnectPeriod: test.period}) var reconnect = false var start = Date.now() @@ -2389,11 +2390,12 @@ module.exports = function (server, config) { } else { end = Date.now() client.end(() => { - if (end - start >= test.period - 200 && end - start <= test.period + 200) { + let reconnectPeriodDuringTest = end - start + if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { // give the connection a 200 ms slush window done() } else { - done(new Error('Strange reconnect period')) + done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) } }) } From 724981ec50faa636354744c8d2ccb51bd2bedfb1 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 5 May 2020 13:59:19 -0700 Subject: [PATCH 018/110] docs: knick knacks here and there (#1087) * docs: reconnect disable sentence * fix: linting * fix: opts --- README.md | 2 +- lib/client.js | 2 +- lib/connect/index.js | 2 +- lib/connect/tls.js | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 71415f0f6..a888fa8c5 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ the `connect` event. Typically a `net.Socket`. * `clean`: `true`, set to false to receive QoS 1 and 2 messages while offline * `reconnectPeriod`: `1000` milliseconds, interval between two - reconnections + reconnections. Disable auto reconnect by setting to `0`. * `connectTimeout`: `30 * 1000` milliseconds, time to wait before a CONNACK is received * `username`: the username required by your broker, if any diff --git a/lib/client.js b/lib/client.js index f73d3c67b..cdb186c87 100644 --- a/lib/client.js +++ b/lib/client.js @@ -789,7 +789,7 @@ MqttClient.prototype.unsubscribe = function () { MqttClient.prototype.end = function (force, opts, cb) { var that = this - debug('end :: (%s)', this.opts.clientId) + debug('end :: (%s)', this.options.clientId) if (force == null || typeof force !== 'boolean') { cb = opts || nop diff --git a/lib/connect/index.js b/lib/connect/index.js index 7496ef352..d496fe985 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -148,7 +148,7 @@ function connect (brokerUrl, opts) { client._reconnectCount++ } - debug('calling streambuilder for',opts.protocol) + debug('calling streambuilder for', opts.protocol) return protocols[opts.protocol](client, opts) } var client = new MqttClient(wrapper, opts) diff --git a/lib/connect/tls.js b/lib/connect/tls.js index e368b33c8..aac296666 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -2,7 +2,6 @@ var tls = require('tls') var debug = require('debug')('mqttjs:tls') - function buildBuilder (mqttClient, opts) { var connection opts.port = opts.port || 8883 From a4d66266802ff16d03eea057d83c626421e04d94 Mon Sep 17 00:00:00 2001 From: burritoIand <230757+burritoIand@users.noreply.github.com> Date: Tue, 5 May 2020 14:40:40 -0700 Subject: [PATCH 019/110] docs: better explain reconnection in readme (#1088) --- README.md | 80 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a888fa8c5..21fd27391 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,63 @@ $env:DEBUG='mqttjs*' ``` + +## About Reconnection + +An important part of any websocket connection is what to do when a connection +drops off and the client needs to reconnect. MQTT has built-in reconnection +support that can be configured to behave in ways that suit the application. + +#### Refresh Authentication Options / Signed Urls with `transformWsUrl` (Websocket Only) + +When an mqtt connection drops and needs to reconnect, it's common to require +that any authentication associated with the connection is kept current with +the underlying auth mechanism. For instance some applications may pass an auth +token with connection options on the initial connection, while other cloud +services may require a url be signed with each connection. + +By the time the reconnect happens in the application lifecycle, the original +auth data may have expired. + +To address this we can use a hook called `transformWsUrl` to manipulate +either of the connection url or the client options at the time of a reconnect. + +Example (update clientId & username on each reconnect): +``` + const transformWsUrl = (url, options, client) => { + client.options.username = `token=${this.get_current_auth_token()}`; + client.options.clientId = `${this.get_updated_clientId()}`; + + return `${this.get_signed_cloud_url(url)`; + } + + const connection = await mqtt.connectAsync(, { + ..., + transformWsUrl: transformUrl, + }); + +``` +Now every time a new WebSocket connection is opened (hopefully not too often), +we will get a fresh signed url or fresh auth token data. + +Note: Currently this hook does _not_ support promises, meaning that in order to +use the latest auth token, you must have some outside mechanism running that +handles application-level authentication refreshing so that the websocket +connection can simply grab the latest valid token or signed url. + + +#### Enabling Reconnection with `reconnectPeriod` option + +To ensure that the mqtt client automatically tries to reconnect when the +connection is dropped, you must set the client option `reconnectPeriod` to a +value greater than 0. A value of 0 will disable reconnection and then terminate +the final connection when it drops. + +The default value is 1000 ms which means it will try to reconnect 1 second +after losing the connection. + + + ## API @@ -641,29 +698,6 @@ you can then use mqtt.js in the browser with the same api than node's one. Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/mcollina/mosca/wiki/MQTT-over-Websockets) to setup [Mosca](http://mcollina.github.io/mosca/)). - -### Signed WebSocket Urls - -If you need to sign an url, for example for [AWS IoT](http://docs.aws.amazon.com/iot/latest/developerguide/protocols.html#mqtt-ws), -then you can pass in a `transformWsUrl` function to the mqtt.connect() options -This is needed because signed urls have an expiry and eventually upon reconnects, a new signed url needs to be created: - -```js -// This module doesn't actually exist, just an example -var awsIotUrlSigner = require('awsIotUrlSigner') -mqtt.connect('wss://a2ukbzaqo9vbpb.iot.ap-southeast-1.amazonaws.com/mqtt', { - transformWsUrl: function (url, options, client) { - // It's possible to inspect some state on options(pre parsed url components) - // and the client (reconnect state etc) - return awsIotUrlSigner(url) - } -}) - -// Now every time a new WebSocket connection is opened (hopefully not that -// often) we get a freshly signed url - -``` - ## About QoS From 5197bf1f5902e97f5307eca43d526ad322b5dd62 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 7 May 2020 01:17:04 -0700 Subject: [PATCH 020/110] docs: adding client flowchart --- doc/MQTTjs.vsdx | Bin 0 -> 113080 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/MQTTjs.vsdx diff --git a/doc/MQTTjs.vsdx b/doc/MQTTjs.vsdx new file mode 100644 index 0000000000000000000000000000000000000000..da01d58c39f4b18b930ee0003ce6ea33bab866c4 GIT binary patch literal 113080 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%0DW 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 Vogxej8T2O`+65rgK|5`j{{e2`j`IKj literal 0 HcmV?d00001 From ef2d5907efd5eed14aa3f46a2bf18b42ee0b3687 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 7 May 2020 18:21:39 +0200 Subject: [PATCH 021/110] Revert "docs: adding client flowchart" This reverts commit 5197bf1f5902e97f5307eca43d526ad322b5dd62. --- doc/MQTTjs.vsdx | Bin 113080 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/MQTTjs.vsdx diff --git a/doc/MQTTjs.vsdx b/doc/MQTTjs.vsdx deleted file mode 100644 index da01d58c39f4b18b930ee0003ce6ea33bab866c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113080 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%0DW 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 Vogxej8T2O`+65rgK|5`j{{e2`j`IKj From 28243fc6ec53c2f30efe154ff11fa3d71b30618b Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 7 May 2020 18:22:20 +0200 Subject: [PATCH 022/110] Bumped v4.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f80324eb..6e72386e7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.0.0", + "version": "4.0.1", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 59783503f715b629c7b99e4c4c706b16627247d9 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 7 May 2020 15:50:05 -0700 Subject: [PATCH 023/110] ci: add debug logs to tests (#1091) --- .github/workflows/nodejs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index bea543018..902be303a 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -29,3 +29,4 @@ jobs: - run: npm test env: CI: true + DEBUG: "mqttjs*" From e3ea11a2f941ea1a5c98f3944311e41ac7e30da2 Mon Sep 17 00:00:00 2001 From: Alexis Tyler Date: Mon, 11 May 2020 15:09:18 +0930 Subject: [PATCH 024/110] chore: move cli to bin directory (#1096) --- bin/mqtt.js | 24 ++++++++++++++++++++++++ mqtt.js | 24 ------------------------ package.json | 2 +- 3 files changed, 25 insertions(+), 25 deletions(-) create mode 100755 bin/mqtt.js mode change 100755 => 100644 mqtt.js diff --git a/bin/mqtt.js b/bin/mqtt.js new file mode 100755 index 000000000..cdb4543b6 --- /dev/null +++ b/bin/mqtt.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node +'use strict' + +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ + +var commist = require('commist')() +var helpMe = require('help-me')() + +commist.register('publish', require('./bin/pub')) +commist.register('subscribe', require('./bin/sub')) +commist.register('version', function () { + console.log('MQTT.js version:', require('./package.json').version) +}) +commist.register('help', helpMe.toStdout) + +if (commist.parse(process.argv.slice(2)) !== null) { + console.log('No such command:', process.argv[2], '\n') + helpMe.toStdout() +} diff --git a/mqtt.js b/mqtt.js old mode 100755 new mode 100644 index d60f7bd6f..ab12375c8 --- a/mqtt.js +++ b/mqtt.js @@ -1,6 +1,3 @@ -#!/usr/bin/env node -'use strict' - /* * Copyright (c) 2015-2015 MQTT.js contributors. * Copyright (c) 2011-2014 Adam Rudd. @@ -18,24 +15,3 @@ module.exports.connect = connect module.exports.MqttClient = MqttClient module.exports.Client = MqttClient module.exports.Store = Store - -function cli () { - var commist = require('commist')() - var helpMe = require('help-me')() - - commist.register('publish', require('./bin/pub')) - commist.register('subscribe', require('./bin/sub')) - commist.register('version', function () { - console.log('MQTT.js version:', require('./package.json').version) - }) - commist.register('help', helpMe.toStdout) - - if (commist.parse(process.argv.slice(2)) !== null) { - console.log('No such command:', process.argv[2], '\n') - helpMe.toStdout() - } -} - -if (require.main === module) { - cli() -} diff --git a/package.json b/package.json index 6e72386e7..0b5007db5 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "bin": { "mqtt_pub": "./bin/pub.js", "mqtt_sub": "./bin/sub.js", - "mqtt": "./mqtt.js" + "mqtt": "./bin/mqtt.js" }, "files": [ "dist/", From 010cfc32a88aaaeb1be3ce4696d9c4659ba0a1df Mon Sep 17 00:00:00 2001 From: Alexis Tyler Date: Mon, 11 May 2020 15:52:52 +0930 Subject: [PATCH 025/110] chore: remove bloat from package (#1097) --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 0b5007db5..8d59a5e28 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,6 @@ "doc", "lib", "bin", - "examples", - "test", "types", "mqtt.js" ], From 162d6aa107a1785480c91b6ffaee8c43dff49f1b Mon Sep 17 00:00:00 2001 From: taoqf Date: Mon, 11 May 2020 04:19:08 -0500 Subject: [PATCH 026/110] types: add on('connect') (#963) --- types/lib/client.d.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts index bef009f5a..9356f3dd3 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -66,6 +66,7 @@ export interface ISubscriptionMap { } } +export declare type OnConnectCallback = (packet: Packet) => void export declare type ClientSubscribeCallback = (err: Error, granted: ISubscriptionGrant[]) => void export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: Packet) => void export declare type OnPacketCallback = (packet: Packet) => void @@ -97,15 +98,15 @@ export declare class MqttClient extends events.EventEmitter { constructor (streamBuilder: (client: MqttClient) => IStream, options: IClientOptions) + public on (event: 'connect', cb: OnConnectCallback): this public on (event: 'message', cb: OnMessageCallback): this public on (event: 'packetsend' | 'packetreceive', cb: OnPacketCallback): this public on (event: 'error', cb: OnErrorCallback): this public on (event: string, cb: Function): this + public once (event: 'connect', cb: OnConnectCallback): this public once (event: 'message', cb: OnMessageCallback): this - public once (event: - 'packetsend' - | 'packetreceive', cb: OnPacketCallback): this + public once (event: 'packetsend' | 'packetreceive', cb: OnPacketCallback): this public once (event: 'error', cb: OnErrorCallback): this public once (event: string, cb: Function): this From cf318062ab748a37a8b1d5e9929a9b051a310fcb Mon Sep 17 00:00:00 2001 From: Jere Date: Mon, 11 May 2020 17:19:47 +0800 Subject: [PATCH 027/110] The protocols parameter of wx.connectSocket should be Array. (#969) Wechat mini app document url: https://developers.weixin.qq.com/miniprogram/dev/api/network/websocket/wx.connectSocket.html. If the protocols value is 'mqtt' instead of ['mqtt'], it will be failed if you use the android device, the interesting thing is iOS works. Both android and ios works if the value is an array. --- lib/connect/wx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connect/wx.js b/lib/connect/wx.js index c5048b5b7..4cb32454c 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -99,7 +99,7 @@ function buildStream (client, opts) { var url = buildUrl(opts, client) socketTask = wx.connectSocket({ url: url, - protocols: websocketSubProtocol + protocols: [websocketSubProtocol] }) proxy = buildProxy() From 2f8316ed0d5303c424e8116aa3a757e826860973 Mon Sep 17 00:00:00 2001 From: Sikkapat Sricheangsa Date: Tue, 12 May 2020 15:41:13 +0700 Subject: [PATCH 028/110] [FIXED] Unsubscribe while topics are in array. (#958) * [FIXED] Unsubscribe while topics are in array. * add eol to make the test pass --- lib/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index cdb186c87..78f3f52ba 100644 --- a/lib/client.js +++ b/lib/client.js @@ -752,7 +752,7 @@ MqttClient.prototype.unsubscribe = function () { if (typeof topic === 'string') { packet.unsubscriptions = [topic] - } else if (typeof topic === 'object' && topic.length) { + } else if (Array.isArray(topic)) { packet.unsubscriptions = topic } From 139997c80e14fb9a1c0d9ada16980e07e1c83168 Mon Sep 17 00:00:00 2001 From: Akiroz Date: Tue, 19 May 2020 16:35:35 +0800 Subject: [PATCH 029/110] Add missing "debug" dependency (#1104) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8d59a5e28..276e32903 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "base64-js": "^1.3.0", "commist": "^1.0.0", "concat-stream": "^1.6.2", + "debug": "^4.1.1", "end-of-stream": "^1.4.1", "es6-map": "^0.1.5", "help-me": "^1.0.1", From 2980e96bb472a3132c3d8850f6d0c73f93513b6a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 19 May 2020 11:14:40 +0200 Subject: [PATCH 030/110] Bumped v4.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 276e32903..514cf82ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.0.1", + "version": "4.1.0", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 43cc1d1f96e32b022ead3c8ce9c6ff4cbe2c3820 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Fri, 5 Jun 2020 00:08:35 -0700 Subject: [PATCH 031/110] fix: path for bin files (#1107) * fix: addressing * fix: path * chore: remove comments --- bin/mqtt.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bin/mqtt.js b/bin/mqtt.js index cdb4543b6..022b33a64 100755 --- a/bin/mqtt.js +++ b/bin/mqtt.js @@ -7,14 +7,17 @@ * * See LICENSE for more information */ - +var path = require('path') var commist = require('commist')() -var helpMe = require('help-me')() +var helpMe = require('help-me')({ + dir: path.join(path.dirname(require.main.filename), '/../doc'), + ext: '.txt' +}) -commist.register('publish', require('./bin/pub')) -commist.register('subscribe', require('./bin/sub')) +commist.register('publish', require('./pub')) +commist.register('subscribe', require('./sub')) commist.register('version', function () { - console.log('MQTT.js version:', require('./package.json').version) + console.log('MQTT.js version:', require('./../package.json').version) }) commist.register('help', helpMe.toStdout) From 5adb12a6f73c63e47ff9acd54bbcaef4f11c4baa Mon Sep 17 00:00:00 2001 From: YuShifan <894402575bt@gmail.com> Date: Fri, 24 Jul 2020 05:04:55 +0800 Subject: [PATCH 032/110] fix(typescript): fix payloadFormatIndicator to boolean type (#1115) --- types/lib/client-options.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index 69bacaed6..e8119f6de 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -94,7 +94,7 @@ export interface IClientOptions extends ISecureClientOptions { * */ properties?: { willDelayInterval?: number, - payloadFormatIndicator?: number, + payloadFormatIndicator?: boolean, messageExpiryInterval?: number, contentType?: string, responseTopic?: string, From e8326ce3baf06a1bcdbd70c33c5178bc06f8959a Mon Sep 17 00:00:00 2001 From: Mark Koopman Date: Wed, 29 Jul 2020 16:56:27 -0400 Subject: [PATCH 033/110] feat(mqtt5): add properties object to publish options --- types/lib/client-options.d.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index e8119f6de..fc4779cd7 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -143,6 +143,19 @@ export interface IClientPublishOptions { * whether or not mark a message as duplicate */ dup?: boolean + /* + * MQTT 5.0 properties object + */ + properties?: { + payloadFormatIndicator?: number, + messageExpiryInterval?: number, + topicAlias?: string, + responseTopic?: string, + correlationData?: Buffer, + userProperties?: Object, + subscriptionIdentifier?: number, + contentType?: string + } /** * callback called when message is put into `outgoingStore` */ From 9c614192dc7f7be20f715b7236f13e0b60717dce Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 30 Jul 2020 11:27:31 -0700 Subject: [PATCH 034/110] fix(mqtt stores): improve error handling and tests (#1133) * fix(mqtt stores): improve error handling and tests * fix: linting * fix: remove mqtt5 properties * fix: remove extra space --- lib/client.js | 7 ++++--- test/abstract_client.js | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/lib/client.js b/lib/client.js index 78f3f52ba..774384f79 100644 --- a/lib/client.js +++ b/lib/client.js @@ -815,13 +815,14 @@ MqttClient.prototype.end = function (force, opts, cb) { function closeStores () { debug('end :: closeStores: closing incoming and outgoing stores') that.disconnected = true - that.incomingStore.close(function () { - that.outgoingStore.close(function () { + that.incomingStore.close(function (e1) { + that.outgoingStore.close(function (e2) { debug('end :: closeStores: emitting end') that.emit('end') if (cb) { + let err = e1 || e2 debug('end :: closeStores: invoking callback with args') - cb() + cb(err) } }) }) diff --git a/test/abstract_client.js b/test/abstract_client.js index 8437f7215..b2577e032 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -90,24 +90,48 @@ module.exports = function (server, config) { }) }) - it('should pass store close error to end callback but not to end listeners', function (done) { + it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { var store = new Store() - var client = connect({outgoingStore: store}) + var client = connect({ incomingStore: store }) store.close = function (cb) { cb(new Error('test')) } client.once('end', function () { if (arguments.length === 0) { - return done() + return } - throw new Error('no argument shoould be passed to event') + throw new Error('no argument should be passed to event') }) client.once('connect', function () { - client.end(function (test) { - if (test && test.message === 'test') { - return + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { + var store = new Store() + var client = connect({ outgoingStore: store }) + + store.close = function (cb) { + cb(new Error('test')) + } + client.once('end', function () { + if (arguments.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', function () { + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() } throw new Error('bad argument passed to callback') }) From eedc2b26cd6063a0b1152432a00f70de5e0b9bae Mon Sep 17 00:00:00 2001 From: Konstantin Nosov Date: Thu, 30 Jul 2020 22:49:10 +0300 Subject: [PATCH 035/110] fix(browser support): correct browser detection for webpack (#1135) currently webpack based projects couldn't import mqtt.js because incorrect browser detection. It's not a problem for react, where node-shims enabled, but a pain for angular (where enabling node-shims isn't supported officially) To fix that issue we need to check if `process` exists (it will be not available in browser) Also due to shims in webpack we need to check are we in process of bundling, or not. --- lib/connect/index.js | 3 ++- lib/connect/ws.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/connect/index.js b/lib/connect/index.js index d496fe985..7153ceac7 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -8,7 +8,8 @@ var debug = require('debug')('mqttjs') var protocols = {} -if (process.title !== 'browser') { +// eslint-disable-next-line camelcase +if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ === 'function') { protocols.mqtt = require('./tcp') protocols.tcp = require('./tcp') protocols.ssl = require('./tls') diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 958562c79..f755dfe10 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -11,8 +11,8 @@ var WSS_OPTIONS = [ 'pfx', 'passphrase' ] -var IS_BROWSER = process.title === 'browser' - +// eslint-disable-next-line camelcase +var IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' function buildUrl (opts, client) { var url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path if (typeof (opts.transformWsUrl) === 'function') { From 963e554d3da2e4149c6f99b4fbe3aad6e620b955 Mon Sep 17 00:00:00 2001 From: Konstantin Nosov Date: Mon, 3 Aug 2020 20:31:43 +0300 Subject: [PATCH 036/110] fix(browser support): do not use process.nextTick without check that it exists (#1136) --- lib/client.js | 9 +++++---- lib/connect/wx.js | 4 ++-- lib/store.js | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/client.js b/lib/client.js index 774384f79..08fc443a1 100644 --- a/lib/client.js +++ b/lib/client.js @@ -12,9 +12,10 @@ var reInterval = require('reinterval') var validations = require('./validations') var xtend = require('xtend') var debug = require('debug')('mqttjs:client') +var nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } var setImmediate = global.setImmediate || function (callback) { // works in node v0.8 - process.nextTick(callback) + nextTick(callback) } var defaultConnectOptions = { keepalive: 60, @@ -303,7 +304,7 @@ MqttClient.prototype._setupStream = function () { function nextTickWork () { if (packets.length) { - process.nextTick(work) + nextTick(work) } else { var done = completeParse completeParse = null @@ -838,8 +839,8 @@ MqttClient.prototype.end = function (force, opts, cb) { debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) that._cleanUp(force, () => { debug('end :: finish :: calling process.nextTick on closeStores') - // var boundProcess = process.nextTick.bind(null, closeStores) - process.nextTick(closeStores.bind(that)) + // var boundProcess = nextTick.bind(null, closeStores) + nextTick(closeStores.bind(that)) }, opts) } diff --git a/lib/connect/wx.js b/lib/connect/wx.js index 4cb32454c..b9c7a0705 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -117,13 +117,13 @@ function buildStream (client, opts) { stream.destroy = destroyRef var self = this - process.nextTick(function () { + setTimeout(function () { socketTask.close({ fail: function () { self._destroy(new Error()) } }) - }) + }, 0) }.bind(stream) bindEventHandler() diff --git a/lib/store.js b/lib/store.js index 97aef436e..5e3a8dc78 100644 --- a/lib/store.js +++ b/lib/store.js @@ -88,9 +88,9 @@ Store.prototype.createStream = function () { destroyed = true - process.nextTick(function () { + setTimeout(function () { self.emit('close') - }) + }, 0) } return stream From b2c121511c7437b64724e9f1e89ebcd27e3c2cce Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Fri, 7 Aug 2020 14:26:00 -0700 Subject: [PATCH 037/110] feat(websockets): websocket-streams to ws (#1108) --- examples/ws/aedes_server.js | 42 ++++++++++++++++++ examples/{wss => ws}/client.js | 22 ++++++---- lib/connect/ws.js | 10 +++-- package.json | 12 +++--- test/abstract_client.js | 6 ++- test/client.js | 33 -------------- test/websocket_client.js | 78 +++++++++++++++++++++++++--------- 7 files changed, 129 insertions(+), 74 deletions(-) create mode 100644 examples/ws/aedes_server.js rename examples/{wss => ws}/client.js (64%) diff --git a/examples/ws/aedes_server.js b/examples/ws/aedes_server.js new file mode 100644 index 000000000..e29032ff4 --- /dev/null +++ b/examples/ws/aedes_server.js @@ -0,0 +1,42 @@ +const aedes = require('aedes')() +const httpServer = require('http').createServer() +const WebSocket = require('ws') +const wsPort = 8080 + +// Here we are creating the Websocket Server that is using the HTTP Server... +const wss = new WebSocket.Server({ server: httpServer }) +wss.on('connection', function connection (ws) { + const duplex = WebSocket.createWebSocketStream(ws) + aedes.handle(duplex) +}) + +httpServer.listen(wsPort, function () { + console.log('websocket server listening on port', wsPort) +}) + +aedes.on('clientError', function (client, err) { + console.log('client error', client.id, err.message, err.stack) +}) + +aedes.on('connectionError', function (client, err) { + console.log('client error', client, err.message, err.stack) +}) + +aedes.on('publish', function (packet, client) { + if (packet && packet.payload) { + console.log('publish packet:', packet.payload.toString()) + } + if (client) { + console.log('message from client', client.id) + } +}) + +aedes.on('subscribe', function (subscriptions, client) { + if (client) { + console.log('subscribe from client', subscriptions, client.id) + } +}) + +aedes.on('client', function (client) { + console.log('new client', client.id) +}) diff --git a/examples/wss/client.js b/examples/ws/client.js similarity index 64% rename from examples/wss/client.js rename to examples/ws/client.js index f294598f1..53d8bc1d7 100644 --- a/examples/wss/client.js +++ b/examples/ws/client.js @@ -1,13 +1,20 @@ 'use strict' -var mqtt = require('mqtt') +var mqtt = require('../../types') var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) -var host = 'wss://localhost:3001/Mosca' +// This sample should be run in tandem with the aedes_server.js file. +// Simply run it: +// $ node aedes_server.js +// +// Then run this file in a separate console: +// $ node websocket_sample.js +// +var host = 'ws://localhost:8080' var options = { - keepalive: 10, + keepalive: 30, clientId: clientId, protocolId: 'MQTT', protocolVersion: 4, @@ -20,11 +27,10 @@ var options = { qos: 0, retain: false }, - username: 'demo', - password: 'demo', rejectUnauthorized: false } +console.log('connecting mqtt client') var client = mqtt.connect(host, options) client.on('error', function (err) { @@ -34,12 +40,10 @@ client.on('error', function (err) { client.on('connect', function () { console.log('client connected:' + clientId) + client.subscribe('topic', { qos: 0 }) + client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) }) -client.subscribe('topic', { qos: 0 }) - -client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) - client.on('message', function (topic, message, packet) { console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) }) diff --git a/lib/connect/ws.js b/lib/connect/ws.js index f755dfe10..dfd8c5140 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,7 +1,7 @@ 'use strict' +var WebSocket = require('ws') var debug = require('debug')('mqttjs:ws') -var websocket = require('websocket-stream') var urlModule = require('url') var WSS_OPTIONS = [ 'rejectUnauthorized', @@ -51,6 +51,7 @@ function setDefaultOpts (opts) { function createWebSocket (client, opts) { debug('createWebSocket') + debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) var websocketSubProtocol = (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) ? 'mqttv3.1' @@ -58,8 +59,11 @@ function createWebSocket (client, opts) { setDefaultOpts(opts) var url = buildUrl(opts, client) - debug('url %s protocol %s', url, websocketSubProtocol) - return websocket(url, [websocketSubProtocol], opts.wsOptions) + debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) + var ws = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) + var duplex = WebSocket.createWebSocketStream(ws, opts.wsOptions) + duplex.url = url + return duplex } function streamBuilder (client, opts) { diff --git a/package.json b/package.json index 514cf82ad..5bff875be 100644 --- a/package.json +++ b/package.json @@ -70,13 +70,13 @@ "es6-map": "^0.1.5", "help-me": "^1.0.1", "inherits": "^2.0.3", - "minimist": "^1.2.0", - "mqtt-packet": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.3.2", "pump": "^3.0.0", "readable-stream": "^2.3.6", "reinterval": "^1.1.0", "split2": "^3.1.0", - "websocket-stream": "^5.1.2", + "ws": "^7.3.1", "xtend": "^4.0.1" }, "devDependencies": { @@ -86,10 +86,11 @@ "chai": "^4.2.0", "codecov": "^3.0.4", "global": "^4.3.2", + "aedes": "^0.42.5", "mkdirp": "^0.5.1", "mocha": "^4.1.0", "mqtt-connection": "^4.0.0", - "nyc": "^15.0.0", + "nyc": "^15.0.1", "pre-commit": "^1.2.2", "rimraf": "^3.0.2", "safe-buffer": "^5.1.2", @@ -101,8 +102,7 @@ "tslint": "^5.11.0", "tslint-config-standard": "^8.0.1", "typescript": "^3.2.2", - "uglify-es": "^3.3.9", - "ws": "^3.3.3" + "uglify-es": "^3.3.9" }, "standard": { "env": [ diff --git a/test/abstract_client.js b/test/abstract_client.js index b2577e032..441c4e812 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -297,8 +297,10 @@ module.exports = function (server, config) { serverClient.once('connect', function (packet) { assert.include(packet.clientId, 'testclient') assert.isFalse(packet.clean) - serverClient.disconnect() - client.end(true, done) + client.end(false, function (err) { + serverClient.disconnect() + done(err) + }) }) }) }) diff --git a/test/client.js b/test/client.js index 83f0800cd..77c5f7445 100644 --- a/test/client.js +++ b/test/client.js @@ -213,39 +213,6 @@ describe('MqttClient', function () { }) }) - it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { - this.timeout(15000) - var actualURL41 = 'wss://localhost:9917/' - var actualURL42 = 'ws://localhost:9918/' - var serverPort41 = serverBuilder(true).listen(ports.PORTAND41) - var serverPort42 = serverBuilder(true).listen(ports.PORTAND42) - - serverPort42.on('listening', function () { - client = mqtt.connect({ - protocol: 'wss', - servers: [ - { port: ports.PORTAND41, host: 'localhost' }, - { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' } - ], - keepalive: 50 - }) - serverPort41.once('client', function () { - assert.equal(client.stream.socket.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') - client.end(true, done) - serverPort41.close() - }) - serverPort42.on('client', function (c) { - assert.equal(client.stream.socket.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') - c.stream.destroy() - serverPort42.close() - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - it('should reconnect if a connack is not received in an interval', function (done) { this.timeout(2000) diff --git a/test/websocket_client.js b/test/websocket_client.js index e9d2d4c79..fc0107b78 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -1,29 +1,30 @@ 'use strict' var http = require('http') -var websocket = require('websocket-stream') -var WebSocketServer = require('ws').Server -var Connection = require('mqtt-connection') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') var abstractClientTests = require('./abstract_client') +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder var mqtt = require('../') var xtend = require('xtend') var assert = require('assert') var port = 9999 -var server = http.createServer() +var httpServer = http.createServer() -function attachWebsocketServer (wsServer) { - var wss = new WebSocketServer({server: wsServer, perMessageDeflate: false}) +function attachWebsocketServer (httpServer) { + var webSocketServer = new WebSocket.Server({server: httpServer, perMessageDeflate: false}) - wss.on('connection', function (ws) { - var stream = websocket(ws) - var connection = new Connection(stream) - - wsServer.emit('client', connection) + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + httpServer.emit('client', connection) stream.on('error', function () {}) connection.on('error', function () {}) }) - return wsServer + return httpServer } function attachClientEventHandlers (client) { @@ -31,7 +32,7 @@ function attachClientEventHandlers (client) { if (packet.clientId === 'invalid') { client.connack({ returnCode: 2 }) } else { - server.emit('connect', client) + httpServer.emit('connect', client) client.connack({returnCode: 0}) } }) @@ -81,9 +82,9 @@ function attachClientEventHandlers (client) { }) } -attachWebsocketServer(server) +attachWebsocketServer(httpServer) -server.on('client', attachClientEventHandlers).listen(port) +httpServer.on('client', attachClientEventHandlers).listen(port) describe('Websocket Client', function () { var baseConfig = { protocol: 'ws', port: port } @@ -94,8 +95,8 @@ describe('Websocket Client', function () { } it('should use mqtt as the protocol by default', function (done) { - server.once('client', function (client) { - assert.strictEqual(client.stream.socket.protocol, 'mqtt') + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqtt') }) mqtt.connect(makeOptions()).on('connect', function () { this.end(true, done) @@ -121,15 +122,15 @@ describe('Websocket Client', function () { }}) mqtt.connect(opts) .on('connect', function () { - assert.equal(this.stream.socket.url, expected) + assert.equal(this.stream.url, expected) assert.equal(actual, expected) this.end(true, done) }) }) it('should use mqttv3.1 as the protocol if using v3.1', function (done) { - server.once('client', function (client) { - assert.strictEqual(client.stream.socket.protocol, 'mqttv3.1') + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqttv3.1') }) var opts = makeOptions({ @@ -142,5 +143,40 @@ describe('Websocket Client', function () { }) }) - abstractClientTests(server, makeOptions()) + describe('reconnecting', () => { + it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { + this.timeout(15000) + var actualURL41 = 'wss://localhost:9917/' + var actualURL42 = 'ws://localhost:9918/' + var serverPort41 = serverBuilder(true).listen(ports.PORTAND41) + var serverPort42 = serverBuilder(true).listen(ports.PORTAND42) + + serverPort42.on('listening', function () { + let client = mqtt.connect({ + protocol: 'wss', + servers: [ + { port: ports.PORTAND41, host: 'localhost' }, + { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' } + ], + keepalive: 50 + }) + serverPort41.once('client', function () { + assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') + client.end(true, done) + serverPort41.close() + }) + serverPort42.on('client', function (c) { + assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') + c.stream.destroy() + serverPort42.close() + }) + + client.once('connect', function () { + client.stream.destroy() + }) + }) + }) + }) + + abstractClientTests(httpServer, makeOptions()) }) From abc7339da7d4dc245a2b0b207c476f43f7422e83 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Wed, 12 Aug 2020 10:17:13 -0700 Subject: [PATCH 038/110] release(4.2.0): bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5bff875be..2ea59329d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.1.0", + "version": "4.2.0", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 40177cac9a7d7e829b21963e1582c3eb9c13f20a Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 24 Aug 2020 13:43:46 -0700 Subject: [PATCH 039/110] fix(websocket): browser in ws (#1145) --- examples/ws/client.js | 2 +- lib/connect/ws.js | 225 +++++++++++++++++++++++++++++++++------ test/browser/server.js | 6 +- test/websocket_client.js | 2 +- 4 files changed, 196 insertions(+), 39 deletions(-) diff --git a/examples/ws/client.js b/examples/ws/client.js index 53d8bc1d7..61524d345 100644 --- a/examples/ws/client.js +++ b/examples/ws/client.js @@ -1,6 +1,6 @@ 'use strict' -var mqtt = require('../../types') +var mqtt = require('../../') var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) diff --git a/lib/connect/ws.js b/lib/connect/ws.js index dfd8c5140..77844e30b 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,9 +1,13 @@ 'use strict' -var WebSocket = require('ws') -var debug = require('debug')('mqttjs:ws') -var urlModule = require('url') -var WSS_OPTIONS = [ +const WS = require('ws') +const debug = require('debug')('mqttjs:ws') +const duplexify = require('duplexify') +const Buffer = require('safe-buffer').Buffer +const urlModule = require('url') +const Transform = require('readable-stream').Transform + +let WSS_OPTIONS = [ 'rejectUnauthorized', 'ca', 'cert', @@ -12,9 +16,9 @@ var WSS_OPTIONS = [ 'passphrase' ] // eslint-disable-next-line camelcase -var IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' +const IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' function buildUrl (opts, client) { - var url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path + let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path if (typeof (opts.transformWsUrl) === 'function') { url = opts.transformWsUrl(url, opts, client) } @@ -22,75 +26,228 @@ function buildUrl (opts, client) { } function setDefaultOpts (opts) { + let options = opts if (!opts.hostname) { - opts.hostname = 'localhost' + options.hostname = 'localhost' } if (!opts.port) { if (opts.protocol === 'wss') { - opts.port = 443 + options.port = 443 } else { - opts.port = 80 + options.port = 80 } } if (!opts.path) { - opts.path = '/' + options.path = '/' } if (!opts.wsOptions) { - opts.wsOptions = {} + options.wsOptions = {} } if (!IS_BROWSER && opts.protocol === 'wss') { // Add cert/key/ca etc options WSS_OPTIONS.forEach(function (prop) { if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) { - opts.wsOptions[prop] = opts[prop] + options.wsOptions[prop] = opts[prop] } }) } + + return options } -function createWebSocket (client, opts) { +function setDefaultBrowserOpts (opts) { + let options = setDefaultOpts(opts) + + if (!options.hostname) { + options.hostname = options.host + } + + if (!options.hostname) { + // Throwing an error in a Web Worker if no `hostname` is given, because we + // can not determine the `hostname` automatically. If connecting to + // localhost, please supply the `hostname` as an argument. + if (typeof (document) === 'undefined') { + throw new Error('Could not determine host. Specify host manually.') + } + const parsed = urlModule.parse(document.URL) + options.hostname = parsed.hostname + + if (!options.port) { + options.port = parsed.port + } + } + + // objectMode should be defined for logic + if (options.objectMode === undefined) { + options.objectMode = !(options.binary === true || options.binary === undefined) + } + + return options +} + +function createWebSocket (client, url, opts) { debug('createWebSocket') debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) - var websocketSubProtocol = + const websocketSubProtocol = (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) ? 'mqttv3.1' : 'mqtt' - setDefaultOpts(opts) - var url = buildUrl(opts, client) debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) - var ws = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) - var duplex = WebSocket.createWebSocketStream(ws, opts.wsOptions) - duplex.url = url - return duplex + let socket = new WS(url, [websocketSubProtocol], opts.wsOptions) + return socket +} + +function createBrowserWebSocket (client, opts) { + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + let url = buildUrl(opts, client) + /* global WebSocket */ + let socket = new WebSocket(url, [websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + return socket } function streamBuilder (client, opts) { - return createWebSocket(client, opts) + debug('streamBuilder') + let options = setDefaultOpts(opts) + const url = buildUrl(options, client) + let socket = createWebSocket(client, url, options) + let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) + webSocketStream.url = url + return webSocketStream } function browserStreamBuilder (client, opts) { debug('browserStreamBuilder') - if (!opts.hostname) { - opts.hostname = opts.host + let stream + let options = setDefaultBrowserOpts(opts) + // sets the maximum socket buffer size before throttling + const bufferSize = options.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + let socket = createBrowserWebSocket(client, opts) + + let proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) + + if (!opts.objectMode) { + proxy._writev = writev } + proxy.on('close', () => { socket.close() }) - if (!opts.hostname) { - // Throwing an error in a Web Worker if no `hostname` is given, because we - // can not determine the `hostname` automatically. If connecting to - // localhost, please supply the `hostname` as an argument. - if (typeof (document) === 'undefined') { - throw new Error('Could not determine host. Specify host manually.') + const eventListenerSupport = (typeof socket.addEventListener === 'undefined') + + // was already open when passed in + if (socket.readyState === socket.OPEN) { + stream = proxy + } else { + stream = stream = duplexify(undefined, undefined, opts) + if (!opts.objectMode) { + stream._writev = writev } - var parsed = urlModule.parse(document.URL) - opts.hostname = parsed.hostname - if (!opts.port) { - opts.port = parsed.port + if (eventListenerSupport) { + socket.addEventListener('open', onopen) + } else { + socket.onopen = onopen } } - return createWebSocket(client, opts) + + stream.socket = socket + + if (eventListenerSupport) { + socket.addEventListener('close', onclose) + socket.addEventListener('error', onerror) + socket.addEventListener('message', onmessage) + } else { + socket.onclose = onclose + socket.onerror = onerror + socket.onmessage = onmessage + } + + // methods for browserStreamBuilder + + function buildProxy (options, socketWrite, socketEnd) { + let proxy = new Transform({ + objectModeMode: options.objectMode + }) + + proxy._write = socketWrite + proxy._flush = socketEnd + + return proxy + } + + function onopen () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + } + + function onclose () { + stream.end() + stream.destroy() + } + + function onerror (err) { + stream.destroy(err) + } + + function onmessage (event) { + let data = event.data + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + } + + // this is to be enabled only if objectMode is false + function writev (chunks, cb) { + const buffers = new Array(chunks.length) + for (let i = 0; i < chunks.length; i++) { + if (typeof chunks[i].chunk === 'string') { + buffers[i] = Buffer.from(chunks[i], 'utf8') + } else { + buffers[i] = chunks[i].chunk + } + } + + this._write(Buffer.concat(buffers), 'binary', cb) + } + + function socketWriteBrowser (chunk, enc, next) { + if (socket.bufferedAmount > bufferSize) { + // throttle data until buffered amount is reduced. + setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) + } + + if (coerceToBuffer && typeof chunk === 'string') { + chunk = Buffer.from(chunk, 'utf8') + } + + try { + socket.send(chunk) + } catch (err) { + return next(err) + } + + next() + } + + function socketEndBrowser (done) { + socket.close() + done() + } + + // end methods for browserStreamBuilder + + return stream } if (IS_BROWSER) { diff --git a/test/browser/server.js b/test/browser/server.js index 0b5e96516..75a9a8994 100644 --- a/test/browser/server.js +++ b/test/browser/server.js @@ -1,8 +1,8 @@ 'use strict' var handleClient -var websocket = require('websocket-stream') -var WebSocketServer = require('ws').Server +var WS = require('ws') +var WebSocketServer = WS.Server var Connection = require('mqtt-connection') var http = require('http') @@ -109,7 +109,7 @@ function start (startPort, done) { return ws.close() } - stream = websocket(ws) + stream = WS.createWebSocketStream(ws) connection = new Connection(stream) handleClient.call(server, connection) }) diff --git a/test/websocket_client.js b/test/websocket_client.js index fc0107b78..55c8d088c 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -103,7 +103,7 @@ describe('Websocket Client', function () { }) }) - it('should be able transform the url (for e.g. to sign it)', function (done) { + it('should be able to transform the url (for e.g. to sign it)', function (done) { var baseUrl = 'ws://localhost:9999/mqtt' var sig = '?AUTH=token' var expected = baseUrl + sig From e91d2c0f365b6d97f511dac88cffec52ce4135e7 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 24 Aug 2020 13:47:20 -0700 Subject: [PATCH 040/110] release(4.2.1): bump package version (#1149) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ea59329d..1630637fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.0", + "version": "4.2.1", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From f764af6bebe11a65a8770f28bd372a724e0da1fb Mon Sep 17 00:00:00 2001 From: Konstantin Nosov Date: Mon, 21 Sep 2020 17:57:14 +0300 Subject: [PATCH 041/110] fix (browser support): correct protocol logic #1140 (#1154) https://github.com/mqttjs/MQTT.js/issues/1140 --- lib/connect/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connect/index.js b/lib/connect/index.js index 7153ceac7..9df335d39 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -9,7 +9,7 @@ var debug = require('debug')('mqttjs') var protocols = {} // eslint-disable-next-line camelcase -if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ === 'function') { +if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ !== 'function') { protocols.mqtt = require('./tcp') protocols.tcp = require('./tcp') protocols.ssl = require('./tls') From e72c185c7e08ad9e6ca96eaef340d8fb65b97fe6 Mon Sep 17 00:00:00 2001 From: Grzegorz Baranski Date: Mon, 28 Sep 2020 23:52:34 +0200 Subject: [PATCH 042/110] docs: how to use MQTT.js with React --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 21fd27391..d73980a2f 100644 --- a/README.md +++ b/README.md @@ -696,6 +696,44 @@ you can then use mqtt.js in the browser with the same api than node's one. ``` +### React +``` +npm install -g webpack // Install webpack globally +npm install mqtt // Install MQTT library +cd node_modules/mqtt +npm install . // Install dev deps at current dir +webpack mqtt.js --output-library mqtt // Build + +// now you can import the library with ES6 import, commonJS not tested +``` + + +```javascript +import React from 'react'; +import mqtt from 'mqtt'; + +export default () => { + const [connectionStatus, setConnectionStatus] = React.useState(false); + const [messages, setMessages] = React.useState([]); + + useEffect(() => { + const client = mqtt.connect(SOME_URL); + client.on('connect', () => setConnectionStatus(true)); + client.on('message', (topic, payload, packet) => { + setMessages(messages.concat(payload.toString())); + }); + }, []); + + return ( + <> + {lastMessages.map((message) => ( +

{message}

+ ) + + ) +} +``` + Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/mcollina/mosca/wiki/MQTT-over-Websockets) to setup [Mosca](http://mcollina.github.io/mosca/)). From 6cea539fb62cd7193b11a345049a9a82f118f20b Mon Sep 17 00:00:00 2001 From: Mihail Malo Date: Tue, 29 Sep 2020 19:29:22 +0300 Subject: [PATCH 043/110] chore: renovations to node v10 (#1167) * Declare nodejs v10 minimum vesion * Remove travis.yml Project now uses github actions * Remove bash condition from tslint script That version of node is no longer supported * Change test port from 3000 this will avoid spurious failures in development where many developers have 3000 occupied * Remove Map polyfill dependency All currently supported versions exhibit the required behavior, which is codified in the specification * Remove base64-js dependency This depenency is only used once, for the creation of a Buffer object. Buffer has built in base64 deconding capabilities. * Remove readable-stream dependency We no longer support versions of nodejs with incomplete of broken implementations of node streams v3.0.0 specification * Remove "safe-buffer" dependency I additionally audited all uses of Buffer builtin, and found that only safe methods were used, none of the depricated ones. * Update concat-stream The latest version uses a less prehistoric readable-stream dependency Maybe this will fix CI issue on nodejs 10 * Remove through2 dependency * Move end-of-stream to devDependencies It is only used in a test --- .travis.yml | 24 ------------------------ bin/pub.js | 2 +- lib/client.js | 2 +- lib/connect/ali.js | 6 ++---- lib/connect/ws.js | 3 +-- lib/connect/wx.js | 2 +- lib/store.js | 14 +------------- package.json | 13 ++++--------- test/client.js | 5 ++--- test/client_mqtt5.js | 1 - test/helpers/server_process.js | 2 +- test/util.js | 16 +++++++++------- 12 files changed, 23 insertions(+), 67 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d4f4c603d..000000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: node_js -sudo: false -node_js: -- '6' -- '8' -- '10' -- '11' -env: -# For compiling optional extensions -addons: - sauce_connect: true - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 -env: - global: - - secure: ODwb1nuf12e0Ja/HgPwZh4aU01G8tTYZliSHI7ZWmYzGv3Yde6UnATeFG8sxGnWPRXTmaZelZfzhq7Aco1fEUnPm31af3z/iaV60iNmz6E1ifTR+oX7jxIvCDtHwBrgevrmIH8vrEUm/kQqDnFNrGG8Cc2xw/LjsNUG1wuWD+I4= - - secure: buYrn01nPzsiduIQ5oqYTlBdDtM9WKP6gqoyq7IsutHb9sfwh9I6pUYsLibUo4Fq2um9QeXRZ4h1JLKK9xzDVSBpIGGaVzI4ClenfNt9O20IBGBnXcmEKPiRNYF4DkrqZzgx/OVWa6xzcRQI2R1ASQfoyfdpPAnqWXbfalSNkzs= - - secure: IJRxzV03o76uiL4tCw/Zk0Es6tS/ATlQNIpQxZOyRLBoGTmZfZRKRxiESCKUASHudJgNIlw0kar2/LSJjMlYC4KnlrMJOLCYakXW+CWySe4q/f+qbrcdSK1+DZpjyr6Rmo654td/DD5KjNF3UgwBbi1GkE5fd4UL9HI5mPqDpqw= - - CXX=g++-4.8 -script: - - npm run ci diff --git a/bin/pub.js b/bin/pub.js index 94b066b40..fb7d0d63f 100755 --- a/bin/pub.js +++ b/bin/pub.js @@ -7,7 +7,7 @@ var pump = require('pump') var path = require('path') var fs = require('fs') var concat = require('concat-stream') -var Writable = require('readable-stream').Writable +var Writable = require('stream').Writable var helpMe = require('help-me')({ dir: path.join(__dirname, '..', 'doc') }) diff --git a/lib/client.js b/lib/client.js index 08fc443a1..bd06948e0 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,7 +6,7 @@ var EventEmitter = require('events').EventEmitter var Store = require('./store') var mqttPacket = require('mqtt-packet') -var Writable = require('readable-stream').Writable +var Writable = require('stream').Writable var inherits = require('inherits') var reInterval = require('reinterval') var validations = require('./validations') diff --git a/lib/connect/ali.js b/lib/connect/ali.js index 1f3c72580..691b6d874 100644 --- a/lib/connect/ali.js +++ b/lib/connect/ali.js @@ -1,8 +1,7 @@ 'use strict' -var Transform = require('readable-stream').Transform +var Transform = require('stream').Transform var duplexify = require('duplexify') -var base64 = require('base64-js') /* global FileReader */ var my @@ -72,8 +71,7 @@ function bindEventHandler () { my.onSocketMessage(function (res) { if (typeof res.data === 'string') { - var array = base64.toByteArray(res.data) - var buffer = Buffer.from(array) + var buffer = Buffer.from(res.data, 'base64') proxy.push(buffer) } else { var reader = new FileReader() diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 77844e30b..ffb91859a 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -3,9 +3,8 @@ const WS = require('ws') const debug = require('debug')('mqttjs:ws') const duplexify = require('duplexify') -const Buffer = require('safe-buffer').Buffer const urlModule = require('url') -const Transform = require('readable-stream').Transform +const Transform = require('stream').Transform let WSS_OPTIONS = [ 'rejectUnauthorized', diff --git a/lib/connect/wx.js b/lib/connect/wx.js index b9c7a0705..250f15aac 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -1,6 +1,6 @@ 'use strict' -var Transform = require('readable-stream').Transform +var Transform = require('stream').Transform var duplexify = require('duplexify') /* global wx */ diff --git a/lib/store.js b/lib/store.js index 5e3a8dc78..ac870dae9 100644 --- a/lib/store.js +++ b/lib/store.js @@ -5,24 +5,12 @@ */ var xtend = require('xtend') -var Readable = require('readable-stream').Readable +var Readable = require('stream').Readable var streamsOpts = { objectMode: true } var defaultStoreOptions = { clean: true } -/** - * es6-map can preserve insertion order even if ES version is older. - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Description - * It should be noted that a Map which is a map of an object, especially - * a dictionary of dictionaries, will only map to the object's insertion - * order. In ES2015 this is ordered for objects but for older versions of - * ES, this may be random and not ordered. - * - */ -var Map = require('es6-map') - /** * In-memory implementation of the message store * This can actually be saved into files. diff --git a/package.json b/package.json index 1630637fa..874ad4c30 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "scripts": { "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", "pretest": "standard | snazzy", - "tslint": "if [[ \"`node -v`\" != \"v4.3.2\" ]]; then tslint types/**/*.d.ts; fi", + "tslint": "tslint types/**/*.d.ts", "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", "typescript-compile-execute": "node test/typescript/*.js", "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", @@ -53,7 +53,7 @@ "mqtt.js" ], "engines": { - "node": ">=4.0.0" + "node": ">=10.0.0" }, "browser": { "./mqtt.js": "./lib/connect/index.js", @@ -62,18 +62,14 @@ "net": false }, "dependencies": { - "base64-js": "^1.3.0", "commist": "^1.0.0", - "concat-stream": "^1.6.2", + "concat-stream": "^2.0.0", "debug": "^4.1.1", - "end-of-stream": "^1.4.1", - "es6-map": "^0.1.5", "help-me": "^1.0.1", "inherits": "^2.0.3", "minimist": "^1.2.5", "mqtt-packet": "^6.3.2", "pump": "^3.0.0", - "readable-stream": "^2.3.6", "reinterval": "^1.1.0", "split2": "^3.1.0", "ws": "^7.3.1", @@ -85,6 +81,7 @@ "browserify": "^16.5.0", "chai": "^4.2.0", "codecov": "^3.0.4", + "end-of-stream": "^1.4.1", "global": "^4.3.2", "aedes": "^0.42.5", "mkdirp": "^0.5.1", @@ -93,12 +90,10 @@ "nyc": "^15.0.1", "pre-commit": "^1.2.2", "rimraf": "^3.0.2", - "safe-buffer": "^5.1.2", "should": "^13.2.1", "sinon": "^9.0.0", "snazzy": "^8.0.0", "standard": "^11.0.1", - "through2": "^3.0.0", "tslint": "^5.11.0", "tslint-config-standard": "^8.0.1", "typescript": "^3.2.2", diff --git a/test/client.js b/test/client.js index 77c5f7445..b311cda7f 100644 --- a/test/client.js +++ b/test/client.js @@ -8,8 +8,7 @@ var abstractClientTests = require('./abstract_client') var net = require('net') var eos = require('end-of-stream') var mqttPacket = require('mqtt-packet') -var Buffer = require('safe-buffer').Buffer -var Duplex = require('readable-stream').Duplex +var Duplex = require('stream').Duplex var Connection = require('mqtt-connection') var MqttServer = require('./server').MqttServer var util = require('util') @@ -203,7 +202,7 @@ describe('MqttClient', function () { } }) - client = mqtt.connect({ port: 3000, host: 'localhost', keepalive: 1 }) + client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) client.once('connect', function () { innerServer.kill('SIGINT') // mocks server shutdown client.once('close', function () { diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index 28809e154..2535fd323 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -2,7 +2,6 @@ var mqtt = require('..') var abstractClientTests = require('./abstract_client') -var Buffer = require('safe-buffer').Buffer var MqttServer = require('./server').MqttServer var assert = require('chai').assert var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder diff --git a/test/helpers/server_process.js b/test/helpers/server_process.js index 7558bebf6..1d1095cb3 100644 --- a/test/helpers/server_process.js +++ b/test/helpers/server_process.js @@ -6,4 +6,4 @@ new MqttServer(function (client) { client.on('connect', function () { client.connack({ returnCode: 0 }) }) -}).listen(3000, 'localhost') +}).listen(3481, 'localhost') diff --git a/test/util.js b/test/util.js index 813bbd904..273846574 100644 --- a/test/util.js +++ b/test/util.js @@ -1,13 +1,15 @@ 'use strict' -var through = require('through2') +var Transform = require('stream').Transform module.exports.testStream = function () { - return through(function (buf, enc, cb) { - var that = this - setImmediate(function () { - that.push(buf) - cb() - }) + return new Transform({ + transform (buf, enc, cb) { + var that = this + setImmediate(function () { + that.push(buf) + cb() + }) + } }) } From 04184e16d349d020a520c0f77391f421a6755816 Mon Sep 17 00:00:00 2001 From: Mihail Malo Date: Tue, 29 Sep 2020 23:27:59 +0300 Subject: [PATCH 044/110] fix: use 'readable-stream' instead of 'stream' (#1170) --- bin/pub.js | 2 +- lib/client.js | 2 +- lib/connect/ali.js | 2 +- lib/connect/ws.js | 2 +- lib/connect/wx.js | 2 +- lib/store.js | 2 +- package.json | 1 + test/client.js | 2 +- test/util.js | 2 +- 9 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bin/pub.js b/bin/pub.js index fb7d0d63f..94b066b40 100755 --- a/bin/pub.js +++ b/bin/pub.js @@ -7,7 +7,7 @@ var pump = require('pump') var path = require('path') var fs = require('fs') var concat = require('concat-stream') -var Writable = require('stream').Writable +var Writable = require('readable-stream').Writable var helpMe = require('help-me')({ dir: path.join(__dirname, '..', 'doc') }) diff --git a/lib/client.js b/lib/client.js index bd06948e0..08fc443a1 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,7 +6,7 @@ var EventEmitter = require('events').EventEmitter var Store = require('./store') var mqttPacket = require('mqtt-packet') -var Writable = require('stream').Writable +var Writable = require('readable-stream').Writable var inherits = require('inherits') var reInterval = require('reinterval') var validations = require('./validations') diff --git a/lib/connect/ali.js b/lib/connect/ali.js index 691b6d874..e7fe6a3c5 100644 --- a/lib/connect/ali.js +++ b/lib/connect/ali.js @@ -1,6 +1,6 @@ 'use strict' -var Transform = require('stream').Transform +var Transform = require('readable-stream').Transform var duplexify = require('duplexify') /* global FileReader */ diff --git a/lib/connect/ws.js b/lib/connect/ws.js index ffb91859a..5cb2bdd5e 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -4,7 +4,7 @@ const WS = require('ws') const debug = require('debug')('mqttjs:ws') const duplexify = require('duplexify') const urlModule = require('url') -const Transform = require('stream').Transform +const Transform = require('readable-stream').Transform let WSS_OPTIONS = [ 'rejectUnauthorized', diff --git a/lib/connect/wx.js b/lib/connect/wx.js index 250f15aac..b9c7a0705 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -1,6 +1,6 @@ 'use strict' -var Transform = require('stream').Transform +var Transform = require('readable-stream').Transform var duplexify = require('duplexify') /* global wx */ diff --git a/lib/store.js b/lib/store.js index ac870dae9..efbfabf09 100644 --- a/lib/store.js +++ b/lib/store.js @@ -5,7 +5,7 @@ */ var xtend = require('xtend') -var Readable = require('stream').Readable +var Readable = require('readable-stream').Readable var streamsOpts = { objectMode: true } var defaultStoreOptions = { clean: true diff --git a/package.json b/package.json index 874ad4c30..a6ec81230 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "minimist": "^1.2.5", "mqtt-packet": "^6.3.2", "pump": "^3.0.0", + "readable-stream": "^3.6.0", "reinterval": "^1.1.0", "split2": "^3.1.0", "ws": "^7.3.1", diff --git a/test/client.js b/test/client.js index b311cda7f..179ce7b1a 100644 --- a/test/client.js +++ b/test/client.js @@ -8,7 +8,7 @@ var abstractClientTests = require('./abstract_client') var net = require('net') var eos = require('end-of-stream') var mqttPacket = require('mqtt-packet') -var Duplex = require('stream').Duplex +var Duplex = require('readable-stream').Duplex var Connection = require('mqtt-connection') var MqttServer = require('./server').MqttServer var util = require('util') diff --git a/test/util.js b/test/util.js index 273846574..0dd559cb9 100644 --- a/test/util.js +++ b/test/util.js @@ -1,6 +1,6 @@ 'use strict' -var Transform = require('stream').Transform +var Transform = require('readable-stream').Transform module.exports.testStream = function () { return new Transform({ From 00b3183d5d583f16b180b89f7d27ab39bab4bc44 Mon Sep 17 00:00:00 2001 From: Hans Klunder Date: Fri, 2 Oct 2020 21:35:46 +0200 Subject: [PATCH 045/110] fix created new TLS certs with longer key lengths --- test/helpers/tls-cert.pem | 32 ++++++++++++++++++----------- test/helpers/tls-csr.pem | 11 ---------- test/helpers/tls-key.pem | 43 +++++++++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 38 deletions(-) delete mode 100644 test/helpers/tls-csr.pem diff --git a/test/helpers/tls-cert.pem b/test/helpers/tls-cert.pem index 898bf9a57..8b6be1c91 100644 --- a/test/helpers/tls-cert.pem +++ b/test/helpers/tls-cert.pem @@ -1,14 +1,22 @@ -----BEGIN CERTIFICATE----- -MIICKTCCAZICCQDRSYqWgZyJmjANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTQwNjEzMTAwMzAzWhcN -MjQwNjEwMTAwMzAzWjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0 -ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwls -b2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMzFv8+9EBb1sG07 -TjdtbRksRwF7/CZsOWe+ef4ZYPolC5lzvNVYXsBIjL+ilhyKopBbwnOuX9+6FmYO -G/N1lDZRssolGoOVM+1ma3Whmxz8C1g+xi95nP2OqtwP5Du6xhvOM265CiMaf8DH -n63ZFxyi3d1CdNGamNQvrybCzJn7AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEABmyp -3wyBGjb2zSHK5pF9c9GXyHRL4/FkP6qzU5NWrfVAowdOczctJbc3hxPh34Encbr6 -KijYnbdP7/f8aZrStLGqgFYL3SHZY3zvgLTzOmGr9reHUkubHtN+mWHeYy1wVe3D -qEOI8ygT4olVZmWAD+VLKgAb0J07rA/PKf82fBI= +MIIDkzCCAnugAwIBAgIUKq35JCwofQRXirn9WuUcjNjGt5MwDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTIwMDgyMjE4MDcwNloXDTMwMDgyMDE4MDcwNlowWTELMAkGA1UEBhMCQVUxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArTgcoNC3gV1yIwMJ3geQCO1iGL7E4GwiGL6h+EyPU011w5bAH9+Q +ftGy8XaNjTJWMu6E+tFf5r+AWE314s0QJc7NsfSpy8LATcUc/Z3XlyTkHN9IMScn +Rmk+J6FVprvi06Ab64LWyIGLd9DC19taw7xF0EO31jA41Vrs3q88jzjH9U6yYMhw +GAfAPg5L5f0Q1hIz51mgLbqT5zbOE5h3ahZcfmyeR5+UjbS2LuIBem1FNPYwyUAg +jK9AJieb4WVrRgfgIvKEsZQbYtltf9TfWAxVHJVIC0gu+Dhmi6JI6NbZZ1ngYFjJ +uY91MN/Zu23NW5iTSE90x5iYJgQg0ot5/QIDAQABo1MwUTAdBgNVHQ4EFgQUNI0h +Z+Q1vtev6jjdkYTNOJ9R7TAwHwYDVR0jBBgwFoAUNI0hZ+Q1vtev6jjdkYTNOJ9R +7TAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAclulOFJE7zSo +YG0TF2PSc3yHdVYgL6MJnSf1rTQygO4XFIPdxlHtYiWeENDzc3drF2p8qRk2nidv +uxzyDJ9L+K83Jl2QC404uD+bHl/N9M5qF+hZHL6pfuMKv3UZUxPt2bDWtzl96wmg +XASC+R4AFb54XjRuRwCg8o7U/ILi8A4Q1uyM7dVwmztuy0QQpMJg01c/5Sr3brY0 +qAlsl8EYBRtSVVb/c7CwbKT3b5aitqKm25WK3wWvTOE1VVyYxdNHW4IsX+eYB0Z3 +dQ7ZQeb9TYp6taaaC5avk7e6J5n6emHhpzbnHk0dNpKjmZeBrI9yfqdXqLJWdEbG +AvPDUVfo/g== -----END CERTIFICATE----- diff --git a/test/helpers/tls-csr.pem b/test/helpers/tls-csr.pem deleted file mode 100644 index be3a561d6..000000000 --- a/test/helpers/tls-csr.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBmTCCAQICAQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx -ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJbG9j -YWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMxb/PvRAW9bBtO043 -bW0ZLEcBe/wmbDlnvnn+GWD6JQuZc7zVWF7ASIy/opYciqKQW8Jzrl/fuhZmDhvz -dZQ2UbLKJRqDlTPtZmt1oZsc/AtYPsYveZz9jqrcD+Q7usYbzjNuuQojGn/Ax5+t -2Rccot3dQnTRmpjUL68mwsyZ+wIDAQABoAAwDQYJKoZIhvcNAQEFBQADgYEALjPb -zOEL8ahD+UFxwVCXTq4MsKwMlyZCcEVY0CksAgWpCkWr54JUp832p3nEylPRj/gx -8fKWzz5DiO3RER8fzmkb+Kwa+JvXVHmTFzemxYGnxS/HRlF0ZoeAIgvq6ouIrqm9 -1P9gsuYmA5vtfc6Y/NVlSrcSYFH4ADF5DcRTi2Q= ------END CERTIFICATE REQUEST----- diff --git a/test/helpers/tls-key.pem b/test/helpers/tls-key.pem index 965e54fd4..a6d427a79 100644 --- a/test/helpers/tls-key.pem +++ b/test/helpers/tls-key.pem @@ -1,15 +1,28 @@ ------BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQDMxb/PvRAW9bBtO043bW0ZLEcBe/wmbDlnvnn+GWD6JQuZc7zV -WF7ASIy/opYciqKQW8Jzrl/fuhZmDhvzdZQ2UbLKJRqDlTPtZmt1oZsc/AtYPsYv -eZz9jqrcD+Q7usYbzjNuuQojGn/Ax5+t2Rccot3dQnTRmpjUL68mwsyZ+wIDAQAB -AoGARg7p/xL6LEDGqbh+nCwOBWzGplVbAXJJeZsLdcoNCcge3dNhKcTgNf0cWnwv -y3gLAkTClH12Q78Q5r2xBmyV1hqyEb9lrIqAlSS5GjnTWWhyzspcjKZWR5PAjOYo -LlxNpCegWEjOUpD4Lwf9yjEu+xrDGVmsLF0PPRkAM32qh9ECQQD1vzyFr/hSn7Rh -6IFFbLAVkIvsy+1Ca7tF6/7byHCdwqS5oUKaY+9DAr0TE+br87N2IzUCU5X7Cv74 -m+YiqhBlAkEA1VDfpq8puyIq2F6Ftx0xpYMv6XKhuRyAziT/DzIBdFVeOMIgUuk0 -7E4W0N/gDmUmEQFl3HYzUfdZrTUKzjzq3wJAZflsKOGDfu2skXBErEVUsC4iEinx -Ez3XIUWzpQoAyUYqyqjDFYPglgL96Hu6uDCRSLWFWqjKtLi0Yv92OO4vDQJASuAk -YQHDCCiqGWC0Vt4sewhdXPgbxDo5DCL4VIEc+ZStiga6CeBJ71hJse+jWeovPnDb -LFNhGDhWhfHEZTgEyQJAXNuypDS5l73LPvc+yduPZiNEtwae9KbWaZUwC683a81s -mkT7uroNYyK9ptZrz/LMJJotkqCjigXaA3kuzuNUCQ== ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCtOByg0LeBXXIj +AwneB5AI7WIYvsTgbCIYvqH4TI9TTXXDlsAf35B+0bLxdo2NMlYy7oT60V/mv4BY +TfXizRAlzs2x9KnLwsBNxRz9ndeXJOQc30gxJydGaT4noVWmu+LToBvrgtbIgYt3 +0MLX21rDvEXQQ7fWMDjVWuzerzyPOMf1TrJgyHAYB8A+Dkvl/RDWEjPnWaAtupPn +Ns4TmHdqFlx+bJ5Hn5SNtLYu4gF6bUU09jDJQCCMr0AmJ5vhZWtGB+Ai8oSxlBti +2W1/1N9YDFUclUgLSC74OGaLokjo1tlnWeBgWMm5j3Uw39m7bc1bmJNIT3THmJgm +BCDSi3n9AgMBAAECggEAYjCymb52p1BvSMWKLGAhF85okxpgw87ILTqy2eucO15n +aS3lTqwOXrVEOHg5mVZ1Yn2ux/cz47ueZ3AZ+CzCAIyQMVY9ghGtrOgVnPaCpVz2 +Kh+v7p0BOHqkDxb3VIKg+9GAwiny0soMYyjlqjLf6qCo+nvIlBPVw6u9JiYzsANT +EVaC6iEdvwpEG1ZFtzH08Z3/xjhlvDiPDnrfPDFyWZga+J4WJeL0US48vDfufdSJ +lQM0NveF7cdkbKjLiizlYgq8CSOHjMz6OWHS0SFT7iR7GlC5ADpyeMmJLRInLgmE +HZV3/FC1IYQzSHk1WNG/MP3RnzCA8NTj1ehE5mfDyQKBgQDYcTnpXRBd2sozAxzN +dCVf1PtZ5WBmCmk74Ndr/o8wiHkQ7+E3+zee78c45ZQ+P9iEvaygqGTRetItecBA +WxlOQ9z0CmAg1xI2hNIpImAR8Qohr1bEHuzQhdO8LkOuxtj6yP/FDxkKYQAI9C6v +Lx6zo6o4XD5Et4wbUJVkwamrVwKBgQDM4JaxOPHcIVFuYAqxonQrv9bFB7Eew0Fj +qdrQ/flsgz8FyZtThxF9b+7280y5XNJ4tNUKtDcat4cH3jeWfa467DLKjTKkWdJR +iR4MGbsONXWoWPHPQ0GJZY/p3iqn9/OvBZh1k7NsXPmfAVRqMWjNws/WcWSb7Mgq +dBN3A37EywKBgEg6UKcNhV6smnk3eq8dKTO3sUEoiGjE5KU0vO6u/j2l7TC3vCKg +VMlXHtZf1n6Hc8uoOClMyIgXQngmfv965xD1GJDfvYB4BP3oiPFtJT4Xf9gJ2RyN +bV2Qqz3K+o8ikFnwJVovVZ3fDNHwGnwfb1FnNnCkZ6sqzTh4RcJf1iz1AoGARwD7 +GNaMc+cUKrWcXy3XJyZoT4a36tpuuhSu4kly/RmLaP0TGOKxvBBj+DAgAgnaY70A +LKKCin7ajG6GQ2CxVnhvreU7jNwYWOu1fyoXuvfqG/sfat57QxvwwXOewvHbAWhm +CzGyODcMx/+U+uy+zrjagQ5xeNyaDqSF7nRGpfsCgYAA7b/GlldodAJkZAiqejIc +SArscos57stZfYyNICJq7Ye4qpzuWSrQKa5GtseSbvnz5yLzLuDe3Lr5HmYLypOc +wC0JlKeTBMTObsGN0LixrXXRiuyQyCfmuvKu8WfKIlpZMUB5zgHYE8TAvm0BZjq9 ++FUHwoRBoG3Qn04Uj9CCNg== +-----END PRIVATE KEY----- From 66f7de719aed6f05919d0d3d1b0e9796eb6e332e Mon Sep 17 00:00:00 2001 From: roblan Date: Mon, 5 Oct 2020 20:10:42 +0200 Subject: [PATCH 046/110] docs: consistency in callback and mId (#1171) * docs: callback naming consistency * docs: change mid to mId, camel case consistency --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d73980a2f..caea5d595 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ Unsubscribe from a topic or topics ------------------------------------------------------- -### mqtt.Client#end([force], [options], [cb]) +### mqtt.Client#end([force], [options], [callback]) Close the client, accepts the following options: @@ -505,19 +505,19 @@ Close the client, accepts the following options: * `reasonString`: representing the reason for the disconnect `string`, * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, * `serverReference`: String which can be used by the Client to identify another Server to use `string` -* `cb`: will be called when the client is closed. This parameter is +* `callback`: will be called when the client is closed. This parameter is optional. ------------------------------------------------------- -### mqtt.Client#removeOutgoingMessage(mid) +### mqtt.Client#removeOutgoingMessage(mId) Remove a message from the outgoingStore. The outgoing callback will be called with Error('Message removed') if the message is removed. After this function is called, the messageId is released and becomes reusable. -* `mid`: The messageId of the message in the outgoingStore. +* `mId`: The messageId of the message in the outgoingStore. ------------------------------------------------------- @@ -715,7 +715,7 @@ import mqtt from 'mqtt'; export default () => { const [connectionStatus, setConnectionStatus] = React.useState(false); const [messages, setMessages] = React.useState([]); - + useEffect(() => { const client = mqtt.connect(SOME_URL); client.on('connect', () => setConnectionStatus(true)); @@ -723,7 +723,7 @@ export default () => { setMessages(messages.concat(payload.toString())); }); }, []); - + return ( <> {lastMessages.map((message) => ( From 541f201834968eeee5b8599e3b29d8daecd4aac4 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 5 Oct 2020 11:20:14 -0700 Subject: [PATCH 047/110] fix: check if client connected when reconnecting (#1162) This fixes Bug #1152, where calling reconnect can cause endless connect/disconnect loop. --- lib/client.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/client.js b/lib/client.js index 08fc443a1..07e8de115 100644 --- a/lib/client.js +++ b/lib/client.js @@ -783,6 +783,7 @@ MqttClient.prototype.unsubscribe = function () { * * @returns {MqttClient} this - for chaining * @param {Boolean} force - do not wait for all in-flight messages to be acked + * @param {Object} opts - added to the disconnect packet * @param {Function} cb - called when the client has been closed * * @api public @@ -929,8 +930,13 @@ MqttClient.prototype.reconnect = function (opts) { MqttClient.prototype._reconnect = function () { debug('_reconnect: emitting reconnect to client') this.emit('reconnect') - debug('_reconnect: calling _setupStream') - this._setupStream() + if (this.connected) { + this.end(() => { this._setupStream() }) + debug('client already connected. disconnecting first.') + } else { + debug('_reconnect: calling _setupStream') + this._setupStream() + } } /** From b5b3814e4bd6362001e5ac597fdf8f7ce67cc180 Mon Sep 17 00:00:00 2001 From: Elena Horton <52430760+elhorton@users.noreply.github.com> Date: Mon, 5 Oct 2020 15:02:27 -0700 Subject: [PATCH 048/110] Create syncToDevOps.yml --- .github/workflows/syncToDevOps.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/syncToDevOps.yml diff --git a/.github/workflows/syncToDevOps.yml b/.github/workflows/syncToDevOps.yml new file mode 100644 index 000000000..b3a1e7ad5 --- /dev/null +++ b/.github/workflows/syncToDevOps.yml @@ -0,0 +1,23 @@ + +name: Sync issue to Azure DevOps work item + +"on": + issues: + types: + [opened, edited, deleted, closed, reopened, labeled, unlabeled] + +jobs: + alert: + runs-on: ubuntu-latest + steps: + - uses: danhellem/github-actions-issue-to-work-item@master + env: + ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}" + github_token: "${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}" + ado_organization: "${{ secrets.ADO_ORGANIZATION }}" + ado_project: "${{ secrets.ADO_PROJECT }}" + ado_area_path: "${{ secrets.ADO_AREA_PATH }}" + ado_wit: "Bug" + ado_new_state: "New" + ado_close_state: "Done" + ado_bypassrules: false From 70a247c29e0b05ddd8755e7b9c8c41a4c25b431b Mon Sep 17 00:00:00 2001 From: Hans Klunder Date: Tue, 6 Oct 2020 19:50:07 +0200 Subject: [PATCH 049/110] fix: replace url.parse by WHATWG URL API (#1147) fixes #1130 --- examples/wss/client_with_proxy.js | 5 ++--- lib/connect/index.js | 25 ++++++++++++++++--------- lib/connect/ws.js | 4 ++-- test/browser/test.js | 3 +-- test/mqtt.js | 2 +- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/examples/wss/client_with_proxy.js b/examples/wss/client_with_proxy.js index a91e53d39..49513907a 100644 --- a/examples/wss/client_with_proxy.js +++ b/examples/wss/client_with_proxy.js @@ -2,7 +2,6 @@ var mqtt = require('mqtt') var HttpsProxyAgent = require('https-proxy-agent') -var url = require('url') /* host: host of the endpoint you want to connect e.g. my.mqqt.host.com path: path to you endpoint e.g. '/foo/bar/mqtt' @@ -13,8 +12,8 @@ proxy: your proxy e.g. proxy.foo.bar.com port: http proxy port e.g. 8080 */ var proxy = process.env.http_proxy || 'http://:' -var parsed = url.parse(endpoint) -var proxyOpts = url.parse(proxy) +var parsed = new URL(endpoint) +var proxyOpts = new URL(proxy) // true for wss proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true var agent = new HttpsProxyAgent(proxyOpts) diff --git a/lib/connect/index.js b/lib/connect/index.js index 9df335d39..915209089 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -2,8 +2,6 @@ var MqttClient = require('../client') var Store = require('../store') -var url = require('url') -var xtend = require('xtend') var debug = require('debug')('mqttjs') var protocols = {} @@ -60,23 +58,32 @@ function connect (brokerUrl, opts) { opts = opts || {} if (brokerUrl) { - var parsed = url.parse(brokerUrl, true) - if (parsed.port != null) { - parsed.port = Number(parsed.port) - } - - opts = xtend(parsed, opts) - if (opts.protocol === null) { throw new Error('Missing protocol') } + var parsed = new URL(brokerUrl) + // the URL object is a bit special, so copy individual + // items to the opts object + opts.hostname = parsed.hostname + opts.host = parsed.host + opts.protocol = parsed.protocol + opts.port = Number(parsed.port) || null + opts.username = parsed.username + opts.password = parsed.password + opts.searchParams = parsed.searchParams opts.protocol = opts.protocol.replace(/:$/, '') } // merge in the auth options if supplied + // legacy support for url.parse objects (now deprecated in node.js) parseAuthOptions(opts) // support clientId passed in the query string of the url + if (opts.searchParams && typeof opts.searchParams.get('clientId') === 'string') { + opts.clientId = opts.searchParams.get('clientId') + } + + // legacy support for url.parse objects (now deprecated in node.js) if (opts.query && typeof opts.query.clientId === 'string') { opts.clientId = opts.query.clientId } diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 5cb2bdd5e..ecbfff679 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -3,7 +3,7 @@ const WS = require('ws') const debug = require('debug')('mqttjs:ws') const duplexify = require('duplexify') -const urlModule = require('url') +const Buffer = require('safe-buffer').Buffer const Transform = require('readable-stream').Transform let WSS_OPTIONS = [ @@ -69,7 +69,7 @@ function setDefaultBrowserOpts (opts) { if (typeof (document) === 'undefined') { throw new Error('Could not determine host. Specify host manually.') } - const parsed = urlModule.parse(document.URL) + const parsed = new URL(document.URL) options.hostname = parsed.hostname if (!options.port) { diff --git a/test/browser/test.js b/test/browser/test.js index 2adc86c75..fd25abafb 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -1,9 +1,8 @@ 'use strict' var mqtt = require('../../lib/connect') -var _URL = require('url') var xtend = require('xtend') -var parsed = _URL.parse(document.URL) +var parsed = new URL(document.URL) var isHttps = parsed.protocol === 'https:' var port = parsed.port || (isHttps ? 443 : 80) var host = parsed.hostname diff --git a/test/mqtt.js b/test/mqtt.js index f55d04a33..a96e18fee 100644 --- a/test/mqtt.js +++ b/test/mqtt.js @@ -18,7 +18,7 @@ describe('mqtt', function () { (function () { var c = mqtt.connect('foo.bar.com') c.end() - }).should.throw('Missing protocol') + }).should.throw('Invalid URL: foo.bar.com') }) it('should throw an error when called with no protocol specified - with options', function () { From 5c6aeb7c424d354e7e8a6cb19d8236a78546270d Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 26 Oct 2020 18:25:17 -0700 Subject: [PATCH 050/110] release(4.2.2): bump package version (#1187) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6ec81230..2f89b6de7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.1", + "version": "4.2.2", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 94075d84f2eb02d79440dc9b386742ea544bac16 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 26 Oct 2020 18:39:57 -0700 Subject: [PATCH 051/110] remove Buffer --- lib/connect/ws.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/connect/ws.js b/lib/connect/ws.js index ecbfff679..4b623184b 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -3,7 +3,6 @@ const WS = require('ws') const debug = require('debug')('mqttjs:ws') const duplexify = require('duplexify') -const Buffer = require('safe-buffer').Buffer const Transform = require('readable-stream').Transform let WSS_OPTIONS = [ From 298dbb2e7e11e390794128b694a40986497b374c Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 27 Oct 2020 12:36:42 -0700 Subject: [PATCH 052/110] fix(secure): do not override password and username (#1190) --- lib/connect/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connect/index.js b/lib/connect/index.js index 915209089..e865d54fa 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -68,8 +68,8 @@ function connect (brokerUrl, opts) { opts.host = parsed.host opts.protocol = parsed.protocol opts.port = Number(parsed.port) || null - opts.username = parsed.username - opts.password = parsed.password + opts.username = opts.username || parsed.username + opts.password = opts.password || parsed.password opts.searchParams = parsed.searchParams opts.protocol = opts.protocol.replace(/:$/, '') } From 2de81e6b885f90fa7a11fcd49cf77615661107eb Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 27 Oct 2020 12:45:26 -0700 Subject: [PATCH 053/110] release(4.2.3): secure connection bugfix (#1191) * release(4.2.3): secure connection bugfix * fix package.json --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2f89b6de7..4bcbf3de5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.2", + "version": "4.2.3", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", @@ -68,13 +68,13 @@ "help-me": "^1.0.1", "inherits": "^2.0.3", "minimist": "^1.2.5", - "mqtt-packet": "^6.3.2", + "mqtt-packet": "^6.6.0", "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", "split2": "^3.1.0", "ws": "^7.3.1", - "xtend": "^4.0.1" + "xtend": "^4.0.2" }, "devDependencies": { "@types/node": "^10.0.0", From 62405653b33ec5e5e0c8077e3bc9e9ee9a335cbe Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 29 Oct 2020 13:32:31 -0700 Subject: [PATCH 054/110] fix(ws): add all parts of object to opts (#1194) this is to make sure that all the options are included in the URL object. --- lib/connect/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/connect/index.js b/lib/connect/index.js index e865d54fa..a0a0f159f 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -62,15 +62,22 @@ function connect (brokerUrl, opts) { throw new Error('Missing protocol') } var parsed = new URL(brokerUrl) + // the URL object is a bit special, so copy individual // items to the opts object - opts.hostname = parsed.hostname + opts.hash = parsed.hash opts.host = parsed.host - opts.protocol = parsed.protocol + opts.hostname = parsed.hostname + opts.href = parsed.href + opts.origin = parsed.origin + opts.pathname = parsed.pathname opts.port = Number(parsed.port) || null + opts.protocol = parsed.protocol opts.username = opts.username || parsed.username opts.password = opts.password || parsed.password + opts.search = parsed.search opts.searchParams = parsed.searchParams + opts.path = parsed.pathname + parsed.search opts.protocol = opts.protocol.replace(/:$/, '') } From 57ceb2681638c0515c416ffdfd91dc1248597461 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 29 Oct 2020 13:44:52 -0700 Subject: [PATCH 055/110] release(4.2.4): bump package version (#1195) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4bcbf3de5..fed6ace80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.3", + "version": "4.2.4", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 928a0a4b59ea6dc195953dc72558a408edb7c456 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 11 Nov 2020 20:38:18 +0100 Subject: [PATCH 056/110] docs: replace moscajs with aedes (#1198) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index caea5d595..86a335eb3 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,12 @@ Hello mqtt If you want to run your own MQTT broker, you can use [Mosquitto](http://mosquitto.org) or -[Mosca](http://mcollina.github.io/mosca/), and launch it. -You can also use a test instance: test.mosquitto.org and test.mosca.io -are both public. +[Aedes-cli](https://github.com/moscajs/aedes-cli), and launch it. + +You can also use a test instance: test.mosquitto.org. If you do not want to install a separate broker, you can try using the -[mqtt-connection](https://www.npmjs.com/package/mqtt-connection). +[Aedes](https://github.com/moscajs/aedes). to use MQTT.js in the browser see the [browserify](#browserify) section @@ -734,7 +734,7 @@ export default () => { } ``` -Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/mcollina/mosca/wiki/MQTT-over-Websockets) to setup [Mosca](http://mcollina.github.io/mosca/)). +Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/moscajs/aedes/blob/master/docs/Examples.md#mqtt-server-over-websocket-using-server-factory) to setup [Aedes](https://github.com/moscajs/aedes)). ## About QoS From 6a0e50a52214f5e3b221d9f3d0bb86c5896e84c1 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Wed, 11 Nov 2020 19:39:12 +0000 Subject: [PATCH 057/110] fix(auth opts): Default to null for false-y values (#1197) --- lib/connect/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connect/index.js b/lib/connect/index.js index a0a0f159f..30a2ece28 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -73,8 +73,8 @@ function connect (brokerUrl, opts) { opts.pathname = parsed.pathname opts.port = Number(parsed.port) || null opts.protocol = parsed.protocol - opts.username = opts.username || parsed.username - opts.password = opts.password || parsed.password + opts.username = opts.username || parsed.username || null + opts.password = opts.password || parsed.password || null opts.search = parsed.search opts.searchParams = parsed.searchParams opts.path = parsed.pathname + parsed.search From 974e393b6267c8e6b55371992a6babc904010265 Mon Sep 17 00:00:00 2001 From: Konstantin Nosov Date: Wed, 11 Nov 2020 23:54:35 +0200 Subject: [PATCH 058/110] fix #1175 use correct comparsion in eventListenerSupport check --- lib/connect/ws.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 4b623184b..7a35e121d 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -140,7 +140,7 @@ function browserStreamBuilder (client, opts) { } proxy.on('close', () => { socket.close() }) - const eventListenerSupport = (typeof socket.addEventListener === 'undefined') + const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') // was already open when passed in if (socket.readyState === socket.OPEN) { From ba901441bed9ebbc81f0db494d32c90a2aa30f8e Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 12 Nov 2020 08:39:35 -0800 Subject: [PATCH 059/110] release(4.2.5): bump package.json (#1212) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fed6ace80..d78924153 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.4", + "version": "4.2.5", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From a3dd38ed4374b0baa359430472f34078369ef02c Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 24 Nov 2020 15:36:18 -0800 Subject: [PATCH 060/110] fix(websockets): revert URL WHATWG changes --- examples/wss/client_with_proxy.js | 5 +++-- lib/connect/index.js | 33 ++++++++++--------------------- test/browser/test.js | 3 ++- test/mqtt.js | 2 +- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/examples/wss/client_with_proxy.js b/examples/wss/client_with_proxy.js index 49513907a..4a0d9f3c9 100644 --- a/examples/wss/client_with_proxy.js +++ b/examples/wss/client_with_proxy.js @@ -1,6 +1,7 @@ 'use strict' var mqtt = require('mqtt') +var url = require('url') var HttpsProxyAgent = require('https-proxy-agent') /* host: host of the endpoint you want to connect e.g. my.mqqt.host.com @@ -12,8 +13,8 @@ proxy: your proxy e.g. proxy.foo.bar.com port: http proxy port e.g. 8080 */ var proxy = process.env.http_proxy || 'http://:' -var parsed = new URL(endpoint) -var proxyOpts = new URL(proxy) +var parsed = url.parse(endpoint) +var proxyOpts = url.parse(proxy) // true for wss proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true var agent = new HttpsProxyAgent(proxyOpts) diff --git a/lib/connect/index.js b/lib/connect/index.js index 30a2ece28..97e7b4c15 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -2,6 +2,8 @@ var MqttClient = require('../client') var Store = require('../store') +var url = require('url') +var xtend = require('xtend') var debug = require('debug')('mqttjs') var protocols = {} @@ -58,39 +60,24 @@ function connect (brokerUrl, opts) { opts = opts || {} if (brokerUrl) { + var parsed = url.parse(brokerUrl, true) + if (parsed.port != null) { + parsed.port = Number(parsed.port) + } + + opts = xtend(parsed, opts) + if (opts.protocol === null) { throw new Error('Missing protocol') } - var parsed = new URL(brokerUrl) - - // the URL object is a bit special, so copy individual - // items to the opts object - opts.hash = parsed.hash - opts.host = parsed.host - opts.hostname = parsed.hostname - opts.href = parsed.href - opts.origin = parsed.origin - opts.pathname = parsed.pathname - opts.port = Number(parsed.port) || null - opts.protocol = parsed.protocol - opts.username = opts.username || parsed.username || null - opts.password = opts.password || parsed.password || null - opts.search = parsed.search - opts.searchParams = parsed.searchParams - opts.path = parsed.pathname + parsed.search + opts.protocol = opts.protocol.replace(/:$/, '') } // merge in the auth options if supplied - // legacy support for url.parse objects (now deprecated in node.js) parseAuthOptions(opts) // support clientId passed in the query string of the url - if (opts.searchParams && typeof opts.searchParams.get('clientId') === 'string') { - opts.clientId = opts.searchParams.get('clientId') - } - - // legacy support for url.parse objects (now deprecated in node.js) if (opts.query && typeof opts.query.clientId === 'string') { opts.clientId = opts.query.clientId } diff --git a/test/browser/test.js b/test/browser/test.js index fd25abafb..8e9cd42e3 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -2,7 +2,8 @@ var mqtt = require('../../lib/connect') var xtend = require('xtend') -var parsed = new URL(document.URL) +var _URL = require('url') +var parsed = _URL.parse(document.URL) var isHttps = parsed.protocol === 'https:' var port = parsed.port || (isHttps ? 443 : 80) var host = parsed.hostname diff --git a/test/mqtt.js b/test/mqtt.js index a96e18fee..f55d04a33 100644 --- a/test/mqtt.js +++ b/test/mqtt.js @@ -18,7 +18,7 @@ describe('mqtt', function () { (function () { var c = mqtt.connect('foo.bar.com') c.end() - }).should.throw('Invalid URL: foo.bar.com') + }).should.throw('Missing protocol') }) it('should throw an error when called with no protocol specified - with options', function () { From 13e630487f0ce116d9419f2777445782ce34cae5 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 24 Nov 2020 15:48:47 -0800 Subject: [PATCH 061/110] release(4.2.6): bump package version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d78924153..081a2a755 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.5", + "version": "4.2.6", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", @@ -35,7 +35,7 @@ "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" }, "pre-commit": [ - "test", + "pretest", "tslint" ], "bin": { From 8291aecfb7cc7046a4916a10e1f407a134bdab62 Mon Sep 17 00:00:00 2001 From: Cameron Elliott Date: Sun, 6 Dec 2020 20:33:03 -0800 Subject: [PATCH 062/110] 66% smaller browserify output. cut dependencies. ws/wss warning for newbies --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 86a335eb3..b64e50b61 100644 --- a/README.md +++ b/README.md @@ -652,13 +652,21 @@ const client = connect('alis://test.mosquitto.org'); In order to use MQTT.js as a browserify module you can either require it in your browserify bundles or build it as a stand alone module. The exported module is AMD/CommonJs compatible and it will add an object in the global space. -```javascript -npm install -g browserify // install browserify -cd node_modules/mqtt -npm install . // install dev dependencies -browserify mqtt.js -s mqtt > browserMqtt.js // require mqtt in your client-side app +```bash +mkdir tmpdir +cd tmpdir +npm install mqtt +npm install browserify +npm install tinyify +cd node_modules/mqtt/ +npm install . +npx browserify mqtt.js -s mqtt >browserMqtt.js // use script tag +# show size for compressed browser transfer +gzip ### Webpack From 4bd3f3c7e0180646e38d2acc490d511c0547e556 Mon Sep 17 00:00:00 2001 From: ogis-yamazaki Date: Fri, 15 Jan 2021 13:57:47 +0900 Subject: [PATCH 063/110] fix multi protocol test mechanism. Signed-off-by: ogis-yamazaki --- test/abstract_client.js | 70 ++++++++++++++----------- test/client.js | 4 +- test/client_mqtt5.js | 2 +- test/server_helpers_for_client_tests.js | 55 ++++++++++++++++--- test/websocket_client.js | 23 +++++--- 5 files changed, 106 insertions(+), 48 deletions(-) diff --git a/test/abstract_client.js b/test/abstract_client.js index 441c4e812..4c8b0fa77 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -7,10 +7,10 @@ var should = require('chai').should var sinon = require('sinon') var mqtt = require('../') var xtend = require('xtend') -var MqttServer = require('./server').MqttServer var Store = require('./../lib/store') var assert = require('chai').assert var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder module.exports = function (server, config) { var version = config.protocolVersion || 4 @@ -598,9 +598,10 @@ module.exports = function (server, config) { var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) var publishCount = 0 - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { serverClient.on('connect', function () { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('publish', function (packet) { if (packet.qos !== 0) { @@ -626,7 +627,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: false, @@ -1292,13 +1293,14 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { // errors are not interesting for this test // but they might happen on some platforms serverClient.on('error', function () {}) serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('publish', function (packet) { serverClient.puback({messageId: packet.messageId}) @@ -1321,7 +1323,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: false, @@ -2655,9 +2657,10 @@ module.exports = function (server, config) { it('should not resubscribe when reconnecting if suback is error', function (done) { var tryReconnect = true var reconnectEvent = false - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('subscribe', function (packet) { serverClient.suback({ @@ -2671,7 +2674,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND49, function () { - var client = mqtt.connect({ + var client = connect({ port: ports.PORTAND49, host: 'localhost', reconnectPeriod: 100 @@ -2708,9 +2711,10 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) if (reconnect) { serverClient.pubrel({ messageId: 1 }) } @@ -2741,7 +2745,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: false, @@ -2768,9 +2772,10 @@ module.exports = function (server, config) { it('should clear outgoing if close from server', function (done) { var reconnect = false var client = {} - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('subscribe', function (packet) { if (reconnect) { @@ -2787,11 +2792,12 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: true, clientId: 'cid1', + keepalive: 1, reconnectPeriod: 0 }) @@ -2821,9 +2827,10 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('publish', function (packet) { if (reconnect) { @@ -2842,7 +2849,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: false, @@ -2866,9 +2873,10 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('publish', function (packet) { if (reconnect) { @@ -2887,7 +2895,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: false, @@ -2911,9 +2919,10 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('publish', function (packet) { if (!reconnect) { @@ -2937,7 +2946,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: false, @@ -2963,13 +2972,14 @@ module.exports = function (server, config) { var client = {} var incomingStore = new mqtt.Store({ clean: false }) var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = new MqttServer(function (serverClient) { + var server2 = serverBuilder(config.protocol, function (serverClient) { // errors are not interesting for this test // but they might happen on some platforms serverClient.on('error', function () {}) serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) }) serverClient.on('publish', function (packet) { serverClient.puback({messageId: packet.messageId}) @@ -3003,7 +3013,7 @@ module.exports = function (server, config) { }) server2.listen(ports.PORTAND50, function () { - client = mqtt.connect({ + client = connect({ port: ports.PORTAND50, host: 'localhost', clean: false, @@ -3105,7 +3115,7 @@ module.exports = function (server, config) { }) it('should resubscribe even if disconnect is before suback', function (done) { - var client = mqtt.connect(Object.assign({ reconnectPeriod: 100 }, config)) + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) var subscribeCount = 0 var connectCount = 0 @@ -3136,7 +3146,7 @@ module.exports = function (server, config) { }) it('should resubscribe exactly once', function (done) { - var client = mqtt.connect(Object.assign({ reconnectPeriod: 100 }, config)) + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) var subscribeCount = 0 server.on('client', function (serverClient) { diff --git a/test/client.js b/test/client.js index 179ce7b1a..084bfed95 100644 --- a/test/client.js +++ b/test/client.js @@ -18,7 +18,7 @@ var debug = require('debug')('TEST:client') describe('MqttClient', function () { var client - var server = serverBuilder() + var server = serverBuilder('mqtt') var config = {protocol: 'mqtt', port: ports.PORT} server.listen(ports.PORT) @@ -277,7 +277,7 @@ describe('MqttClient', function () { it('should not keep requeueing the first message when offline', function (done) { this.timeout(2500) - var server2 = serverBuilder().listen(ports.PORTAND45) + var server2 = serverBuilder('mqtt').listen(ports.PORTAND45) client = mqtt.connect({ port: ports.PORTAND45, host: 'localhost', diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index 2535fd323..48e1bcb6a 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -8,7 +8,7 @@ var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder var ports = require('./helpers/port_list') describe('MQTT 5.0', function () { - var server = serverBuilder().listen(ports.PORTAND115) + var server = serverBuilder('mqtt').listen(ports.PORTAND115) var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } abstractClientTests(server, config) diff --git a/test/server_helpers_for_client_tests.js b/test/server_helpers_for_client_tests.js index 34f1a8d35..375b96bb6 100644 --- a/test/server_helpers_for_client_tests.js +++ b/test/server_helpers_for_client_tests.js @@ -1,16 +1,26 @@ 'use strict' var MqttServer = require('./server').MqttServer -var MqttServerNoWait = require('./server').MqttServerNoWait +var MqttSecureServer = require('./server').MqttSecureServer var debug = require('debug')('TEST:server_helpers') +var path = require('path') +var fs = require('fs') +var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') +var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') + +var http = require('http') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') + /** * This will build the client for the server to use during testing, and set up the * server side client based on mqtt-connection for handling MQTT messages. - * @param {boolean} fastFlag + * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' + * @param {Function} handler - event handler */ -function serverBuilder (fastFlag) { - var handler = function (serverClient) { +function serverBuilder (protocol, handler) { + var defaultHandler = function (serverClient) { serverClient.on('auth', function (packet) { var rc = 'reasonCode' var connack = {} @@ -90,10 +100,39 @@ function serverBuilder (fastFlag) { debug('disconnected from server') }) } - if (fastFlag) { - return new MqttServerNoWait(handler) - } else { - return new MqttServer(handler) + + if (!handler) { + handler = defaultHandler + } + + switch (protocol) { + case 'mqtt': + return new MqttServer(handler) + case 'mqtts': + return new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) + }, + handler) + case 'ws': + var attachWebsocketServer = function (server) { + var webSocketServer = new WebSocket.Server({server: server, perMessageDeflate: false}) + + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + server.emit('client', connection) + stream.on('error', function () {}) + connection.on('error', function () {}) + connection.on('close', function () {}) + }) + } + + var httpServer = http.createServer() + attachWebsocketServer(httpServer) + httpServer.on('client', handler) + return httpServer } } diff --git a/test/websocket_client.js b/test/websocket_client.js index 55c8d088c..a7f59897a 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -5,7 +5,7 @@ var WebSocket = require('ws') var MQTTConnection = require('mqtt-connection') var abstractClientTests = require('./abstract_client') var ports = require('./helpers/port_list') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var MqttServerNoWait = require('./server').MqttServerNoWait var mqtt = require('../') var xtend = require('xtend') var assert = require('assert') @@ -145,27 +145,36 @@ describe('Websocket Client', function () { describe('reconnecting', () => { it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { + var serverPort42Connected = false + var handler = function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + }) + } this.timeout(15000) var actualURL41 = 'wss://localhost:9917/' var actualURL42 = 'ws://localhost:9918/' - var serverPort41 = serverBuilder(true).listen(ports.PORTAND41) - var serverPort42 = serverBuilder(true).listen(ports.PORTAND42) + var serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) + var serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) serverPort42.on('listening', function () { let client = mqtt.connect({ protocol: 'wss', servers: [ - { port: ports.PORTAND41, host: 'localhost' }, - { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' } + { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, + { port: ports.PORTAND41, host: 'localhost' } ], keepalive: 50 }) - serverPort41.once('client', function () { + serverPort41.once('client', function (c) { assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') + assert(serverPort42Connected) + c.stream.destroy() client.end(true, done) serverPort41.close() }) - serverPort42.on('client', function (c) { + serverPort42.once('client', function (c) { + serverPort42Connected = true assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') c.stream.destroy() serverPort42.close() From e04e0f81ce6df37a007987de44ac9b34053d7c48 Mon Sep 17 00:00:00 2001 From: ogis-yamazaki Date: Mon, 25 Jan 2021 09:43:22 +0900 Subject: [PATCH 064/110] fix #1235, WebSocket client does not emit close event when disconnected from the server side. --- lib/connect/ws.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 4b623184b..748a67b48 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -117,6 +117,7 @@ function streamBuilder (client, opts) { let socket = createWebSocket(client, url, options) let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) webSocketStream.url = url + socket.on('close', () => { webSocketStream.end() }) return webSocketStream } From 746c0bcb79213366d24c85cbd826a4bf01cc29b2 Mon Sep 17 00:00:00 2001 From: bkp7 Date: Fri, 12 Feb 2021 10:17:36 +0000 Subject: [PATCH 065/110] Improved TypeScript declarations for userProperties userProperties changed from being object to being {[index: string]: string[]} (name-value string pairs) Also changed OnConnectCallback and OnMessageCallback to use IConnectPacket and IPublishPacket respectively. These are the specific packets relevant to these callbacks rather than the more generic Packet. NB Also changed package.json to link to version of mqtt-packet with the equivalent changes which are required. This needs to be updated prior to Pulling. --- package.json | 2 +- types/index.d.ts | 3 ++- types/lib/client-options.d.ts | 8 ++++---- types/lib/client.d.ts | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 081a2a755..606627597 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "help-me": "^1.0.1", "inherits": "^2.0.3", "minimist": "^1.2.5", - "mqtt-packet": "^6.6.0", + "mqtt-packet": "git://github.com/bkp7/mqtt-packet.git#8647df5315909bb3ac17b6b5635bcd659e172fce", "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", diff --git a/types/index.d.ts b/types/index.d.ts index f743390f6..9bca9c2ff 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -23,5 +23,6 @@ export { IPingreqPacket, IPingrespPacket, IDisconnectPacket, - Packet + Packet, + UserProperties } from 'mqtt-packet' diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index fc4779cd7..cbcfb5a2e 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -1,6 +1,6 @@ import { MqttClient } from './client' import { Store } from './store' -import { QoS } from 'mqtt-packet' +import { QoS, UserProperties } from 'mqtt-packet' export declare type StorePutCallback = () => void @@ -99,7 +99,7 @@ export interface IClientOptions extends ISecureClientOptions { contentType?: string, responseTopic?: string, correlationData?: Buffer, - userProperties?: Object + userProperties?: UserProperties } } transformWsUrl?: (url: string, options: IClientOptions, client: MqttClient) => string, @@ -110,7 +110,7 @@ export interface IClientOptions extends ISecureClientOptions { topicAliasMaximum?: number, requestResponseInformation?: boolean, requestProblemInformation?: boolean, - userProperties?: Object, + userProperties?: UserProperties, authenticationMethod?: string, authenticationData?: Buffer } @@ -152,7 +152,7 @@ export interface IClientPublishOptions { topicAlias?: string, responseTopic?: string, correlationData?: Buffer, - userProperties?: Object, + userProperties?: UserProperties, subscriptionIdentifier?: number, contentType?: string } diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts index 9356f3dd3..40c8fc474 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -8,7 +8,7 @@ import { IClientReconnectOptions } from './client-options' import { Store } from './store' -import { Packet, QoS } from 'mqtt-packet' +import { Packet, IConnectPacket, IPublishPacket, QoS } from 'mqtt-packet' export interface ISubscriptionGrant { /** @@ -66,9 +66,9 @@ export interface ISubscriptionMap { } } -export declare type OnConnectCallback = (packet: Packet) => void +export declare type OnConnectCallback = (packet: IConnectPacket) => void export declare type ClientSubscribeCallback = (err: Error, granted: ISubscriptionGrant[]) => void -export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: Packet) => void +export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: IPublishPacket) => void export declare type OnPacketCallback = (packet: Packet) => void export declare type OnErrorCallback = (error: Error) => void export declare type PacketCallback = (error?: Error, packet?: Packet) => any From 063aa311d302a5daf8e3f19771b80d4aa2756dca Mon Sep 17 00:00:00 2001 From: bkp7 Date: Fri, 12 Feb 2021 19:54:32 +0000 Subject: [PATCH 066/110] change reference to mqtt-packet v6.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 606627597..1772e9006 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "help-me": "^1.0.1", "inherits": "^2.0.3", "minimist": "^1.2.5", - "mqtt-packet": "git://github.com/bkp7/mqtt-packet.git#8647df5315909bb3ac17b6b5635bcd659e172fce", + "mqtt-packet": "^6.8.0", "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", From 2203585d79aa68d1acb2d889b5f35768a13ebe4c Mon Sep 17 00:00:00 2001 From: bkp7 Date: Wed, 24 Feb 2021 23:13:00 +0000 Subject: [PATCH 067/110] reverse out changes to client.d.ts dealt with in separate PR --- types/lib/client.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts index 40c8fc474..9356f3dd3 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -8,7 +8,7 @@ import { IClientReconnectOptions } from './client-options' import { Store } from './store' -import { Packet, IConnectPacket, IPublishPacket, QoS } from 'mqtt-packet' +import { Packet, QoS } from 'mqtt-packet' export interface ISubscriptionGrant { /** @@ -66,9 +66,9 @@ export interface ISubscriptionMap { } } -export declare type OnConnectCallback = (packet: IConnectPacket) => void +export declare type OnConnectCallback = (packet: Packet) => void export declare type ClientSubscribeCallback = (err: Error, granted: ISubscriptionGrant[]) => void -export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: IPublishPacket) => void +export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: Packet) => void export declare type OnPacketCallback = (packet: Packet) => void export declare type OnErrorCallback = (error: Error) => void export declare type PacketCallback = (error?: Error, packet?: Packet) => any From 61bcbe66281cc5de7217d9836ab86803af043662 Mon Sep 17 00:00:00 2001 From: bkp7 Date: Wed, 24 Feb 2021 23:25:11 +0000 Subject: [PATCH 068/110] fix missing event types changes to client.d.ts to fix missing event types and also to specifically refer to specific packet types rather than the generic 'Packet' --- types/lib/client.d.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts index 9356f3dd3..62416484d 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -8,7 +8,7 @@ import { IClientReconnectOptions } from './client-options' import { Store } from './store' -import { Packet, QoS } from 'mqtt-packet' +import { Packet, IConnectPacket, IPublishPacket, IDisconnectPacket, QoS } from 'mqtt-packet' export interface ISubscriptionGrant { /** @@ -66,9 +66,10 @@ export interface ISubscriptionMap { } } -export declare type OnConnectCallback = (packet: Packet) => void +export declare type OnConnectCallback = (packet: IConnectPacket) => void +export declare type OnDisconnectCallback = (packet: IDisconnectPacket) => void export declare type ClientSubscribeCallback = (err: Error, granted: ISubscriptionGrant[]) => void -export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: Packet) => void +export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: IPublishPacket) => void export declare type OnPacketCallback = (packet: Packet) => void export declare type OnErrorCallback = (error: Error) => void export declare type PacketCallback = (error?: Error, packet?: Packet) => any @@ -101,13 +102,19 @@ export declare class MqttClient extends events.EventEmitter { public on (event: 'connect', cb: OnConnectCallback): this public on (event: 'message', cb: OnMessageCallback): this public on (event: 'packetsend' | 'packetreceive', cb: OnPacketCallback): this + public on (event: 'disconnect', cb: OnDisconnectCallback): this public on (event: 'error', cb: OnErrorCallback): this + public on (event: 'close', cb: OnCloseCallback): this + public on (event: 'end' | 'reconnect' | 'offline' | 'outgoingEmpty', cb: () => void): this public on (event: string, cb: Function): this public once (event: 'connect', cb: OnConnectCallback): this public once (event: 'message', cb: OnMessageCallback): this public once (event: 'packetsend' | 'packetreceive', cb: OnPacketCallback): this + public once (event: 'disconnect', cb: OnDisconnectCallback): this public once (event: 'error', cb: OnErrorCallback): this + public once (event: 'close', cb: OnCloseCallback): this + public once (event: 'end' | 'reconnect' | 'offline' | 'outgoingEmpty', cb: () => void): this public once (event: string, cb: Function): this /** From 51c5c02a92fdf562928f0a916cf54e6a55f4ceb7 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Sun, 7 Mar 2021 15:09:16 -0500 Subject: [PATCH 069/110] improved type definition for 'wsOptions' --- types/lib/client-options.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index fc4779cd7..43fc81d93 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -1,6 +1,8 @@ import { MqttClient } from './client' import { Store } from './store' import { QoS } from 'mqtt-packet' +import { ClientOptions } from 'ws' +import { ClientRequestArgs } from 'http' export declare type StorePutCallback = () => void @@ -11,9 +13,7 @@ export interface IClientOptions extends ISecureClientOptions { path?: string protocol?: 'wss' | 'ws' | 'mqtt' | 'mqtts' | 'tcp' | 'ssl' | 'wx' | 'wxs' - wsOptions?: { - [x: string]: any - } + wsOptions?: ClientOptions | ClientRequestArgs /** * 10 seconds, set to 0 to disable */ From d93c1937be9cff9193da8864b9d826cebc7ec917 Mon Sep 17 00:00:00 2001 From: ogis-yamazaki Date: Mon, 8 Mar 2021 14:10:31 +0900 Subject: [PATCH 070/110] The stream's _writableState may still be false when the WebSocket close event occurs. In this case, it turns out that destory is not called. ```this._writableState.finished``` is still false. https://github.com/websockets/ws/blob/a74dd2ee88ca87e1e0af7062331996bc35f311a6/lib/stream.js#L21 So I changed it to call the destory function instead of the end function. --- lib/connect/ws.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 748a67b48..4c9d249de 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -117,7 +117,7 @@ function streamBuilder (client, opts) { let socket = createWebSocket(client, url, options) let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) webSocketStream.url = url - socket.on('close', () => { webSocketStream.end() }) + socket.on('close', () => { webSocketStream.destroy() }) return webSocketStream } From 845561e0cddd74bf620a414577d8d5e3fea66bf5 Mon Sep 17 00:00:00 2001 From: Hyeon Kim Date: Thu, 8 Apr 2021 20:16:42 +0900 Subject: [PATCH 071/110] Add missing 'duplexify' dependency 'duplexify' is used by MQTT.js but not present in package.json References: https://github.com/mqttjs/MQTT.js/blob/37b12cb9/lib/connect/ali.js#L4 https://github.com/mqttjs/MQTT.js/blob/37b12cb9/lib/connect/wx.js#L4 https://github.com/mqttjs/MQTT.js/blob/37b12cb9/lib/connect/ws.js#L5 https://github.com/mqttjs/MQTT.js/issues/1215 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 081a2a755..671561ed4 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "commist": "^1.0.0", "concat-stream": "^2.0.0", "debug": "^4.1.1", + "duplexify": "^4.1.1", "help-me": "^1.0.1", "inherits": "^2.0.3", "minimist": "^1.2.5", From 8ef5ffc298dbaace376f525ead21244b54c03e74 Mon Sep 17 00:00:00 2001 From: Laurent Goderre Date: Thu, 10 Jun 2021 09:53:26 -0400 Subject: [PATCH 072/110] Fix production vulnerability --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 081a2a755..d9dc1b449 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "commist": "^1.0.0", "concat-stream": "^2.0.0", "debug": "^4.1.1", - "help-me": "^1.0.1", + "help-me": "^3.0.0", "inherits": "^2.0.3", "minimist": "^1.2.5", "mqtt-packet": "^6.6.0", From 949e22a9fbc4d4e222b4e213d46e0e6ce53e413c Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 15 Jun 2021 12:33:04 -0700 Subject: [PATCH 073/110] remove 10.x from gate The gate has been failing for a while and there isn't the bandwidth to fix the failures in 10.x. Going to remove it so the gate is more useful in actually showing if tests are passing. --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 902be303a..561fe9c8f 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [12.x, 14.x] fail-fast: false steps: From 0eb339653a2bce980dcd084c62a5147da0cb804e Mon Sep 17 00:00:00 2001 From: Takatoshi Kondo Date: Tue, 2 Feb 2021 22:12:13 +0900 Subject: [PATCH 074/110] Add non conflict unique messageId support. In order to avoid messageId allocating and registering conflict, during store processing, publish, subscribe, and unsubscribe functions are enqueued. The enqueued functions are invoked when the store processing will be finished. During the invocation, messageId is allocated. messageIds could be run out. In this case, stop invocation. The rest of functions in the queue are remained. When puback, pubcomp, suback, or unsuback is received, the messageId is deallocated and become reusable, so the queue invocation process is continued. --- README.md | 1 + lib/client.js | 381 +++++++++++------- lib/default-message-id-provider.js | 69 ++++ lib/unique-message-id-provider.js | 64 +++ mqtt.js | 4 + package.json | 1 + test/client.js | 20 - test/helpers/port_list.js | 6 +- test/message-id-provider.js | 91 +++++ .../broker-connect-subscribe-and-publish.ts | 5 +- test/unique_message_id_provider_client.js | 21 + types/index.d.ts | 2 + types/lib/client-options.d.ts | 4 +- types/lib/default-message-id-provider.d.ts | 49 +++ types/lib/message-id-provider.d.ts | 40 ++ types/lib/unique-message-id-provider.d.ts | 48 +++ 16 files changed, 642 insertions(+), 164 deletions(-) create mode 100644 lib/default-message-id-provider.js create mode 100644 lib/unique-message-id-provider.js create mode 100644 test/message-id-provider.js create mode 100644 test/unique_message_id_provider_client.js create mode 100644 types/lib/default-message-id-provider.d.ts create mode 100644 types/lib/message-id-provider.d.ts create mode 100644 types/lib/unique-message-id-provider.d.ts diff --git a/README.md b/README.md index 86a335eb3..db627fd0d 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,7 @@ the `connect` event. Typically a `net.Socket`. urls which upon reconnect can have become expired. * `resubscribe` : if connection is broken and reconnects, subscribed topics are automatically subscribed again (default `true`) + * `messageIdProvider`: custom messageId provider. when `new UniqueMessageIdProvider()` is set, then non conflict messageId is provided. In case mqtts (mqtt over tls) is required, the `options` object is passed through to diff --git a/lib/client.js b/lib/client.js index 07e8de115..c481e7a04 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,6 +6,7 @@ var EventEmitter = require('events').EventEmitter var Store = require('./store') var mqttPacket = require('mqtt-packet') +var DefaultMessageIdProvider = require('./default-message-id-provider') var Writable = require('readable-stream').Writable var inherits = require('inherits') var reInterval = require('reinterval') @@ -184,6 +185,8 @@ function MqttClient (streamBuilder, options) { this.streamBuilder = streamBuilder + this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider + // Inflight message storages this.outgoingStore = options.outgoingStore || new Store() this.incomingStore = options.incomingStore || new Store() @@ -213,11 +216,8 @@ function MqttClient (streamBuilder, options) { this._storeProcessing = false // Packet Ids are put into the store during store processing this._packetIdsDuringStoreProcessing = {} - /** - * MessageIDs starting with 1 - * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 - */ - this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) + // Store processing queue + this._storeProcessingQueue = [] // Inflight callbacks this.outgoing = {} @@ -240,15 +240,29 @@ function MqttClient (streamBuilder, options) { packet = entry.packet debug('deliver :: call _sendPacket for %o', packet) - that._sendPacket( - packet, - function (err) { - if (entry.cb) { - entry.cb(err) + var send = true + if (packet.messageId && packet.messageId !== 0) { + if (!that.messageIdProvider.register(packet.messageId)) { + packet.messageeId = that.messageIdProvider.allocate() + if (packet.messageId === null) { + send = false } - deliver() } - ) + } + if (send) { + that._sendPacket( + packet, + function (err) { + if (entry.cb) { + entry.cb(err) + } + deliver() + } + ) + } else { + debug('messageId: %d has already used.', packet.messageId) + deliver() + } } debug('connect :: sending queued packets') @@ -490,60 +504,72 @@ MqttClient.prototype.publish = function (topic, message, opts, callback) { return this } - packet = { - cmd: 'publish', - topic: topic, - payload: message, - qos: opts.qos, - retain: opts.retain, - messageId: this._nextId(), - dup: opts.dup - } - - if (options.protocolVersion === 5) { - packet.properties = opts.properties - if ((!options.properties && packet.properties && packet.properties.topicAlias) || ((opts.properties && options.properties) && - ((opts.properties.topicAlias && options.properties.topicAliasMaximum && opts.properties.topicAlias > options.properties.topicAliasMaximum) || - (!options.properties.topicAliasMaximum && opts.properties.topicAlias)))) { - /* - if we are don`t setup topic alias or - topic alias maximum less than topic alias or - server don`t give topic alias maximum, - we are removing topic alias from packet - */ - delete packet.properties.topicAlias + var that = this + var publishProc = function () { + var messageId = 0 + if (opts.qos === 1 || opts.qos === 2) { + messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + } + packet = { + cmd: 'publish', + topic: topic, + payload: message, + qos: opts.qos, + retain: opts.retain, + messageId: messageId, + dup: opts.dup } - } - debug('publish :: qos', opts.qos) - switch (opts.qos) { - case 1: - case 2: - // Add to callbacks - this.outgoing[packet.messageId] = { - volatile: false, - cb: callback || nop + if (options.protocolVersion === 5) { + packet.properties = opts.properties + if ((!options.properties && packet.properties && packet.properties.topicAlias) || ((opts.properties && options.properties) && + ((opts.properties.topicAlias && options.properties.topicAliasMaximum && opts.properties.topicAlias > options.properties.topicAliasMaximum) || + (!options.properties.topicAliasMaximum && opts.properties.topicAlias)))) { + /* + if we are don`t setup topic alias or + topic alias maximum less than topic alias or + server don`t give topic alias maximum, + we are removing topic alias from packet + */ + delete packet.properties.topicAlias } - if (this._storeProcessing) { - debug('_storeProcessing enabled') - this._packetIdsDuringStoreProcessing[packet.messageId] = false - this._storePacket(packet, undefined, opts.cbStorePut) - } else { + } + + debug('publish :: qos', opts.qos) + switch (opts.qos) { + case 1: + case 2: + // Add to callbacks + that.outgoing[packet.messageId] = { + volatile: false, + cb: callback || nop + } debug('MqttClient:publish: packet cmd: %s', packet.cmd) - this._sendPacket(packet, undefined, opts.cbStorePut) - } - break - default: - if (this._storeProcessing) { - debug('_storeProcessing enabled') - this._storePacket(packet, callback, opts.cbStorePut) - } else { + that._sendPacket(packet, undefined, opts.cbStorePut) + break + default: debug('MqttClient:publish: packet cmd: %s', packet.cmd) - this._sendPacket(packet, callback, opts.cbStorePut) - } - break + that._sendPacket(packet, callback, opts.cbStorePut) + break + } + return true } + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': publishProc, + 'cbStorePut': opts.cbStorePut, + 'callback': callback + } + ) + } else { + publishProc() + } return this } @@ -564,7 +590,7 @@ MqttClient.prototype.publish = function (topic, message, opts, callback) { * @example client.subscribe('topic', console.log); */ MqttClient.prototype.subscribe = function () { - var packet + var that = this var args = new Array(arguments.length) for (var i = 0; i < arguments.length; i++) { args[i] = arguments[i] @@ -574,8 +600,6 @@ MqttClient.prototype.subscribe = function () { var resubscribe = obj.resubscribe var callback = args.pop() || nop var opts = args.pop() - var invalidTopic - var that = this var version = this.options.protocolVersion delete obj.resubscribe @@ -589,7 +613,7 @@ MqttClient.prototype.subscribe = function () { callback = nop } - invalidTopic = validations.validateTopics(obj) + var invalidTopic = validations.validateTopics(obj) if (invalidTopic !== null) { setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) return this @@ -654,59 +678,79 @@ MqttClient.prototype.subscribe = function () { }) } - packet = { - cmd: 'subscribe', - subscriptions: subs, - qos: 1, - retain: false, - dup: false, - messageId: this._nextId() - } - - if (opts.properties) { - packet.properties = opts.properties - } - if (!subs.length) { callback(null, []) - return + return this } - // subscriptions to resubscribe to in case of disconnect - if (this.options.resubscribe) { - debug('subscribe :: resubscribe true') - var topics = [] - subs.forEach(function (sub) { - if (that.options.reconnectPeriod > 0) { - var topic = { qos: sub.qos } - if (version === 5) { - topic.nl = sub.nl || false - topic.rap = sub.rap || false - topic.rh = sub.rh || 0 - topic.properties = sub.properties + var subscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + + var packet = { + cmd: 'subscribe', + subscriptions: subs, + qos: 1, + retain: false, + dup: false, + messageId: messageId + } + + if (opts.properties) { + packet.properties = opts.properties + } + + // subscriptions to resubscribe to in case of disconnect + if (that.options.resubscribe) { + debug('subscribe :: resubscribe true') + var topics = [] + subs.forEach(function (sub) { + if (that.options.reconnectPeriod > 0) { + var topic = { qos: sub.qos } + if (version === 5) { + topic.nl = sub.nl || false + topic.rap = sub.rap || false + topic.rh = sub.rh || 0 + topic.properties = sub.properties + } + that._resubscribeTopics[sub.topic] = topic + topics.push(sub.topic) } - that._resubscribeTopics[sub.topic] = topic - topics.push(sub.topic) - } - }) - that.messageIdToTopic[packet.messageId] = topics - } + }) + that.messageIdToTopic[packet.messageId] = topics + } - this.outgoing[packet.messageId] = { - volatile: true, - cb: function (err, packet) { - if (!err) { - var granted = packet.granted - for (var i = 0; i < granted.length; i += 1) { - subs[i].qos = granted[i] + that.outgoing[packet.messageId] = { + volatile: true, + cb: function (err, packet) { + if (!err) { + var granted = packet.granted + for (var i = 0; i < granted.length; i += 1) { + subs[i].qos = granted[i] + } } - } - callback(err, subs) + callback(err, subs) + } } + debug('subscribe :: call _sendPacket') + that._sendPacket(packet) + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': subscribeProc, + 'callback': callback + } + ) + } else { + subscribeProc() } - debug('subscribe :: call _sendPacket') - this._sendPacket(packet) return this } @@ -724,11 +768,6 @@ MqttClient.prototype.subscribe = function () { * @example client.unsubscribe('topic', console.log); */ MqttClient.prototype.unsubscribe = function () { - var packet = { - cmd: 'unsubscribe', - qos: 1, - messageId: this._nextId() - } var that = this var args = new Array(arguments.length) for (var i = 0; i < arguments.length; i++) { @@ -737,7 +776,6 @@ MqttClient.prototype.unsubscribe = function () { var topic = args.shift() var callback = args.pop() || nop var opts = args.pop() - if (typeof topic === 'string') { topic = [topic] } @@ -747,33 +785,65 @@ MqttClient.prototype.unsubscribe = function () { callback = nop } - if (this._checkDisconnecting(callback)) { + var invalidTopic = validations.validateTopics(topic) + if (invalidTopic !== null) { + setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) return this } - if (typeof topic === 'string') { - packet.unsubscriptions = [topic] - } else if (Array.isArray(topic)) { - packet.unsubscriptions = topic + if (that._checkDisconnecting(callback)) { + return this } - if (this.options.resubscribe) { - packet.unsubscriptions.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } + var unsubscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + var packet = { + cmd: 'unsubscribe', + qos: 1, + messageId: messageId + } - if (typeof opts === 'object' && opts.properties) { - packet.properties = opts.properties - } + if (typeof topic === 'string') { + packet.unsubscriptions = [topic] + } else if (Array.isArray(topic)) { + packet.unsubscriptions = topic + } + + if (that.options.resubscribe) { + packet.unsubscriptions.forEach(function (topic) { + delete that._resubscribeTopics[topic] + }) + } - this.outgoing[packet.messageId] = { - volatile: true, - cb: callback + if (typeof opts === 'object' && opts.properties) { + packet.properties = opts.properties + } + + that.outgoing[packet.messageId] = { + volatile: true, + cb: callback + } + + debug('unsubscribe: call _sendPacket') + that._sendPacket(packet) + + return true } - debug('unsubscribe: call _sendPacket') - this._sendPacket(packet) + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': unsubscribeProc, + 'callback': callback + } + ) + } else { + unsubscribeProc() + } return this } @@ -874,7 +944,7 @@ MqttClient.prototype.end = function (force, opts, cb) { * @returns {MqttClient} this - for chaining * @api public * - * @example client.removeOutgoingMessage(client.getLastMessageId()); + * @example client.removeOutgoingMessage(client.getLastAllocated()); */ MqttClient.prototype.removeOutgoingMessage = function (messageId) { var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null @@ -1334,6 +1404,8 @@ MqttClient.prototype._handleAck = function (packet) { } delete this.outgoing[messageId] this.outgoingStore.del(packet, cb) + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() break case 'pubrec': response = { @@ -1353,6 +1425,7 @@ MqttClient.prototype._handleAck = function (packet) { break case 'suback': delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { if ((packet.granted[grantedI] & 0x80) !== 0) { // suback with Failure status @@ -1364,10 +1437,13 @@ MqttClient.prototype._handleAck = function (packet) { } } } + this._invokeStoreProcessingQueue() cb(null, packet) break case 'unsuback': delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() cb(null) break default: @@ -1425,13 +1501,7 @@ MqttClient.prototype._handleDisconnect = function (packet) { * @return unsigned int */ MqttClient.prototype._nextId = function () { - // id becomes current state of this.nextId and increments afterwards - var id = this.nextId++ - // Ensure 16 bit unsigned int (max 65535, nextId got one higher) - if (this.nextId === 65536) { - this.nextId = 1 - } - return id + return this.messageIdProvider.allocate() } /** @@ -1439,7 +1509,7 @@ MqttClient.prototype._nextId = function () { * @return unsigned int */ MqttClient.prototype.getLastMessageId = function () { - return (this.nextId === 1) ? 65535 : (this.nextId - 1) + return this.messageIdProvider.getLastAllocated() } /** @@ -1486,6 +1556,7 @@ MqttClient.prototype._onConnect = function (packet) { var that = this + this.messageIdProvider.clear() this._setupPingTimer() this._resubscribe(packet) @@ -1502,6 +1573,7 @@ MqttClient.prototype._onConnect = function (packet) { that.once('close', remove) outStore.on('error', function (err) { clearStoreProcessing() + that._flushStoreProcessingQueue() that.removeListener('close', remove) that.emit('error', err) }) @@ -1509,6 +1581,7 @@ MqttClient.prototype._onConnect = function (packet) { function remove () { outStore.destroy() outStore = null + that._flushStoreProcessingQueue() clearStoreProcessing() } @@ -1550,7 +1623,11 @@ MqttClient.prototype._onConnect = function (packet) { } } that._packetIdsDuringStoreProcessing[packet.messageId] = true - that._sendPacket(packet) + if (that.messageIdProvider.register(packet.messageId)) { + that._sendPacket(packet) + } else { + debug('messageId: %d has already used.', packet.messageId) + } } else if (outStore.destroy) { outStore.destroy() } @@ -1567,6 +1644,7 @@ MqttClient.prototype._onConnect = function (packet) { if (allProcessed) { clearStoreProcessing() that.removeListener('close', remove) + that._invokeAllStoreProcessingQueue() that.emit('connect', packet) } else { startStreamProcess() @@ -1578,4 +1656,27 @@ MqttClient.prototype._onConnect = function (packet) { startStreamProcess() } +MqttClient.prototype._invokeStoreProcessingQueue = function () { + if (this._storeProcessingQueue.length > 0) { + var f = this._storeProcessingQueue[0] + if (f && f.invoke()) { + this._storeProcessingQueue.shift() + return true + } + } + return false +} + +MqttClient.prototype._invokeAllStoreProcessingQueue = function () { + while (this._invokeStoreProcessingQueue()) {} +} + +MqttClient.prototype._flushStoreProcessingQueue = function () { + for (var f of this._storeProcessingQueue) { + if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) + if (f.callback) f.callback(new Error('Connection closed')) + } + this._storeProcessingQueue.splice(0) +} + module.exports = MqttClient diff --git a/lib/default-message-id-provider.js b/lib/default-message-id-provider.js new file mode 100644 index 000000000..c0a953f3f --- /dev/null +++ b/lib/default-message-id-provider.js @@ -0,0 +1,69 @@ +'use strict' + +/** + * DefaultMessageAllocator constructor + * @constructor + */ +function DefaultMessageIdProvider () { + if (!(this instanceof DefaultMessageIdProvider)) { + return new DefaultMessageIdProvider() + } + + /** + * MessageIDs starting with 1 + * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 + */ + this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) +} + +/** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.allocate = function () { + // id becomes current state of this.nextId and increments afterwards + var id = this.nextId++ + // Ensure 16 bit unsigned int (max 65535, nextId got one higher) + if (this.nextId === 65536) { + this.nextId = 1 + } + return id +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.getLastAllocated = function () { + return (this.nextId === 1) ? 65535 : (this.nextId - 1) +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +DefaultMessageIdProvider.prototype.register = function (messageId) { + return true +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +DefaultMessageIdProvider.prototype.deallocate = function (messageId) { +} + +/** + * clear + * Deallocate all messageIds. + */ +DefaultMessageIdProvider.prototype.clear = function () { +} + +module.exports = DefaultMessageIdProvider diff --git a/lib/unique-message-id-provider.js b/lib/unique-message-id-provider.js new file mode 100644 index 000000000..5d2203f14 --- /dev/null +++ b/lib/unique-message-id-provider.js @@ -0,0 +1,64 @@ +'use strict' + +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * UniqueMessageAllocator constructor + * @constructor + */ +function UniqueMessageIdProvider () { + if (!(this instanceof UniqueMessageIdProvider)) { + return new UniqueMessageIdProvider() + } + + this.numberAllocator = new NumberAllocator(1, 65535) +} + +/** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ +UniqueMessageIdProvider.prototype.allocate = function () { + this.lastId = this.numberAllocator.alloc() + return this.lastId +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +UniqueMessageIdProvider.prototype.getLastAllocated = function () { + return this.lastId +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +UniqueMessageIdProvider.prototype.register = function (messageId) { + return this.numberAllocator.use(messageId) +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +UniqueMessageIdProvider.prototype.deallocate = function (messageId) { + this.numberAllocator.free(messageId) +} + +/** + * clear + * Deallocate all messageIds. + */ +UniqueMessageIdProvider.prototype.clear = function () { + this.numberAllocator.clear() +} + +module.exports = UniqueMessageIdProvider diff --git a/mqtt.js b/mqtt.js index ab12375c8..c8b94fda1 100644 --- a/mqtt.js +++ b/mqtt.js @@ -8,6 +8,8 @@ var MqttClient = require('./lib/client') var connect = require('./lib/connect') var Store = require('./lib/store') +var DefaultMessageIdProvider = require('./lib/default-message-id-provider') +var UniqueMessageIdProvider = require('./lib/unique-message-id-provider') module.exports.connect = connect @@ -15,3 +17,5 @@ module.exports.connect = connect module.exports.MqttClient = MqttClient module.exports.Client = MqttClient module.exports.Store = Store +module.exports.DefaultMessageIdProvider = DefaultMessageIdProvider +module.exports.UniqueMessageIdProvider = UniqueMessageIdProvider diff --git a/package.json b/package.json index 0ce555a23..0dde135ea 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "inherits": "^2.0.3", "minimist": "^1.2.5", "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.7", "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", diff --git a/test/client.js b/test/client.js index 084bfed95..4ea052ab8 100644 --- a/test/client.js +++ b/test/client.js @@ -54,26 +54,6 @@ describe('MqttClient', function () { client.end() }) - it('should return 1 once the internal counter reached limit', function () { - client = mqtt.connect(config) - client.nextId = 65535 - - assert.equal(client._nextId(), 65535) - assert.equal(client._nextId(), 1) - client.end() - }) - - it('should return 65535 for last message id once the internal counter reached limit', function () { - client = mqtt.connect(config) - client.nextId = 65535 - - assert.equal(client._nextId(), 65535) - assert.equal(client.getLastMessageId(), 65535) - assert.equal(client._nextId(), 1) - assert.equal(client.getLastMessageId(), 1) - client.end() - }) - it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { var server2 = new MqttServer(function (serverClient) { serverClient.on('connect', function (packet) { diff --git a/test/helpers/port_list.js b/test/helpers/port_list.js index 46253bf21..89648b3c0 100644 --- a/test/helpers/port_list.js +++ b/test/helpers/port_list.js @@ -1,4 +1,5 @@ var PORT = 9876 +var PORTAND40 = PORT + 40 var PORTAND41 = PORT + 41 var PORTAND42 = PORT + 42 var PORTAND43 = PORT + 43 @@ -19,9 +20,11 @@ var PORTAND119 = PORT + 119 var PORTAND316 = PORT + 316 var PORTAND326 = PORT + 326 var PORTAND327 = PORT + 327 +var PORTAND400 = PORT + 400 module.exports = { PORT, + PORTAND40, PORTAND41, PORTAND42, PORTAND43, @@ -41,5 +44,6 @@ module.exports = { PORTAND119, PORTAND316, PORTAND326, - PORTAND327 + PORTAND327, + PORTAND400 } diff --git a/test/message-id-provider.js b/test/message-id-provider.js new file mode 100644 index 000000000..2f84bdf35 --- /dev/null +++ b/test/message-id-provider.js @@ -0,0 +1,91 @@ +'use strict' +var assert = require('chai').assert +var DefaultMessageIdProvider = require('../lib/default-message-id-provider') +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') + +describe('message id provider', function () { + describe('default', function () { + it('should return 1 once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.allocate(), 1) + }) + + it('should return 65535 for last message id once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.getLastAllocated(), 65535) + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + }) + it('should return true when register with non allocated messageId', function () { + var provider = new DefaultMessageIdProvider() + assert.equal(provider.register(10), true) + }) + }) + describe('unique', function () { + it('should return 1, 2, 3.., when allocate', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + }) + it('should skip registerd messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.register(2), true) + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 3) + }) + it('should return false register allocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.register(1), false) + assert.equal(provider.register(5), true) + assert.equal(provider.register(5), false) + }) + it('should retrun correct last messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.register(2), true) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.allocate(), 3) + assert.equal(provider.getLastAllocated(), 3) + }) + it('should be reusable deallocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + provider.deallocate(2) + assert.equal(provider.allocate(), 2) + }) + it('should allocate all messageId and then return null', function () { + var provider = new UniqueMessageIdProvider() + for (var i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.deallocate(10000) + assert.equal(provider.allocate(), 10000) + assert.equal(provider.allocate(), null) + }) + it('should all messageId reallocatable after clear', function () { + var provider = new UniqueMessageIdProvider() + var i + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.clear() + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + }) + }) +}) diff --git a/test/typescript/broker-connect-subscribe-and-publish.ts b/test/typescript/broker-connect-subscribe-and-publish.ts index ecdb363cc..359e752a7 100644 --- a/test/typescript/broker-connect-subscribe-and-publish.ts +++ b/test/typescript/broker-connect-subscribe-and-publish.ts @@ -1,12 +1,13 @@ // relative path uses package.json {"types":"types/index.d.ts", ...} -import {IClientOptions, Client, connect, IConnackPacket} from '../..' +import {IClientOptions, Client, connect, IConnackPacket, UniqueMessageIdProvider} from '../..' const BROKER = 'test.mosquitto.org' const PAYLOAD_WILL = Buffer.from('bye from TS') const PAYLOAD_QOS = Buffer.from('hello from TS (with qos=2)') const PAYLOAD_RETAIN = 'hello from TS (with retain=true)' const TOPIC = 'typescript-test-' + Math.random().toString(16).substr(2) -const opts: IClientOptions = {will: {topic: TOPIC, payload: PAYLOAD_WILL, qos: 0, retain: false}} +const opts: IClientOptions = {will: {topic: TOPIC, payload: PAYLOAD_WILL, qos: 0, retain: false}, + messageIdProvider: new UniqueMessageIdProvider()} console.log(`connect(${JSON.stringify(BROKER)})`) const client:Client = connect(`mqtt://${BROKER}`, opts) diff --git a/test/unique_message_id_provider_client.js b/test/unique_message_id_provider_client.js new file mode 100644 index 000000000..933d85b82 --- /dev/null +++ b/test/unique_message_id_provider_client.js @@ -0,0 +1,21 @@ +'use strict' + +var abstractClientTests = require('./abstract_client') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') +var ports = require('./helpers/port_list') + +describe('UniqueMessageIdProviderMqttClient', function () { + var server = serverBuilder('mqtt') + var config = {protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider()} + server.listen(ports.PORTAND400) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) +}) diff --git a/types/index.d.ts b/types/index.d.ts index 9bca9c2ff..a3496b103 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -26,3 +26,5 @@ export { Packet, UserProperties } from 'mqtt-packet' +export { IMessageIdProvider } from './lib/message-id-provider' +export { UniqueMessageIdProvider } from './lib/unique-message-id-provider' diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index cbcfb5a2e..fb388304d 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -1,6 +1,7 @@ import { MqttClient } from './client' import { Store } from './store' import { QoS, UserProperties } from 'mqtt-packet' +import { IMessageIdProvider } from './message-id-provider' export declare type StorePutCallback = () => void @@ -113,7 +114,8 @@ export interface IClientOptions extends ISecureClientOptions { userProperties?: UserProperties, authenticationMethod?: string, authenticationData?: Buffer - } + }, + messageIdProvider?: IMessageIdProvider } export interface ISecureClientOptions { /** diff --git a/types/lib/default-message-id-provider.d.ts b/types/lib/default-message-id-provider.d.ts new file mode 100644 index 000000000..fafaa4c9b --- /dev/null +++ b/types/lib/default-message-id-provider.d.ts @@ -0,0 +1,49 @@ +import { IMessageIdProvider } from './message-id-provider' + +/** + * DefaultMessageIdProvider + * This is compatible behavior with the original MQTT.js internal messageId allocation. + */ +declare class DefaultMessageIdProvider implements IMessageIdProvider { + /** + * DefaultMessageIdProvider constructor. + * Randomize initial messageId + * @constructor + */ + constructor () + + /** + * Return the current messageId and increment the current messageId. + * @return {Number} - messageId + */ + public allocate (): Number | null + + /** + * Get the last allocated messageId. + * @return {Number} - messageId. + */ + public getLastAllocated (): Number | null + + /** + * Register the messageId. + * This function actually nothing and always return true. + * @param {Number} num - The messageId to request use. + * @return {Boolean} - If `num` was not occupied, then return true, otherwise return false. + */ + public register (num: Number): Boolean + + /** + * Deallocate the messageId. + * This function actually nothing. + * @param {Number} num - The messageId to deallocate. + */ + public deallocate (num: Number): void + + /** + * Clear all occupied messageIds. + * This function actually nothing. + */ + public clear (): void +} + +export { DefaultMessageIdProvider } diff --git a/types/lib/message-id-provider.d.ts b/types/lib/message-id-provider.d.ts new file mode 100644 index 000000000..9468cf3e2 --- /dev/null +++ b/types/lib/message-id-provider.d.ts @@ -0,0 +1,40 @@ +/** + * MessageIdProvider + */ +declare interface IMessageIdProvider { + /** + * Allocate the first vacant messageId. The messageId become occupied status. + * @return {Number} - The first vacant messageId. If all messageIds are occupied, return null. + */ + allocate (): Number | null + + /** + * Get the last allocated messageId. + * @return {Number} - messageId. + */ + getLastAllocated (): Number | null + + /** + * Register the messageId. The messageId become occupied status. + * If the messageId has already been occupied, then return false. + * @param {Number} num - The messageId to request use. + * @return {Boolean} - If `num` was not occupied, then return true, otherwise return false. + */ + register (num: Number): Boolean + + /** + * Deallocate the messageId. The messageId become vacant status. + * @param {Number} num - The messageId to deallocate. The messageId must be occupied status. + * In other words, the messageId must be allocated by allocate() or + * occupied by register(). + */ + deallocate (num: Number): void + + /** + * Clear all occupied messageIds. + * The all messageIds are set to vacant status. + */ + clear (): void +} + +export { IMessageIdProvider } diff --git a/types/lib/unique-message-id-provider.d.ts b/types/lib/unique-message-id-provider.d.ts new file mode 100644 index 000000000..0941b2865 --- /dev/null +++ b/types/lib/unique-message-id-provider.d.ts @@ -0,0 +1,48 @@ +import { IMessageIdProvider } from './message-id-provider' + +/** + * UniqueMessageIdProvider + */ +declare class UniqueMessageIdProvider implements IMessageIdProvider { + /** + * UniqueMessageIdProvider constructor. + * @constructor + */ + constructor () + + /** + * Allocate the first vacant messageId. The messageId become occupied status. + * @return {Number} - The first vacant messageId. If all messageIds are occupied, return null. + */ + public allocate (): Number | null + + /** + * Get the last allocated messageId. + * @return {Number} - messageId. + */ + public getLastAllocated (): Number | null + + /** + * Register the messageId. The messageId become occupied status. + * If the messageId has already been occupied, then return false. + * @param {Number} num - The messageId to request use. + * @return {Boolean} - If `num` was not occupied, then return true, otherwise return false. + */ + public register (num: Number): Boolean + + /** + * Deallocate the messageId. The messageId become vacant status. + * @param {Number} num - The messageId to deallocate. The messageId must be occupied status. + * In other words, the messageId must be allocated by allocate() or + * occupied by register(). + */ + public deallocate (num: Number): void + + /** + * Clear all occupied messageIds. + * The all messageIds are set to vacant status. + */ + public clear (): void +} + +export { UniqueMessageIdProvider } From 7c1368fee5bd78b3b5ceb8bc38edb855bfc9cdd8 Mon Sep 17 00:00:00 2001 From: Takatoshi Kondo Date: Thu, 4 Feb 2021 19:52:02 +0900 Subject: [PATCH 075/110] Fixed test server helper sometimes write after end. It caused Uncaught Error: write after end as follows. It had happened very subtle timing. ``` 1) Websocket Client auto reconnect should resubscribe when reconnecting: Uncaught Error: write after end at writeAfterEnd (node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:288:12) at Connection.Writable.write (node_modules/duplexify/node_modules/readable-stream/lib/_stream_writable.js:332:20) at Connection. [as pingresp] (node_modules/mqtt-connection/connection.js:95:10) at Connection. (test/server_helpers_for_client_tests.js:96:20) at Connection.emitPacket (node_modules/mqtt-connection/connection.js:10:8) at addChunk (node_modules/duplexify/node_modules/readable-stream/lib/_stream_readable.js:291:12) at readableAddChunk (node_modules/duplexify/node_modules/readable-stream/lib/_stream_readable.js:278:11) at Connection.Readable.push (node_modules/duplexify/node_modules/readable-stream/lib/_stream_readable.js:245:10) at Connection.Duplexify._forward (node_modules/duplexify/index.js:170:26) at DestroyableTransform.onreadable (node_modules/duplexify/index.js:134:10) at emitReadable_ (node_modules/through2/node_modules/readable-stream/lib/_stream_readable.js:504:10) at emitReadable (node_modules/through2/node_modules/readable-stream/lib/_stream_readable.js:498:62) at addChunk (node_modules/through2/node_modules/readable-stream/lib/_stream_readable.js:298:29) at readableAddChunk (node_modules/through2/node_modules/readable-stream/lib/_stream_readable.js:278:11) at DestroyableTransform.Readable.push (node_modules/through2/node_modules/readable-stream/lib/_stream_readable.js:245:10) at DestroyableTransform.Transform.push (node_modules/through2/node_modules/readable-stream/lib/_stream_transform.js:148:32) at Parser.push (node_modules/mqtt-connection/lib/parseStream.js:19:12) at Parser._newPacket (node_modules/mqtt-packet/parser.js:672:12) at Parser.parse (node_modules/mqtt-packet/parser.js:43:45) at DestroyableTransform.process [as _transform] (node_modules/mqtt-connection/lib/parseStream.js:14:17) at DestroyableTransform.Transform._read (node_modules/through2/node_modules/readable-stream/lib/_stream_transform.js:184:10) at DestroyableTransform.Transform._write (node_modules/through2/node_modules/readable-stream/lib/_stream_transform.js:172:83) at doWrite (node_modules/through2/node_modules/readable-stream/lib/_stream_writable.js:428:64) at writeOrBuffer (node_modules/through2/node_modules/readable-stream/lib/_stream_writable.js:417:5) at DestroyableTransform.Writable.write (node_modules/through2/node_modules/readable-stream/lib/_stream_writable.js:334:11) at Socket.ondata (internal/streams/readable.js:719:22) at addChunk (internal/streams/readable.js:309:12) at readableAddChunk (internal/streams/readable.js:284:9) at Socket.Readable.push (internal/streams/readable.js:223:10) at TCP.onStreamRead (internal/stream_base_commons.js:188:23) at TCP.callbackTrampoline (internal/async_hooks.js:131:14) ``` --- test/server_helpers_for_client_tests.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/server_helpers_for_client_tests.js b/test/server_helpers_for_client_tests.js index 375b96bb6..9527d47e2 100644 --- a/test/server_helpers_for_client_tests.js +++ b/test/server_helpers_for_client_tests.js @@ -22,12 +22,14 @@ var MQTTConnection = require('mqtt-connection') function serverBuilder (protocol, handler) { var defaultHandler = function (serverClient) { serverClient.on('auth', function (packet) { + if (serverClient.writable) return false var rc = 'reasonCode' var connack = {} connack[rc] = 0 serverClient.connack(connack) }) serverClient.on('connect', function (packet) { + if (!serverClient.writable) return false var rc = 'returnCode' var connack = {} if (serverClient.options && serverClient.options.protocolVersion === 5) { @@ -52,6 +54,7 @@ function serverBuilder (protocol, handler) { }) serverClient.on('publish', function (packet) { + if (!serverClient.writable) return false setImmediate(function () { switch (packet.qos) { case 0: @@ -67,10 +70,12 @@ function serverBuilder (protocol, handler) { }) serverClient.on('pubrel', function (packet) { + if (!serverClient.writable) return false serverClient.pubcomp(packet) }) serverClient.on('pubrec', function (packet) { + if (!serverClient.writable) return false serverClient.pubrel(packet) }) @@ -79,6 +84,7 @@ function serverBuilder (protocol, handler) { }) serverClient.on('subscribe', function (packet) { + if (!serverClient.writable) return false serverClient.suback({ messageId: packet.messageId, granted: packet.subscriptions.map(function (e) { @@ -88,11 +94,13 @@ function serverBuilder (protocol, handler) { }) serverClient.on('unsubscribe', function (packet) { + if (!serverClient.writable) return false packet.granted = packet.unsubscriptions.map(function () { return 0 }) serverClient.unsuback(packet) }) serverClient.on('pingreq', function () { + if (!serverClient.writable) return false serverClient.pingresp() }) From f3401a78658cefd74216cddc164f534cf1bdc4f2 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 21 Jun 2021 10:22:27 -0700 Subject: [PATCH 076/110] Update client-options.d.ts --- types/lib/client-options.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index e8ececde5..c9c667d85 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -1,5 +1,5 @@ import { MqttClient } from './client' -import { Store } from './store'' +import { Store } from './store' import { ClientOptions } from 'ws' import { ClientRequestArgs } from 'http' import { QoS, UserProperties } from 'mqtt-packet' From e6fc579b3f761ac9e3bbcb79c463d8ffe840dbf5 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 21 Jun 2021 10:48:58 -0700 Subject: [PATCH 077/110] release: 4.2.7 --- CHANGELOG.md | 23 +++++++++++++++++++++++ package.json | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..fb66a11a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Release History + +## 4.2.7 + +### PR + +#1287 - Fix production vulnerabilities (#1289) + +#1215 - Add missing 'duplexify' dependency (#1266) + +Improve type definition for 'wsOptions' (#1256) + +Improve Typescript Declaratiosn for userProperties (#1249) + +#1235 - Call the end on the WebSocket stream when WebSocket close event is emitted. (#1239) + +#1201 - Uncaught TypeError: net.createConnection is not a function. (#1236) + +Improve Documentation for Browserify (#1224) + +# v4.2.6 and Below + +The release history has beend documented in the GitHub releases and tags historically. \ No newline at end of file diff --git a/package.json b/package.json index 0ce555a23..678c736b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.6", + "version": "4.2.7", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 997944380702c17d6b144b499685e591b3178c11 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 21 Jun 2021 11:37:20 -0700 Subject: [PATCH 078/110] fix: websocket and typescript --- package.json | 2 +- types/lib/client.d.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 678c736b0..f52db0b7f 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "readable-stream": "^3.6.0", "reinterval": "^1.1.0", "split2": "^3.1.0", - "ws": "^7.3.1", + "ws": "^7.5.0", "xtend": "^4.0.2" }, "devDependencies": { diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts index 62416484d..7821a96d7 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -71,9 +71,10 @@ export declare type OnDisconnectCallback = (packet: IDisconnectPacket) => void export declare type ClientSubscribeCallback = (err: Error, granted: ISubscriptionGrant[]) => void export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: IPublishPacket) => void export declare type OnPacketCallback = (packet: Packet) => void +export declare type OnCloseCallback = () => void export declare type OnErrorCallback = (error: Error) => void export declare type PacketCallback = (error?: Error, packet?: Packet) => any -export declare type CloseCallback = () => void +export declare type CloseCallback = (error?: Error) => void export interface IStream extends events.EventEmitter { pipe (to: any): any From 9be3e3d299ca2ba978c24f499c03607f61e372eb Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 21 Jun 2021 11:49:22 -0700 Subject: [PATCH 079/110] 4.2.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f52db0b7f..7f94be668 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.7", + "version": "4.2.8", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", From 3b7b74fb46c3e3287962335910de5b5fac86211d Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 21 Jun 2021 11:51:47 -0700 Subject: [PATCH 080/110] update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb66a11a2..57736f28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 4.2.8 + +### PR + +Fix ws vulnerability and typescript bug (#1292) + ## 4.2.7 ### PR @@ -18,6 +24,6 @@ Improve Typescript Declaratiosn for userProperties (#1249) Improve Documentation for Browserify (#1224) -# v4.2.6 and Below +## v4.2.6 and Below The release history has beend documented in the GitHub releases and tags historically. \ No newline at end of file From 3907b67fc63c30564e47db50f242ccf0a5aa5cbe Mon Sep 17 00:00:00 2001 From: Takatoshi Kondo Date: Tue, 22 Jun 2021 07:39:09 +0900 Subject: [PATCH 081/110] Fix the comment. --- lib/unique-message-id-provider.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/unique-message-id-provider.js b/lib/unique-message-id-provider.js index 5d2203f14..6ffd4bde6 100644 --- a/lib/unique-message-id-provider.js +++ b/lib/unique-message-id-provider.js @@ -18,7 +18,8 @@ function UniqueMessageIdProvider () { * allocate * * Get the next messageId. - * @return unsigned int + * @return if messageId is fully allocated then return null, + * otherwise return the smallest usable unsigned int messageId. */ UniqueMessageIdProvider.prototype.allocate = function () { this.lastId = this.numberAllocator.alloc() From a18380f27e641ec014f1aae2f65c2d4cee265540 Mon Sep 17 00:00:00 2001 From: Vishnu Reddy Date: Fri, 24 Sep 2021 08:15:10 -0700 Subject: [PATCH 082/110] Add note to README about vNext discussions --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index b4497dc97..64e2ff8ad 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written in JavaScript for node.js and the browser. +* [__MQTT.js vNext__](#vnext) * [Upgrade notes](#notes) * [Installation](#install) * [Example](#example) @@ -23,6 +24,9 @@ MQTT.js is an OPEN Open Source Project, see the [Contributing](#contributing) se [![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) + +## Discussion on the next major version of MQTT.js +There are discussions happening on the future of MQTT.js and the next major version (vNext). We invite the community to provide their thoughts and feedback in [this GitHub discussion](https://github.com/mqttjs/MQTT.js/discussions/1324) ## Important notes for existing users From 25784d565cc92a9873691b8dead646f0b86bf1f0 Mon Sep 17 00:00:00 2001 From: maikelsson <33843499+maikelsson@users.noreply.github.com> Date: Tue, 28 Sep 2021 10:37:11 +0300 Subject: [PATCH 083/110] Update README.md Fixed typo in the React code example. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64e2ff8ad..a97dff2cd 100644 --- a/README.md +++ b/README.md @@ -739,7 +739,7 @@ export default () => { return ( <> - {lastMessages.map((message) => ( + {messages.map((message) => (

{message}

) From c92b877292d314e3e0b5d8f84b7f4b68a266aba2 Mon Sep 17 00:00:00 2001 From: Takatoshi Kondo Date: Tue, 5 Oct 2021 05:39:18 +0900 Subject: [PATCH 084/110] fix(client): Refined Topic Alias support. (Implement #1300) (#1301) * Refined Topic Alias support. (Implement #1300) Add automatic topic alias management functionality. - On PUBLISH sending, the client can automatic using/assin Topic Alias (optional). - On PUBLISH receiving, the topic parameter of on message handler is automatically complemented but the packet.topic preserves the original topic. Fix invalid tests. * Fix typo. * Fix comment. * Rename the function name from `removeTopicAlias` to `removeTopicAliasAndRecoverTopicName`. Add comments for caller side of `removeTopicAliasAndRecoverTopicName`. * Captalize label. --- README.md | 38 ++- lib/client.js | 196 +++++++++++-- lib/topic-alias-recv.js | 47 ++++ lib/topic-alias-send.js | 93 +++++++ package.json | 2 + test/client_mqtt5.js | 561 ++++++++++++++++++++++++++++++++++++-- test/helpers/port_list.js | 2 + 7 files changed, 897 insertions(+), 42 deletions(-) create mode 100644 lib/topic-alias-recv.js create mode 100644 lib/topic-alias-send.js diff --git a/README.md b/README.md index a97dff2cd..cebd1ca8a 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,41 @@ the final connection when it drops. The default value is 1000 ms which means it will try to reconnect 1 second after losing the connection. + +## About Topic Alias Management +### Enabling automatic Topic Alias using +If the client sets the option `autoUseTopicAlias:true` then MQTT.js uses existing topic alias automatically. + +example scenario: +``` +1. PUBLISH topic:'t1', ta:1 (register) +2. PUBLISH topic:'t1' -> topic:'', ta:1 (auto use existing map entry) +3. PUBLISH topic:'t2', ta:1 (register overwrite) +4. PUBLISH topic:'t2' -> topic:'', ta:1 (auto use existing map entry based on the receent map) +5. PUBLISH topic:'t1' (t1 is no longer mapped to ta:1) +``` + +User doesn't need to manage which topic is mapped to which topic alias. +If the user want to register topic alias, then publish topic with topic alias. +If the user want to use topic alias, then publish topic without topic alias. If there is a mapped topic alias then added it as a property and update the topic to empty string. + +### Enabling automatic Topic Alias assign + +If the client sets the option `autoAssignTopicAlias:true` then MQTT.js uses existing topic alias automatically. +If no topic alias exists, then assign a new vacant topic alias automatically. If topic alias is fully used, then LRU(Least Recently Used) topic-alias entry is overwritten. + +example scenario: +``` +The broker returns CONNACK (TopicAliasMaximum:3) +1. PUBLISH topic:'t1' -> 't1', ta:1 (auto assign t1:1 and register) +2. PUBLISH topic:'t1' -> '' , ta:1 (auto use existing map entry) +3. PUBLISH topic:'t2' -> 't2', ta:2 (auto assign t1:2 and register. 2 was vacant) +4. PUBLISH topic:'t3' -> 't3', ta:3 (auto assign t1:3 and register. 3 was vacant) +5. PUBLISH topic:'t4' -> 't4', ta:1 (LRU entry is overwritten) +``` + +Also user can manually register topic-alias pair using PUBLISH topic:'some', ta:X. It works well with automatic topic alias assign. ## API @@ -295,6 +329,8 @@ the `connect` event. Typically a `net.Socket`. ```js customHandleAcks: function(topic, message, packet, done) {/*some logic wit colling done(error, reasonCode)*/} ``` + * `autoUseTopicAlias`: enabling automatic Topic Alias using functionality + * `autoAssignTopicAlias`: enabling automatic Topic Alias assign functionality * `properties`: properties MQTT 5.0. `object` that supports the following properties: * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, @@ -665,7 +701,7 @@ npm install browserify npm install tinyify cd node_modules/mqtt/ npm install . -npx browserify mqtt.js -s mqtt >browserMqtt.js // use script tag +npx browserify mqtt.js -s mqtt >browserMqtt.js // use script tag # show size for compressed browser transfer gzip 0) { + if (options.topicAliasMaximum > 0xffff) { + debug('MqttClient :: options.topicAliasMaximum is out of range') + } else { + this.topicAliasRecv = new TopicAliasRecv(options.topicAliasMaximum) + } + } + // Send queued packets this.on('connect', function () { var queue = this.queue @@ -282,6 +382,10 @@ function MqttClient (streamBuilder, options) { that.pingTimer = null } + if (this.topicAliasRecv) { + this.topicAliasRecv.clear() + } + debug('close :: calling _setupReconnect') this._setupReconnect() }) @@ -378,6 +482,14 @@ MqttClient.prototype._setupStream = function () { debug('_setupStream: sending packet `connect`') connectPacket = Object.create(this.options) connectPacket.cmd = 'connect' + if (this.topicAliasRecv) { + if (!connectPacket.properties) { + connectPacket.properties = {} + } + if (this.topicAliasRecv) { + connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max + } + } // avoid message queue sendPacket(this, connectPacket) @@ -526,17 +638,6 @@ MqttClient.prototype.publish = function (topic, message, opts, callback) { if (options.protocolVersion === 5) { packet.properties = opts.properties - if ((!options.properties && packet.properties && packet.properties.topicAlias) || ((opts.properties && options.properties) && - ((opts.properties.topicAlias && options.properties.topicAliasMaximum && opts.properties.topicAlias > options.properties.topicAliasMaximum) || - (!options.properties.topicAliasMaximum && opts.properties.topicAlias)))) { - /* - if we are don`t setup topic alias or - topic alias maximum less than topic alias or - server don`t give topic alias maximum, - we are removing topic alias from packet - */ - delete packet.properties.topicAlias - } } debug('publish :: qos', opts.qos) @@ -1102,6 +1203,13 @@ MqttClient.prototype._cleanUp = function (forced, done) { MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { debug('_sendPacket :: (%s) :: start', this.options.clientId) cbStorePut = cbStorePut || nop + cb = cb || nop + + var err = applyTopicAlias(this, packet) + if (err) { + cb(err) + return + } if (!this.connected) { debug('_sendPacket :: client not connected. Storing packet offline.') @@ -1154,12 +1262,23 @@ MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { debug('_storePacket :: cb? %s', !!cb) cbStorePut = cbStorePut || nop + var storePacket = packet + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + var err = removeTopicAliasAndRecoverTopicName(this, storePacket) + if (err) { + return cb && cb(err) + } + } // check that the packet is not a qos of 0, or that the command is not a publish - if (((packet.qos || 0) === 0 && this.queueQoSZero) || packet.cmd !== 'publish') { - this.queue.push({ packet: packet, cb: cb }) - } else if (packet.qos > 0) { - cb = this.outgoing[packet.messageId] ? this.outgoing[packet.messageId].cb : null - this.outgoingStore.put(packet, function (err) { + if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { + this.queue.push({ packet: storePacket, cb: cb }) + } else if (storePacket.qos > 0) { + cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null + this.outgoingStore.put(storePacket, function (err) { if (err) { return cb && cb(err) } @@ -1237,11 +1356,17 @@ MqttClient.prototype._handleConnack = function (packet) { var rc = version === 5 ? packet.reasonCode : packet.returnCode clearTimeout(this.connackTimer) + delete this.topicAliasSend if (packet.properties) { if (packet.properties.topicAliasMaximum) { - if (!options.properties) { options.properties = {} } - options.properties.topicAliasMaximum = packet.properties.topicAliasMaximum + if (packet.properties.topicAliasMaximum > 0xffff) { + this.emit('error', new Error('topicAliasMaximum from broker is out of range')) + return + } + if (packet.properties.topicAliasMaximum > 0) { + this.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) + } } if (packet.properties.serverKeepAlive && options.keepalive) { options.keepalive = packet.properties.serverKeepAlive @@ -1303,6 +1428,39 @@ MqttClient.prototype._handlePublish = function (packet, done) { var that = this var options = this.options var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] + if (this.options.protocolVersion === 5) { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + if (typeof alias !== 'undefined') { + if (topic.length === 0) { + if (alias > 0 && alias <= 0xffff) { + var gotTopic = this.topicAliasRecv.getTopicByAlias(alias) + if (gotTopic) { + topic = gotTopic + debug('_handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: unregistered topic alias. alias: %d', alias) + this.emit('error', new Error('Received unregistered Topic Alias')) + return + } + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } else { + if (this.topicAliasRecv.put(topic, alias)) { + debug('_handlePublish :: registered topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } + } + } debug('_handlePublish: qos %d', qos) switch (qos) { case 2: { diff --git a/lib/topic-alias-recv.js b/lib/topic-alias-recv.js new file mode 100644 index 000000000..553341100 --- /dev/null +++ b/lib/topic-alias-recv.js @@ -0,0 +1,47 @@ +'use strict' + +/** + * Topic Alias receiving manager + * This holds alias to topic map + * @param {Number} [max] - topic alias maximum entries + */ +function TopicAliasRecv (max) { + if (!(this instanceof TopicAliasRecv)) { + return new TopicAliasRecv(max) + } + this.aliasToTopic = {} + this.max = max +} + +/** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ +TopicAliasRecv.prototype.put = function (topic, alias) { + if (alias === 0 || alias > this.max) { + return false + } + this.aliasToTopic[alias] = topic + this.length = Object.keys(this.aliasToTopic).length + return true +} + +/** + * Get topic by alias + * @param {String} [topic] - topic + * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined + */ +TopicAliasRecv.prototype.getTopicByAlias = function (alias) { + return this.aliasToTopic[alias] +} + +/** + * Clear all entries + */ +TopicAliasRecv.prototype.clear = function () { + this.aliasToTopic = {} +} + +module.exports = TopicAliasRecv diff --git a/lib/topic-alias-send.js b/lib/topic-alias-send.js new file mode 100644 index 000000000..f3abf2084 --- /dev/null +++ b/lib/topic-alias-send.js @@ -0,0 +1,93 @@ +'use strict' + +/** + * Module dependencies + */ +var LruMap = require('collections/lru-map') +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * Topic Alias sending manager + * This holds both topic to alias and alias to topic map + * @param {Number} [max] - topic alias maximum entries + */ +function TopicAliasSend (max) { + if (!(this instanceof TopicAliasSend)) { + return new TopicAliasSend(max) + } + + if (max > 0) { + this.aliasToTopic = new LruMap() + this.topicToAlias = {} + this.numberAllocator = new NumberAllocator(1, max) + this.max = max + this.length = 0 + } +} + +/** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ +TopicAliasSend.prototype.put = function (topic, alias) { + if (alias === 0 || alias > this.max) { + return false + } + const entry = this.aliasToTopic.get(alias) + if (entry) { + delete this.topicToAlias[entry.topic] + } + this.aliasToTopic.set(alias, {'topic': topic, 'alias': alias}) + this.topicToAlias[topic] = alias + this.numberAllocator.use(alias) + this.length = this.aliasToTopic.length + return true +} + +/** + * Get topic by alias + * @param {Number} [alias] - topic alias + * @returns {String} - if mapped topic exists return topic, otherwise return undefined + */ +TopicAliasSend.prototype.getTopicByAlias = function (alias) { + const entry = this.aliasToTopic.get(alias) + if (typeof entry === 'undefined') return entry + return entry.topic +} + +/** + * Get topic by alias + * @param {String} [topic] - topic + * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined + */ +TopicAliasSend.prototype.getAliasByTopic = function (topic) { + const alias = this.topicToAlias[topic] + if (typeof alias !== 'undefined') { + this.aliasToTopic.get(alias) // LRU update + } + return alias +} + +/** + * Clear all entries + */ +TopicAliasSend.prototype.clear = function () { + this.aliasToTopic.clear() + this.topicToAlias = {} + this.numberAllocator.clear() + this.length = 0 +} + +/** + * Get Least Recently Used (LRU) topic alias + * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias + */ +TopicAliasSend.prototype.getLruAlias = function () { + const alias = this.numberAllocator.firstVacant() + if (alias) return alias + return this.aliasToTopic.min().alias +} + +module.exports = TopicAliasSend diff --git a/package.json b/package.json index 3be7ba77e..0f9261059 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "net": false }, "dependencies": { + "collections": "^5.1.12", "commist": "^1.0.0", "concat-stream": "^2.0.0", "debug": "^4.1.1", @@ -73,6 +74,7 @@ "number-allocator": "^1.0.7", "pump": "^3.0.0", "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", "reinterval": "^1.1.0", "split2": "^3.1.0", "ws": "^7.5.0", diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index 48e1bcb6a..fd2bb9979 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -13,29 +13,548 @@ describe('MQTT 5.0', function () { abstractClientTests(server, config) - // var server = serverBuilder().listen(ports.PORTAND115) - - var topicAliasTests = [ - {properties: {}, name: 'should allow any topicAlias when no topicAliasMaximum provided in settings'}, - {properties: { topicAliasMaximum: 15 }, name: 'should not allow topicAlias > topicAliasMaximum when topicAliasMaximum provided in settings'} - ] - - topicAliasTests.forEach(function (test) { - it(test.name, function (done) { - this.timeout(15000) - server.once('client', function (serverClient) { - serverClient.on('publish', function (packet) { - if (packet.properties && packet.properties.topicAlias) { - done(new Error('Packet should not have topicAlias')) - return false - } else { - serverClient.end(done) + it('topic should be complemented on receive', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + assert.strictEqual(packet.properties.topicAliasMaximum, 3) + serverClient.connack({ + reasonCode: 0 + }) + // register topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // overwrite registered topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test2', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('message', function (topic, messagee, packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 3: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }) + + it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoUseTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias by autoApplyTopicAlias + client.publish('test1', 'Message') + }) + }) + + it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoAssignTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 2) + break + case 2: + assert.strictEqual(packet.topic, 'test3') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 3: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 4: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 5: + assert.strictEqual(packet.topic, 'test4') + assert.strictEqual(packet.properties.topicAlias, 2) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message') + client.publish('test2', 'Message') + client.publish('test3', 'Message') + + // use topicAlias + client.publish('test1', 'Message') + client.publish('test3', 'Message') + + // renew LRU topicAlias + client.publish('test4', 'Message') + }) + }) + + it('topicAlias should be removed and topic restored on resend', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + serverClient.puback({messageId: packet.messageId}) + break + case 3: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + serverClient.puback({messageId: packet.messageId}) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('topicAlias should be removed and topic restored on offline publish', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + assert.strictEqual(packet.qos, 1) + serverClient.puback({messageId: packet.messageId}) + break + case 1: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + assert.strictEqual(packet.qos, 0) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias3 + if (packet.properties) { + alias3 = packet.properties.topicAlias + } + assert.strictEqual(alias3, undefined) + assert.strictEqual(packet.qos, 0) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('close', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 } }) }) - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: test.properties} - var client = mqtt.connect(opts) - client.publish('t/h', 'Message', { properties: { topicAlias: 22 } }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 4 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 1 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 4 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH topicAlias:0', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 0 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: '', // use topic alias + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } // in range topic alias + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received unregistered Topic Alias') + server103.close() + client.end(true, done) }) }) @@ -85,7 +604,6 @@ describe('MQTT 5.0', function () { serverClient.connack({ reasonCode: 0, properties: { - topicAliasMaximum: 15, serverKeepAlive: 16, maximumPacketSize: 95 } @@ -105,7 +623,6 @@ describe('MQTT 5.0', function () { var client = mqtt.connect(opts) client.on('connect', function () { assert.strictEqual(client.options.keepalive, 16) - assert.strictEqual(client.options.properties.topicAliasMaximum, 15) assert.strictEqual(client.options.properties.maximumPacketSize, 95) server116.close() client.end(true, done) diff --git a/test/helpers/port_list.js b/test/helpers/port_list.js index 89648b3c0..dc77ef07a 100644 --- a/test/helpers/port_list.js +++ b/test/helpers/port_list.js @@ -11,6 +11,7 @@ var PORTAND48 = PORT + 48 var PORTAND49 = PORT + 49 var PORTAND50 = PORT + 50 var PORTAND72 = PORT + 72 +var PORTAND103 = PORT + 103 var PORTAND114 = PORT + 114 var PORTAND115 = PORT + 115 var PORTAND116 = PORT + 116 @@ -36,6 +37,7 @@ module.exports = { PORTAND49, PORTAND50, PORTAND72, + PORTAND103, PORTAND114, PORTAND115, PORTAND116, From e3e15c3d791615a8fcab46b331678dd5a5a755a0 Mon Sep 17 00:00:00 2001 From: ccarcaci Date: Mon, 4 Oct 2021 23:13:10 +0200 Subject: [PATCH 085/110] fix(typescript): OnConnectCallback with specs expecting Connack packet (#1333) * Set noImplicitAny to false in test/typescript/tsconfig.json due to a compilation error resolving ws types OnConnectCallback receives back a Connack packet as stated in the MQTT specs * Add @types/ws in devDependencies Restore noImplicitAny typescript rule --- package.json | 3 ++- types/lib/client.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0f9261059..712dc0350 100644 --- a/package.json +++ b/package.json @@ -82,13 +82,14 @@ }, "devDependencies": { "@types/node": "^10.0.0", + "@types/ws": "^8.2.0", + "aedes": "^0.42.5", "airtap": "^3.0.0", "browserify": "^16.5.0", "chai": "^4.2.0", "codecov": "^3.0.4", "end-of-stream": "^1.4.1", "global": "^4.3.2", - "aedes": "^0.42.5", "mkdirp": "^0.5.1", "mocha": "^4.1.0", "mqtt-connection": "^4.0.0", diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts index 7821a96d7..c439fe89e 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -8,7 +8,7 @@ import { IClientReconnectOptions } from './client-options' import { Store } from './store' -import { Packet, IConnectPacket, IPublishPacket, IDisconnectPacket, QoS } from 'mqtt-packet' +import { Packet, IConnectPacket, IPublishPacket, IDisconnectPacket, QoS, IConnackPacket } from 'mqtt-packet' export interface ISubscriptionGrant { /** @@ -66,7 +66,7 @@ export interface ISubscriptionMap { } } -export declare type OnConnectCallback = (packet: IConnectPacket) => void +export declare type OnConnectCallback = (packet: IConnackPacket) => void export declare type OnDisconnectCallback = (packet: IDisconnectPacket) => void export declare type ClientSubscribeCallback = (err: Error, granted: ISubscriptionGrant[]) => void export declare type OnMessageCallback = (topic: string, payload: Buffer, packet: IPublishPacket) => void From 7466819d62a5db554e41bf75e939a90f0dc46fe6 Mon Sep 17 00:00:00 2001 From: Takatoshi Kondo Date: Tue, 12 Oct 2021 00:08:42 +0900 Subject: [PATCH 086/110] fix(resubscribe): message id allocate twice (#1337) * fix: messageeId * Fix messageId allocate twice on deliver. resubscribe is out of MQTT spec. It is MQTT.js expansion. On connect sequence, the following three steps are defined by the MQTT Spec. 1. The client sends CONNECT to the broker with CleanStart:false 2. The broker sends CONNACK to the client with SessionPresent:true if session exists 3. The client re-sends in-flight PUBLISH and PUBREL messages resubscribe was processed between the step 2 and step 3. It's too early. The resubscribe might allocate messageId that is the same as PUBLISH or PUBREL packet. It is not good. So I moved resubscribe process to after the step 3. * Removed invalid fallback code. * Stored CONNACK packet instead of sessionPresent. Co-authored-by: Yoseph Maguire --- lib/client.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/client.js b/lib/client.js index eebd418bd..540a11780 100644 --- a/lib/client.js +++ b/lib/client.js @@ -335,6 +335,7 @@ function MqttClient (streamBuilder, options) { var packet = null if (!entry) { + that._resubscribe() return } @@ -343,10 +344,7 @@ function MqttClient (streamBuilder, options) { var send = true if (packet.messageId && packet.messageId !== 0) { if (!that.messageIdProvider.register(packet.messageId)) { - packet.messageeId = that.messageIdProvider.allocate() - if (packet.messageId === null) { - send = false - } + send = false } } if (send) { @@ -360,7 +358,7 @@ function MqttClient (streamBuilder, options) { } ) } else { - debug('messageId: %d has already used.', packet.messageId) + debug('messageId: %d has already used. The message is skipped and removed.', packet.messageId) deliver() } } @@ -1674,11 +1672,11 @@ MqttClient.prototype.getLastMessageId = function () { * _resubscribe * @api private */ -MqttClient.prototype._resubscribe = function (connack) { +MqttClient.prototype._resubscribe = function () { debug('_resubscribe') var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) if (!this._firstConnection && - (this.options.clean || (this.options.protocolVersion === 5 && !connack.sessionPresent)) && + (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && _resubscribeTopicsKeys.length > 0) { if (this.options.resubscribe) { if (this.options.protocolVersion === 5) { @@ -1714,9 +1712,9 @@ MqttClient.prototype._onConnect = function (packet) { var that = this + this.connackPacket = packet this.messageIdProvider.clear() this._setupPingTimer() - this._resubscribe(packet) this.connected = true From 59fab369d2738edcf62306a67375763d737bc4ad Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 19 Oct 2021 14:22:18 -0700 Subject: [PATCH 087/110] fix: types (#1341) --- .gitignore | 1 - .npmrc | 1 - README.md | 1666 +++--- benchmarks/bombing.js | 52 +- benchmarks/throughputCounter.js | 44 +- bin/mqtt.js | 54 +- bin/pub.js | 292 +- bin/sub.js | 246 +- example.js | 22 +- examples/client/secure-client.js | 48 +- examples/client/simple-both.js | 26 +- examples/client/simple-publish.js | 14 +- examples/client/simple-subscribe.js | 18 +- examples/tls client/mqttclient.js | 96 +- examples/ws/client.js | 106 +- examples/wss/client_with_proxy.js | 116 +- lib/client.js | 3676 ++++++------ lib/connect/ali.js | 256 +- lib/connect/index.js | 328 +- lib/connect/tcp.js | 42 +- lib/connect/tls.js | 90 +- lib/connect/ws.js | 512 +- lib/connect/wx.js | 268 +- lib/default-message-id-provider.js | 138 +- lib/store.js | 256 +- lib/topic-alias-send.js | 186 +- lib/unique-message-id-provider.js | 130 +- lib/validations.js | 104 +- mqtt.js | 42 +- package.json | 226 +- test/abstract_client.js | 6354 ++++++++++----------- test/abstract_store.js | 270 +- test/browser/server.js | 264 +- test/browser/test.js | 184 +- test/client.js | 972 ++-- test/client_mqtt5.js | 2106 +++---- test/helpers/port_list.js | 102 +- test/helpers/server.js | 106 +- test/helpers/server_process.js | 18 +- test/message-id-provider.js | 182 +- test/mqtt.js | 460 +- test/mqtt_store.js | 18 +- test/secure_client.js | 376 +- test/server.js | 188 +- test/server_helpers_for_client_tests.js | 294 +- test/store.js | 20 +- test/unique_message_id_provider_client.js | 42 +- test/util.js | 30 +- test/websocket_client.js | 382 +- types/lib/client-options.d.ts | 2 +- 50 files changed, 10712 insertions(+), 10714 deletions(-) diff --git a/.gitignore b/.gitignore index 5c315db7f..6a69f7d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ coverage test/typescript/.idea/* test/typescript/*.js test/typescript/*.map -package-lock.json # VS Code stuff **/typings/** **/.vscode/** diff --git a/.npmrc b/.npmrc index c1ca392fe..e69de29bb 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ -package-lock = false diff --git a/README.md b/README.md index cebd1ca8a..2b8a19b3e 100644 --- a/README.md +++ b/README.md @@ -1,833 +1,833 @@ -![mqtt.js](https://raw.githubusercontent.com/mqttjs/MQTT.js/137ee0e3940c1f01049a30248c70f24dc6e6f829/MQTT.js.png) -======= - -![Github Test Status](https://github.com/mqttjs/MQTT.js/workflows/MQTT.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/mqttjs/MQTT.js/branch/master/graph/badge.svg)](https://codecov.io/gh/mqttjs/MQTT.js) - -MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written -in JavaScript for node.js and the browser. - -* [__MQTT.js vNext__](#vnext) -* [Upgrade notes](#notes) -* [Installation](#install) -* [Example](#example) -* [Command Line Tools](#cli) -* [API](#api) -* [Browser](#browser) -* [Weapp](#weapp) -* [About QoS](#qos) -* [TypeScript](#typescript) -* [Contributing](#contributing) -* [License](#license) - -MQTT.js is an OPEN Open Source Project, see the [Contributing](#contributing) section to find out what this means. - -[![JavaScript Style -Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) - - -## Discussion on the next major version of MQTT.js -There are discussions happening on the future of MQTT.js and the next major version (vNext). We invite the community to provide their thoughts and feedback in [this GitHub discussion](https://github.com/mqttjs/MQTT.js/discussions/1324) - - -## Important notes for existing users - -__v4.0.0__ (Released 04/2020) removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to -debug logging, along with some feature additions. - -As a __breaking change__, by default a error handler is built into the MQTT.js client, so if any -errors are emitted and the user has not created an event handler on the client for errors, the client will -not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been -added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. - -__v3.0.0__ adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. - -__Note:__ MQTT v5 support is experimental as it has not been implemented by brokers yet. - -__v2.0.0__ removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending -packets. It also removes all the deprecated functionality in v1.0.0, -mainly `mqtt.createConnection` and `mqtt.Server`. From v2.0.0, -subscriptions are restored upon reconnection if `clean: true`. -v1.x.x is now in *LTS*, and it will keep being supported as long as -there are v0.8, v0.10 and v0.12 users. - -As a __breaking change__, the `encoding` option in the old client is -removed, and now everything is UTF-8 with the exception of the -`password` in the CONNECT message and `payload` in the PUBLISH message, -which are `Buffer`. - -Another __breaking change__ is that MQTT.js now defaults to MQTT v3.1.1, -so to support old brokers, please read the [client options doc](#client). - -__v1.0.0__ improves the overall architecture of the project, which is now -split into three components: MQTT.js keeps the Client, -[mqtt-connection](http://npm.im/mqtt-connection) includes the barebone -Connection code for server-side usage, and [mqtt-packet](http://npm.im/mqtt-packet) -includes the protocol parser and generator. The new Client improves -performance by a 30% factor, embeds Websocket support -([MOWS](http://npm.im/mows) is now deprecated), and it has a better -support for QoS 1 and 2. The previous API is still supported but -deprecated, as such, it is not documented in this README. - - -## Installation - -```sh -npm install mqtt --save -``` - - -## Example - -For the sake of simplicity, let's put the subscriber and the publisher in the same file: - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('mqtt://test.mosquitto.org') - -client.on('connect', function () { - client.subscribe('presence', function (err) { - if (!err) { - client.publish('presence', 'Hello mqtt') - } - }) -}) - -client.on('message', function (topic, message) { - // message is Buffer - console.log(message.toString()) - client.end() -}) -``` - -output: -``` -Hello mqtt -``` - -If you want to run your own MQTT broker, you can use -[Mosquitto](http://mosquitto.org) or -[Aedes-cli](https://github.com/moscajs/aedes-cli), and launch it. - -You can also use a test instance: test.mosquitto.org. - -If you do not want to install a separate broker, you can try using the -[Aedes](https://github.com/moscajs/aedes). - -to use MQTT.js in the browser see the [browserify](#browserify) section - - -## Promise support - -If you want to use the new [async-await](https://blog.risingstack.com/async-await-node-js-7-nightly/) functionality in JavaScript, or just prefer using Promises instead of callbacks, [async-mqtt](https://github.com/mqttjs/async-mqtt) is a wrapper over MQTT.js which uses promises instead of callbacks when possible. - - -## Command Line Tools - -MQTT.js bundles a command to interact with a broker. -In order to have it available on your path, you should install MQTT.js -globally: - -```sh -npm install mqtt -g -``` - -Then, on one terminal - -``` -mqtt sub -t 'hello' -h 'test.mosquitto.org' -v -``` - -On another - -``` -mqtt pub -t 'hello' -h 'test.mosquitto.org' -m 'from MQTT.js' -``` - -See `mqtt help ` for the command help. - - -## Debug Logs - -MQTT.js uses the [debug](https://www.npmjs.com/package/debug#cmd) package for debugging purposes. To enable debug logs, add the following environment variable on runtime : -```ps -# (example using PowerShell, the VS Code default) -$env:DEBUG='mqttjs*' - -``` - - -## About Reconnection - -An important part of any websocket connection is what to do when a connection -drops off and the client needs to reconnect. MQTT has built-in reconnection -support that can be configured to behave in ways that suit the application. - -#### Refresh Authentication Options / Signed Urls with `transformWsUrl` (Websocket Only) - -When an mqtt connection drops and needs to reconnect, it's common to require -that any authentication associated with the connection is kept current with -the underlying auth mechanism. For instance some applications may pass an auth -token with connection options on the initial connection, while other cloud -services may require a url be signed with each connection. - -By the time the reconnect happens in the application lifecycle, the original -auth data may have expired. - -To address this we can use a hook called `transformWsUrl` to manipulate -either of the connection url or the client options at the time of a reconnect. - -Example (update clientId & username on each reconnect): -``` - const transformWsUrl = (url, options, client) => { - client.options.username = `token=${this.get_current_auth_token()}`; - client.options.clientId = `${this.get_updated_clientId()}`; - - return `${this.get_signed_cloud_url(url)`; - } - - const connection = await mqtt.connectAsync(, { - ..., - transformWsUrl: transformUrl, - }); - -``` -Now every time a new WebSocket connection is opened (hopefully not too often), -we will get a fresh signed url or fresh auth token data. - -Note: Currently this hook does _not_ support promises, meaning that in order to -use the latest auth token, you must have some outside mechanism running that -handles application-level authentication refreshing so that the websocket -connection can simply grab the latest valid token or signed url. - - -#### Enabling Reconnection with `reconnectPeriod` option - -To ensure that the mqtt client automatically tries to reconnect when the -connection is dropped, you must set the client option `reconnectPeriod` to a -value greater than 0. A value of 0 will disable reconnection and then terminate -the final connection when it drops. - -The default value is 1000 ms which means it will try to reconnect 1 second -after losing the connection. - - -## About Topic Alias Management - -### Enabling automatic Topic Alias using -If the client sets the option `autoUseTopicAlias:true` then MQTT.js uses existing topic alias automatically. - -example scenario: -``` -1. PUBLISH topic:'t1', ta:1 (register) -2. PUBLISH topic:'t1' -> topic:'', ta:1 (auto use existing map entry) -3. PUBLISH topic:'t2', ta:1 (register overwrite) -4. PUBLISH topic:'t2' -> topic:'', ta:1 (auto use existing map entry based on the receent map) -5. PUBLISH topic:'t1' (t1 is no longer mapped to ta:1) -``` - -User doesn't need to manage which topic is mapped to which topic alias. -If the user want to register topic alias, then publish topic with topic alias. -If the user want to use topic alias, then publish topic without topic alias. If there is a mapped topic alias then added it as a property and update the topic to empty string. - -### Enabling automatic Topic Alias assign - -If the client sets the option `autoAssignTopicAlias:true` then MQTT.js uses existing topic alias automatically. -If no topic alias exists, then assign a new vacant topic alias automatically. If topic alias is fully used, then LRU(Least Recently Used) topic-alias entry is overwritten. - -example scenario: -``` -The broker returns CONNACK (TopicAliasMaximum:3) -1. PUBLISH topic:'t1' -> 't1', ta:1 (auto assign t1:1 and register) -2. PUBLISH topic:'t1' -> '' , ta:1 (auto use existing map entry) -3. PUBLISH topic:'t2' -> 't2', ta:2 (auto assign t1:2 and register. 2 was vacant) -4. PUBLISH topic:'t3' -> 't3', ta:3 (auto assign t1:3 and register. 3 was vacant) -5. PUBLISH topic:'t4' -> 't4', ta:1 (LRU entry is overwritten) -``` - -Also user can manually register topic-alias pair using PUBLISH topic:'some', ta:X. It works well with automatic topic alias assign. - - -## API - - * mqtt.connect() - * mqtt.Client() - * mqtt.Client#publish() - * mqtt.Client#subscribe() - * mqtt.Client#unsubscribe() - * mqtt.Client#end() - * mqtt.Client#removeOutgoingMessage() - * mqtt.Client#reconnect() - * mqtt.Client#handleMessage() - * mqtt.Client#connected - * mqtt.Client#reconnecting - * mqtt.Client#getLastMessageId() - * mqtt.Store() - * mqtt.Store#put() - * mqtt.Store#del() - * mqtt.Store#createStream() - * mqtt.Store#close() - -------------------------------------------------------- - -### mqtt.connect([url], options) - -Connects to the broker specified by the given url and options and -returns a [Client](#client). - -The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', -'tls', 'ws', 'wss'. The URL can also be an object as returned by -[`URL.parse()`](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost), -in that case the two objects are merged, i.e. you can pass a single -object with both the URL and the connect options. - -You can also specify a `servers` options with content: `[{ host: -'localhost', port: 1883 }, ... ]`, in that case that array is iterated -at every connect. - -For all MQTT-related options, see the [Client](#client) -constructor. - -------------------------------------------------------- - -### mqtt.Client(streamBuilder, options) - -The `Client` class wraps a client connection to an -MQTT broker over an arbitrary transport method (TCP, TLS, -WebSocket, ecc). - -`Client` automatically handles the following: - -* Regular server pings -* QoS flow -* Automatic reconnections -* Start publishing before being connected - -The arguments are: - -* `streamBuilder` is a function that returns a subclass of the `Stream` class that supports -the `connect` event. Typically a `net.Socket`. -* `options` is the client connection options (see: the [connect packet](https://github.com/mcollina/mqtt-packet#connect)). Defaults: - * `wsOptions`: is the WebSocket connection options. Default is `{}`. - It's specific for WebSockets. For possible options have a look at: https://github.com/websockets/ws/blob/master/doc/ws.md. - * `keepalive`: `60` seconds, set to `0` to disable - * `reschedulePings`: reschedule ping messages after sending packets (default `true`) - * `clientId`: `'mqttjs_' + Math.random().toString(16).substr(2, 8)` - * `protocolId`: `'MQTT'` - * `protocolVersion`: `4` - * `clean`: `true`, set to false to receive QoS 1 and 2 messages while - offline - * `reconnectPeriod`: `1000` milliseconds, interval between two - reconnections. Disable auto reconnect by setting to `0`. - * `connectTimeout`: `30 * 1000` milliseconds, time to wait before a - CONNACK is received - * `username`: the username required by your broker, if any - * `password`: the password required by your broker, if any - * `incomingStore`: a [Store](#store) for the incoming packets - * `outgoingStore`: a [Store](#store) for the outgoing packets - * `queueQoSZero`: if connection is broken, queue outgoing QoS zero messages (default `true`) - * `customHandleAcks`: MQTT 5 feature of custom handling puback and pubrec packets. Its callback: - ```js - customHandleAcks: function(topic, message, packet, done) {/*some logic wit colling done(error, reasonCode)*/} - ``` - * `autoUseTopicAlias`: enabling automatic Topic Alias using functionality - * `autoAssignTopicAlias`: enabling automatic Topic Alias assign functionality - * `properties`: properties MQTT 5.0. - `object` that supports the following properties: - * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, - * `receiveMaximum`: representing the Receive Maximum value `number`, - * `maximumPacketSize`: representing the Maximum Packet Size the Client is willing to accept `number`, - * `topicAliasMaximum`: representing the Topic Alias Maximum value indicates the highest value that the Client will accept as a Topic Alias sent by the Server `number`, - * `requestResponseInformation`: The Client uses this value to request the Server to return Response Information in the CONNACK `boolean`, - * `requestProblemInformation`: The Client uses this value to indicate whether the Reason String or User Properties are sent in the case of failures `boolean`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - * `authenticationMethod`: the name of the authentication method used for extended authentication `string`, - * `authenticationData`: Binary Data containing authentication data `binary` - * `authPacket`: settings for auth packet `object` - * `will`: a message that will sent by the broker automatically when - the client disconnect badly. The format is: - * `topic`: the topic to publish - * `payload`: the message to publish - * `qos`: the QoS - * `retain`: the retain flag - * `properties`: properties of will by MQTT 5.0: - * `willDelayInterval`: representing the Will Delay Interval in seconds `number`, - * `payloadFormatIndicator`: Will Message is UTF-8 Encoded Character Data or not `boolean`, - * `messageExpiryInterval`: value is the lifetime of the Will Message in seconds and is sent as the Publication Expiry Interval when the Server publishes the Will Message `number`, - * `contentType`: describing the content of the Will Message `string`, - * `responseTopic`: String which is used as the Topic Name for a response message `string`, - * `correlationData`: The Correlation Data is used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` - * `transformWsUrl` : optional `(url, options, client) => url` function - For ws/wss protocols only. Can be used to implement signing - urls which upon reconnect can have become expired. - * `resubscribe` : if connection is broken and reconnects, - subscribed topics are automatically subscribed again (default `true`) - * `messageIdProvider`: custom messageId provider. when `new UniqueMessageIdProvider()` is set, then non conflict messageId is provided. - -In case mqtts (mqtt over tls) is required, the `options` object is -passed through to -[`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback). -If you are using a **self-signed certificate**, pass the `rejectUnauthorized: false` option. -Beware that you are exposing yourself to man in the middle attacks, so it is a configuration -that is not recommended for production environments. - -If you are connecting to a broker that supports only MQTT 3.1 (not -3.1.1 compliant), you should pass these additional options: - -```js -{ - protocolId: 'MQIsdp', - protocolVersion: 3 -} -``` - -This is confirmed on RabbitMQ 3.2.4, and on Mosquitto < 1.3. Mosquitto -version 1.3 and 1.4 works fine without those. - -#### Event `'connect'` - -`function (connack) {}` - -Emitted on successful (re)connection (i.e. connack rc=0). -* `connack` received connack packet. When `clean` connection option is `false` and server has a previous session -for `clientId` connection option, then `connack.sessionPresent` flag is `true`. When that is the case, -you may rely on stored session and prefer not to send subscribe commands for the client. - -#### Event `'reconnect'` - -`function () {}` - -Emitted when a reconnect starts. - -#### Event `'close'` - -`function () {}` - -Emitted after a disconnection. - -#### Event `'disconnect'` - -`function (packet) {}` - -Emitted after receiving disconnect packet from broker. MQTT 5.0 feature. - -#### Event `'offline'` - -`function () {}` - -Emitted when the client goes offline. - -#### Event `'error'` - -`function (error) {}` - -Emitted when the client cannot connect (i.e. connack rc != 0) or when a -parsing error occurs. - -The following TLS errors will be emitted as an `error` event: - -* `ECONNREFUSED` -* `ECONNRESET` -* `EADDRINUSE` -* `ENOTFOUND` - -#### Event `'end'` - -`function () {}` - -Emitted when mqtt.Client#end() is called. -If a callback was passed to `mqtt.Client#end()`, this event is emitted once the -callback returns. - -#### Event `'message'` - -`function (topic, message, packet) {}` - -Emitted when the client receives a publish packet -* `topic` topic of the received packet -* `message` payload of the received packet -* `packet` received packet, as defined in - [mqtt-packet](https://github.com/mcollina/mqtt-packet#publish) - -#### Event `'packetsend'` - -`function (packet) {}` - -Emitted when the client sends any packet. This includes .published() packets -as well as packets used by MQTT for managing subscriptions and connections -* `packet` received packet, as defined in - [mqtt-packet](https://github.com/mcollina/mqtt-packet) - -#### Event `'packetreceive'` - -`function (packet) {}` - -Emitted when the client receives any packet. This includes packets from -subscribed topics as well as packets used by MQTT for managing subscriptions -and connections -* `packet` received packet, as defined in - [mqtt-packet](https://github.com/mcollina/mqtt-packet) - -------------------------------------------------------- - -### mqtt.Client#publish(topic, message, [options], [callback]) - -Publish a message to a topic - -* `topic` is the topic to publish to, `String` -* `message` is the message to publish, `Buffer` or `String` -* `options` is the options to publish with, including: - * `qos` QoS level, `Number`, default `0` - * `retain` retain flag, `Boolean`, default `false` - * `dup` mark as duplicate flag, `Boolean`, default `false` - * `properties`: MQTT 5.0 properties `object` - * `payloadFormatIndicator`: Payload is UTF-8 Encoded Character Data or not `boolean`, - * `messageExpiryInterval`: the lifetime of the Application Message in seconds `number`, - * `topicAlias`: value that is used to identify the Topic instead of using the Topic Name `number`, - * `responseTopic`: String which is used as the Topic Name for a response message `string`, - * `correlationData`: used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - * `subscriptionIdentifier`: representing the identifier of the subscription `number`, - * `contentType`: String describing the content of the Application Message `string` - * `cbStorePut` - `function ()`, fired when message is put into `outgoingStore` if QoS is `1` or `2`. -* `callback` - `function (err)`, fired when the QoS handling completes, - or at the next tick if QoS 0. An error occurs if client is disconnecting. - -------------------------------------------------------- - -### mqtt.Client#subscribe(topic/topic array/topic object, [options], [callback]) - -Subscribe to a topic or topics - -* `topic` is a `String` topic to subscribe to or an `Array` of - topics to subscribe to. It can also be an object, it has as object - keys the topic name and as value the QoS, like `{'test1': {qos: 0}, 'test2': {qos: 1}}`. - MQTT `topic` wildcard characters are supported (`+` - for single level and `#` - for multi level) -* `options` is the options to subscribe with, including: - * `qos` QoS subscription level, default 0 - * `nl` No Local MQTT 5.0 flag (If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the ClientID of the publishing connection) - * `rap` Retain as Published MQTT 5.0 flag (If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If false, Application Messages forwarded using this subscription have the RETAIN flag set to 0.) - * `rh` Retain Handling MQTT 5.0 (This option specifies whether retained messages are sent when the subscription is established.) - * `properties`: `object` - * `subscriptionIdentifier`: representing the identifier of the subscription `number`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` -* `callback` - `function (err, granted)` - callback fired on suback where: - * `err` a subscription error or an error that occurs when client is disconnecting - * `granted` is an array of `{topic, qos}` where: - * `topic` is a subscribed to topic - * `qos` is the granted QoS level on it - -------------------------------------------------------- - -### mqtt.Client#unsubscribe(topic/topic array, [options], [callback]) - -Unsubscribe from a topic or topics - -* `topic` is a `String` topic or an array of topics to unsubscribe from -* `options`: options of unsubscribe. - * `properties`: `object` - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` -* `callback` - `function (err)`, fired on unsuback. An error occurs if client is disconnecting. - -------------------------------------------------------- - -### mqtt.Client#end([force], [options], [callback]) - -Close the client, accepts the following options: - -* `force`: passing it to true will close the client right away, without - waiting for the in-flight messages to be acked. This parameter is - optional. -* `options`: options of disconnect. - * `reasonCode`: Disconnect Reason Code `number` - * `properties`: `object` - * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, - * `reasonString`: representing the reason for the disconnect `string`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - * `serverReference`: String which can be used by the Client to identify another Server to use `string` -* `callback`: will be called when the client is closed. This parameter is - optional. - -------------------------------------------------------- - -### mqtt.Client#removeOutgoingMessage(mId) - -Remove a message from the outgoingStore. -The outgoing callback will be called with Error('Message removed') if the message is removed. - -After this function is called, the messageId is released and becomes reusable. - -* `mId`: The messageId of the message in the outgoingStore. - -------------------------------------------------------- - -### mqtt.Client#reconnect() - -Connect again using the same options as connect() - -------------------------------------------------------- - -### mqtt.Client#handleMessage(packet, callback) - -Handle messages with backpressure support, one at a time. -Override at will, but __always call `callback`__, or the client -will hang. - -------------------------------------------------------- - -### mqtt.Client#connected - -Boolean : set to `true` if the client is connected. `false` otherwise. - -------------------------------------------------------- - -### mqtt.Client#getLastMessageId() - -Number : get last message id. This is for sent messages only. - -------------------------------------------------------- - -### mqtt.Client#reconnecting - -Boolean : set to `true` if the client is trying to reconnect to the server. `false` otherwise. - -------------------------------------------------------- - -### mqtt.Store(options) - -In-memory implementation of the message store. - -* `options` is the store options: - * `clean`: `true`, clean inflight messages when close is called (default `true`) - -Other implementations of `mqtt.Store`: - -* [mqtt-level-store](http://npm.im/mqtt-level-store) which uses - [Level-browserify](http://npm.im/level-browserify) to store the inflight - data, making it usable both in Node and the Browser. -* [mqtt-nedb-store](https://github.com/behrad/mqtt-nedb-store) which - uses [nedb](https://www.npmjs.com/package/nedb) to store the inflight - data. -* [mqtt-localforage-store](http://npm.im/mqtt-localforage-store) which uses - [localForage](http://npm.im/localforage) to store the inflight - data, making it usable in the Browser without browserify. - -------------------------------------------------------- - -### mqtt.Store#put(packet, callback) - -Adds a packet to the store, a packet is -anything that has a `messageId` property. -The callback is called when the packet has been stored. - -------------------------------------------------------- - -### mqtt.Store#createStream() - -Creates a stream with all the packets in the store. - -------------------------------------------------------- - -### mqtt.Store#del(packet, cb) - -Removes a packet from the store, a packet is -anything that has a `messageId` property. -The callback is called when the packet has been removed. - -------------------------------------------------------- - -### mqtt.Store#close(cb) - -Closes the Store. - - -## Browser - - -### Via CDN - -The MQTT.js bundle is available through http://unpkg.com, specifically -at https://unpkg.com/mqtt/dist/mqtt.min.js. -See http://unpkg.com for the full documentation on version ranges. - - -## WeChat Mini Program -Support [WeChat Mini Program](https://mp.weixin.qq.com/). See [Doc](https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-socket.html). - - -## Example(js) - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('wxs://test.mosquitto.org') -``` - -## Example(ts) - -```ts -import { connect } from 'mqtt'; -const client = connect('wxs://test.mosquitto.org'); -``` - -## Ali Mini Program -Surport [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). See [Doc](https://docs.alipay.com/mini/developer/getting-started). - - -## Example(js) - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('alis://test.mosquitto.org') -``` - -## Example(ts) - -```ts -import { connect } from 'mqtt'; -const client = connect('alis://test.mosquitto.org'); -``` - - -### Browserify - -In order to use MQTT.js as a browserify module you can either require it in your browserify bundles or build it as a stand alone module. The exported module is AMD/CommonJs compatible and it will add an object in the global space. - -```bash -mkdir tmpdir -cd tmpdir -npm install mqtt -npm install browserify -npm install tinyify -cd node_modules/mqtt/ -npm install . -npx browserify mqtt.js -s mqtt >browserMqtt.js // use script tag -# show size for compressed browser transfer -gzip -### Webpack - -Just like browserify, export MQTT.js as library. The exported module would be `var mqtt = xxx` and it will add an object in the global space. You could also export module in other [formats (AMD/CommonJS/others)](http://webpack.github.io/docs/configuration.html#output-librarytarget) by setting **output.libraryTarget** in webpack configuration. - -```javascript -npm install -g webpack // install webpack - -cd node_modules/mqtt -npm install . // install dev dependencies -webpack mqtt.js ./browserMqtt.js --output-library mqtt -``` - -you can then use mqtt.js in the browser with the same api than node's one. - -```html - - - test Ws mqtt.js - - - - - - -``` - -### React -``` -npm install -g webpack // Install webpack globally -npm install mqtt // Install MQTT library -cd node_modules/mqtt -npm install . // Install dev deps at current dir -webpack mqtt.js --output-library mqtt // Build - -// now you can import the library with ES6 import, commonJS not tested -``` - - -```javascript -import React from 'react'; -import mqtt from 'mqtt'; - -export default () => { - const [connectionStatus, setConnectionStatus] = React.useState(false); - const [messages, setMessages] = React.useState([]); - - useEffect(() => { - const client = mqtt.connect(SOME_URL); - client.on('connect', () => setConnectionStatus(true)); - client.on('message', (topic, payload, packet) => { - setMessages(messages.concat(payload.toString())); - }); - }, []); - - return ( - <> - {messages.map((message) => ( -

{message}

- ) - - ) -} -``` - -Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/moscajs/aedes/blob/master/docs/Examples.md#mqtt-server-over-websocket-using-server-factory) to setup [Aedes](https://github.com/moscajs/aedes)). - - -## About QoS - -Here is how QoS works: - -* QoS 0 : received **at most once** : The packet is sent, and that's it. There is no validation about whether it has been received. -* QoS 1 : received **at least once** : The packet is sent and stored as long as the client has not received a confirmation from the server. MQTT ensures that it *will* be received, but there can be duplicates. -* QoS 2 : received **exactly once** : Same as QoS 1 but there is no duplicates. - -About data consumption, obviously, QoS 2 > QoS 1 > QoS 0, if that's a concern to you. - - -## Usage with TypeScript -This repo bundles TypeScript definition files for use in TypeScript projects and to support tools that can read `.d.ts` files. - -### Pre-requisites -Before you can begin using these TypeScript definitions with your project, you need to make sure your project meets a few of these requirements: - * TypeScript >= 2.1 - * Set tsconfig.json: `{"compilerOptions" : {"moduleResolution" : "node"}, ...}` - * Includes the TypeScript definitions for node. You can use npm to install this by typing the following into a terminal window: - `npm install --save-dev @types/node` - - -## Contributing - -MQTT.js is an **OPEN Open Source Project**. This means that: - -> Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. - -See the [CONTRIBUTING.md](https://github.com/mqttjs/MQTT.js/blob/master/CONTRIBUTING.md) file for more details. - -### Contributors - -MQTT.js is only possible due to the excellent work of the following contributors: - - - - - - -
Adam RuddGitHub/adamvrTwitter/@adam_vr
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
Maxime AgorGitHub/4rzaelTwitter/@4rzael
Siarhei BuntsevichGitHub/scarry1992
- - -## License - -MIT +![mqtt.js](https://raw.githubusercontent.com/mqttjs/MQTT.js/137ee0e3940c1f01049a30248c70f24dc6e6f829/MQTT.js.png) +======= + +![Github Test Status](https://github.com/mqttjs/MQTT.js/workflows/MQTT.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/mqttjs/MQTT.js/branch/master/graph/badge.svg)](https://codecov.io/gh/mqttjs/MQTT.js) + +MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written +in JavaScript for node.js and the browser. + +* [__MQTT.js vNext__](#vnext) +* [Upgrade notes](#notes) +* [Installation](#install) +* [Example](#example) +* [Command Line Tools](#cli) +* [API](#api) +* [Browser](#browser) +* [Weapp](#weapp) +* [About QoS](#qos) +* [TypeScript](#typescript) +* [Contributing](#contributing) +* [License](#license) + +MQTT.js is an OPEN Open Source Project, see the [Contributing](#contributing) section to find out what this means. + +[![JavaScript Style +Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) + + +## Discussion on the next major version of MQTT.js +There are discussions happening on the future of MQTT.js and the next major version (vNext). We invite the community to provide their thoughts and feedback in [this GitHub discussion](https://github.com/mqttjs/MQTT.js/discussions/1324) + + +## Important notes for existing users + +__v4.0.0__ (Released 04/2020) removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to +debug logging, along with some feature additions. + +As a __breaking change__, by default a error handler is built into the MQTT.js client, so if any +errors are emitted and the user has not created an event handler on the client for errors, the client will +not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been +added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. + +__v3.0.0__ adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. + +__Note:__ MQTT v5 support is experimental as it has not been implemented by brokers yet. + +__v2.0.0__ removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending +packets. It also removes all the deprecated functionality in v1.0.0, +mainly `mqtt.createConnection` and `mqtt.Server`. From v2.0.0, +subscriptions are restored upon reconnection if `clean: true`. +v1.x.x is now in *LTS*, and it will keep being supported as long as +there are v0.8, v0.10 and v0.12 users. + +As a __breaking change__, the `encoding` option in the old client is +removed, and now everything is UTF-8 with the exception of the +`password` in the CONNECT message and `payload` in the PUBLISH message, +which are `Buffer`. + +Another __breaking change__ is that MQTT.js now defaults to MQTT v3.1.1, +so to support old brokers, please read the [client options doc](#client). + +__v1.0.0__ improves the overall architecture of the project, which is now +split into three components: MQTT.js keeps the Client, +[mqtt-connection](http://npm.im/mqtt-connection) includes the barebone +Connection code for server-side usage, and [mqtt-packet](http://npm.im/mqtt-packet) +includes the protocol parser and generator. The new Client improves +performance by a 30% factor, embeds Websocket support +([MOWS](http://npm.im/mows) is now deprecated), and it has a better +support for QoS 1 and 2. The previous API is still supported but +deprecated, as such, it is not documented in this README. + + +## Installation + +```sh +npm install mqtt --save +``` + + +## Example + +For the sake of simplicity, let's put the subscriber and the publisher in the same file: + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('mqtt://test.mosquitto.org') + +client.on('connect', function () { + client.subscribe('presence', function (err) { + if (!err) { + client.publish('presence', 'Hello mqtt') + } + }) +}) + +client.on('message', function (topic, message) { + // message is Buffer + console.log(message.toString()) + client.end() +}) +``` + +output: +``` +Hello mqtt +``` + +If you want to run your own MQTT broker, you can use +[Mosquitto](http://mosquitto.org) or +[Aedes-cli](https://github.com/moscajs/aedes-cli), and launch it. + +You can also use a test instance: test.mosquitto.org. + +If you do not want to install a separate broker, you can try using the +[Aedes](https://github.com/moscajs/aedes). + +to use MQTT.js in the browser see the [browserify](#browserify) section + + +## Promise support + +If you want to use the new [async-await](https://blog.risingstack.com/async-await-node-js-7-nightly/) functionality in JavaScript, or just prefer using Promises instead of callbacks, [async-mqtt](https://github.com/mqttjs/async-mqtt) is a wrapper over MQTT.js which uses promises instead of callbacks when possible. + + +## Command Line Tools + +MQTT.js bundles a command to interact with a broker. +In order to have it available on your path, you should install MQTT.js +globally: + +```sh +npm install mqtt -g +``` + +Then, on one terminal + +``` +mqtt sub -t 'hello' -h 'test.mosquitto.org' -v +``` + +On another + +``` +mqtt pub -t 'hello' -h 'test.mosquitto.org' -m 'from MQTT.js' +``` + +See `mqtt help ` for the command help. + + +## Debug Logs + +MQTT.js uses the [debug](https://www.npmjs.com/package/debug#cmd) package for debugging purposes. To enable debug logs, add the following environment variable on runtime : +```ps +# (example using PowerShell, the VS Code default) +$env:DEBUG='mqttjs*' + +``` + + +## About Reconnection + +An important part of any websocket connection is what to do when a connection +drops off and the client needs to reconnect. MQTT has built-in reconnection +support that can be configured to behave in ways that suit the application. + +#### Refresh Authentication Options / Signed Urls with `transformWsUrl` (Websocket Only) + +When an mqtt connection drops and needs to reconnect, it's common to require +that any authentication associated with the connection is kept current with +the underlying auth mechanism. For instance some applications may pass an auth +token with connection options on the initial connection, while other cloud +services may require a url be signed with each connection. + +By the time the reconnect happens in the application lifecycle, the original +auth data may have expired. + +To address this we can use a hook called `transformWsUrl` to manipulate +either of the connection url or the client options at the time of a reconnect. + +Example (update clientId & username on each reconnect): +``` + const transformWsUrl = (url, options, client) => { + client.options.username = `token=${this.get_current_auth_token()}`; + client.options.clientId = `${this.get_updated_clientId()}`; + + return `${this.get_signed_cloud_url(url)`; + } + + const connection = await mqtt.connectAsync(, { + ..., + transformWsUrl: transformUrl, + }); + +``` +Now every time a new WebSocket connection is opened (hopefully not too often), +we will get a fresh signed url or fresh auth token data. + +Note: Currently this hook does _not_ support promises, meaning that in order to +use the latest auth token, you must have some outside mechanism running that +handles application-level authentication refreshing so that the websocket +connection can simply grab the latest valid token or signed url. + + +#### Enabling Reconnection with `reconnectPeriod` option + +To ensure that the mqtt client automatically tries to reconnect when the +connection is dropped, you must set the client option `reconnectPeriod` to a +value greater than 0. A value of 0 will disable reconnection and then terminate +the final connection when it drops. + +The default value is 1000 ms which means it will try to reconnect 1 second +after losing the connection. + + +## About Topic Alias Management + +### Enabling automatic Topic Alias using +If the client sets the option `autoUseTopicAlias:true` then MQTT.js uses existing topic alias automatically. + +example scenario: +``` +1. PUBLISH topic:'t1', ta:1 (register) +2. PUBLISH topic:'t1' -> topic:'', ta:1 (auto use existing map entry) +3. PUBLISH topic:'t2', ta:1 (register overwrite) +4. PUBLISH topic:'t2' -> topic:'', ta:1 (auto use existing map entry based on the receent map) +5. PUBLISH topic:'t1' (t1 is no longer mapped to ta:1) +``` + +User doesn't need to manage which topic is mapped to which topic alias. +If the user want to register topic alias, then publish topic with topic alias. +If the user want to use topic alias, then publish topic without topic alias. If there is a mapped topic alias then added it as a property and update the topic to empty string. + +### Enabling automatic Topic Alias assign + +If the client sets the option `autoAssignTopicAlias:true` then MQTT.js uses existing topic alias automatically. +If no topic alias exists, then assign a new vacant topic alias automatically. If topic alias is fully used, then LRU(Least Recently Used) topic-alias entry is overwritten. + +example scenario: +``` +The broker returns CONNACK (TopicAliasMaximum:3) +1. PUBLISH topic:'t1' -> 't1', ta:1 (auto assign t1:1 and register) +2. PUBLISH topic:'t1' -> '' , ta:1 (auto use existing map entry) +3. PUBLISH topic:'t2' -> 't2', ta:2 (auto assign t1:2 and register. 2 was vacant) +4. PUBLISH topic:'t3' -> 't3', ta:3 (auto assign t1:3 and register. 3 was vacant) +5. PUBLISH topic:'t4' -> 't4', ta:1 (LRU entry is overwritten) +``` + +Also user can manually register topic-alias pair using PUBLISH topic:'some', ta:X. It works well with automatic topic alias assign. + + +## API + + * mqtt.connect() + * mqtt.Client() + * mqtt.Client#publish() + * mqtt.Client#subscribe() + * mqtt.Client#unsubscribe() + * mqtt.Client#end() + * mqtt.Client#removeOutgoingMessage() + * mqtt.Client#reconnect() + * mqtt.Client#handleMessage() + * mqtt.Client#connected + * mqtt.Client#reconnecting + * mqtt.Client#getLastMessageId() + * mqtt.Store() + * mqtt.Store#put() + * mqtt.Store#del() + * mqtt.Store#createStream() + * mqtt.Store#close() + +------------------------------------------------------- + +### mqtt.connect([url], options) + +Connects to the broker specified by the given url and options and +returns a [Client](#client). + +The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', +'tls', 'ws', 'wss'. The URL can also be an object as returned by +[`URL.parse()`](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost), +in that case the two objects are merged, i.e. you can pass a single +object with both the URL and the connect options. + +You can also specify a `servers` options with content: `[{ host: +'localhost', port: 1883 }, ... ]`, in that case that array is iterated +at every connect. + +For all MQTT-related options, see the [Client](#client) +constructor. + +------------------------------------------------------- + +### mqtt.Client(streamBuilder, options) + +The `Client` class wraps a client connection to an +MQTT broker over an arbitrary transport method (TCP, TLS, +WebSocket, ecc). + +`Client` automatically handles the following: + +* Regular server pings +* QoS flow +* Automatic reconnections +* Start publishing before being connected + +The arguments are: + +* `streamBuilder` is a function that returns a subclass of the `Stream` class that supports +the `connect` event. Typically a `net.Socket`. +* `options` is the client connection options (see: the [connect packet](https://github.com/mcollina/mqtt-packet#connect)). Defaults: + * `wsOptions`: is the WebSocket connection options. Default is `{}`. + It's specific for WebSockets. For possible options have a look at: https://github.com/websockets/ws/blob/master/doc/ws.md. + * `keepalive`: `60` seconds, set to `0` to disable + * `reschedulePings`: reschedule ping messages after sending packets (default `true`) + * `clientId`: `'mqttjs_' + Math.random().toString(16).substr(2, 8)` + * `protocolId`: `'MQTT'` + * `protocolVersion`: `4` + * `clean`: `true`, set to false to receive QoS 1 and 2 messages while + offline + * `reconnectPeriod`: `1000` milliseconds, interval between two + reconnections. Disable auto reconnect by setting to `0`. + * `connectTimeout`: `30 * 1000` milliseconds, time to wait before a + CONNACK is received + * `username`: the username required by your broker, if any + * `password`: the password required by your broker, if any + * `incomingStore`: a [Store](#store) for the incoming packets + * `outgoingStore`: a [Store](#store) for the outgoing packets + * `queueQoSZero`: if connection is broken, queue outgoing QoS zero messages (default `true`) + * `customHandleAcks`: MQTT 5 feature of custom handling puback and pubrec packets. Its callback: + ```js + customHandleAcks: function(topic, message, packet, done) {/*some logic wit colling done(error, reasonCode)*/} + ``` + * `autoUseTopicAlias`: enabling automatic Topic Alias using functionality + * `autoAssignTopicAlias`: enabling automatic Topic Alias assign functionality + * `properties`: properties MQTT 5.0. + `object` that supports the following properties: + * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, + * `receiveMaximum`: representing the Receive Maximum value `number`, + * `maximumPacketSize`: representing the Maximum Packet Size the Client is willing to accept `number`, + * `topicAliasMaximum`: representing the Topic Alias Maximum value indicates the highest value that the Client will accept as a Topic Alias sent by the Server `number`, + * `requestResponseInformation`: The Client uses this value to request the Server to return Response Information in the CONNACK `boolean`, + * `requestProblemInformation`: The Client uses this value to indicate whether the Reason String or User Properties are sent in the case of failures `boolean`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, + * `authenticationMethod`: the name of the authentication method used for extended authentication `string`, + * `authenticationData`: Binary Data containing authentication data `binary` + * `authPacket`: settings for auth packet `object` + * `will`: a message that will sent by the broker automatically when + the client disconnect badly. The format is: + * `topic`: the topic to publish + * `payload`: the message to publish + * `qos`: the QoS + * `retain`: the retain flag + * `properties`: properties of will by MQTT 5.0: + * `willDelayInterval`: representing the Will Delay Interval in seconds `number`, + * `payloadFormatIndicator`: Will Message is UTF-8 Encoded Character Data or not `boolean`, + * `messageExpiryInterval`: value is the lifetime of the Will Message in seconds and is sent as the Publication Expiry Interval when the Server publishes the Will Message `number`, + * `contentType`: describing the content of the Will Message `string`, + * `responseTopic`: String which is used as the Topic Name for a response message `string`, + * `correlationData`: The Correlation Data is used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` + * `transformWsUrl` : optional `(url, options, client) => url` function + For ws/wss protocols only. Can be used to implement signing + urls which upon reconnect can have become expired. + * `resubscribe` : if connection is broken and reconnects, + subscribed topics are automatically subscribed again (default `true`) + * `messageIdProvider`: custom messageId provider. when `new UniqueMessageIdProvider()` is set, then non conflict messageId is provided. + +In case mqtts (mqtt over tls) is required, the `options` object is +passed through to +[`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback). +If you are using a **self-signed certificate**, pass the `rejectUnauthorized: false` option. +Beware that you are exposing yourself to man in the middle attacks, so it is a configuration +that is not recommended for production environments. + +If you are connecting to a broker that supports only MQTT 3.1 (not +3.1.1 compliant), you should pass these additional options: + +```js +{ + protocolId: 'MQIsdp', + protocolVersion: 3 +} +``` + +This is confirmed on RabbitMQ 3.2.4, and on Mosquitto < 1.3. Mosquitto +version 1.3 and 1.4 works fine without those. + +#### Event `'connect'` + +`function (connack) {}` + +Emitted on successful (re)connection (i.e. connack rc=0). +* `connack` received connack packet. When `clean` connection option is `false` and server has a previous session +for `clientId` connection option, then `connack.sessionPresent` flag is `true`. When that is the case, +you may rely on stored session and prefer not to send subscribe commands for the client. + +#### Event `'reconnect'` + +`function () {}` + +Emitted when a reconnect starts. + +#### Event `'close'` + +`function () {}` + +Emitted after a disconnection. + +#### Event `'disconnect'` + +`function (packet) {}` + +Emitted after receiving disconnect packet from broker. MQTT 5.0 feature. + +#### Event `'offline'` + +`function () {}` + +Emitted when the client goes offline. + +#### Event `'error'` + +`function (error) {}` + +Emitted when the client cannot connect (i.e. connack rc != 0) or when a +parsing error occurs. + +The following TLS errors will be emitted as an `error` event: + +* `ECONNREFUSED` +* `ECONNRESET` +* `EADDRINUSE` +* `ENOTFOUND` + +#### Event `'end'` + +`function () {}` + +Emitted when mqtt.Client#end() is called. +If a callback was passed to `mqtt.Client#end()`, this event is emitted once the +callback returns. + +#### Event `'message'` + +`function (topic, message, packet) {}` + +Emitted when the client receives a publish packet +* `topic` topic of the received packet +* `message` payload of the received packet +* `packet` received packet, as defined in + [mqtt-packet](https://github.com/mcollina/mqtt-packet#publish) + +#### Event `'packetsend'` + +`function (packet) {}` + +Emitted when the client sends any packet. This includes .published() packets +as well as packets used by MQTT for managing subscriptions and connections +* `packet` received packet, as defined in + [mqtt-packet](https://github.com/mcollina/mqtt-packet) + +#### Event `'packetreceive'` + +`function (packet) {}` + +Emitted when the client receives any packet. This includes packets from +subscribed topics as well as packets used by MQTT for managing subscriptions +and connections +* `packet` received packet, as defined in + [mqtt-packet](https://github.com/mcollina/mqtt-packet) + +------------------------------------------------------- + +### mqtt.Client#publish(topic, message, [options], [callback]) + +Publish a message to a topic + +* `topic` is the topic to publish to, `String` +* `message` is the message to publish, `Buffer` or `String` +* `options` is the options to publish with, including: + * `qos` QoS level, `Number`, default `0` + * `retain` retain flag, `Boolean`, default `false` + * `dup` mark as duplicate flag, `Boolean`, default `false` + * `properties`: MQTT 5.0 properties `object` + * `payloadFormatIndicator`: Payload is UTF-8 Encoded Character Data or not `boolean`, + * `messageExpiryInterval`: the lifetime of the Application Message in seconds `number`, + * `topicAlias`: value that is used to identify the Topic instead of using the Topic Name `number`, + * `responseTopic`: String which is used as the Topic Name for a response message `string`, + * `correlationData`: used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, + * `subscriptionIdentifier`: representing the identifier of the subscription `number`, + * `contentType`: String describing the content of the Application Message `string` + * `cbStorePut` - `function ()`, fired when message is put into `outgoingStore` if QoS is `1` or `2`. +* `callback` - `function (err)`, fired when the QoS handling completes, + or at the next tick if QoS 0. An error occurs if client is disconnecting. + +------------------------------------------------------- + +### mqtt.Client#subscribe(topic/topic array/topic object, [options], [callback]) + +Subscribe to a topic or topics + +* `topic` is a `String` topic to subscribe to or an `Array` of + topics to subscribe to. It can also be an object, it has as object + keys the topic name and as value the QoS, like `{'test1': {qos: 0}, 'test2': {qos: 1}}`. + MQTT `topic` wildcard characters are supported (`+` - for single level and `#` - for multi level) +* `options` is the options to subscribe with, including: + * `qos` QoS subscription level, default 0 + * `nl` No Local MQTT 5.0 flag (If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the ClientID of the publishing connection) + * `rap` Retain as Published MQTT 5.0 flag (If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If false, Application Messages forwarded using this subscription have the RETAIN flag set to 0.) + * `rh` Retain Handling MQTT 5.0 (This option specifies whether retained messages are sent when the subscription is established.) + * `properties`: `object` + * `subscriptionIdentifier`: representing the identifier of the subscription `number`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` +* `callback` - `function (err, granted)` + callback fired on suback where: + * `err` a subscription error or an error that occurs when client is disconnecting + * `granted` is an array of `{topic, qos}` where: + * `topic` is a subscribed to topic + * `qos` is the granted QoS level on it + +------------------------------------------------------- + +### mqtt.Client#unsubscribe(topic/topic array, [options], [callback]) + +Unsubscribe from a topic or topics + +* `topic` is a `String` topic or an array of topics to unsubscribe from +* `options`: options of unsubscribe. + * `properties`: `object` + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` +* `callback` - `function (err)`, fired on unsuback. An error occurs if client is disconnecting. + +------------------------------------------------------- + +### mqtt.Client#end([force], [options], [callback]) + +Close the client, accepts the following options: + +* `force`: passing it to true will close the client right away, without + waiting for the in-flight messages to be acked. This parameter is + optional. +* `options`: options of disconnect. + * `reasonCode`: Disconnect Reason Code `number` + * `properties`: `object` + * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, + * `reasonString`: representing the reason for the disconnect `string`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, + * `serverReference`: String which can be used by the Client to identify another Server to use `string` +* `callback`: will be called when the client is closed. This parameter is + optional. + +------------------------------------------------------- + +### mqtt.Client#removeOutgoingMessage(mId) + +Remove a message from the outgoingStore. +The outgoing callback will be called with Error('Message removed') if the message is removed. + +After this function is called, the messageId is released and becomes reusable. + +* `mId`: The messageId of the message in the outgoingStore. + +------------------------------------------------------- + +### mqtt.Client#reconnect() + +Connect again using the same options as connect() + +------------------------------------------------------- + +### mqtt.Client#handleMessage(packet, callback) + +Handle messages with backpressure support, one at a time. +Override at will, but __always call `callback`__, or the client +will hang. + +------------------------------------------------------- + +### mqtt.Client#connected + +Boolean : set to `true` if the client is connected. `false` otherwise. + +------------------------------------------------------- + +### mqtt.Client#getLastMessageId() + +Number : get last message id. This is for sent messages only. + +------------------------------------------------------- + +### mqtt.Client#reconnecting + +Boolean : set to `true` if the client is trying to reconnect to the server. `false` otherwise. + +------------------------------------------------------- + +### mqtt.Store(options) + +In-memory implementation of the message store. + +* `options` is the store options: + * `clean`: `true`, clean inflight messages when close is called (default `true`) + +Other implementations of `mqtt.Store`: + +* [mqtt-level-store](http://npm.im/mqtt-level-store) which uses + [Level-browserify](http://npm.im/level-browserify) to store the inflight + data, making it usable both in Node and the Browser. +* [mqtt-nedb-store](https://github.com/behrad/mqtt-nedb-store) which + uses [nedb](https://www.npmjs.com/package/nedb) to store the inflight + data. +* [mqtt-localforage-store](http://npm.im/mqtt-localforage-store) which uses + [localForage](http://npm.im/localforage) to store the inflight + data, making it usable in the Browser without browserify. + +------------------------------------------------------- + +### mqtt.Store#put(packet, callback) + +Adds a packet to the store, a packet is +anything that has a `messageId` property. +The callback is called when the packet has been stored. + +------------------------------------------------------- + +### mqtt.Store#createStream() + +Creates a stream with all the packets in the store. + +------------------------------------------------------- + +### mqtt.Store#del(packet, cb) + +Removes a packet from the store, a packet is +anything that has a `messageId` property. +The callback is called when the packet has been removed. + +------------------------------------------------------- + +### mqtt.Store#close(cb) + +Closes the Store. + + +## Browser + + +### Via CDN + +The MQTT.js bundle is available through http://unpkg.com, specifically +at https://unpkg.com/mqtt/dist/mqtt.min.js. +See http://unpkg.com for the full documentation on version ranges. + + +## WeChat Mini Program +Support [WeChat Mini Program](https://mp.weixin.qq.com/). See [Doc](https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-socket.html). + + +## Example(js) + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('wxs://test.mosquitto.org') +``` + +## Example(ts) + +```ts +import { connect } from 'mqtt'; +const client = connect('wxs://test.mosquitto.org'); +``` + +## Ali Mini Program +Surport [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). See [Doc](https://docs.alipay.com/mini/developer/getting-started). + + +## Example(js) + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('alis://test.mosquitto.org') +``` + +## Example(ts) + +```ts +import { connect } from 'mqtt'; +const client = connect('alis://test.mosquitto.org'); +``` + + +### Browserify + +In order to use MQTT.js as a browserify module you can either require it in your browserify bundles or build it as a stand alone module. The exported module is AMD/CommonJs compatible and it will add an object in the global space. + +```bash +mkdir tmpdir +cd tmpdir +npm install mqtt +npm install browserify +npm install tinyify +cd node_modules/mqtt/ +npm install . +npx browserify mqtt.js -s mqtt >browserMqtt.js // use script tag +# show size for compressed browser transfer +gzip +### Webpack + +Just like browserify, export MQTT.js as library. The exported module would be `var mqtt = xxx` and it will add an object in the global space. You could also export module in other [formats (AMD/CommonJS/others)](http://webpack.github.io/docs/configuration.html#output-librarytarget) by setting **output.libraryTarget** in webpack configuration. + +```javascript +npm install -g webpack // install webpack + +cd node_modules/mqtt +npm install . // install dev dependencies +webpack mqtt.js ./browserMqtt.js --output-library mqtt +``` + +you can then use mqtt.js in the browser with the same api than node's one. + +```html + + + test Ws mqtt.js + + + + + + +``` + +### React +``` +npm install -g webpack // Install webpack globally +npm install mqtt // Install MQTT library +cd node_modules/mqtt +npm install . // Install dev deps at current dir +webpack mqtt.js --output-library mqtt // Build + +// now you can import the library with ES6 import, commonJS not tested +``` + + +```javascript +import React from 'react'; +import mqtt from 'mqtt'; + +export default () => { + const [connectionStatus, setConnectionStatus] = React.useState(false); + const [messages, setMessages] = React.useState([]); + + useEffect(() => { + const client = mqtt.connect(SOME_URL); + client.on('connect', () => setConnectionStatus(true)); + client.on('message', (topic, payload, packet) => { + setMessages(messages.concat(payload.toString())); + }); + }, []); + + return ( + <> + {messages.map((message) => ( +

{message}

+ ) + + ) +} +``` + +Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/moscajs/aedes/blob/master/docs/Examples.md#mqtt-server-over-websocket-using-server-factory) to setup [Aedes](https://github.com/moscajs/aedes)). + + +## About QoS + +Here is how QoS works: + +* QoS 0 : received **at most once** : The packet is sent, and that's it. There is no validation about whether it has been received. +* QoS 1 : received **at least once** : The packet is sent and stored as long as the client has not received a confirmation from the server. MQTT ensures that it *will* be received, but there can be duplicates. +* QoS 2 : received **exactly once** : Same as QoS 1 but there is no duplicates. + +About data consumption, obviously, QoS 2 > QoS 1 > QoS 0, if that's a concern to you. + + +## Usage with TypeScript +This repo bundles TypeScript definition files for use in TypeScript projects and to support tools that can read `.d.ts` files. + +### Pre-requisites +Before you can begin using these TypeScript definitions with your project, you need to make sure your project meets a few of these requirements: + * TypeScript >= 2.1 + * Set tsconfig.json: `{"compilerOptions" : {"moduleResolution" : "node"}, ...}` + * Includes the TypeScript definitions for node. You can use npm to install this by typing the following into a terminal window: + `npm install --save-dev @types/node` + + +## Contributing + +MQTT.js is an **OPEN Open Source Project**. This means that: + +> Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. + +See the [CONTRIBUTING.md](https://github.com/mqttjs/MQTT.js/blob/master/CONTRIBUTING.md) file for more details. + +### Contributors + +MQTT.js is only possible due to the excellent work of the following contributors: + + + + + + +
Adam RuddGitHub/adamvrTwitter/@adam_vr
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
Maxime AgorGitHub/4rzaelTwitter/@4rzael
Siarhei BuntsevichGitHub/scarry1992
+ + +## License + +MIT diff --git a/benchmarks/bombing.js b/benchmarks/bombing.js index adef01445..a08fd206b 100755 --- a/benchmarks/bombing.js +++ b/benchmarks/bombing.js @@ -1,26 +1,26 @@ -#! /usr/bin/env node - -var mqtt = require('../') -var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 }) - -var sent = 0 -var interval = 5000 - -function count () { - console.log('sent/s', sent / interval * 1000) - sent = 0 -} - -setInterval(count, interval) - -function publish () { - sent++ - client.publish('test', 'payload', publish) -} - -client.on('connect', publish) - -client.on('error', function () { - console.log('reconnect!') - client.stream.end() -}) +#! /usr/bin/env node + +var mqtt = require('../') +var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 }) + +var sent = 0 +var interval = 5000 + +function count () { + console.log('sent/s', sent / interval * 1000) + sent = 0 +} + +setInterval(count, interval) + +function publish () { + sent++ + client.publish('test', 'payload', publish) +} + +client.on('connect', publish) + +client.on('error', function () { + console.log('reconnect!') + client.stream.end() +}) diff --git a/benchmarks/throughputCounter.js b/benchmarks/throughputCounter.js index 0b778ef2c..90c15fc9d 100755 --- a/benchmarks/throughputCounter.js +++ b/benchmarks/throughputCounter.js @@ -1,22 +1,22 @@ -#! /usr/bin/env node - -var mqtt = require('../') - -var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 }) -var counter = 0 -var interval = 5000 - -function count () { - console.log('received/s', counter / interval * 1000) - counter = 0 -} - -setInterval(count, interval) - -client.on('connect', function () { - count() - this.subscribe('test') - this.on('message', function () { - counter++ - }) -}) +#! /usr/bin/env node + +var mqtt = require('../') + +var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 }) +var counter = 0 +var interval = 5000 + +function count () { + console.log('received/s', counter / interval * 1000) + counter = 0 +} + +setInterval(count, interval) + +client.on('connect', function () { + count() + this.subscribe('test') + this.on('message', function () { + counter++ + }) +}) diff --git a/bin/mqtt.js b/bin/mqtt.js index 022b33a64..4a277306e 100755 --- a/bin/mqtt.js +++ b/bin/mqtt.js @@ -1,27 +1,27 @@ -#!/usr/bin/env node -'use strict' - -/* - * Copyright (c) 2015-2015 MQTT.js contributors. - * Copyright (c) 2011-2014 Adam Rudd. - * - * See LICENSE for more information - */ -var path = require('path') -var commist = require('commist')() -var helpMe = require('help-me')({ - dir: path.join(path.dirname(require.main.filename), '/../doc'), - ext: '.txt' -}) - -commist.register('publish', require('./pub')) -commist.register('subscribe', require('./sub')) -commist.register('version', function () { - console.log('MQTT.js version:', require('./../package.json').version) -}) -commist.register('help', helpMe.toStdout) - -if (commist.parse(process.argv.slice(2)) !== null) { - console.log('No such command:', process.argv[2], '\n') - helpMe.toStdout() -} +#!/usr/bin/env node +'use strict' + +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ +var path = require('path') +var commist = require('commist')() +var helpMe = require('help-me')({ + dir: path.join(path.dirname(require.main.filename), '/../doc'), + ext: '.txt' +}) + +commist.register('publish', require('./pub')) +commist.register('subscribe', require('./sub')) +commist.register('version', function () { + console.log('MQTT.js version:', require('./../package.json').version) +}) +commist.register('help', helpMe.toStdout) + +if (commist.parse(process.argv.slice(2)) !== null) { + console.log('No such command:', process.argv[2], '\n') + helpMe.toStdout() +} diff --git a/bin/pub.js b/bin/pub.js index 94b066b40..aefa4b7b6 100755 --- a/bin/pub.js +++ b/bin/pub.js @@ -1,146 +1,146 @@ -#!/usr/bin/env node - -'use strict' - -var mqtt = require('../') -var pump = require('pump') -var path = require('path') -var fs = require('fs') -var concat = require('concat-stream') -var Writable = require('readable-stream').Writable -var helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') -}) -var minimist = require('minimist') -var split2 = require('split2') - -function send (args) { - var client = mqtt.connect(args) - client.on('connect', function () { - client.publish(args.topic, args.message, args, function (err) { - if (err) { - console.warn(err) - } - client.end() - }) - }) - client.on('error', function (err) { - console.warn(err) - client.end() - }) -} - -function multisend (args) { - var client = mqtt.connect(args) - var sender = new Writable({ - objectMode: true - }) - sender._write = function (line, enc, cb) { - client.publish(args.topic, line.trim(), args, cb) - } - - client.on('connect', function () { - pump(process.stdin, split2(), sender, function (err) { - client.end() - if (err) { - throw err - } - }) - }) -} - -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'message', 'clientId', 'i', 'id'], - boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - message: 'm', - qos: 'q', - clientId: ['i', 'id'], - retain: 'r', - username: 'u', - password: 'P', - stdin: 's', - multiline: 'M', - protocol: ['C', 'l'], - help: 'H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - topic: '', - message: '' - } - }) - - if (args.help) { - return helpMe.toStdout('publish') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - args.topic = (args.topic || args._.shift()).toString() - args.message = (args.message || args._.shift()).toString() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('publish') - } - - if (args.stdin) { - if (args.multiline) { - multisend(args) - } else { - process.stdin.pipe(concat(function (data) { - args.message = data - send(args) - })) - } - } else { - send(args) - } -} - -module.exports = start - -if (require.main === module) { - start(process.argv.slice(2)) -} +#!/usr/bin/env node + +'use strict' + +var mqtt = require('../') +var pump = require('pump') +var path = require('path') +var fs = require('fs') +var concat = require('concat-stream') +var Writable = require('readable-stream').Writable +var helpMe = require('help-me')({ + dir: path.join(__dirname, '..', 'doc') +}) +var minimist = require('minimist') +var split2 = require('split2') + +function send (args) { + var client = mqtt.connect(args) + client.on('connect', function () { + client.publish(args.topic, args.message, args, function (err) { + if (err) { + console.warn(err) + } + client.end() + }) + }) + client.on('error', function (err) { + console.warn(err) + client.end() + }) +} + +function multisend (args) { + var client = mqtt.connect(args) + var sender = new Writable({ + objectMode: true + }) + sender._write = function (line, enc, cb) { + client.publish(args.topic, line.trim(), args, cb) + } + + client.on('connect', function () { + pump(process.stdin, split2(), sender, function (err) { + client.end() + if (err) { + throw err + } + }) + }) +} + +function start (args) { + args = minimist(args, { + string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'message', 'clientId', 'i', 'id'], + boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + message: 'm', + qos: 'q', + clientId: ['i', 'id'], + retain: 'r', + username: 'u', + password: 'P', + stdin: 's', + multiline: 'M', + protocol: ['C', 'l'], + help: 'H', + ca: 'cafile' + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + topic: '', + message: '' + } + }) + + if (args.help) { + return helpMe.toStdout('publish') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + args.topic = (args.topic || args._.shift()).toString() + args.message = (args.message || args._.shift()).toString() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('publish') + } + + if (args.stdin) { + if (args.multiline) { + multisend(args) + } else { + process.stdin.pipe(concat(function (data) { + args.message = data + send(args) + })) + } + } else { + send(args) + } +} + +module.exports = start + +if (require.main === module) { + start(process.argv.slice(2)) +} diff --git a/bin/sub.js b/bin/sub.js index 14bc57458..4c94ceb54 100755 --- a/bin/sub.js +++ b/bin/sub.js @@ -1,123 +1,123 @@ -#!/usr/bin/env node - -var mqtt = require('../') -var path = require('path') -var fs = require('fs') -var helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') -}) -var minimist = require('minimist') - -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'clientId', 'i', 'id'], - boolean: ['stdin', 'help', 'clean', 'insecure'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - qos: 'q', - clean: 'c', - keepalive: 'k', - clientId: ['i', 'id'], - username: 'u', - password: 'P', - protocol: ['C', 'l'], - verbose: 'v', - help: '-H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - clean: true, - keepAlive: 30 // 30 sec - } - }) - - if (args.help) { - return helpMe.toStdout('subscribe') - } - - args.topic = args.topic || args._.shift() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('subscribe') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - args.keepAlive = args['keep-alive'] - - var client = mqtt.connect(args) - - client.on('connect', function () { - client.subscribe(args.topic, { qos: args.qos }, function (err, result) { - if (err) { - console.error(err) - process.exit(1) - } - - result.forEach(function (sub) { - if (sub.qos > 2) { - console.error('subscription negated to', sub.topic, 'with code', sub.qos) - process.exit(1) - } - }) - }) - }) - - client.on('message', function (topic, payload) { - if (args.verbose) { - console.log(topic, payload.toString()) - } else { - console.log(payload.toString()) - } - }) - - client.on('error', function (err) { - console.warn(err) - client.end() - }) -} - -module.exports = start - -if (require.main === module) { - start(process.argv.slice(2)) -} +#!/usr/bin/env node + +var mqtt = require('../') +var path = require('path') +var fs = require('fs') +var helpMe = require('help-me')({ + dir: path.join(__dirname, '..', 'doc') +}) +var minimist = require('minimist') + +function start (args) { + args = minimist(args, { + string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'clientId', 'i', 'id'], + boolean: ['stdin', 'help', 'clean', 'insecure'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + qos: 'q', + clean: 'c', + keepalive: 'k', + clientId: ['i', 'id'], + username: 'u', + password: 'P', + protocol: ['C', 'l'], + verbose: 'v', + help: '-H', + ca: 'cafile' + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + clean: true, + keepAlive: 30 // 30 sec + } + }) + + if (args.help) { + return helpMe.toStdout('subscribe') + } + + args.topic = args.topic || args._.shift() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('subscribe') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + args.keepAlive = args['keep-alive'] + + var client = mqtt.connect(args) + + client.on('connect', function () { + client.subscribe(args.topic, { qos: args.qos }, function (err, result) { + if (err) { + console.error(err) + process.exit(1) + } + + result.forEach(function (sub) { + if (sub.qos > 2) { + console.error('subscription negated to', sub.topic, 'with code', sub.qos) + process.exit(1) + } + }) + }) + }) + + client.on('message', function (topic, payload) { + if (args.verbose) { + console.log(topic, payload.toString()) + } else { + console.log(payload.toString()) + } + }) + + client.on('error', function (err) { + console.warn(err) + client.end() + }) +} + +module.exports = start + +if (require.main === module) { + start(process.argv.slice(2)) +} diff --git a/example.js b/example.js index ba14bf949..91b0bfde6 100644 --- a/example.js +++ b/example.js @@ -1,11 +1,11 @@ -var mqtt = require('./') -var client = mqtt.connect('mqtt://test.mosquitto.org') - -client.subscribe('presence') -client.publish('presence', 'Hello mqtt') - -client.on('message', function (topic, message) { - console.log(message.toString()) -}) - -client.end() +var mqtt = require('./') +var client = mqtt.connect('mqtt://test.mosquitto.org') + +client.subscribe('presence') +client.publish('presence', 'Hello mqtt') + +client.on('message', function (topic, message) { + console.log(message.toString()) +}) + +client.end() diff --git a/examples/client/secure-client.js b/examples/client/secure-client.js index bf9b6f092..fefe65d73 100644 --- a/examples/client/secure-client.js +++ b/examples/client/secure-client.js @@ -1,24 +1,24 @@ -'use strict' - -var mqtt = require('../..') -var path = require('path') -var fs = require('fs') -var KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-key.pem')) -var CERT = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-cert.pem')) - -var PORT = 8443 - -var options = { - port: PORT, - key: KEY, - cert: CERT, - rejectUnauthorized: false -} - -var client = mqtt.connect(options) - -client.subscribe('messages') -client.publish('messages', 'Current time is: ' + new Date()) -client.on('message', function (topic, message) { - console.log(message) -}) +'use strict' + +var mqtt = require('../..') +var path = require('path') +var fs = require('fs') +var KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-key.pem')) +var CERT = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-cert.pem')) + +var PORT = 8443 + +var options = { + port: PORT, + key: KEY, + cert: CERT, + rejectUnauthorized: false +} + +var client = mqtt.connect(options) + +client.subscribe('messages') +client.publish('messages', 'Current time is: ' + new Date()) +client.on('message', function (topic, message) { + console.log(message) +}) diff --git a/examples/client/simple-both.js b/examples/client/simple-both.js index 8e9268b5f..58a048465 100644 --- a/examples/client/simple-both.js +++ b/examples/client/simple-both.js @@ -1,13 +1,13 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -// or var client = mqtt.connect({ port: 1883, host: '192.168.1.100', keepalive: 10000}); - -client.subscribe('presence') -client.publish('presence', 'bin hier') -client.on('message', function (topic, message) { - console.log(message) -}) -client.end() +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +// or var client = mqtt.connect({ port: 1883, host: '192.168.1.100', keepalive: 10000}); + +client.subscribe('presence') +client.publish('presence', 'bin hier') +client.on('message', function (topic, message) { + console.log(message) +}) +client.end() diff --git a/examples/client/simple-publish.js b/examples/client/simple-publish.js index a8b0f89b6..4f8274c4a 100644 --- a/examples/client/simple-publish.js +++ b/examples/client/simple-publish.js @@ -1,7 +1,7 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -client.publish('presence', 'hello!') -client.end() +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +client.publish('presence', 'hello!') +client.end() diff --git a/examples/client/simple-subscribe.js b/examples/client/simple-subscribe.js index 7989b9c22..f2c6d2c4a 100644 --- a/examples/client/simple-subscribe.js +++ b/examples/client/simple-subscribe.js @@ -1,9 +1,9 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -client.subscribe('presence') -client.on('message', function (topic, message) { - console.log(message) -}) +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +client.subscribe('presence') +client.on('message', function (topic, message) { + console.log(message) +}) diff --git a/examples/tls client/mqttclient.js b/examples/tls client/mqttclient.js index 392fcb39c..d9bb4693a 100644 --- a/examples/tls client/mqttclient.js +++ b/examples/tls client/mqttclient.js @@ -1,48 +1,48 @@ -'use strict' - -/** ************************** IMPORTANT NOTE *********************************** - - The certificate used on this example has been generated for a host named stark. - So as host we SHOULD use stark if we want the server to be authorized. - For testing this we should add on the computer running this example a line on - the hosts file: - /etc/hosts [UNIX] - OR - \System32\drivers\etc\hosts [Windows] - - The line to add on the file should be as follows: - stark - *******************************************************************************/ - -var mqtt = require('mqtt') -var fs = require('fs') -var path = require('path') -var KEY = fs.readFileSync(path.join(__dirname, '/tls-key.pem')) -var CERT = fs.readFileSync(path.join(__dirname, '/tls-cert.pem')) -var TRUSTED_CA_LIST = fs.readFileSync(path.join(__dirname, '/crt.ca.cg.pem')) - -var PORT = 1883 -var HOST = 'stark' - -var options = { - port: PORT, - host: HOST, - key: KEY, - cert: CERT, - rejectUnauthorized: true, - // The CA list will be used to determine if server is authorized - ca: TRUSTED_CA_LIST, - protocol: 'mqtts' -} - -var client = mqtt.connect(options) - -client.subscribe('messages') -client.publish('messages', 'Current time is: ' + new Date()) -client.on('message', function (topic, message) { - console.log(message) -}) - -client.on('connect', function () { - console.log('Connected') -}) +'use strict' + +/** ************************** IMPORTANT NOTE *********************************** + + The certificate used on this example has been generated for a host named stark. + So as host we SHOULD use stark if we want the server to be authorized. + For testing this we should add on the computer running this example a line on + the hosts file: + /etc/hosts [UNIX] + OR + \System32\drivers\etc\hosts [Windows] + + The line to add on the file should be as follows: + stark + *******************************************************************************/ + +var mqtt = require('mqtt') +var fs = require('fs') +var path = require('path') +var KEY = fs.readFileSync(path.join(__dirname, '/tls-key.pem')) +var CERT = fs.readFileSync(path.join(__dirname, '/tls-cert.pem')) +var TRUSTED_CA_LIST = fs.readFileSync(path.join(__dirname, '/crt.ca.cg.pem')) + +var PORT = 1883 +var HOST = 'stark' + +var options = { + port: PORT, + host: HOST, + key: KEY, + cert: CERT, + rejectUnauthorized: true, + // The CA list will be used to determine if server is authorized + ca: TRUSTED_CA_LIST, + protocol: 'mqtts' +} + +var client = mqtt.connect(options) + +client.subscribe('messages') +client.publish('messages', 'Current time is: ' + new Date()) +client.on('message', function (topic, message) { + console.log(message) +}) + +client.on('connect', function () { + console.log('Connected') +}) diff --git a/examples/ws/client.js b/examples/ws/client.js index 61524d345..9349c2971 100644 --- a/examples/ws/client.js +++ b/examples/ws/client.js @@ -1,53 +1,53 @@ -'use strict' - -var mqtt = require('../../') - -var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) - -// This sample should be run in tandem with the aedes_server.js file. -// Simply run it: -// $ node aedes_server.js -// -// Then run this file in a separate console: -// $ node websocket_sample.js -// -var host = 'ws://localhost:8080' - -var options = { - keepalive: 30, - clientId: clientId, - protocolId: 'MQTT', - protocolVersion: 4, - clean: true, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - will: { - topic: 'WillMsg', - payload: 'Connection Closed abnormally..!', - qos: 0, - retain: false - }, - rejectUnauthorized: false -} - -console.log('connecting mqtt client') -var client = mqtt.connect(host, options) - -client.on('error', function (err) { - console.log(err) - client.end() -}) - -client.on('connect', function () { - console.log('client connected:' + clientId) - client.subscribe('topic', { qos: 0 }) - client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) -}) - -client.on('message', function (topic, message, packet) { - console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) -}) - -client.on('close', function () { - console.log(clientId + ' disconnected') -}) +'use strict' + +var mqtt = require('../../') + +var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) + +// This sample should be run in tandem with the aedes_server.js file. +// Simply run it: +// $ node aedes_server.js +// +// Then run this file in a separate console: +// $ node websocket_sample.js +// +var host = 'ws://localhost:8080' + +var options = { + keepalive: 30, + clientId: clientId, + protocolId: 'MQTT', + protocolVersion: 4, + clean: true, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + will: { + topic: 'WillMsg', + payload: 'Connection Closed abnormally..!', + qos: 0, + retain: false + }, + rejectUnauthorized: false +} + +console.log('connecting mqtt client') +var client = mqtt.connect(host, options) + +client.on('error', function (err) { + console.log(err) + client.end() +}) + +client.on('connect', function () { + console.log('client connected:' + clientId) + client.subscribe('topic', { qos: 0 }) + client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) +}) + +client.on('message', function (topic, message, packet) { + console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) +}) + +client.on('close', function () { + console.log(clientId + ' disconnected') +}) diff --git a/examples/wss/client_with_proxy.js b/examples/wss/client_with_proxy.js index 4a0d9f3c9..657fe3700 100644 --- a/examples/wss/client_with_proxy.js +++ b/examples/wss/client_with_proxy.js @@ -1,58 +1,58 @@ -'use strict' - -var mqtt = require('mqtt') -var url = require('url') -var HttpsProxyAgent = require('https-proxy-agent') -/* -host: host of the endpoint you want to connect e.g. my.mqqt.host.com -path: path to you endpoint e.g. '/foo/bar/mqtt' -*/ -var endpoint = 'wss://' -/* create proxy agent -proxy: your proxy e.g. proxy.foo.bar.com -port: http proxy port e.g. 8080 -*/ -var proxy = process.env.http_proxy || 'http://:' -var parsed = url.parse(endpoint) -var proxyOpts = url.parse(proxy) -// true for wss -proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true -var agent = new HttpsProxyAgent(proxyOpts) -var wsOptions = { - agent: agent - // other wsOptions - // foo:'bar' -} -var mqttOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - clientId: 'testClient', - wsOptions: wsOptions -} - -var client = mqtt.connect(parsed, mqttOptions) - -client.on('connect', function () { - console.log('connected') -}) - -client.on('error', function (a) { - console.log('error!' + a) -}) - -client.on('offline', function (a) { - console.log('lost connection!' + a) -}) - -client.on('close', function (a) { - console.log('connection closed!' + a) -}) - -client.on('message', function (topic, message) { - console.log(message.toString()) -}) +'use strict' + +var mqtt = require('mqtt') +var url = require('url') +var HttpsProxyAgent = require('https-proxy-agent') +/* +host: host of the endpoint you want to connect e.g. my.mqqt.host.com +path: path to you endpoint e.g. '/foo/bar/mqtt' +*/ +var endpoint = 'wss://' +/* create proxy agent +proxy: your proxy e.g. proxy.foo.bar.com +port: http proxy port e.g. 8080 +*/ +var proxy = process.env.http_proxy || 'http://:' +var parsed = url.parse(endpoint) +var proxyOpts = url.parse(proxy) +// true for wss +proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true +var agent = new HttpsProxyAgent(proxyOpts) +var wsOptions = { + agent: agent + // other wsOptions + // foo:'bar' +} +var mqttOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + clientId: 'testClient', + wsOptions: wsOptions +} + +var client = mqtt.connect(parsed, mqttOptions) + +client.on('connect', function () { + console.log('connected') +}) + +client.on('error', function (a) { + console.log('error!' + a) +}) + +client.on('offline', function (a) { + console.log('lost connection!' + a) +}) + +client.on('close', function (a) { + console.log('connection closed!' + a) +}) + +client.on('message', function (topic, message) { + console.log(message.toString()) +}) diff --git a/lib/client.js b/lib/client.js index 540a11780..6eaeb35ac 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,1838 +1,1838 @@ -'use strict' - -/** - * Module dependencies - */ -var EventEmitter = require('events').EventEmitter -var Store = require('./store') -var TopicAliasRecv = require('./topic-alias-recv') -var TopicAliasSend = require('./topic-alias-send') -var mqttPacket = require('mqtt-packet') -var DefaultMessageIdProvider = require('./default-message-id-provider') -var Writable = require('readable-stream').Writable -var inherits = require('inherits') -var reInterval = require('reinterval') -var clone = require('rfdc/default') -var validations = require('./validations') -var xtend = require('xtend') -var debug = require('debug')('mqttjs:client') -var nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } -var setImmediate = global.setImmediate || function (callback) { - // works in node v0.8 - nextTick(callback) -} -var defaultConnectOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - resubscribe: true -} - -var socketErrors = [ - 'ECONNREFUSED', - 'EADDRINUSE', - 'ECONNRESET', - 'ENOTFOUND' -] - -// Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND. - -var errors = { - 0: '', - 1: 'Unacceptable protocol version', - 2: 'Identifier rejected', - 3: 'Server unavailable', - 4: 'Bad username or password', - 5: 'Not authorized', - 16: 'No matching subscribers', - 17: 'No subscription existed', - 128: 'Unspecified error', - 129: 'Malformed Packet', - 130: 'Protocol Error', - 131: 'Implementation specific error', - 132: 'Unsupported Protocol Version', - 133: 'Client Identifier not valid', - 134: 'Bad User Name or Password', - 135: 'Not authorized', - 136: 'Server unavailable', - 137: 'Server busy', - 138: 'Banned', - 139: 'Server shutting down', - 140: 'Bad authentication method', - 141: 'Keep Alive timeout', - 142: 'Session taken over', - 143: 'Topic Filter invalid', - 144: 'Topic Name invalid', - 145: 'Packet identifier in use', - 146: 'Packet Identifier not found', - 147: 'Receive Maximum exceeded', - 148: 'Topic Alias invalid', - 149: 'Packet too large', - 150: 'Message rate too high', - 151: 'Quota exceeded', - 152: 'Administrative action', - 153: 'Payload format invalid', - 154: 'Retain not supported', - 155: 'QoS not supported', - 156: 'Use another server', - 157: 'Server moved', - 158: 'Shared Subscriptions not supported', - 159: 'Connection rate exceeded', - 160: 'Maximum connect time', - 161: 'Subscription Identifiers not supported', - 162: 'Wildcard Subscriptions not supported' -} - -function defaultId () { - return 'mqttjs_' + Math.random().toString(16).substr(2, 8) -} - -function applyTopicAlias (client, packet) { - if (client.options.protocolVersion === 5) { - if (packet.cmd === 'publish') { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - var topic = packet.topic.toString() - if (client.topicAliasSend) { - if (alias) { - if (topic.length !== 0) { - // register topic alias - debug('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) - if (!client.topicAliasSend.put(topic, alias)) { - debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } else { - if (topic.length !== 0) { - if (client.options.autoAssignTopicAlias) { - alias = client.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto assign(use) topic: %s - alias: %d', topic, alias) - } else { - alias = client.topicAliasSend.getLruAlias() - client.topicAliasSend.put(topic, alias) - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto assign topic: %s - alias: %d', topic, alias) - } - } else if (client.options.autoUseTopicAlias) { - alias = client.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) - } - } - } - } - } else if (alias) { - debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } -} - -function removeTopicAliasAndRecoverTopicName (client, packet) { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - - var topic = packet.topic.toString() - if (topic.length === 0) { - // restore topic from alias - if (typeof alias === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - topic = client.topicAliasSend.getTopicByAlias(alias) - if (typeof topic === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - packet.topic = topic - } - } - } - if (alias) { - delete packet.properties.topicAlias - } -} - -function sendPacket (client, packet, cb) { - debug('sendPacket :: packet: %O', packet) - debug('sendPacket :: emitting `packetsend`') - - client.emit('packetsend', packet) - - debug('sendPacket :: writing to stream') - var result = mqttPacket.writeToStream(packet, client.stream, client.options) - debug('sendPacket :: writeToStream result %s', result) - if (!result && cb) { - debug('sendPacket :: handle events on `drain` once through callback.') - client.stream.once('drain', cb) - } else if (cb) { - debug('sendPacket :: invoking cb') - cb() - } -} - -function flush (queue) { - if (queue) { - debug('flush: queue exists? %b', !!(queue)) - Object.keys(queue).forEach(function (messageId) { - if (typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - delete queue[messageId] - } - }) - } -} - -function flushVolatile (queue) { - if (queue) { - debug('flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') - Object.keys(queue).forEach(function (messageId) { - if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - delete queue[messageId] - } - }) - } -} - -function storeAndSend (client, packet, cb, cbStorePut) { - debug('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) - var storePacket = packet - var err - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - err = removeTopicAliasAndRecoverTopicName(client, storePacket) - if (err) { - return cb && cb(err) - } - } - client.outgoingStore.put(storePacket, function storedPacket (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - sendPacket(client, packet, cb) - }) -} - -function nop (error) { - debug('nop ::', error) -} - -/** - * MqttClient constructor - * - * @param {Stream} stream - stream - * @param {Object} [options] - connection options - * (see Connection#connect) - */ -function MqttClient (streamBuilder, options) { - var k - var that = this - - if (!(this instanceof MqttClient)) { - return new MqttClient(streamBuilder, options) - } - - this.options = options || {} - - // Defaults - for (k in defaultConnectOptions) { - if (typeof this.options[k] === 'undefined') { - this.options[k] = defaultConnectOptions[k] - } else { - this.options[k] = options[k] - } - } - - debug('MqttClient :: options.protocol', options.protocol) - debug('MqttClient :: options.protocolVersion', options.protocolVersion) - debug('MqttClient :: options.username', options.username) - debug('MqttClient :: options.keepalive', options.keepalive) - debug('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) - debug('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) - debug('MqttClient :: options.topicAliasMaximum', options.topicAliasMaximum) - - this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() - - debug('MqttClient :: clientId', this.options.clientId) - - this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } - - this.streamBuilder = streamBuilder - - this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider - - // Inflight message storages - this.outgoingStore = options.outgoingStore || new Store() - this.incomingStore = options.incomingStore || new Store() - - // Should QoS zero messages be queued when the connection is broken? - this.queueQoSZero = options.queueQoSZero === undefined ? true : options.queueQoSZero - - // map of subscribed topics to support reconnection - this._resubscribeTopics = {} - - // map of a subscribe messageId and a topic - this.messageIdToTopic = {} - - // Ping timer, setup in _setupPingTimer - this.pingTimer = null - // Is the client connected? - this.connected = false - // Are we disconnecting? - this.disconnecting = false - // Packet queue - this.queue = [] - // connack timer - this.connackTimer = null - // Reconnect timer - this.reconnectTimer = null - // Is processing store? - this._storeProcessing = false - // Packet Ids are put into the store during store processing - this._packetIdsDuringStoreProcessing = {} - // Store processing queue - this._storeProcessingQueue = [] - - // Inflight callbacks - this.outgoing = {} - - // True if connection is first time. - this._firstConnection = true - - if (options.topicAliasMaximum > 0) { - if (options.topicAliasMaximum > 0xffff) { - debug('MqttClient :: options.topicAliasMaximum is out of range') - } else { - this.topicAliasRecv = new TopicAliasRecv(options.topicAliasMaximum) - } - } - - // Send queued packets - this.on('connect', function () { - var queue = this.queue - - function deliver () { - var entry = queue.shift() - debug('deliver :: entry %o', entry) - var packet = null - - if (!entry) { - that._resubscribe() - return - } - - packet = entry.packet - debug('deliver :: call _sendPacket for %o', packet) - var send = true - if (packet.messageId && packet.messageId !== 0) { - if (!that.messageIdProvider.register(packet.messageId)) { - send = false - } - } - if (send) { - that._sendPacket( - packet, - function (err) { - if (entry.cb) { - entry.cb(err) - } - deliver() - } - ) - } else { - debug('messageId: %d has already used. The message is skipped and removed.', packet.messageId) - deliver() - } - } - - debug('connect :: sending queued packets') - deliver() - }) - - this.on('close', function () { - debug('close :: connected set to `false`') - this.connected = false - - debug('close :: clearing connackTimer') - clearTimeout(this.connackTimer) - - debug('close :: clearing ping timer') - if (that.pingTimer !== null) { - that.pingTimer.clear() - that.pingTimer = null - } - - if (this.topicAliasRecv) { - this.topicAliasRecv.clear() - } - - debug('close :: calling _setupReconnect') - this._setupReconnect() - }) - EventEmitter.call(this) - - debug('MqttClient :: setting up stream') - this._setupStream() -} -inherits(MqttClient, EventEmitter) - -/** - * setup the event handlers in the inner stream. - * - * @api private - */ -MqttClient.prototype._setupStream = function () { - var connectPacket - var that = this - var writable = new Writable() - var parser = mqttPacket.parser(this.options) - var completeParse = null - var packets = [] - - debug('_setupStream :: calling method to clear reconnect') - this._clearReconnect() - - debug('_setupStream :: using streamBuilder provided to client to create stream') - this.stream = this.streamBuilder(this) - - parser.on('packet', function (packet) { - debug('parser :: on packet push to packets array.') - packets.push(packet) - }) - - function nextTickWork () { - if (packets.length) { - nextTick(work) - } else { - var done = completeParse - completeParse = null - done() - } - } - - function work () { - debug('work :: getting next packet in queue') - var packet = packets.shift() - - if (packet) { - debug('work :: packet pulled from queue') - that._handlePacket(packet, nextTickWork) - } else { - debug('work :: no packets in queue') - var done = completeParse - completeParse = null - debug('work :: done flag is %s', !!(done)) - if (done) done() - } - } - - writable._write = function (buf, enc, done) { - completeParse = done - debug('writable stream :: parsing buffer') - parser.parse(buf) - work() - } - - function streamErrorHandler (error) { - debug('streamErrorHandler :: error', error.message) - if (socketErrors.includes(error.code)) { - // handle error - debug('streamErrorHandler :: emitting error') - that.emit('error', error) - } else { - nop(error) - } - } - - debug('_setupStream :: pipe stream to writable stream') - this.stream.pipe(writable) - - // Suppress connection errors - this.stream.on('error', streamErrorHandler) - - // Echo stream close - this.stream.on('close', function () { - debug('(%s)stream :: on close', that.options.clientId) - flushVolatile(that.outgoing) - debug('stream: emit close to MqttClient') - that.emit('close') - }) - - // Send a connect packet - debug('_setupStream: sending packet `connect`') - connectPacket = Object.create(this.options) - connectPacket.cmd = 'connect' - if (this.topicAliasRecv) { - if (!connectPacket.properties) { - connectPacket.properties = {} - } - if (this.topicAliasRecv) { - connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max - } - } - // avoid message queue - sendPacket(this, connectPacket) - - // Echo connection errors - parser.on('error', this.emit.bind(this, 'error')) - - // auth - if (this.options.properties) { - if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { - that.end(() => - this.emit('error', new Error('Packet has no Authentication Method') - )) - return this - } - if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { - var authPacket = xtend({cmd: 'auth', reasonCode: 0}, this.options.authPacket) - sendPacket(this, authPacket) - } - } - - // many drain listeners are needed for qos 1 callbacks if the connection is intermittent - this.stream.setMaxListeners(1000) - - clearTimeout(this.connackTimer) - this.connackTimer = setTimeout(function () { - debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') - that._cleanUp(true) - }, this.options.connectTimeout) -} - -MqttClient.prototype._handlePacket = function (packet, done) { - var options = this.options - - if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { - this.emit('error', new Error('exceeding packets size ' + packet.cmd)) - this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) - return this - } - debug('_handlePacket :: emitting packetreceive') - this.emit('packetreceive', packet) - - switch (packet.cmd) { - case 'publish': - this._handlePublish(packet, done) - break - case 'puback': - case 'pubrec': - case 'pubcomp': - case 'suback': - case 'unsuback': - this._handleAck(packet) - done() - break - case 'pubrel': - this._handlePubrel(packet, done) - break - case 'connack': - this._handleConnack(packet) - done() - break - case 'pingresp': - this._handlePingresp(packet) - done() - break - case 'disconnect': - this._handleDisconnect(packet) - done() - break - default: - // do nothing - // maybe we should do an error handling - // or just log it - break - } -} - -MqttClient.prototype._checkDisconnecting = function (callback) { - if (this.disconnecting) { - if (callback) { - callback(new Error('client disconnecting')) - } else { - this.emit('error', new Error('client disconnecting')) - } - } - return this.disconnecting -} - -/** - * publish - publish to - * - * @param {String} topic - topic to publish to - * @param {String, Buffer} message - message to publish - * @param {Object} [opts] - publish options, includes: - * {Number} qos - qos level to publish on - * {Boolean} retain - whether or not to retain the message - * {Boolean} dup - whether or not mark a message as duplicate - * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` - * @param {Function} [callback] - function(err){} - * called when publish succeeds or fails - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.publish('topic', 'message'); - * @example - * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); - * @example client.publish('topic', 'message', console.log); - */ -MqttClient.prototype.publish = function (topic, message, opts, callback) { - debug('publish :: message `%s` to topic `%s`', message, topic) - var packet - var options = this.options - - // .publish(topic, payload, cb); - if (typeof opts === 'function') { - callback = opts - opts = null - } - - // default opts - var defaultOpts = {qos: 0, retain: false, dup: false} - opts = xtend(defaultOpts, opts) - - if (this._checkDisconnecting(callback)) { - return this - } - - var that = this - var publishProc = function () { - var messageId = 0 - if (opts.qos === 1 || opts.qos === 2) { - messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - } - packet = { - cmd: 'publish', - topic: topic, - payload: message, - qos: opts.qos, - retain: opts.retain, - messageId: messageId, - dup: opts.dup - } - - if (options.protocolVersion === 5) { - packet.properties = opts.properties - } - - debug('publish :: qos', opts.qos) - switch (opts.qos) { - case 1: - case 2: - // Add to callbacks - that.outgoing[packet.messageId] = { - volatile: false, - cb: callback || nop - } - debug('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, undefined, opts.cbStorePut) - break - default: - debug('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, callback, opts.cbStorePut) - break - } - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': publishProc, - 'cbStorePut': opts.cbStorePut, - 'callback': callback - } - ) - } else { - publishProc() - } - return this -} - -/** - * subscribe - subscribe to - * - * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} - * @param {Object} [opts] - optional subscription options, includes: - * {Number} qos - subscribe qos level - * @param {Function} [callback] - function(err, granted){} where: - * {Error} err - subscription error (none at the moment!) - * {Array} granted - array of {topic: 't', qos: 0} - * @returns {MqttClient} this - for chaining - * @api public - * @example client.subscribe('topic'); - * @example client.subscribe('topic', {qos: 1}); - * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); - * @example client.subscribe('topic', console.log); - */ -MqttClient.prototype.subscribe = function () { - var that = this - var args = new Array(arguments.length) - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - var subs = [] - var obj = args.shift() - var resubscribe = obj.resubscribe - var callback = args.pop() || nop - var opts = args.pop() - var version = this.options.protocolVersion - - delete obj.resubscribe - - if (typeof obj === 'string') { - obj = [obj] - } - - if (typeof callback !== 'function') { - opts = callback - callback = nop - } - - var invalidTopic = validations.validateTopics(obj) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (this._checkDisconnecting(callback)) { - debug('subscribe: discconecting true') - return this - } - - var defaultOpts = { - qos: 0 - } - if (version === 5) { - defaultOpts.nl = false - defaultOpts.rap = false - defaultOpts.rh = 0 - } - opts = xtend(defaultOpts, opts) - - if (Array.isArray(obj)) { - obj.forEach(function (topic) { - debug('subscribe: array topic %s', topic) - if (!that._resubscribeTopics.hasOwnProperty(topic) || - that._resubscribeTopics[topic].qos < opts.qos || - resubscribe) { - var currentOpts = { - topic: topic, - qos: opts.qos - } - if (version === 5) { - currentOpts.nl = opts.nl - currentOpts.rap = opts.rap - currentOpts.rh = opts.rh - currentOpts.properties = opts.properties - } - debug('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) - subs.push(currentOpts) - } - }) - } else { - Object - .keys(obj) - .forEach(function (k) { - debug('subscribe: object topic %s', k) - if (!that._resubscribeTopics.hasOwnProperty(k) || - that._resubscribeTopics[k].qos < obj[k].qos || - resubscribe) { - var currentOpts = { - topic: k, - qos: obj[k].qos - } - if (version === 5) { - currentOpts.nl = obj[k].nl - currentOpts.rap = obj[k].rap - currentOpts.rh = obj[k].rh - currentOpts.properties = opts.properties - } - debug('subscribe: pushing `%s` to subs list', currentOpts) - subs.push(currentOpts) - } - }) - } - - if (!subs.length) { - callback(null, []) - return this - } - - var subscribeProc = function () { - var messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - - var packet = { - cmd: 'subscribe', - subscriptions: subs, - qos: 1, - retain: false, - dup: false, - messageId: messageId - } - - if (opts.properties) { - packet.properties = opts.properties - } - - // subscriptions to resubscribe to in case of disconnect - if (that.options.resubscribe) { - debug('subscribe :: resubscribe true') - var topics = [] - subs.forEach(function (sub) { - if (that.options.reconnectPeriod > 0) { - var topic = { qos: sub.qos } - if (version === 5) { - topic.nl = sub.nl || false - topic.rap = sub.rap || false - topic.rh = sub.rh || 0 - topic.properties = sub.properties - } - that._resubscribeTopics[sub.topic] = topic - topics.push(sub.topic) - } - }) - that.messageIdToTopic[packet.messageId] = topics - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: function (err, packet) { - if (!err) { - var granted = packet.granted - for (var i = 0; i < granted.length; i += 1) { - subs[i].qos = granted[i] - } - } - - callback(err, subs) - } - } - debug('subscribe :: call _sendPacket') - that._sendPacket(packet) - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': subscribeProc, - 'callback': callback - } - ) - } else { - subscribeProc() - } - - return this -} - -/** - * unsubscribe - unsubscribe from topic(s) - * - * @param {String, Array} topic - topics to unsubscribe from - * @param {Object} [opts] - optional subscription options, includes: - * {Object} properties - properties of unsubscribe packet - * @param {Function} [callback] - callback fired on unsuback - * @returns {MqttClient} this - for chaining - * @api public - * @example client.unsubscribe('topic'); - * @example client.unsubscribe('topic', console.log); - */ -MqttClient.prototype.unsubscribe = function () { - var that = this - var args = new Array(arguments.length) - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - var topic = args.shift() - var callback = args.pop() || nop - var opts = args.pop() - if (typeof topic === 'string') { - topic = [topic] - } - - if (typeof callback !== 'function') { - opts = callback - callback = nop - } - - var invalidTopic = validations.validateTopics(topic) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (that._checkDisconnecting(callback)) { - return this - } - - var unsubscribeProc = function () { - var messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - var packet = { - cmd: 'unsubscribe', - qos: 1, - messageId: messageId - } - - if (typeof topic === 'string') { - packet.unsubscriptions = [topic] - } else if (Array.isArray(topic)) { - packet.unsubscriptions = topic - } - - if (that.options.resubscribe) { - packet.unsubscriptions.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } - - if (typeof opts === 'object' && opts.properties) { - packet.properties = opts.properties - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: callback - } - - debug('unsubscribe: call _sendPacket') - that._sendPacket(packet) - - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': unsubscribeProc, - 'callback': callback - } - ) - } else { - unsubscribeProc() - } - - return this -} - -/** - * end - close connection - * - * @returns {MqttClient} this - for chaining - * @param {Boolean} force - do not wait for all in-flight messages to be acked - * @param {Object} opts - added to the disconnect packet - * @param {Function} cb - called when the client has been closed - * - * @api public - */ -MqttClient.prototype.end = function (force, opts, cb) { - var that = this - - debug('end :: (%s)', this.options.clientId) - - if (force == null || typeof force !== 'boolean') { - cb = opts || nop - opts = force - force = false - if (typeof opts !== 'object') { - cb = opts - opts = null - if (typeof cb !== 'function') { - cb = nop - } - } - } - - if (typeof opts !== 'object') { - cb = opts - opts = null - } - - debug('end :: cb? %s', !!cb) - cb = cb || nop - - function closeStores () { - debug('end :: closeStores: closing incoming and outgoing stores') - that.disconnected = true - that.incomingStore.close(function (e1) { - that.outgoingStore.close(function (e2) { - debug('end :: closeStores: emitting end') - that.emit('end') - if (cb) { - let err = e1 || e2 - debug('end :: closeStores: invoking callback with args') - cb(err) - } - }) - }) - if (that._deferredReconnect) { - that._deferredReconnect() - } - } - - function finish () { - // defer closesStores of an I/O cycle, - // just to make sure things are - // ok for websockets - debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) - that._cleanUp(force, () => { - debug('end :: finish :: calling process.nextTick on closeStores') - // var boundProcess = nextTick.bind(null, closeStores) - nextTick(closeStores.bind(that)) - }, opts) - } - - if (this.disconnecting) { - cb() - return this - } - - this._clearReconnect() - - this.disconnecting = true - - if (!force && Object.keys(this.outgoing).length > 0) { - // wait 10ms, just to be sure we received all of it - debug('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) - this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) - } else { - debug('end :: (%s) :: immediately calling finish', that.options.clientId) - finish() - } - - return this -} - -/** - * removeOutgoingMessage - remove a message in outgoing store - * the outgoing callback will be called withe Error('Message removed') if the message is removed - * - * @param {Number} messageId - messageId to remove message - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.removeOutgoingMessage(client.getLastAllocated()); - */ -MqttClient.prototype.removeOutgoingMessage = function (messageId) { - var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null - delete this.outgoing[messageId] - this.outgoingStore.del({messageId: messageId}, function () { - cb(new Error('Message removed')) - }) - return this -} - -/** - * reconnect - connect again using the same options as connect() - * - * @param {Object} [opts] - optional reconnect options, includes: - * {Store} incomingStore - a store for the incoming packets - * {Store} outgoingStore - a store for the outgoing packets - * if opts is not given, current stores are used - * @returns {MqttClient} this - for chaining - * - * @api public - */ -MqttClient.prototype.reconnect = function (opts) { - debug('client reconnect') - var that = this - var f = function () { - if (opts) { - that.options.incomingStore = opts.incomingStore - that.options.outgoingStore = opts.outgoingStore - } else { - that.options.incomingStore = null - that.options.outgoingStore = null - } - that.incomingStore = that.options.incomingStore || new Store() - that.outgoingStore = that.options.outgoingStore || new Store() - that.disconnecting = false - that.disconnected = false - that._deferredReconnect = null - that._reconnect() - } - - if (this.disconnecting && !this.disconnected) { - this._deferredReconnect = f - } else { - f() - } - return this -} - -/** - * _reconnect - implement reconnection - * @api privateish - */ -MqttClient.prototype._reconnect = function () { - debug('_reconnect: emitting reconnect to client') - this.emit('reconnect') - if (this.connected) { - this.end(() => { this._setupStream() }) - debug('client already connected. disconnecting first.') - } else { - debug('_reconnect: calling _setupStream') - this._setupStream() - } -} - -/** - * _setupReconnect - setup reconnect timer - */ -MqttClient.prototype._setupReconnect = function () { - var that = this - - if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { - if (!this.reconnecting) { - debug('_setupReconnect :: emit `offline` state') - this.emit('offline') - debug('_setupReconnect :: set `reconnecting` to `true`') - this.reconnecting = true - } - debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) - that.reconnectTimer = setInterval(function () { - debug('reconnectTimer :: reconnect triggered!') - that._reconnect() - }, that.options.reconnectPeriod) - } else { - debug('_setupReconnect :: doing nothing...') - } -} - -/** - * _clearReconnect - clear the reconnect timer - */ -MqttClient.prototype._clearReconnect = function () { - debug('_clearReconnect : clearing reconnect timer') - if (this.reconnectTimer) { - clearInterval(this.reconnectTimer) - this.reconnectTimer = null - } -} - -/** - * _cleanUp - clean up on connection end - * @api private - */ -MqttClient.prototype._cleanUp = function (forced, done) { - var opts = arguments[2] - if (done) { - debug('_cleanUp :: done callback provided for on stream close') - this.stream.on('close', done) - } - - debug('_cleanUp :: forced? %s', forced) - if (forced) { - if ((this.options.reconnectPeriod === 0) && this.options.clean) { - flush(this.outgoing) - } - debug('_cleanUp :: (%s) :: destroying stream', this.options.clientId) - this.stream.destroy() - } else { - var packet = xtend({ cmd: 'disconnect' }, opts) - debug('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) - this._sendPacket( - packet, - setImmediate.bind( - null, - this.stream.end.bind(this.stream) - ) - ) - } - - if (!this.disconnecting) { - debug('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') - this._clearReconnect() - this._setupReconnect() - } - - if (this.pingTimer !== null) { - debug('_cleanUp :: clearing pingTimer') - this.pingTimer.clear() - this.pingTimer = null - } - - if (done && !this.connected) { - debug('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) - this.stream.removeListener('close', done) - done() - } -} - -/** - * _sendPacket - send or queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @api private - */ -MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { - debug('_sendPacket :: (%s) :: start', this.options.clientId) - cbStorePut = cbStorePut || nop - cb = cb || nop - - var err = applyTopicAlias(this, packet) - if (err) { - cb(err) - return - } - - if (!this.connected) { - debug('_sendPacket :: client not connected. Storing packet offline.') - this._storePacket(packet, cb, cbStorePut) - return - } - - // When sending a packet, reschedule the ping timer - this._shiftPingInterval() - - switch (packet.cmd) { - case 'publish': - break - case 'pubrel': - storeAndSend(this, packet, cb, cbStorePut) - return - default: - sendPacket(this, packet, cb) - return - } - - switch (packet.qos) { - case 2: - case 1: - storeAndSend(this, packet, cb, cbStorePut) - break - /** - * no need of case here since it will be caught by default - * and jshint comply that before default it must be a break - * anyway it will result in -1 evaluation - */ - case 0: - /* falls through */ - default: - sendPacket(this, packet, cb) - break - } - debug('_sendPacket :: (%s) :: end', this.options.clientId) -} - -/** - * _storePacket - queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @api private - */ -MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { - debug('_storePacket :: packet: %o', packet) - debug('_storePacket :: cb? %s', !!cb) - cbStorePut = cbStorePut || nop - - var storePacket = packet - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - var err = removeTopicAliasAndRecoverTopicName(this, storePacket) - if (err) { - return cb && cb(err) - } - } - // check that the packet is not a qos of 0, or that the command is not a publish - if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { - this.queue.push({ packet: storePacket, cb: cb }) - } else if (storePacket.qos > 0) { - cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null - this.outgoingStore.put(storePacket, function (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - }) - } else if (cb) { - cb(new Error('No connection to broker')) - } -} - -/** - * _setupPingTimer - setup the ping timer - * - * @api private - */ -MqttClient.prototype._setupPingTimer = function () { - debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) - var that = this - - if (!this.pingTimer && this.options.keepalive) { - this.pingResp = true - this.pingTimer = reInterval(function () { - that._checkPing() - }, this.options.keepalive * 1000) - } -} - -/** - * _shiftPingInterval - reschedule the ping interval - * - * @api private - */ -MqttClient.prototype._shiftPingInterval = function () { - if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { - this.pingTimer.reschedule(this.options.keepalive * 1000) - } -} -/** - * _checkPing - check if a pingresp has come back, and ping the server again - * - * @api private - */ -MqttClient.prototype._checkPing = function () { - debug('_checkPing :: checking ping...') - if (this.pingResp) { - debug('_checkPing :: ping response received. Clearing flag and sending `pingreq`') - this.pingResp = false - this._sendPacket({ cmd: 'pingreq' }) - } else { - // do a forced cleanup since socket will be in bad shape - debug('_checkPing :: calling _cleanUp with force true') - this._cleanUp(true) - } -} - -/** - * _handlePingresp - handle a pingresp - * - * @api private - */ -MqttClient.prototype._handlePingresp = function () { - this.pingResp = true -} - -/** - * _handleConnack - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handleConnack = function (packet) { - debug('_handleConnack') - var options = this.options - var version = options.protocolVersion - var rc = version === 5 ? packet.reasonCode : packet.returnCode - - clearTimeout(this.connackTimer) - delete this.topicAliasSend - - if (packet.properties) { - if (packet.properties.topicAliasMaximum) { - if (packet.properties.topicAliasMaximum > 0xffff) { - this.emit('error', new Error('topicAliasMaximum from broker is out of range')) - return - } - if (packet.properties.topicAliasMaximum > 0) { - this.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) - } - } - if (packet.properties.serverKeepAlive && options.keepalive) { - options.keepalive = packet.properties.serverKeepAlive - this._shiftPingInterval() - } - if (packet.properties.maximumPacketSize) { - if (!options.properties) { options.properties = {} } - options.properties.maximumPacketSize = packet.properties.maximumPacketSize - } - } - - if (rc === 0) { - this.reconnecting = false - this._onConnect(packet) - } else if (rc > 0) { - var err = new Error('Connection refused: ' + errors[rc]) - err.code = rc - this.emit('error', err) - } -} - -/** - * _handlePublish - * - * @param {Object} packet - * @api private - */ -/* -those late 2 case should be rewrite to comply with coding style: - -case 1: -case 0: - // do not wait sending a puback - // no callback passed - if (1 === qos) { - this._sendPacket({ - cmd: 'puback', - messageId: messageId - }); - } - // emit the message event for both qos 1 and 0 - this.emit('message', topic, message, packet); - this.handleMessage(packet, done); - break; -default: - // do nothing but every switch mus have a default - // log or throw an error about unknown qos - break; - -for now i just suppressed the warnings -*/ -MqttClient.prototype._handlePublish = function (packet, done) { - debug('_handlePublish: packet %o', packet) - done = typeof done !== 'undefined' ? done : nop - var topic = packet.topic.toString() - var message = packet.payload - var qos = packet.qos - var messageId = packet.messageId - var that = this - var options = this.options - var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] - if (this.options.protocolVersion === 5) { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - if (typeof alias !== 'undefined') { - if (topic.length === 0) { - if (alias > 0 && alias <= 0xffff) { - var gotTopic = this.topicAliasRecv.getTopicByAlias(alias) - if (gotTopic) { - topic = gotTopic - debug('_handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) - } else { - debug('_handlePublish :: unregistered topic alias. alias: %d', alias) - this.emit('error', new Error('Received unregistered Topic Alias')) - return - } - } else { - debug('_handlePublish :: topic alias out of range. alias: %d', alias) - this.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } else { - if (this.topicAliasRecv.put(topic, alias)) { - debug('_handlePublish :: registered topic: %s - alias: %d', topic, alias) - } else { - debug('_handlePublish :: topic alias out of range. alias: %d', alias) - this.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } - } - } - debug('_handlePublish: qos %d', qos) - switch (qos) { - case 2: { - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return that.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for pubrec')) } - if (code) { - that._sendPacket({cmd: 'pubrec', messageId: messageId, reasonCode: code}, done) - } else { - that.incomingStore.put(packet, function () { - that._sendPacket({cmd: 'pubrec', messageId: messageId}, done) - }) - } - }) - break - } - case 1: { - // emit the message event - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return that.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for puback')) } - if (!code) { that.emit('message', topic, message, packet) } - that.handleMessage(packet, function (err) { - if (err) { - return done && done(err) - } - that._sendPacket({cmd: 'puback', messageId: messageId, reasonCode: code}, done) - }) - }) - break - } - case 0: - // emit the message event - this.emit('message', topic, message, packet) - this.handleMessage(packet, done) - break - default: - // do nothing - debug('_handlePublish: unknown QoS. Doing nothing.') - // log or throw an error about unknown qos - break - } -} - -/** - * Handle messages with backpressure support, one at a time. - * Override at will. - * - * @param Packet packet the packet - * @param Function callback call when finished - * @api public - */ -MqttClient.prototype.handleMessage = function (packet, callback) { - callback() -} - -/** - * _handleAck - * - * @param {Object} packet - * @api private - */ - -MqttClient.prototype._handleAck = function (packet) { - /* eslint no-fallthrough: "off" */ - var messageId = packet.messageId - var type = packet.cmd - var response = null - var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null - var that = this - var err - - if (!cb) { - debug('_handleAck :: Server sent an ack in error. Ignoring.') - // Server sent an ack in error, ignore it. - return - } - - // Process - debug('_handleAck :: packet type', type) - switch (type) { - case 'pubcomp': - // same thing as puback for QoS 2 - case 'puback': - var pubackRC = packet.reasonCode - // Callback - we're done - if (pubackRC && pubackRC > 0 && pubackRC !== 16) { - err = new Error('Publish error: ' + errors[pubackRC]) - err.code = pubackRC - cb(err, packet) - } - delete this.outgoing[messageId] - this.outgoingStore.del(packet, cb) - this.messageIdProvider.deallocate(messageId) - this._invokeStoreProcessingQueue() - break - case 'pubrec': - response = { - cmd: 'pubrel', - qos: 2, - messageId: messageId - } - var pubrecRC = packet.reasonCode - - if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { - err = new Error('Publish error: ' + errors[pubrecRC]) - err.code = pubrecRC - cb(err, packet) - } else { - this._sendPacket(response) - } - break - case 'suback': - delete this.outgoing[messageId] - this.messageIdProvider.deallocate(messageId) - for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { - if ((packet.granted[grantedI] & 0x80) !== 0) { - // suback with Failure status - var topics = this.messageIdToTopic[messageId] - if (topics) { - topics.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } - } - } - this._invokeStoreProcessingQueue() - cb(null, packet) - break - case 'unsuback': - delete this.outgoing[messageId] - this.messageIdProvider.deallocate(messageId) - this._invokeStoreProcessingQueue() - cb(null) - break - default: - that.emit('error', new Error('unrecognized packet type')) - } - - if (this.disconnecting && - Object.keys(this.outgoing).length === 0) { - this.emit('outgoingEmpty') - } -} - -/** - * _handlePubrel - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handlePubrel = function (packet, callback) { - debug('handling pubrel packet') - callback = typeof callback !== 'undefined' ? callback : nop - var messageId = packet.messageId - var that = this - - var comp = {cmd: 'pubcomp', messageId: messageId} - - that.incomingStore.get(packet, function (err, pub) { - if (!err) { - that.emit('message', pub.topic, pub.payload, pub) - that.handleMessage(pub, function (err) { - if (err) { - return callback(err) - } - that.incomingStore.del(pub, nop) - that._sendPacket(comp, callback) - }) - } else { - that._sendPacket(comp, callback) - } - }) -} - -/** - * _handleDisconnect - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handleDisconnect = function (packet) { - this.emit('disconnect', packet) -} - -/** - * _nextId - * @return unsigned int - */ -MqttClient.prototype._nextId = function () { - return this.messageIdProvider.allocate() -} - -/** - * getLastMessageId - * @return unsigned int - */ -MqttClient.prototype.getLastMessageId = function () { - return this.messageIdProvider.getLastAllocated() -} - -/** - * _resubscribe - * @api private - */ -MqttClient.prototype._resubscribe = function () { - debug('_resubscribe') - var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) - if (!this._firstConnection && - (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && - _resubscribeTopicsKeys.length > 0) { - if (this.options.resubscribe) { - if (this.options.protocolVersion === 5) { - debug('_resubscribe: protocolVersion 5') - for (var topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { - var resubscribeTopic = {} - resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] - resubscribeTopic.resubscribe = true - this.subscribe(resubscribeTopic, {properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties}) - } - } else { - this._resubscribeTopics.resubscribe = true - this.subscribe(this._resubscribeTopics) - } - } else { - this._resubscribeTopics = {} - } - } - - this._firstConnection = false -} - -/** - * _onConnect - * - * @api private - */ -MqttClient.prototype._onConnect = function (packet) { - if (this.disconnected) { - this.emit('connect', packet) - return - } - - var that = this - - this.connackPacket = packet - this.messageIdProvider.clear() - this._setupPingTimer() - - this.connected = true - - function startStreamProcess () { - var outStore = that.outgoingStore.createStream() - - function clearStoreProcessing () { - that._storeProcessing = false - that._packetIdsDuringStoreProcessing = {} - } - - that.once('close', remove) - outStore.on('error', function (err) { - clearStoreProcessing() - that._flushStoreProcessingQueue() - that.removeListener('close', remove) - that.emit('error', err) - }) - - function remove () { - outStore.destroy() - outStore = null - that._flushStoreProcessingQueue() - clearStoreProcessing() - } - - function storeDeliver () { - // edge case, we wrapped this twice - if (!outStore) { - return - } - that._storeProcessing = true - - var packet = outStore.read(1) - - var cb - - if (!packet) { - // read when data is available in the future - outStore.once('readable', storeDeliver) - return - } - - // Skip already processed store packets - if (that._packetIdsDuringStoreProcessing[packet.messageId]) { - storeDeliver() - return - } - - // Avoid unnecessary stream read operations when disconnected - if (!that.disconnecting && !that.reconnectTimer) { - cb = that.outgoing[packet.messageId] ? that.outgoing[packet.messageId].cb : null - that.outgoing[packet.messageId] = { - volatile: false, - cb: function (err, status) { - // Ensure that the original callback passed in to publish gets invoked - if (cb) { - cb(err, status) - } - - storeDeliver() - } - } - that._packetIdsDuringStoreProcessing[packet.messageId] = true - if (that.messageIdProvider.register(packet.messageId)) { - that._sendPacket(packet) - } else { - debug('messageId: %d has already used.', packet.messageId) - } - } else if (outStore.destroy) { - outStore.destroy() - } - } - - outStore.on('end', function () { - var allProcessed = true - for (var id in that._packetIdsDuringStoreProcessing) { - if (!that._packetIdsDuringStoreProcessing[id]) { - allProcessed = false - break - } - } - if (allProcessed) { - clearStoreProcessing() - that.removeListener('close', remove) - that._invokeAllStoreProcessingQueue() - that.emit('connect', packet) - } else { - startStreamProcess() - } - }) - storeDeliver() - } - // start flowing - startStreamProcess() -} - -MqttClient.prototype._invokeStoreProcessingQueue = function () { - if (this._storeProcessingQueue.length > 0) { - var f = this._storeProcessingQueue[0] - if (f && f.invoke()) { - this._storeProcessingQueue.shift() - return true - } - } - return false -} - -MqttClient.prototype._invokeAllStoreProcessingQueue = function () { - while (this._invokeStoreProcessingQueue()) {} -} - -MqttClient.prototype._flushStoreProcessingQueue = function () { - for (var f of this._storeProcessingQueue) { - if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) - if (f.callback) f.callback(new Error('Connection closed')) - } - this._storeProcessingQueue.splice(0) -} - -module.exports = MqttClient +'use strict' + +/** + * Module dependencies + */ +var EventEmitter = require('events').EventEmitter +var Store = require('./store') +var TopicAliasRecv = require('./topic-alias-recv') +var TopicAliasSend = require('./topic-alias-send') +var mqttPacket = require('mqtt-packet') +var DefaultMessageIdProvider = require('./default-message-id-provider') +var Writable = require('readable-stream').Writable +var inherits = require('inherits') +var reInterval = require('reinterval') +var clone = require('rfdc/default') +var validations = require('./validations') +var xtend = require('xtend') +var debug = require('debug')('mqttjs:client') +var nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } +var setImmediate = global.setImmediate || function (callback) { + // works in node v0.8 + nextTick(callback) +} +var defaultConnectOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true +} + +var socketErrors = [ + 'ECONNREFUSED', + 'EADDRINUSE', + 'ECONNRESET', + 'ENOTFOUND' +] + +// Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND. + +var errors = { + 0: '', + 1: 'Unacceptable protocol version', + 2: 'Identifier rejected', + 3: 'Server unavailable', + 4: 'Bad username or password', + 5: 'Not authorized', + 16: 'No matching subscribers', + 17: 'No subscription existed', + 128: 'Unspecified error', + 129: 'Malformed Packet', + 130: 'Protocol Error', + 131: 'Implementation specific error', + 132: 'Unsupported Protocol Version', + 133: 'Client Identifier not valid', + 134: 'Bad User Name or Password', + 135: 'Not authorized', + 136: 'Server unavailable', + 137: 'Server busy', + 138: 'Banned', + 139: 'Server shutting down', + 140: 'Bad authentication method', + 141: 'Keep Alive timeout', + 142: 'Session taken over', + 143: 'Topic Filter invalid', + 144: 'Topic Name invalid', + 145: 'Packet identifier in use', + 146: 'Packet Identifier not found', + 147: 'Receive Maximum exceeded', + 148: 'Topic Alias invalid', + 149: 'Packet too large', + 150: 'Message rate too high', + 151: 'Quota exceeded', + 152: 'Administrative action', + 153: 'Payload format invalid', + 154: 'Retain not supported', + 155: 'QoS not supported', + 156: 'Use another server', + 157: 'Server moved', + 158: 'Shared Subscriptions not supported', + 159: 'Connection rate exceeded', + 160: 'Maximum connect time', + 161: 'Subscription Identifiers not supported', + 162: 'Wildcard Subscriptions not supported' +} + +function defaultId () { + return 'mqttjs_' + Math.random().toString(16).substr(2, 8) +} + +function applyTopicAlias (client, packet) { + if (client.options.protocolVersion === 5) { + if (packet.cmd === 'publish') { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + var topic = packet.topic.toString() + if (client.topicAliasSend) { + if (alias) { + if (topic.length !== 0) { + // register topic alias + debug('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) + if (!client.topicAliasSend.put(topic, alias)) { + debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + return new Error('Sending Topic Alias out of range') + } + } + } else { + if (topic.length !== 0) { + if (client.options.autoAssignTopicAlias) { + alias = client.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto assign(use) topic: %s - alias: %d', topic, alias) + } else { + alias = client.topicAliasSend.getLruAlias() + client.topicAliasSend.put(topic, alias) + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto assign topic: %s - alias: %d', topic, alias) + } + } else if (client.options.autoUseTopicAlias) { + alias = client.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) + } + } + } + } + } else if (alias) { + debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + return new Error('Sending Topic Alias out of range') + } + } + } +} + +function removeTopicAliasAndRecoverTopicName (client, packet) { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + + var topic = packet.topic.toString() + if (topic.length === 0) { + // restore topic from alias + if (typeof alias === 'undefined') { + return new Error('Unregistered Topic Alias') + } else { + topic = client.topicAliasSend.getTopicByAlias(alias) + if (typeof topic === 'undefined') { + return new Error('Unregistered Topic Alias') + } else { + packet.topic = topic + } + } + } + if (alias) { + delete packet.properties.topicAlias + } +} + +function sendPacket (client, packet, cb) { + debug('sendPacket :: packet: %O', packet) + debug('sendPacket :: emitting `packetsend`') + + client.emit('packetsend', packet) + + debug('sendPacket :: writing to stream') + var result = mqttPacket.writeToStream(packet, client.stream, client.options) + debug('sendPacket :: writeToStream result %s', result) + if (!result && cb) { + debug('sendPacket :: handle events on `drain` once through callback.') + client.stream.once('drain', cb) + } else if (cb) { + debug('sendPacket :: invoking cb') + cb() + } +} + +function flush (queue) { + if (queue) { + debug('flush: queue exists? %b', !!(queue)) + Object.keys(queue).forEach(function (messageId) { + if (typeof queue[messageId].cb === 'function') { + queue[messageId].cb(new Error('Connection closed')) + delete queue[messageId] + } + }) + } +} + +function flushVolatile (queue) { + if (queue) { + debug('flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') + Object.keys(queue).forEach(function (messageId) { + if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { + queue[messageId].cb(new Error('Connection closed')) + delete queue[messageId] + } + }) + } +} + +function storeAndSend (client, packet, cb, cbStorePut) { + debug('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) + var storePacket = packet + var err + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + err = removeTopicAliasAndRecoverTopicName(client, storePacket) + if (err) { + return cb && cb(err) + } + } + client.outgoingStore.put(storePacket, function storedPacket (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + sendPacket(client, packet, cb) + }) +} + +function nop (error) { + debug('nop ::', error) +} + +/** + * MqttClient constructor + * + * @param {Stream} stream - stream + * @param {Object} [options] - connection options + * (see Connection#connect) + */ +function MqttClient (streamBuilder, options) { + var k + var that = this + + if (!(this instanceof MqttClient)) { + return new MqttClient(streamBuilder, options) + } + + this.options = options || {} + + // Defaults + for (k in defaultConnectOptions) { + if (typeof this.options[k] === 'undefined') { + this.options[k] = defaultConnectOptions[k] + } else { + this.options[k] = options[k] + } + } + + debug('MqttClient :: options.protocol', options.protocol) + debug('MqttClient :: options.protocolVersion', options.protocolVersion) + debug('MqttClient :: options.username', options.username) + debug('MqttClient :: options.keepalive', options.keepalive) + debug('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) + debug('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) + debug('MqttClient :: options.topicAliasMaximum', options.topicAliasMaximum) + + this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() + + debug('MqttClient :: clientId', this.options.clientId) + + this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } + + this.streamBuilder = streamBuilder + + this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider + + // Inflight message storages + this.outgoingStore = options.outgoingStore || new Store() + this.incomingStore = options.incomingStore || new Store() + + // Should QoS zero messages be queued when the connection is broken? + this.queueQoSZero = options.queueQoSZero === undefined ? true : options.queueQoSZero + + // map of subscribed topics to support reconnection + this._resubscribeTopics = {} + + // map of a subscribe messageId and a topic + this.messageIdToTopic = {} + + // Ping timer, setup in _setupPingTimer + this.pingTimer = null + // Is the client connected? + this.connected = false + // Are we disconnecting? + this.disconnecting = false + // Packet queue + this.queue = [] + // connack timer + this.connackTimer = null + // Reconnect timer + this.reconnectTimer = null + // Is processing store? + this._storeProcessing = false + // Packet Ids are put into the store during store processing + this._packetIdsDuringStoreProcessing = {} + // Store processing queue + this._storeProcessingQueue = [] + + // Inflight callbacks + this.outgoing = {} + + // True if connection is first time. + this._firstConnection = true + + if (options.topicAliasMaximum > 0) { + if (options.topicAliasMaximum > 0xffff) { + debug('MqttClient :: options.topicAliasMaximum is out of range') + } else { + this.topicAliasRecv = new TopicAliasRecv(options.topicAliasMaximum) + } + } + + // Send queued packets + this.on('connect', function () { + var queue = this.queue + + function deliver () { + var entry = queue.shift() + debug('deliver :: entry %o', entry) + var packet = null + + if (!entry) { + that._resubscribe() + return + } + + packet = entry.packet + debug('deliver :: call _sendPacket for %o', packet) + var send = true + if (packet.messageId && packet.messageId !== 0) { + if (!that.messageIdProvider.register(packet.messageId)) { + send = false + } + } + if (send) { + that._sendPacket( + packet, + function (err) { + if (entry.cb) { + entry.cb(err) + } + deliver() + } + ) + } else { + debug('messageId: %d has already used. The message is skipped and removed.', packet.messageId) + deliver() + } + } + + debug('connect :: sending queued packets') + deliver() + }) + + this.on('close', function () { + debug('close :: connected set to `false`') + this.connected = false + + debug('close :: clearing connackTimer') + clearTimeout(this.connackTimer) + + debug('close :: clearing ping timer') + if (that.pingTimer !== null) { + that.pingTimer.clear() + that.pingTimer = null + } + + if (this.topicAliasRecv) { + this.topicAliasRecv.clear() + } + + debug('close :: calling _setupReconnect') + this._setupReconnect() + }) + EventEmitter.call(this) + + debug('MqttClient :: setting up stream') + this._setupStream() +} +inherits(MqttClient, EventEmitter) + +/** + * setup the event handlers in the inner stream. + * + * @api private + */ +MqttClient.prototype._setupStream = function () { + var connectPacket + var that = this + var writable = new Writable() + var parser = mqttPacket.parser(this.options) + var completeParse = null + var packets = [] + + debug('_setupStream :: calling method to clear reconnect') + this._clearReconnect() + + debug('_setupStream :: using streamBuilder provided to client to create stream') + this.stream = this.streamBuilder(this) + + parser.on('packet', function (packet) { + debug('parser :: on packet push to packets array.') + packets.push(packet) + }) + + function nextTickWork () { + if (packets.length) { + nextTick(work) + } else { + var done = completeParse + completeParse = null + done() + } + } + + function work () { + debug('work :: getting next packet in queue') + var packet = packets.shift() + + if (packet) { + debug('work :: packet pulled from queue') + that._handlePacket(packet, nextTickWork) + } else { + debug('work :: no packets in queue') + var done = completeParse + completeParse = null + debug('work :: done flag is %s', !!(done)) + if (done) done() + } + } + + writable._write = function (buf, enc, done) { + completeParse = done + debug('writable stream :: parsing buffer') + parser.parse(buf) + work() + } + + function streamErrorHandler (error) { + debug('streamErrorHandler :: error', error.message) + if (socketErrors.includes(error.code)) { + // handle error + debug('streamErrorHandler :: emitting error') + that.emit('error', error) + } else { + nop(error) + } + } + + debug('_setupStream :: pipe stream to writable stream') + this.stream.pipe(writable) + + // Suppress connection errors + this.stream.on('error', streamErrorHandler) + + // Echo stream close + this.stream.on('close', function () { + debug('(%s)stream :: on close', that.options.clientId) + flushVolatile(that.outgoing) + debug('stream: emit close to MqttClient') + that.emit('close') + }) + + // Send a connect packet + debug('_setupStream: sending packet `connect`') + connectPacket = Object.create(this.options) + connectPacket.cmd = 'connect' + if (this.topicAliasRecv) { + if (!connectPacket.properties) { + connectPacket.properties = {} + } + if (this.topicAliasRecv) { + connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max + } + } + // avoid message queue + sendPacket(this, connectPacket) + + // Echo connection errors + parser.on('error', this.emit.bind(this, 'error')) + + // auth + if (this.options.properties) { + if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { + that.end(() => + this.emit('error', new Error('Packet has no Authentication Method') + )) + return this + } + if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { + var authPacket = xtend({cmd: 'auth', reasonCode: 0}, this.options.authPacket) + sendPacket(this, authPacket) + } + } + + // many drain listeners are needed for qos 1 callbacks if the connection is intermittent + this.stream.setMaxListeners(1000) + + clearTimeout(this.connackTimer) + this.connackTimer = setTimeout(function () { + debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') + that._cleanUp(true) + }, this.options.connectTimeout) +} + +MqttClient.prototype._handlePacket = function (packet, done) { + var options = this.options + + if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { + this.emit('error', new Error('exceeding packets size ' + packet.cmd)) + this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) + return this + } + debug('_handlePacket :: emitting packetreceive') + this.emit('packetreceive', packet) + + switch (packet.cmd) { + case 'publish': + this._handlePublish(packet, done) + break + case 'puback': + case 'pubrec': + case 'pubcomp': + case 'suback': + case 'unsuback': + this._handleAck(packet) + done() + break + case 'pubrel': + this._handlePubrel(packet, done) + break + case 'connack': + this._handleConnack(packet) + done() + break + case 'pingresp': + this._handlePingresp(packet) + done() + break + case 'disconnect': + this._handleDisconnect(packet) + done() + break + default: + // do nothing + // maybe we should do an error handling + // or just log it + break + } +} + +MqttClient.prototype._checkDisconnecting = function (callback) { + if (this.disconnecting) { + if (callback) { + callback(new Error('client disconnecting')) + } else { + this.emit('error', new Error('client disconnecting')) + } + } + return this.disconnecting +} + +/** + * publish - publish to + * + * @param {String} topic - topic to publish to + * @param {String, Buffer} message - message to publish + * @param {Object} [opts] - publish options, includes: + * {Number} qos - qos level to publish on + * {Boolean} retain - whether or not to retain the message + * {Boolean} dup - whether or not mark a message as duplicate + * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` + * @param {Function} [callback] - function(err){} + * called when publish succeeds or fails + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.publish('topic', 'message'); + * @example + * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); + * @example client.publish('topic', 'message', console.log); + */ +MqttClient.prototype.publish = function (topic, message, opts, callback) { + debug('publish :: message `%s` to topic `%s`', message, topic) + var packet + var options = this.options + + // .publish(topic, payload, cb); + if (typeof opts === 'function') { + callback = opts + opts = null + } + + // default opts + var defaultOpts = {qos: 0, retain: false, dup: false} + opts = xtend(defaultOpts, opts) + + if (this._checkDisconnecting(callback)) { + return this + } + + var that = this + var publishProc = function () { + var messageId = 0 + if (opts.qos === 1 || opts.qos === 2) { + messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + } + packet = { + cmd: 'publish', + topic: topic, + payload: message, + qos: opts.qos, + retain: opts.retain, + messageId: messageId, + dup: opts.dup + } + + if (options.protocolVersion === 5) { + packet.properties = opts.properties + } + + debug('publish :: qos', opts.qos) + switch (opts.qos) { + case 1: + case 2: + // Add to callbacks + that.outgoing[packet.messageId] = { + volatile: false, + cb: callback || nop + } + debug('MqttClient:publish: packet cmd: %s', packet.cmd) + that._sendPacket(packet, undefined, opts.cbStorePut) + break + default: + debug('MqttClient:publish: packet cmd: %s', packet.cmd) + that._sendPacket(packet, callback, opts.cbStorePut) + break + } + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': publishProc, + 'cbStorePut': opts.cbStorePut, + 'callback': callback + } + ) + } else { + publishProc() + } + return this +} + +/** + * subscribe - subscribe to + * + * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} + * @param {Object} [opts] - optional subscription options, includes: + * {Number} qos - subscribe qos level + * @param {Function} [callback] - function(err, granted){} where: + * {Error} err - subscription error (none at the moment!) + * {Array} granted - array of {topic: 't', qos: 0} + * @returns {MqttClient} this - for chaining + * @api public + * @example client.subscribe('topic'); + * @example client.subscribe('topic', {qos: 1}); + * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); + * @example client.subscribe('topic', console.log); + */ +MqttClient.prototype.subscribe = function () { + var that = this + var args = new Array(arguments.length) + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i] + } + var subs = [] + var obj = args.shift() + var resubscribe = obj.resubscribe + var callback = args.pop() || nop + var opts = args.pop() + var version = this.options.protocolVersion + + delete obj.resubscribe + + if (typeof obj === 'string') { + obj = [obj] + } + + if (typeof callback !== 'function') { + opts = callback + callback = nop + } + + var invalidTopic = validations.validateTopics(obj) + if (invalidTopic !== null) { + setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) + return this + } + + if (this._checkDisconnecting(callback)) { + debug('subscribe: discconecting true') + return this + } + + var defaultOpts = { + qos: 0 + } + if (version === 5) { + defaultOpts.nl = false + defaultOpts.rap = false + defaultOpts.rh = 0 + } + opts = xtend(defaultOpts, opts) + + if (Array.isArray(obj)) { + obj.forEach(function (topic) { + debug('subscribe: array topic %s', topic) + if (!that._resubscribeTopics.hasOwnProperty(topic) || + that._resubscribeTopics[topic].qos < opts.qos || + resubscribe) { + var currentOpts = { + topic: topic, + qos: opts.qos + } + if (version === 5) { + currentOpts.nl = opts.nl + currentOpts.rap = opts.rap + currentOpts.rh = opts.rh + currentOpts.properties = opts.properties + } + debug('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) + subs.push(currentOpts) + } + }) + } else { + Object + .keys(obj) + .forEach(function (k) { + debug('subscribe: object topic %s', k) + if (!that._resubscribeTopics.hasOwnProperty(k) || + that._resubscribeTopics[k].qos < obj[k].qos || + resubscribe) { + var currentOpts = { + topic: k, + qos: obj[k].qos + } + if (version === 5) { + currentOpts.nl = obj[k].nl + currentOpts.rap = obj[k].rap + currentOpts.rh = obj[k].rh + currentOpts.properties = opts.properties + } + debug('subscribe: pushing `%s` to subs list', currentOpts) + subs.push(currentOpts) + } + }) + } + + if (!subs.length) { + callback(null, []) + return this + } + + var subscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + + var packet = { + cmd: 'subscribe', + subscriptions: subs, + qos: 1, + retain: false, + dup: false, + messageId: messageId + } + + if (opts.properties) { + packet.properties = opts.properties + } + + // subscriptions to resubscribe to in case of disconnect + if (that.options.resubscribe) { + debug('subscribe :: resubscribe true') + var topics = [] + subs.forEach(function (sub) { + if (that.options.reconnectPeriod > 0) { + var topic = { qos: sub.qos } + if (version === 5) { + topic.nl = sub.nl || false + topic.rap = sub.rap || false + topic.rh = sub.rh || 0 + topic.properties = sub.properties + } + that._resubscribeTopics[sub.topic] = topic + topics.push(sub.topic) + } + }) + that.messageIdToTopic[packet.messageId] = topics + } + + that.outgoing[packet.messageId] = { + volatile: true, + cb: function (err, packet) { + if (!err) { + var granted = packet.granted + for (var i = 0; i < granted.length; i += 1) { + subs[i].qos = granted[i] + } + } + + callback(err, subs) + } + } + debug('subscribe :: call _sendPacket') + that._sendPacket(packet) + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': subscribeProc, + 'callback': callback + } + ) + } else { + subscribeProc() + } + + return this +} + +/** + * unsubscribe - unsubscribe from topic(s) + * + * @param {String, Array} topic - topics to unsubscribe from + * @param {Object} [opts] - optional subscription options, includes: + * {Object} properties - properties of unsubscribe packet + * @param {Function} [callback] - callback fired on unsuback + * @returns {MqttClient} this - for chaining + * @api public + * @example client.unsubscribe('topic'); + * @example client.unsubscribe('topic', console.log); + */ +MqttClient.prototype.unsubscribe = function () { + var that = this + var args = new Array(arguments.length) + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i] + } + var topic = args.shift() + var callback = args.pop() || nop + var opts = args.pop() + if (typeof topic === 'string') { + topic = [topic] + } + + if (typeof callback !== 'function') { + opts = callback + callback = nop + } + + var invalidTopic = validations.validateTopics(topic) + if (invalidTopic !== null) { + setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) + return this + } + + if (that._checkDisconnecting(callback)) { + return this + } + + var unsubscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + var packet = { + cmd: 'unsubscribe', + qos: 1, + messageId: messageId + } + + if (typeof topic === 'string') { + packet.unsubscriptions = [topic] + } else if (Array.isArray(topic)) { + packet.unsubscriptions = topic + } + + if (that.options.resubscribe) { + packet.unsubscriptions.forEach(function (topic) { + delete that._resubscribeTopics[topic] + }) + } + + if (typeof opts === 'object' && opts.properties) { + packet.properties = opts.properties + } + + that.outgoing[packet.messageId] = { + volatile: true, + cb: callback + } + + debug('unsubscribe: call _sendPacket') + that._sendPacket(packet) + + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': unsubscribeProc, + 'callback': callback + } + ) + } else { + unsubscribeProc() + } + + return this +} + +/** + * end - close connection + * + * @returns {MqttClient} this - for chaining + * @param {Boolean} force - do not wait for all in-flight messages to be acked + * @param {Object} opts - added to the disconnect packet + * @param {Function} cb - called when the client has been closed + * + * @api public + */ +MqttClient.prototype.end = function (force, opts, cb) { + var that = this + + debug('end :: (%s)', this.options.clientId) + + if (force == null || typeof force !== 'boolean') { + cb = opts || nop + opts = force + force = false + if (typeof opts !== 'object') { + cb = opts + opts = null + if (typeof cb !== 'function') { + cb = nop + } + } + } + + if (typeof opts !== 'object') { + cb = opts + opts = null + } + + debug('end :: cb? %s', !!cb) + cb = cb || nop + + function closeStores () { + debug('end :: closeStores: closing incoming and outgoing stores') + that.disconnected = true + that.incomingStore.close(function (e1) { + that.outgoingStore.close(function (e2) { + debug('end :: closeStores: emitting end') + that.emit('end') + if (cb) { + let err = e1 || e2 + debug('end :: closeStores: invoking callback with args') + cb(err) + } + }) + }) + if (that._deferredReconnect) { + that._deferredReconnect() + } + } + + function finish () { + // defer closesStores of an I/O cycle, + // just to make sure things are + // ok for websockets + debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) + that._cleanUp(force, () => { + debug('end :: finish :: calling process.nextTick on closeStores') + // var boundProcess = nextTick.bind(null, closeStores) + nextTick(closeStores.bind(that)) + }, opts) + } + + if (this.disconnecting) { + cb() + return this + } + + this._clearReconnect() + + this.disconnecting = true + + if (!force && Object.keys(this.outgoing).length > 0) { + // wait 10ms, just to be sure we received all of it + debug('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) + this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) + } else { + debug('end :: (%s) :: immediately calling finish', that.options.clientId) + finish() + } + + return this +} + +/** + * removeOutgoingMessage - remove a message in outgoing store + * the outgoing callback will be called withe Error('Message removed') if the message is removed + * + * @param {Number} messageId - messageId to remove message + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.removeOutgoingMessage(client.getLastAllocated()); + */ +MqttClient.prototype.removeOutgoingMessage = function (messageId) { + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null + delete this.outgoing[messageId] + this.outgoingStore.del({messageId: messageId}, function () { + cb(new Error('Message removed')) + }) + return this +} + +/** + * reconnect - connect again using the same options as connect() + * + * @param {Object} [opts] - optional reconnect options, includes: + * {Store} incomingStore - a store for the incoming packets + * {Store} outgoingStore - a store for the outgoing packets + * if opts is not given, current stores are used + * @returns {MqttClient} this - for chaining + * + * @api public + */ +MqttClient.prototype.reconnect = function (opts) { + debug('client reconnect') + var that = this + var f = function () { + if (opts) { + that.options.incomingStore = opts.incomingStore + that.options.outgoingStore = opts.outgoingStore + } else { + that.options.incomingStore = null + that.options.outgoingStore = null + } + that.incomingStore = that.options.incomingStore || new Store() + that.outgoingStore = that.options.outgoingStore || new Store() + that.disconnecting = false + that.disconnected = false + that._deferredReconnect = null + that._reconnect() + } + + if (this.disconnecting && !this.disconnected) { + this._deferredReconnect = f + } else { + f() + } + return this +} + +/** + * _reconnect - implement reconnection + * @api privateish + */ +MqttClient.prototype._reconnect = function () { + debug('_reconnect: emitting reconnect to client') + this.emit('reconnect') + if (this.connected) { + this.end(() => { this._setupStream() }) + debug('client already connected. disconnecting first.') + } else { + debug('_reconnect: calling _setupStream') + this._setupStream() + } +} + +/** + * _setupReconnect - setup reconnect timer + */ +MqttClient.prototype._setupReconnect = function () { + var that = this + + if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { + if (!this.reconnecting) { + debug('_setupReconnect :: emit `offline` state') + this.emit('offline') + debug('_setupReconnect :: set `reconnecting` to `true`') + this.reconnecting = true + } + debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) + that.reconnectTimer = setInterval(function () { + debug('reconnectTimer :: reconnect triggered!') + that._reconnect() + }, that.options.reconnectPeriod) + } else { + debug('_setupReconnect :: doing nothing...') + } +} + +/** + * _clearReconnect - clear the reconnect timer + */ +MqttClient.prototype._clearReconnect = function () { + debug('_clearReconnect : clearing reconnect timer') + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer) + this.reconnectTimer = null + } +} + +/** + * _cleanUp - clean up on connection end + * @api private + */ +MqttClient.prototype._cleanUp = function (forced, done) { + var opts = arguments[2] + if (done) { + debug('_cleanUp :: done callback provided for on stream close') + this.stream.on('close', done) + } + + debug('_cleanUp :: forced? %s', forced) + if (forced) { + if ((this.options.reconnectPeriod === 0) && this.options.clean) { + flush(this.outgoing) + } + debug('_cleanUp :: (%s) :: destroying stream', this.options.clientId) + this.stream.destroy() + } else { + var packet = xtend({ cmd: 'disconnect' }, opts) + debug('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) + this._sendPacket( + packet, + setImmediate.bind( + null, + this.stream.end.bind(this.stream) + ) + ) + } + + if (!this.disconnecting) { + debug('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') + this._clearReconnect() + this._setupReconnect() + } + + if (this.pingTimer !== null) { + debug('_cleanUp :: clearing pingTimer') + this.pingTimer.clear() + this.pingTimer = null + } + + if (done && !this.connected) { + debug('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) + this.stream.removeListener('close', done) + done() + } +} + +/** + * _sendPacket - send or queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @api private + */ +MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { + debug('_sendPacket :: (%s) :: start', this.options.clientId) + cbStorePut = cbStorePut || nop + cb = cb || nop + + var err = applyTopicAlias(this, packet) + if (err) { + cb(err) + return + } + + if (!this.connected) { + debug('_sendPacket :: client not connected. Storing packet offline.') + this._storePacket(packet, cb, cbStorePut) + return + } + + // When sending a packet, reschedule the ping timer + this._shiftPingInterval() + + switch (packet.cmd) { + case 'publish': + break + case 'pubrel': + storeAndSend(this, packet, cb, cbStorePut) + return + default: + sendPacket(this, packet, cb) + return + } + + switch (packet.qos) { + case 2: + case 1: + storeAndSend(this, packet, cb, cbStorePut) + break + /** + * no need of case here since it will be caught by default + * and jshint comply that before default it must be a break + * anyway it will result in -1 evaluation + */ + case 0: + /* falls through */ + default: + sendPacket(this, packet, cb) + break + } + debug('_sendPacket :: (%s) :: end', this.options.clientId) +} + +/** + * _storePacket - queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @api private + */ +MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { + debug('_storePacket :: packet: %o', packet) + debug('_storePacket :: cb? %s', !!cb) + cbStorePut = cbStorePut || nop + + var storePacket = packet + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + var err = removeTopicAliasAndRecoverTopicName(this, storePacket) + if (err) { + return cb && cb(err) + } + } + // check that the packet is not a qos of 0, or that the command is not a publish + if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { + this.queue.push({ packet: storePacket, cb: cb }) + } else if (storePacket.qos > 0) { + cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null + this.outgoingStore.put(storePacket, function (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + }) + } else if (cb) { + cb(new Error('No connection to broker')) + } +} + +/** + * _setupPingTimer - setup the ping timer + * + * @api private + */ +MqttClient.prototype._setupPingTimer = function () { + debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) + var that = this + + if (!this.pingTimer && this.options.keepalive) { + this.pingResp = true + this.pingTimer = reInterval(function () { + that._checkPing() + }, this.options.keepalive * 1000) + } +} + +/** + * _shiftPingInterval - reschedule the ping interval + * + * @api private + */ +MqttClient.prototype._shiftPingInterval = function () { + if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { + this.pingTimer.reschedule(this.options.keepalive * 1000) + } +} +/** + * _checkPing - check if a pingresp has come back, and ping the server again + * + * @api private + */ +MqttClient.prototype._checkPing = function () { + debug('_checkPing :: checking ping...') + if (this.pingResp) { + debug('_checkPing :: ping response received. Clearing flag and sending `pingreq`') + this.pingResp = false + this._sendPacket({ cmd: 'pingreq' }) + } else { + // do a forced cleanup since socket will be in bad shape + debug('_checkPing :: calling _cleanUp with force true') + this._cleanUp(true) + } +} + +/** + * _handlePingresp - handle a pingresp + * + * @api private + */ +MqttClient.prototype._handlePingresp = function () { + this.pingResp = true +} + +/** + * _handleConnack + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handleConnack = function (packet) { + debug('_handleConnack') + var options = this.options + var version = options.protocolVersion + var rc = version === 5 ? packet.reasonCode : packet.returnCode + + clearTimeout(this.connackTimer) + delete this.topicAliasSend + + if (packet.properties) { + if (packet.properties.topicAliasMaximum) { + if (packet.properties.topicAliasMaximum > 0xffff) { + this.emit('error', new Error('topicAliasMaximum from broker is out of range')) + return + } + if (packet.properties.topicAliasMaximum > 0) { + this.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) + } + } + if (packet.properties.serverKeepAlive && options.keepalive) { + options.keepalive = packet.properties.serverKeepAlive + this._shiftPingInterval() + } + if (packet.properties.maximumPacketSize) { + if (!options.properties) { options.properties = {} } + options.properties.maximumPacketSize = packet.properties.maximumPacketSize + } + } + + if (rc === 0) { + this.reconnecting = false + this._onConnect(packet) + } else if (rc > 0) { + var err = new Error('Connection refused: ' + errors[rc]) + err.code = rc + this.emit('error', err) + } +} + +/** + * _handlePublish + * + * @param {Object} packet + * @api private + */ +/* +those late 2 case should be rewrite to comply with coding style: + +case 1: +case 0: + // do not wait sending a puback + // no callback passed + if (1 === qos) { + this._sendPacket({ + cmd: 'puback', + messageId: messageId + }); + } + // emit the message event for both qos 1 and 0 + this.emit('message', topic, message, packet); + this.handleMessage(packet, done); + break; +default: + // do nothing but every switch mus have a default + // log or throw an error about unknown qos + break; + +for now i just suppressed the warnings +*/ +MqttClient.prototype._handlePublish = function (packet, done) { + debug('_handlePublish: packet %o', packet) + done = typeof done !== 'undefined' ? done : nop + var topic = packet.topic.toString() + var message = packet.payload + var qos = packet.qos + var messageId = packet.messageId + var that = this + var options = this.options + var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] + if (this.options.protocolVersion === 5) { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + if (typeof alias !== 'undefined') { + if (topic.length === 0) { + if (alias > 0 && alias <= 0xffff) { + var gotTopic = this.topicAliasRecv.getTopicByAlias(alias) + if (gotTopic) { + topic = gotTopic + debug('_handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: unregistered topic alias. alias: %d', alias) + this.emit('error', new Error('Received unregistered Topic Alias')) + return + } + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } else { + if (this.topicAliasRecv.put(topic, alias)) { + debug('_handlePublish :: registered topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } + } + } + debug('_handlePublish: qos %d', qos) + switch (qos) { + case 2: { + options.customHandleAcks(topic, message, packet, function (error, code) { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { return that.emit('error', error) } + if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for pubrec')) } + if (code) { + that._sendPacket({cmd: 'pubrec', messageId: messageId, reasonCode: code}, done) + } else { + that.incomingStore.put(packet, function () { + that._sendPacket({cmd: 'pubrec', messageId: messageId}, done) + }) + } + }) + break + } + case 1: { + // emit the message event + options.customHandleAcks(topic, message, packet, function (error, code) { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { return that.emit('error', error) } + if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for puback')) } + if (!code) { that.emit('message', topic, message, packet) } + that.handleMessage(packet, function (err) { + if (err) { + return done && done(err) + } + that._sendPacket({cmd: 'puback', messageId: messageId, reasonCode: code}, done) + }) + }) + break + } + case 0: + // emit the message event + this.emit('message', topic, message, packet) + this.handleMessage(packet, done) + break + default: + // do nothing + debug('_handlePublish: unknown QoS. Doing nothing.') + // log or throw an error about unknown qos + break + } +} + +/** + * Handle messages with backpressure support, one at a time. + * Override at will. + * + * @param Packet packet the packet + * @param Function callback call when finished + * @api public + */ +MqttClient.prototype.handleMessage = function (packet, callback) { + callback() +} + +/** + * _handleAck + * + * @param {Object} packet + * @api private + */ + +MqttClient.prototype._handleAck = function (packet) { + /* eslint no-fallthrough: "off" */ + var messageId = packet.messageId + var type = packet.cmd + var response = null + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null + var that = this + var err + + if (!cb) { + debug('_handleAck :: Server sent an ack in error. Ignoring.') + // Server sent an ack in error, ignore it. + return + } + + // Process + debug('_handleAck :: packet type', type) + switch (type) { + case 'pubcomp': + // same thing as puback for QoS 2 + case 'puback': + var pubackRC = packet.reasonCode + // Callback - we're done + if (pubackRC && pubackRC > 0 && pubackRC !== 16) { + err = new Error('Publish error: ' + errors[pubackRC]) + err.code = pubackRC + cb(err, packet) + } + delete this.outgoing[messageId] + this.outgoingStore.del(packet, cb) + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() + break + case 'pubrec': + response = { + cmd: 'pubrel', + qos: 2, + messageId: messageId + } + var pubrecRC = packet.reasonCode + + if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { + err = new Error('Publish error: ' + errors[pubrecRC]) + err.code = pubrecRC + cb(err, packet) + } else { + this._sendPacket(response) + } + break + case 'suback': + delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) + for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { + if ((packet.granted[grantedI] & 0x80) !== 0) { + // suback with Failure status + var topics = this.messageIdToTopic[messageId] + if (topics) { + topics.forEach(function (topic) { + delete that._resubscribeTopics[topic] + }) + } + } + } + this._invokeStoreProcessingQueue() + cb(null, packet) + break + case 'unsuback': + delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() + cb(null) + break + default: + that.emit('error', new Error('unrecognized packet type')) + } + + if (this.disconnecting && + Object.keys(this.outgoing).length === 0) { + this.emit('outgoingEmpty') + } +} + +/** + * _handlePubrel + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handlePubrel = function (packet, callback) { + debug('handling pubrel packet') + callback = typeof callback !== 'undefined' ? callback : nop + var messageId = packet.messageId + var that = this + + var comp = {cmd: 'pubcomp', messageId: messageId} + + that.incomingStore.get(packet, function (err, pub) { + if (!err) { + that.emit('message', pub.topic, pub.payload, pub) + that.handleMessage(pub, function (err) { + if (err) { + return callback(err) + } + that.incomingStore.del(pub, nop) + that._sendPacket(comp, callback) + }) + } else { + that._sendPacket(comp, callback) + } + }) +} + +/** + * _handleDisconnect + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handleDisconnect = function (packet) { + this.emit('disconnect', packet) +} + +/** + * _nextId + * @return unsigned int + */ +MqttClient.prototype._nextId = function () { + return this.messageIdProvider.allocate() +} + +/** + * getLastMessageId + * @return unsigned int + */ +MqttClient.prototype.getLastMessageId = function () { + return this.messageIdProvider.getLastAllocated() +} + +/** + * _resubscribe + * @api private + */ +MqttClient.prototype._resubscribe = function () { + debug('_resubscribe') + var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) + if (!this._firstConnection && + (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && + _resubscribeTopicsKeys.length > 0) { + if (this.options.resubscribe) { + if (this.options.protocolVersion === 5) { + debug('_resubscribe: protocolVersion 5') + for (var topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { + var resubscribeTopic = {} + resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] + resubscribeTopic.resubscribe = true + this.subscribe(resubscribeTopic, {properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties}) + } + } else { + this._resubscribeTopics.resubscribe = true + this.subscribe(this._resubscribeTopics) + } + } else { + this._resubscribeTopics = {} + } + } + + this._firstConnection = false +} + +/** + * _onConnect + * + * @api private + */ +MqttClient.prototype._onConnect = function (packet) { + if (this.disconnected) { + this.emit('connect', packet) + return + } + + var that = this + + this.connackPacket = packet + this.messageIdProvider.clear() + this._setupPingTimer() + + this.connected = true + + function startStreamProcess () { + var outStore = that.outgoingStore.createStream() + + function clearStoreProcessing () { + that._storeProcessing = false + that._packetIdsDuringStoreProcessing = {} + } + + that.once('close', remove) + outStore.on('error', function (err) { + clearStoreProcessing() + that._flushStoreProcessingQueue() + that.removeListener('close', remove) + that.emit('error', err) + }) + + function remove () { + outStore.destroy() + outStore = null + that._flushStoreProcessingQueue() + clearStoreProcessing() + } + + function storeDeliver () { + // edge case, we wrapped this twice + if (!outStore) { + return + } + that._storeProcessing = true + + var packet = outStore.read(1) + + var cb + + if (!packet) { + // read when data is available in the future + outStore.once('readable', storeDeliver) + return + } + + // Skip already processed store packets + if (that._packetIdsDuringStoreProcessing[packet.messageId]) { + storeDeliver() + return + } + + // Avoid unnecessary stream read operations when disconnected + if (!that.disconnecting && !that.reconnectTimer) { + cb = that.outgoing[packet.messageId] ? that.outgoing[packet.messageId].cb : null + that.outgoing[packet.messageId] = { + volatile: false, + cb: function (err, status) { + // Ensure that the original callback passed in to publish gets invoked + if (cb) { + cb(err, status) + } + + storeDeliver() + } + } + that._packetIdsDuringStoreProcessing[packet.messageId] = true + if (that.messageIdProvider.register(packet.messageId)) { + that._sendPacket(packet) + } else { + debug('messageId: %d has already used.', packet.messageId) + } + } else if (outStore.destroy) { + outStore.destroy() + } + } + + outStore.on('end', function () { + var allProcessed = true + for (var id in that._packetIdsDuringStoreProcessing) { + if (!that._packetIdsDuringStoreProcessing[id]) { + allProcessed = false + break + } + } + if (allProcessed) { + clearStoreProcessing() + that.removeListener('close', remove) + that._invokeAllStoreProcessingQueue() + that.emit('connect', packet) + } else { + startStreamProcess() + } + }) + storeDeliver() + } + // start flowing + startStreamProcess() +} + +MqttClient.prototype._invokeStoreProcessingQueue = function () { + if (this._storeProcessingQueue.length > 0) { + var f = this._storeProcessingQueue[0] + if (f && f.invoke()) { + this._storeProcessingQueue.shift() + return true + } + } + return false +} + +MqttClient.prototype._invokeAllStoreProcessingQueue = function () { + while (this._invokeStoreProcessingQueue()) {} +} + +MqttClient.prototype._flushStoreProcessingQueue = function () { + for (var f of this._storeProcessingQueue) { + if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) + if (f.callback) f.callback(new Error('Connection closed')) + } + this._storeProcessingQueue.splice(0) +} + +module.exports = MqttClient diff --git a/lib/connect/ali.js b/lib/connect/ali.js index e7fe6a3c5..1cbb726a5 100644 --- a/lib/connect/ali.js +++ b/lib/connect/ali.js @@ -1,128 +1,128 @@ -'use strict' - -var Transform = require('readable-stream').Transform -var duplexify = require('duplexify') - -/* global FileReader */ -var my -var proxy -var stream -var isInitialized = false - -function buildProxy () { - var proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - my.sendSocketMessage({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function () { - next(new Error()) - } - }) - } - proxy._flush = function socketEnd (done) { - my.closeSocket({ - success: function () { - done() - } - }) - } - - return proxy -} - -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } -} - -function buildUrl (opts, client) { - var protocol = opts.protocol === 'alis' ? 'wss' : 'ws' - var url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function bindEventHandler () { - if (isInitialized) return - - isInitialized = true - - my.onSocketOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - my.onSocketMessage(function (res) { - if (typeof res.data === 'string') { - var buffer = Buffer.from(res.data, 'base64') - proxy.push(buffer) - } else { - var reader = new FileReader() - reader.addEventListener('load', function () { - var data = reader.result - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - reader.readAsArrayBuffer(res.data) - } - }) - - my.onSocketClose(function () { - stream.end() - stream.destroy() - }) - - my.onSocketError(function (res) { - stream.destroy(res) - }) -} - -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host - - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } - - var websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - setDefaultOpts(opts) - - var url = buildUrl(opts, client) - my = opts.my - my.connectSocket({ - url: url, - protocols: websocketSubProtocol - }) - - proxy = buildProxy() - stream = duplexify.obj() - - bindEventHandler() - - return stream -} - -module.exports = buildStream +'use strict' + +var Transform = require('readable-stream').Transform +var duplexify = require('duplexify') + +/* global FileReader */ +var my +var proxy +var stream +var isInitialized = false + +function buildProxy () { + var proxy = new Transform() + proxy._write = function (chunk, encoding, next) { + my.sendSocketMessage({ + data: chunk.buffer, + success: function () { + next() + }, + fail: function () { + next(new Error()) + } + }) + } + proxy._flush = function socketEnd (done) { + my.closeSocket({ + success: function () { + done() + } + }) + } + + return proxy +} + +function setDefaultOpts (opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } +} + +function buildUrl (opts, client) { + var protocol = opts.protocol === 'alis' ? 'wss' : 'ws' + var url = protocol + '://' + opts.hostname + opts.path + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path + } + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function bindEventHandler () { + if (isInitialized) return + + isInitialized = true + + my.onSocketOpen(function () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + my.onSocketMessage(function (res) { + if (typeof res.data === 'string') { + var buffer = Buffer.from(res.data, 'base64') + proxy.push(buffer) + } else { + var reader = new FileReader() + reader.addEventListener('load', function () { + var data = reader.result + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + reader.readAsArrayBuffer(res.data) + } + }) + + my.onSocketClose(function () { + stream.end() + stream.destroy() + }) + + my.onSocketError(function (res) { + stream.destroy(res) + }) +} + +function buildStream (client, opts) { + opts.hostname = opts.hostname || opts.host + + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } + + var websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + setDefaultOpts(opts) + + var url = buildUrl(opts, client) + my = opts.my + my.connectSocket({ + url: url, + protocols: websocketSubProtocol + }) + + proxy = buildProxy() + stream = duplexify.obj() + + bindEventHandler() + + return stream +} + +module.exports = buildStream diff --git a/lib/connect/index.js b/lib/connect/index.js index 97e7b4c15..9fc151c75 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -1,164 +1,164 @@ -'use strict' - -var MqttClient = require('../client') -var Store = require('../store') -var url = require('url') -var xtend = require('xtend') -var debug = require('debug')('mqttjs') - -var protocols = {} - -// eslint-disable-next-line camelcase -if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ !== 'function') { - protocols.mqtt = require('./tcp') - protocols.tcp = require('./tcp') - protocols.ssl = require('./tls') - protocols.tls = require('./tls') - protocols.mqtts = require('./tls') -} else { - protocols.wx = require('./wx') - protocols.wxs = require('./wx') - - protocols.ali = require('./ali') - protocols.alis = require('./ali') -} - -protocols.ws = require('./ws') -protocols.wss = require('./ws') - -/** - * Parse the auth attribute and merge username and password in the options object. - * - * @param {Object} [opts] option object - */ -function parseAuthOptions (opts) { - var matches - if (opts.auth) { - matches = opts.auth.match(/^(.+):(.+)$/) - if (matches) { - opts.username = matches[1] - opts.password = matches[2] - } else { - opts.username = opts.auth - } - } -} - -/** - * connect - connect to an MQTT broker. - * - * @param {String} [brokerUrl] - url of the broker, optional - * @param {Object} opts - see MqttClient#constructor - */ -function connect (brokerUrl, opts) { - debug('connecting to an MQTT broker...') - if ((typeof brokerUrl === 'object') && !opts) { - opts = brokerUrl - brokerUrl = null - } - - opts = opts || {} - - if (brokerUrl) { - var parsed = url.parse(brokerUrl, true) - if (parsed.port != null) { - parsed.port = Number(parsed.port) - } - - opts = xtend(parsed, opts) - - if (opts.protocol === null) { - throw new Error('Missing protocol') - } - - opts.protocol = opts.protocol.replace(/:$/, '') - } - - // merge in the auth options if supplied - parseAuthOptions(opts) - - // support clientId passed in the query string of the url - if (opts.query && typeof opts.query.clientId === 'string') { - opts.clientId = opts.query.clientId - } - - if (opts.cert && opts.key) { - if (opts.protocol) { - if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { - switch (opts.protocol) { - case 'mqtt': - opts.protocol = 'mqtts' - break - case 'ws': - opts.protocol = 'wss' - break - case 'wx': - opts.protocol = 'wxs' - break - case 'ali': - opts.protocol = 'alis' - break - default: - throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!') - } - } - } else { - // A cert and key was provided, however no protocol was specified, so we will throw an error. - throw new Error('Missing secure protocol key') - } - } - - if (!protocols[opts.protocol]) { - var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 - opts.protocol = [ - 'mqtt', - 'mqtts', - 'ws', - 'wss', - 'wx', - 'wxs', - 'ali', - 'alis' - ].filter(function (key, index) { - if (isSecure && index % 2 === 0) { - // Skip insecure protocols when requesting a secure one. - return false - } - return (typeof protocols[key] === 'function') - })[0] - } - - if (opts.clean === false && !opts.clientId) { - throw new Error('Missing clientId for unclean clients') - } - - if (opts.protocol) { - opts.defaultProtocol = opts.protocol - } - - function wrapper (client) { - if (opts.servers) { - if (!client._reconnectCount || client._reconnectCount === opts.servers.length) { - client._reconnectCount = 0 - } - - opts.host = opts.servers[client._reconnectCount].host - opts.port = opts.servers[client._reconnectCount].port - opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol) - opts.hostname = opts.host - - client._reconnectCount++ - } - - debug('calling streambuilder for', opts.protocol) - return protocols[opts.protocol](client, opts) - } - var client = new MqttClient(wrapper, opts) - client.on('error', function () { /* Automatically set up client error handling */ }) - return client -} - -module.exports = connect -module.exports.connect = connect -module.exports.MqttClient = MqttClient -module.exports.Store = Store +'use strict' + +var MqttClient = require('../client') +var Store = require('../store') +var url = require('url') +var xtend = require('xtend') +var debug = require('debug')('mqttjs') + +var protocols = {} + +// eslint-disable-next-line camelcase +if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ !== 'function') { + protocols.mqtt = require('./tcp') + protocols.tcp = require('./tcp') + protocols.ssl = require('./tls') + protocols.tls = require('./tls') + protocols.mqtts = require('./tls') +} else { + protocols.wx = require('./wx') + protocols.wxs = require('./wx') + + protocols.ali = require('./ali') + protocols.alis = require('./ali') +} + +protocols.ws = require('./ws') +protocols.wss = require('./ws') + +/** + * Parse the auth attribute and merge username and password in the options object. + * + * @param {Object} [opts] option object + */ +function parseAuthOptions (opts) { + var matches + if (opts.auth) { + matches = opts.auth.match(/^(.+):(.+)$/) + if (matches) { + opts.username = matches[1] + opts.password = matches[2] + } else { + opts.username = opts.auth + } + } +} + +/** + * connect - connect to an MQTT broker. + * + * @param {String} [brokerUrl] - url of the broker, optional + * @param {Object} opts - see MqttClient#constructor + */ +function connect (brokerUrl, opts) { + debug('connecting to an MQTT broker...') + if ((typeof brokerUrl === 'object') && !opts) { + opts = brokerUrl + brokerUrl = null + } + + opts = opts || {} + + if (brokerUrl) { + var parsed = url.parse(brokerUrl, true) + if (parsed.port != null) { + parsed.port = Number(parsed.port) + } + + opts = xtend(parsed, opts) + + if (opts.protocol === null) { + throw new Error('Missing protocol') + } + + opts.protocol = opts.protocol.replace(/:$/, '') + } + + // merge in the auth options if supplied + parseAuthOptions(opts) + + // support clientId passed in the query string of the url + if (opts.query && typeof opts.query.clientId === 'string') { + opts.clientId = opts.query.clientId + } + + if (opts.cert && opts.key) { + if (opts.protocol) { + if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { + switch (opts.protocol) { + case 'mqtt': + opts.protocol = 'mqtts' + break + case 'ws': + opts.protocol = 'wss' + break + case 'wx': + opts.protocol = 'wxs' + break + case 'ali': + opts.protocol = 'alis' + break + default: + throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!') + } + } + } else { + // A cert and key was provided, however no protocol was specified, so we will throw an error. + throw new Error('Missing secure protocol key') + } + } + + if (!protocols[opts.protocol]) { + var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 + opts.protocol = [ + 'mqtt', + 'mqtts', + 'ws', + 'wss', + 'wx', + 'wxs', + 'ali', + 'alis' + ].filter(function (key, index) { + if (isSecure && index % 2 === 0) { + // Skip insecure protocols when requesting a secure one. + return false + } + return (typeof protocols[key] === 'function') + })[0] + } + + if (opts.clean === false && !opts.clientId) { + throw new Error('Missing clientId for unclean clients') + } + + if (opts.protocol) { + opts.defaultProtocol = opts.protocol + } + + function wrapper (client) { + if (opts.servers) { + if (!client._reconnectCount || client._reconnectCount === opts.servers.length) { + client._reconnectCount = 0 + } + + opts.host = opts.servers[client._reconnectCount].host + opts.port = opts.servers[client._reconnectCount].port + opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol) + opts.hostname = opts.host + + client._reconnectCount++ + } + + debug('calling streambuilder for', opts.protocol) + return protocols[opts.protocol](client, opts) + } + var client = new MqttClient(wrapper, opts) + client.on('error', function () { /* Automatically set up client error handling */ }) + return client +} + +module.exports = connect +module.exports.connect = connect +module.exports.MqttClient = MqttClient +module.exports.Store = Store diff --git a/lib/connect/tcp.js b/lib/connect/tcp.js index 9912102eb..3fe2c0922 100644 --- a/lib/connect/tcp.js +++ b/lib/connect/tcp.js @@ -1,21 +1,21 @@ -'use strict' -var net = require('net') -var debug = require('debug')('mqttjs:tcp') - -/* - variables port and host can be removed since - you have all required information in opts object -*/ -function streamBuilder (client, opts) { - var port, host - opts.port = opts.port || 1883 - opts.hostname = opts.hostname || opts.host || 'localhost' - - port = opts.port - host = opts.hostname - - debug('port %d and host %s', port, host) - return net.createConnection(port, host) -} - -module.exports = streamBuilder +'use strict' +var net = require('net') +var debug = require('debug')('mqttjs:tcp') + +/* + variables port and host can be removed since + you have all required information in opts object +*/ +function streamBuilder (client, opts) { + var port, host + opts.port = opts.port || 1883 + opts.hostname = opts.hostname || opts.host || 'localhost' + + port = opts.port + host = opts.hostname + + debug('port %d and host %s', port, host) + return net.createConnection(port, host) +} + +module.exports = streamBuilder diff --git a/lib/connect/tls.js b/lib/connect/tls.js index aac296666..226bff8b3 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -1,45 +1,45 @@ -'use strict' -var tls = require('tls') -var debug = require('debug')('mqttjs:tls') - -function buildBuilder (mqttClient, opts) { - var connection - opts.port = opts.port || 8883 - opts.host = opts.hostname || opts.host || 'localhost' - opts.servername = opts.host - - opts.rejectUnauthorized = opts.rejectUnauthorized !== false - - delete opts.path - - debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) - - connection = tls.connect(opts) - /* eslint no-use-before-define: [2, "nofunc"] */ - connection.on('secureConnect', function () { - if (opts.rejectUnauthorized && !connection.authorized) { - connection.emit('error', new Error('TLS not authorized')) - } else { - connection.removeListener('error', handleTLSerrors) - } - }) - - function handleTLSerrors (err) { - // How can I get verify this error is a tls error? - if (opts.rejectUnauthorized) { - mqttClient.emit('error', err) - } - - // close this connection to match the behaviour of net - // otherwise all we get is an error from the connection - // and close event doesn't fire. This is a work around - // to enable the reconnect code to work the same as with - // net.createConnection - connection.end() - } - - connection.on('error', handleTLSerrors) - return connection -} - -module.exports = buildBuilder +'use strict' +var tls = require('tls') +var debug = require('debug')('mqttjs:tls') + +function buildBuilder (mqttClient, opts) { + var connection + opts.port = opts.port || 8883 + opts.host = opts.hostname || opts.host || 'localhost' + opts.servername = opts.host + + opts.rejectUnauthorized = opts.rejectUnauthorized !== false + + delete opts.path + + debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) + + connection = tls.connect(opts) + /* eslint no-use-before-define: [2, "nofunc"] */ + connection.on('secureConnect', function () { + if (opts.rejectUnauthorized && !connection.authorized) { + connection.emit('error', new Error('TLS not authorized')) + } else { + connection.removeListener('error', handleTLSerrors) + } + }) + + function handleTLSerrors (err) { + // How can I get verify this error is a tls error? + if (opts.rejectUnauthorized) { + mqttClient.emit('error', err) + } + + // close this connection to match the behaviour of net + // otherwise all we get is an error from the connection + // and close event doesn't fire. This is a work around + // to enable the reconnect code to work the same as with + // net.createConnection + connection.end() + } + + connection.on('error', handleTLSerrors) + return connection +} + +module.exports = buildBuilder diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 5c1d2c691..18646a5a1 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,256 +1,256 @@ -'use strict' - -const WS = require('ws') -const debug = require('debug')('mqttjs:ws') -const duplexify = require('duplexify') -const Transform = require('readable-stream').Transform - -let WSS_OPTIONS = [ - 'rejectUnauthorized', - 'ca', - 'cert', - 'key', - 'pfx', - 'passphrase' -] -// eslint-disable-next-line camelcase -const IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' -function buildUrl (opts, client) { - let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function setDefaultOpts (opts) { - let options = opts - if (!opts.hostname) { - options.hostname = 'localhost' - } - if (!opts.port) { - if (opts.protocol === 'wss') { - options.port = 443 - } else { - options.port = 80 - } - } - if (!opts.path) { - options.path = '/' - } - - if (!opts.wsOptions) { - options.wsOptions = {} - } - if (!IS_BROWSER && opts.protocol === 'wss') { - // Add cert/key/ca etc options - WSS_OPTIONS.forEach(function (prop) { - if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) { - options.wsOptions[prop] = opts[prop] - } - }) - } - - return options -} - -function setDefaultBrowserOpts (opts) { - let options = setDefaultOpts(opts) - - if (!options.hostname) { - options.hostname = options.host - } - - if (!options.hostname) { - // Throwing an error in a Web Worker if no `hostname` is given, because we - // can not determine the `hostname` automatically. If connecting to - // localhost, please supply the `hostname` as an argument. - if (typeof (document) === 'undefined') { - throw new Error('Could not determine host. Specify host manually.') - } - const parsed = new URL(document.URL) - options.hostname = parsed.hostname - - if (!options.port) { - options.port = parsed.port - } - } - - // objectMode should be defined for logic - if (options.objectMode === undefined) { - options.objectMode = !(options.binary === true || options.binary === undefined) - } - - return options -} - -function createWebSocket (client, url, opts) { - debug('createWebSocket') - debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) - let socket = new WS(url, [websocketSubProtocol], opts.wsOptions) - return socket -} - -function createBrowserWebSocket (client, opts) { - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - let url = buildUrl(opts, client) - /* global WebSocket */ - let socket = new WebSocket(url, [websocketSubProtocol]) - socket.binaryType = 'arraybuffer' - return socket -} - -function streamBuilder (client, opts) { - debug('streamBuilder') - let options = setDefaultOpts(opts) - const url = buildUrl(options, client) - let socket = createWebSocket(client, url, options) - let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) - webSocketStream.url = url - socket.on('close', () => { webSocketStream.destroy() }) - return webSocketStream -} - -function browserStreamBuilder (client, opts) { - debug('browserStreamBuilder') - let stream - let options = setDefaultBrowserOpts(opts) - // sets the maximum socket buffer size before throttling - const bufferSize = options.browserBufferSize || 1024 * 512 - - const bufferTimeout = opts.browserBufferTimeout || 1000 - - const coerceToBuffer = !opts.objectMode - - let socket = createBrowserWebSocket(client, opts) - - let proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) - - if (!opts.objectMode) { - proxy._writev = writev - } - proxy.on('close', () => { socket.close() }) - - const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') - - // was already open when passed in - if (socket.readyState === socket.OPEN) { - stream = proxy - } else { - stream = stream = duplexify(undefined, undefined, opts) - if (!opts.objectMode) { - stream._writev = writev - } - - if (eventListenerSupport) { - socket.addEventListener('open', onopen) - } else { - socket.onopen = onopen - } - } - - stream.socket = socket - - if (eventListenerSupport) { - socket.addEventListener('close', onclose) - socket.addEventListener('error', onerror) - socket.addEventListener('message', onmessage) - } else { - socket.onclose = onclose - socket.onerror = onerror - socket.onmessage = onmessage - } - - // methods for browserStreamBuilder - - function buildProxy (options, socketWrite, socketEnd) { - let proxy = new Transform({ - objectModeMode: options.objectMode - }) - - proxy._write = socketWrite - proxy._flush = socketEnd - - return proxy - } - - function onopen () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - } - - function onclose () { - stream.end() - stream.destroy() - } - - function onerror (err) { - stream.destroy(err) - } - - function onmessage (event) { - let data = event.data - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - } - - // this is to be enabled only if objectMode is false - function writev (chunks, cb) { - const buffers = new Array(chunks.length) - for (let i = 0; i < chunks.length; i++) { - if (typeof chunks[i].chunk === 'string') { - buffers[i] = Buffer.from(chunks[i], 'utf8') - } else { - buffers[i] = chunks[i].chunk - } - } - - this._write(Buffer.concat(buffers), 'binary', cb) - } - - function socketWriteBrowser (chunk, enc, next) { - if (socket.bufferedAmount > bufferSize) { - // throttle data until buffered amount is reduced. - setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) - } - - if (coerceToBuffer && typeof chunk === 'string') { - chunk = Buffer.from(chunk, 'utf8') - } - - try { - socket.send(chunk) - } catch (err) { - return next(err) - } - - next() - } - - function socketEndBrowser (done) { - socket.close() - done() - } - - // end methods for browserStreamBuilder - - return stream -} - -if (IS_BROWSER) { - module.exports = browserStreamBuilder -} else { - module.exports = streamBuilder -} +'use strict' + +const WS = require('ws') +const debug = require('debug')('mqttjs:ws') +const duplexify = require('duplexify') +const Transform = require('readable-stream').Transform + +let WSS_OPTIONS = [ + 'rejectUnauthorized', + 'ca', + 'cert', + 'key', + 'pfx', + 'passphrase' +] +// eslint-disable-next-line camelcase +const IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' +function buildUrl (opts, client) { + let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function setDefaultOpts (opts) { + let options = opts + if (!opts.hostname) { + options.hostname = 'localhost' + } + if (!opts.port) { + if (opts.protocol === 'wss') { + options.port = 443 + } else { + options.port = 80 + } + } + if (!opts.path) { + options.path = '/' + } + + if (!opts.wsOptions) { + options.wsOptions = {} + } + if (!IS_BROWSER && opts.protocol === 'wss') { + // Add cert/key/ca etc options + WSS_OPTIONS.forEach(function (prop) { + if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) { + options.wsOptions[prop] = opts[prop] + } + }) + } + + return options +} + +function setDefaultBrowserOpts (opts) { + let options = setDefaultOpts(opts) + + if (!options.hostname) { + options.hostname = options.host + } + + if (!options.hostname) { + // Throwing an error in a Web Worker if no `hostname` is given, because we + // can not determine the `hostname` automatically. If connecting to + // localhost, please supply the `hostname` as an argument. + if (typeof (document) === 'undefined') { + throw new Error('Could not determine host. Specify host manually.') + } + const parsed = new URL(document.URL) + options.hostname = parsed.hostname + + if (!options.port) { + options.port = parsed.port + } + } + + // objectMode should be defined for logic + if (options.objectMode === undefined) { + options.objectMode = !(options.binary === true || options.binary === undefined) + } + + return options +} + +function createWebSocket (client, url, opts) { + debug('createWebSocket') + debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) + let socket = new WS(url, [websocketSubProtocol], opts.wsOptions) + return socket +} + +function createBrowserWebSocket (client, opts) { + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + let url = buildUrl(opts, client) + /* global WebSocket */ + let socket = new WebSocket(url, [websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + return socket +} + +function streamBuilder (client, opts) { + debug('streamBuilder') + let options = setDefaultOpts(opts) + const url = buildUrl(options, client) + let socket = createWebSocket(client, url, options) + let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) + webSocketStream.url = url + socket.on('close', () => { webSocketStream.destroy() }) + return webSocketStream +} + +function browserStreamBuilder (client, opts) { + debug('browserStreamBuilder') + let stream + let options = setDefaultBrowserOpts(opts) + // sets the maximum socket buffer size before throttling + const bufferSize = options.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + let socket = createBrowserWebSocket(client, opts) + + let proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) + + if (!opts.objectMode) { + proxy._writev = writev + } + proxy.on('close', () => { socket.close() }) + + const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') + + // was already open when passed in + if (socket.readyState === socket.OPEN) { + stream = proxy + } else { + stream = stream = duplexify(undefined, undefined, opts) + if (!opts.objectMode) { + stream._writev = writev + } + + if (eventListenerSupport) { + socket.addEventListener('open', onopen) + } else { + socket.onopen = onopen + } + } + + stream.socket = socket + + if (eventListenerSupport) { + socket.addEventListener('close', onclose) + socket.addEventListener('error', onerror) + socket.addEventListener('message', onmessage) + } else { + socket.onclose = onclose + socket.onerror = onerror + socket.onmessage = onmessage + } + + // methods for browserStreamBuilder + + function buildProxy (options, socketWrite, socketEnd) { + let proxy = new Transform({ + objectModeMode: options.objectMode + }) + + proxy._write = socketWrite + proxy._flush = socketEnd + + return proxy + } + + function onopen () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + } + + function onclose () { + stream.end() + stream.destroy() + } + + function onerror (err) { + stream.destroy(err) + } + + function onmessage (event) { + let data = event.data + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + } + + // this is to be enabled only if objectMode is false + function writev (chunks, cb) { + const buffers = new Array(chunks.length) + for (let i = 0; i < chunks.length; i++) { + if (typeof chunks[i].chunk === 'string') { + buffers[i] = Buffer.from(chunks[i], 'utf8') + } else { + buffers[i] = chunks[i].chunk + } + } + + this._write(Buffer.concat(buffers), 'binary', cb) + } + + function socketWriteBrowser (chunk, enc, next) { + if (socket.bufferedAmount > bufferSize) { + // throttle data until buffered amount is reduced. + setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) + } + + if (coerceToBuffer && typeof chunk === 'string') { + chunk = Buffer.from(chunk, 'utf8') + } + + try { + socket.send(chunk) + } catch (err) { + return next(err) + } + + next() + } + + function socketEndBrowser (done) { + socket.close() + done() + } + + // end methods for browserStreamBuilder + + return stream +} + +if (IS_BROWSER) { + module.exports = browserStreamBuilder +} else { + module.exports = streamBuilder +} diff --git a/lib/connect/wx.js b/lib/connect/wx.js index b9c7a0705..2b675079a 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -1,134 +1,134 @@ -'use strict' - -var Transform = require('readable-stream').Transform -var duplexify = require('duplexify') - -/* global wx */ -var socketTask -var proxy -var stream - -function buildProxy () { - var proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - socketTask.send({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function (errMsg) { - next(new Error(errMsg)) - } - }) - } - proxy._flush = function socketEnd (done) { - socketTask.close({ - success: function () { - done() - } - }) - } - - return proxy -} - -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } -} - -function buildUrl (opts, client) { - var protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' - var url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function bindEventHandler () { - socketTask.onOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - socketTask.onMessage(function (res) { - var data = res.data - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - - socketTask.onClose(function () { - stream.end() - stream.destroy() - }) - - socketTask.onError(function (res) { - stream.destroy(new Error(res.errMsg)) - }) -} - -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host - - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } - - var websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - setDefaultOpts(opts) - - var url = buildUrl(opts, client) - socketTask = wx.connectSocket({ - url: url, - protocols: [websocketSubProtocol] - }) - - proxy = buildProxy() - stream = duplexify.obj() - stream._destroy = function (err, cb) { - socketTask.close({ - success: function () { - cb && cb(err) - } - }) - } - - var destroyRef = stream.destroy - stream.destroy = function () { - stream.destroy = destroyRef - - var self = this - setTimeout(function () { - socketTask.close({ - fail: function () { - self._destroy(new Error()) - } - }) - }, 0) - }.bind(stream) - - bindEventHandler() - - return stream -} - -module.exports = buildStream +'use strict' + +var Transform = require('readable-stream').Transform +var duplexify = require('duplexify') + +/* global wx */ +var socketTask +var proxy +var stream + +function buildProxy () { + var proxy = new Transform() + proxy._write = function (chunk, encoding, next) { + socketTask.send({ + data: chunk.buffer, + success: function () { + next() + }, + fail: function (errMsg) { + next(new Error(errMsg)) + } + }) + } + proxy._flush = function socketEnd (done) { + socketTask.close({ + success: function () { + done() + } + }) + } + + return proxy +} + +function setDefaultOpts (opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } +} + +function buildUrl (opts, client) { + var protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' + var url = protocol + '://' + opts.hostname + opts.path + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path + } + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function bindEventHandler () { + socketTask.onOpen(function () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + socketTask.onMessage(function (res) { + var data = res.data + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + + socketTask.onClose(function () { + stream.end() + stream.destroy() + }) + + socketTask.onError(function (res) { + stream.destroy(new Error(res.errMsg)) + }) +} + +function buildStream (client, opts) { + opts.hostname = opts.hostname || opts.host + + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } + + var websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + setDefaultOpts(opts) + + var url = buildUrl(opts, client) + socketTask = wx.connectSocket({ + url: url, + protocols: [websocketSubProtocol] + }) + + proxy = buildProxy() + stream = duplexify.obj() + stream._destroy = function (err, cb) { + socketTask.close({ + success: function () { + cb && cb(err) + } + }) + } + + var destroyRef = stream.destroy + stream.destroy = function () { + stream.destroy = destroyRef + + var self = this + setTimeout(function () { + socketTask.close({ + fail: function () { + self._destroy(new Error()) + } + }) + }, 0) + }.bind(stream) + + bindEventHandler() + + return stream +} + +module.exports = buildStream diff --git a/lib/default-message-id-provider.js b/lib/default-message-id-provider.js index c0a953f3f..d1bcc9ed0 100644 --- a/lib/default-message-id-provider.js +++ b/lib/default-message-id-provider.js @@ -1,69 +1,69 @@ -'use strict' - -/** - * DefaultMessageAllocator constructor - * @constructor - */ -function DefaultMessageIdProvider () { - if (!(this instanceof DefaultMessageIdProvider)) { - return new DefaultMessageIdProvider() - } - - /** - * MessageIDs starting with 1 - * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 - */ - this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) -} - -/** - * allocate - * - * Get the next messageId. - * @return unsigned int - */ -DefaultMessageIdProvider.prototype.allocate = function () { - // id becomes current state of this.nextId and increments afterwards - var id = this.nextId++ - // Ensure 16 bit unsigned int (max 65535, nextId got one higher) - if (this.nextId === 65536) { - this.nextId = 1 - } - return id -} - -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -DefaultMessageIdProvider.prototype.getLastAllocated = function () { - return (this.nextId === 1) ? 65535 : (this.nextId - 1) -} - -/** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ -DefaultMessageIdProvider.prototype.register = function (messageId) { - return true -} - -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -DefaultMessageIdProvider.prototype.deallocate = function (messageId) { -} - -/** - * clear - * Deallocate all messageIds. - */ -DefaultMessageIdProvider.prototype.clear = function () { -} - -module.exports = DefaultMessageIdProvider +'use strict' + +/** + * DefaultMessageAllocator constructor + * @constructor + */ +function DefaultMessageIdProvider () { + if (!(this instanceof DefaultMessageIdProvider)) { + return new DefaultMessageIdProvider() + } + + /** + * MessageIDs starting with 1 + * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 + */ + this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) +} + +/** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.allocate = function () { + // id becomes current state of this.nextId and increments afterwards + var id = this.nextId++ + // Ensure 16 bit unsigned int (max 65535, nextId got one higher) + if (this.nextId === 65536) { + this.nextId = 1 + } + return id +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.getLastAllocated = function () { + return (this.nextId === 1) ? 65535 : (this.nextId - 1) +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +DefaultMessageIdProvider.prototype.register = function (messageId) { + return true +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +DefaultMessageIdProvider.prototype.deallocate = function (messageId) { +} + +/** + * clear + * Deallocate all messageIds. + */ +DefaultMessageIdProvider.prototype.clear = function () { +} + +module.exports = DefaultMessageIdProvider diff --git a/lib/store.js b/lib/store.js index efbfabf09..37809750b 100644 --- a/lib/store.js +++ b/lib/store.js @@ -1,128 +1,128 @@ -'use strict' - -/** - * Module dependencies - */ -var xtend = require('xtend') - -var Readable = require('readable-stream').Readable -var streamsOpts = { objectMode: true } -var defaultStoreOptions = { - clean: true -} - -/** - * In-memory implementation of the message store - * This can actually be saved into files. - * - * @param {Object} [options] - store options - */ -function Store (options) { - if (!(this instanceof Store)) { - return new Store(options) - } - - this.options = options || {} - - // Defaults - this.options = xtend(defaultStoreOptions, options) - - this._inflights = new Map() -} - -/** - * Adds a packet to the store, a packet is - * anything that has a messageId property. - * - */ -Store.prototype.put = function (packet, cb) { - this._inflights.set(packet.messageId, packet) - - if (cb) { - cb() - } - - return this -} - -/** - * Creates a stream with all the packets in the store - * - */ -Store.prototype.createStream = function () { - var stream = new Readable(streamsOpts) - var destroyed = false - var values = [] - var i = 0 - - this._inflights.forEach(function (value, key) { - values.push(value) - }) - - stream._read = function () { - if (!destroyed && i < values.length) { - this.push(values[i++]) - } else { - this.push(null) - } - } - - stream.destroy = function () { - if (destroyed) { - return - } - - var self = this - - destroyed = true - - setTimeout(function () { - self.emit('close') - }, 0) - } - - return stream -} - -/** - * deletes a packet from the store. - */ -Store.prototype.del = function (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - this._inflights.delete(packet.messageId) - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this -} - -/** - * get a packet from the store. - */ -Store.prototype.get = function (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this -} - -/** - * Close the store - */ -Store.prototype.close = function (cb) { - if (this.options.clean) { - this._inflights = null - } - if (cb) { - cb() - } -} - -module.exports = Store +'use strict' + +/** + * Module dependencies + */ +var xtend = require('xtend') + +var Readable = require('readable-stream').Readable +var streamsOpts = { objectMode: true } +var defaultStoreOptions = { + clean: true +} + +/** + * In-memory implementation of the message store + * This can actually be saved into files. + * + * @param {Object} [options] - store options + */ +function Store (options) { + if (!(this instanceof Store)) { + return new Store(options) + } + + this.options = options || {} + + // Defaults + this.options = xtend(defaultStoreOptions, options) + + this._inflights = new Map() +} + +/** + * Adds a packet to the store, a packet is + * anything that has a messageId property. + * + */ +Store.prototype.put = function (packet, cb) { + this._inflights.set(packet.messageId, packet) + + if (cb) { + cb() + } + + return this +} + +/** + * Creates a stream with all the packets in the store + * + */ +Store.prototype.createStream = function () { + var stream = new Readable(streamsOpts) + var destroyed = false + var values = [] + var i = 0 + + this._inflights.forEach(function (value, key) { + values.push(value) + }) + + stream._read = function () { + if (!destroyed && i < values.length) { + this.push(values[i++]) + } else { + this.push(null) + } + } + + stream.destroy = function () { + if (destroyed) { + return + } + + var self = this + + destroyed = true + + setTimeout(function () { + self.emit('close') + }, 0) + } + + return stream +} + +/** + * deletes a packet from the store. + */ +Store.prototype.del = function (packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + this._inflights.delete(packet.messageId) + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this +} + +/** + * get a packet from the store. + */ +Store.prototype.get = function (packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this +} + +/** + * Close the store + */ +Store.prototype.close = function (cb) { + if (this.options.clean) { + this._inflights = null + } + if (cb) { + cb() + } +} + +module.exports = Store diff --git a/lib/topic-alias-send.js b/lib/topic-alias-send.js index f3abf2084..71b10468a 100644 --- a/lib/topic-alias-send.js +++ b/lib/topic-alias-send.js @@ -1,93 +1,93 @@ -'use strict' - -/** - * Module dependencies - */ -var LruMap = require('collections/lru-map') -var NumberAllocator = require('number-allocator').NumberAllocator - -/** - * Topic Alias sending manager - * This holds both topic to alias and alias to topic map - * @param {Number} [max] - topic alias maximum entries - */ -function TopicAliasSend (max) { - if (!(this instanceof TopicAliasSend)) { - return new TopicAliasSend(max) - } - - if (max > 0) { - this.aliasToTopic = new LruMap() - this.topicToAlias = {} - this.numberAllocator = new NumberAllocator(1, max) - this.max = max - this.length = 0 - } -} - -/** - * Insert or update topic - alias entry. - * @param {String} [topic] - topic - * @param {Number} [alias] - topic alias - * @returns {Boolean} - if success return true otherwise false - */ -TopicAliasSend.prototype.put = function (topic, alias) { - if (alias === 0 || alias > this.max) { - return false - } - const entry = this.aliasToTopic.get(alias) - if (entry) { - delete this.topicToAlias[entry.topic] - } - this.aliasToTopic.set(alias, {'topic': topic, 'alias': alias}) - this.topicToAlias[topic] = alias - this.numberAllocator.use(alias) - this.length = this.aliasToTopic.length - return true -} - -/** - * Get topic by alias - * @param {Number} [alias] - topic alias - * @returns {String} - if mapped topic exists return topic, otherwise return undefined - */ -TopicAliasSend.prototype.getTopicByAlias = function (alias) { - const entry = this.aliasToTopic.get(alias) - if (typeof entry === 'undefined') return entry - return entry.topic -} - -/** - * Get topic by alias - * @param {String} [topic] - topic - * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined - */ -TopicAliasSend.prototype.getAliasByTopic = function (topic) { - const alias = this.topicToAlias[topic] - if (typeof alias !== 'undefined') { - this.aliasToTopic.get(alias) // LRU update - } - return alias -} - -/** - * Clear all entries - */ -TopicAliasSend.prototype.clear = function () { - this.aliasToTopic.clear() - this.topicToAlias = {} - this.numberAllocator.clear() - this.length = 0 -} - -/** - * Get Least Recently Used (LRU) topic alias - * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias - */ -TopicAliasSend.prototype.getLruAlias = function () { - const alias = this.numberAllocator.firstVacant() - if (alias) return alias - return this.aliasToTopic.min().alias -} - -module.exports = TopicAliasSend +'use strict' + +/** + * Module dependencies + */ +var LruMap = require('collections/lru-map') +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * Topic Alias sending manager + * This holds both topic to alias and alias to topic map + * @param {Number} [max] - topic alias maximum entries + */ +function TopicAliasSend (max) { + if (!(this instanceof TopicAliasSend)) { + return new TopicAliasSend(max) + } + + if (max > 0) { + this.aliasToTopic = new LruMap() + this.topicToAlias = {} + this.numberAllocator = new NumberAllocator(1, max) + this.max = max + this.length = 0 + } +} + +/** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ +TopicAliasSend.prototype.put = function (topic, alias) { + if (alias === 0 || alias > this.max) { + return false + } + const entry = this.aliasToTopic.get(alias) + if (entry) { + delete this.topicToAlias[entry.topic] + } + this.aliasToTopic.set(alias, {'topic': topic, 'alias': alias}) + this.topicToAlias[topic] = alias + this.numberAllocator.use(alias) + this.length = this.aliasToTopic.length + return true +} + +/** + * Get topic by alias + * @param {Number} [alias] - topic alias + * @returns {String} - if mapped topic exists return topic, otherwise return undefined + */ +TopicAliasSend.prototype.getTopicByAlias = function (alias) { + const entry = this.aliasToTopic.get(alias) + if (typeof entry === 'undefined') return entry + return entry.topic +} + +/** + * Get topic by alias + * @param {String} [topic] - topic + * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined + */ +TopicAliasSend.prototype.getAliasByTopic = function (topic) { + const alias = this.topicToAlias[topic] + if (typeof alias !== 'undefined') { + this.aliasToTopic.get(alias) // LRU update + } + return alias +} + +/** + * Clear all entries + */ +TopicAliasSend.prototype.clear = function () { + this.aliasToTopic.clear() + this.topicToAlias = {} + this.numberAllocator.clear() + this.length = 0 +} + +/** + * Get Least Recently Used (LRU) topic alias + * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias + */ +TopicAliasSend.prototype.getLruAlias = function () { + const alias = this.numberAllocator.firstVacant() + if (alias) return alias + return this.aliasToTopic.min().alias +} + +module.exports = TopicAliasSend diff --git a/lib/unique-message-id-provider.js b/lib/unique-message-id-provider.js index 6ffd4bde6..20e59977f 100644 --- a/lib/unique-message-id-provider.js +++ b/lib/unique-message-id-provider.js @@ -1,65 +1,65 @@ -'use strict' - -var NumberAllocator = require('number-allocator').NumberAllocator - -/** - * UniqueMessageAllocator constructor - * @constructor - */ -function UniqueMessageIdProvider () { - if (!(this instanceof UniqueMessageIdProvider)) { - return new UniqueMessageIdProvider() - } - - this.numberAllocator = new NumberAllocator(1, 65535) -} - -/** - * allocate - * - * Get the next messageId. - * @return if messageId is fully allocated then return null, - * otherwise return the smallest usable unsigned int messageId. - */ -UniqueMessageIdProvider.prototype.allocate = function () { - this.lastId = this.numberAllocator.alloc() - return this.lastId -} - -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -UniqueMessageIdProvider.prototype.getLastAllocated = function () { - return this.lastId -} - -/** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ -UniqueMessageIdProvider.prototype.register = function (messageId) { - return this.numberAllocator.use(messageId) -} - -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -UniqueMessageIdProvider.prototype.deallocate = function (messageId) { - this.numberAllocator.free(messageId) -} - -/** - * clear - * Deallocate all messageIds. - */ -UniqueMessageIdProvider.prototype.clear = function () { - this.numberAllocator.clear() -} - -module.exports = UniqueMessageIdProvider +'use strict' + +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * UniqueMessageAllocator constructor + * @constructor + */ +function UniqueMessageIdProvider () { + if (!(this instanceof UniqueMessageIdProvider)) { + return new UniqueMessageIdProvider() + } + + this.numberAllocator = new NumberAllocator(1, 65535) +} + +/** + * allocate + * + * Get the next messageId. + * @return if messageId is fully allocated then return null, + * otherwise return the smallest usable unsigned int messageId. + */ +UniqueMessageIdProvider.prototype.allocate = function () { + this.lastId = this.numberAllocator.alloc() + return this.lastId +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +UniqueMessageIdProvider.prototype.getLastAllocated = function () { + return this.lastId +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +UniqueMessageIdProvider.prototype.register = function (messageId) { + return this.numberAllocator.use(messageId) +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +UniqueMessageIdProvider.prototype.deallocate = function (messageId) { + this.numberAllocator.free(messageId) +} + +/** + * clear + * Deallocate all messageIds. + */ +UniqueMessageIdProvider.prototype.clear = function () { + this.numberAllocator.clear() +} + +module.exports = UniqueMessageIdProvider diff --git a/lib/validations.js b/lib/validations.js index 1a3277901..452e3ba1a 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -1,52 +1,52 @@ -'use strict' - -/** - * Validate a topic to see if it's valid or not. - * A topic is valid if it follow below rules: - * - Rule #1: If any part of the topic is not `+` or `#`, then it must not contain `+` and '#' - * - Rule #2: Part `#` must be located at the end of the mailbox - * - * @param {String} topic - A topic - * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. - */ -function validateTopic (topic) { - var parts = topic.split('/') - - for (var i = 0; i < parts.length; i++) { - if (parts[i] === '+') { - continue - } - - if (parts[i] === '#') { - // for Rule #2 - return i === parts.length - 1 - } - - if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { - return false - } - } - - return true -} - -/** - * Validate an array of topics to see if any of them is valid or not - * @param {Array} topics - Array of topics - * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one - */ -function validateTopics (topics) { - if (topics.length === 0) { - return 'empty_topic_list' - } - for (var i = 0; i < topics.length; i++) { - if (!validateTopic(topics[i])) { - return topics[i] - } - } - return null -} - -module.exports = { - validateTopics: validateTopics -} +'use strict' + +/** + * Validate a topic to see if it's valid or not. + * A topic is valid if it follow below rules: + * - Rule #1: If any part of the topic is not `+` or `#`, then it must not contain `+` and '#' + * - Rule #2: Part `#` must be located at the end of the mailbox + * + * @param {String} topic - A topic + * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. + */ +function validateTopic (topic) { + var parts = topic.split('/') + + for (var i = 0; i < parts.length; i++) { + if (parts[i] === '+') { + continue + } + + if (parts[i] === '#') { + // for Rule #2 + return i === parts.length - 1 + } + + if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { + return false + } + } + + return true +} + +/** + * Validate an array of topics to see if any of them is valid or not + * @param {Array} topics - Array of topics + * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one + */ +function validateTopics (topics) { + if (topics.length === 0) { + return 'empty_topic_list' + } + for (var i = 0; i < topics.length; i++) { + if (!validateTopic(topics[i])) { + return topics[i] + } + } + return null +} + +module.exports = { + validateTopics: validateTopics +} diff --git a/mqtt.js b/mqtt.js index c8b94fda1..56cd6f04e 100644 --- a/mqtt.js +++ b/mqtt.js @@ -1,21 +1,21 @@ -/* - * Copyright (c) 2015-2015 MQTT.js contributors. - * Copyright (c) 2011-2014 Adam Rudd. - * - * See LICENSE for more information - */ - -var MqttClient = require('./lib/client') -var connect = require('./lib/connect') -var Store = require('./lib/store') -var DefaultMessageIdProvider = require('./lib/default-message-id-provider') -var UniqueMessageIdProvider = require('./lib/unique-message-id-provider') - -module.exports.connect = connect - -// Expose MqttClient -module.exports.MqttClient = MqttClient -module.exports.Client = MqttClient -module.exports.Store = Store -module.exports.DefaultMessageIdProvider = DefaultMessageIdProvider -module.exports.UniqueMessageIdProvider = UniqueMessageIdProvider +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ + +var MqttClient = require('./lib/client') +var connect = require('./lib/connect') +var Store = require('./lib/store') +var DefaultMessageIdProvider = require('./lib/default-message-id-provider') +var UniqueMessageIdProvider = require('./lib/unique-message-id-provider') + +module.exports.connect = connect + +// Expose MqttClient +module.exports.MqttClient = MqttClient +module.exports.Client = MqttClient +module.exports.Store = Store +module.exports.DefaultMessageIdProvider = DefaultMessageIdProvider +module.exports.UniqueMessageIdProvider = UniqueMessageIdProvider diff --git a/package.json b/package.json index 712dc0350..0549681fe 100644 --- a/package.json +++ b/package.json @@ -1,113 +1,113 @@ -{ - "name": "mqtt", - "description": "A library for the MQTT protocol", - "version": "4.2.8", - "contributors": [ - "Adam Rudd ", - "Matteo Collina (https://github.com/mcollina)", - "Siarhei Buntsevich (https://github.com/scarry1992)", - "Yoseph Maguire (https://github.com/YoDaMa)" - ], - "keywords": [ - "mqtt", - "publish/subscribe", - "publish", - "subscribe" - ], - "license": "MIT", - "repository": { - "type": "git", - "url": "git://github.com/mqttjs/MQTT.js.git" - }, - "main": "mqtt.js", - "types": "types/index.d.ts", - "scripts": { - "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", - "pretest": "standard | snazzy", - "tslint": "tslint types/**/*.d.ts", - "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", - "typescript-compile-execute": "node test/typescript/*.js", - "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", - "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && uglifyjs dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", - "prepare": "npm run browser-build", - "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", - "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", - "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" - }, - "pre-commit": [ - "pretest", - "tslint" - ], - "bin": { - "mqtt_pub": "./bin/pub.js", - "mqtt_sub": "./bin/sub.js", - "mqtt": "./bin/mqtt.js" - }, - "files": [ - "dist/", - "CONTRIBUTING.md", - "doc", - "lib", - "bin", - "types", - "mqtt.js" - ], - "engines": { - "node": ">=10.0.0" - }, - "browser": { - "./mqtt.js": "./lib/connect/index.js", - "fs": false, - "tls": false, - "net": false - }, - "dependencies": { - "collections": "^5.1.12", - "commist": "^1.0.0", - "concat-stream": "^2.0.0", - "debug": "^4.1.1", - "duplexify": "^4.1.1", - "help-me": "^3.0.0", - "inherits": "^2.0.3", - "minimist": "^1.2.5", - "mqtt-packet": "^6.8.0", - "number-allocator": "^1.0.7", - "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", - "reinterval": "^1.1.0", - "split2": "^3.1.0", - "ws": "^7.5.0", - "xtend": "^4.0.2" - }, - "devDependencies": { - "@types/node": "^10.0.0", - "@types/ws": "^8.2.0", - "aedes": "^0.42.5", - "airtap": "^3.0.0", - "browserify": "^16.5.0", - "chai": "^4.2.0", - "codecov": "^3.0.4", - "end-of-stream": "^1.4.1", - "global": "^4.3.2", - "mkdirp": "^0.5.1", - "mocha": "^4.1.0", - "mqtt-connection": "^4.0.0", - "nyc": "^15.0.1", - "pre-commit": "^1.2.2", - "rimraf": "^3.0.2", - "should": "^13.2.1", - "sinon": "^9.0.0", - "snazzy": "^8.0.0", - "standard": "^11.0.1", - "tslint": "^5.11.0", - "tslint-config-standard": "^8.0.1", - "typescript": "^3.2.2", - "uglify-es": "^3.3.9" - }, - "standard": { - "env": [ - "mocha" - ] - } -} +{ + "name": "mqtt", + "description": "A library for the MQTT protocol", + "version": "4.2.8", + "contributors": [ + "Adam Rudd ", + "Matteo Collina (https://github.com/mcollina)", + "Siarhei Buntsevich (https://github.com/scarry1992)", + "Yoseph Maguire (https://github.com/YoDaMa)" + ], + "keywords": [ + "mqtt", + "publish/subscribe", + "publish", + "subscribe" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/mqttjs/MQTT.js.git" + }, + "main": "mqtt.js", + "types": "types/index.d.ts", + "scripts": { + "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", + "pretest": "standard | snazzy", + "tslint": "tslint types/**/*.d.ts", + "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", + "typescript-compile-execute": "node test/typescript/*.js", + "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", + "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && uglifyjs dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", + "prepare": "npm run browser-build", + "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", + "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", + "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" + }, + "pre-commit": [ + "pretest", + "tslint" + ], + "bin": { + "mqtt_pub": "./bin/pub.js", + "mqtt_sub": "./bin/sub.js", + "mqtt": "./bin/mqtt.js" + }, + "files": [ + "dist/", + "CONTRIBUTING.md", + "doc", + "lib", + "bin", + "types", + "mqtt.js" + ], + "engines": { + "node": ">=10.0.0" + }, + "browser": { + "./mqtt.js": "./lib/connect/index.js", + "fs": false, + "tls": false, + "net": false + }, + "dependencies": { + "collections": "^5.1.12", + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.7", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", + "reinterval": "^1.1.0", + "split2": "^3.1.0", + "ws": "^7.5.0", + "xtend": "^4.0.2" + }, + "devDependencies": { + "@types/node": "^10.0.0", + "@types/ws": "^8.2.0", + "aedes": "^0.42.5", + "airtap": "^3.0.0", + "browserify": "^16.5.0", + "chai": "^4.2.0", + "codecov": "^3.0.4", + "end-of-stream": "^1.4.1", + "global": "^4.3.2", + "mkdirp": "^0.5.1", + "mocha": "^4.1.0", + "mqtt-connection": "^4.0.0", + "nyc": "^15.0.1", + "pre-commit": "^1.2.2", + "rimraf": "^3.0.2", + "should": "^13.2.1", + "sinon": "^9.0.0", + "snazzy": "^8.0.0", + "standard": "^11.0.1", + "tslint": "^5.11.0", + "tslint-config-standard": "^8.0.1", + "typescript": "^3.2.2", + "uglify-es": "^3.3.9" + }, + "standard": { + "env": [ + "mocha" + ] + } +} diff --git a/test/abstract_client.js b/test/abstract_client.js index 4c8b0fa77..fc1f2096f 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -1,3177 +1,3177 @@ -'use strict' - -/** - * Testing dependencies - */ -var should = require('chai').should -var sinon = require('sinon') -var mqtt = require('../') -var xtend = require('xtend') -var Store = require('./../lib/store') -var assert = require('chai').assert -var ports = require('./helpers/port_list') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder - -module.exports = function (server, config) { - var version = config.protocolVersion || 4 - - function connect (opts) { - opts = xtend(config, opts) - return mqtt.connect(opts) - } - - describe('closing', function () { - it('should emit close if stream closes', function (done) { - var client = connect() - - client.once('connect', function () { - client.stream.end() - }) - client.once('close', function () { - client.end() - done() - }) - }) - - it('should mark the client as disconnected', function (done) { - var client = connect() - - client.once('close', function () { - client.end() - if (!client.connected) { - done() - } else { - done(new Error('Not marked as disconnected')) - } - }) - client.once('connect', function () { - client.stream.end() - }) - }) - - it('should stop ping timer if stream closes', function (done) { - var client = connect() - - client.once('close', function () { - assert.notExists(client.pingTimer) - client.end(true, done) - }) - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.stream.end() - }) - }) - - it('should emit close after end called', function (done) { - var client = connect() - - client.once('close', function () { - done() - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should emit end after end called and client must be disconnected', function (done) { - var client = connect() - - client.once('end', function () { - if (client.disconnected) { - return done() - } - done(new Error('client must be disconnected')) - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { - var store = new Store() - var client = connect({ incomingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { - var store = new Store() - var client = connect({ outgoingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should return `this` if end called twice', function (done) { - var client = connect() - - client.once('connect', function () { - client.end() - var value = client.end() - if (value === client) { - done() - } else { - done(new Error('Not returning client.')) - } - }) - }) - - it('should emit end only on first client end', function (done) { - var client = connect() - - client.once('end', function () { - var timeout = setTimeout(done.bind(null), 200) - client.once('end', function () { - clearTimeout(timeout) - done(new Error('end was emitted twice')) - }) - client.end() - }) - - client.once('connect', client.end.bind(client)) - }) - - it('should stop ping timer after end called', function (done) { - var client = connect() - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end(() => { - assert.notExists(client.pingTimer) - done() - }) - }) - }) - - it('should be able to end even on a failed connection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist'}) - - var timeout = setTimeout(function () { - done(new Error('Failed to end a disconnected client')) - }, 500) - - setTimeout(function () { - client.end(function () { - clearTimeout(timeout) - done() - }) - }, 200) - }) - - it('should emit end even on a failed connection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist'}) - - var timeout = setTimeout(function () { - done(new Error('Disconnected client has failed to emit end')) - }, 500) - - client.once('end', function () { - clearTimeout(timeout) - done() - }) - - // after 200ms manually invoke client.end - setTimeout(() => { - var boundEnd = client.end.bind(client) - boundEnd() - }, 200) - }) - - it.skip('should emit end only once for a reconnecting client', function (done) { - // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. - // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code - // there will be gists showing the difference between a successful test here and a failed test. For now we - // will add the retries syntax because of the flakiness. - var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20}) - setTimeout(done.bind(null), 1000) - var endCallback = function () { - assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') - } - - var spy = sinon.spy(endCallback) - client.on('end', spy) - setTimeout(() => { - client.end.bind(client) - client.end() - }, 300) - }) - }) - - describe('connecting', function () { - it('should connect to the broker', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function () { - done() - client.end() - }) - }) - - it('should send a default client id', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'mqttjs') - client.end(done) - serverClient.disconnect() - }) - }) - }) - - it('should send be clean by default', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.strictEqual(packet.clean, true) - serverClient.disconnect() - done() - }) - }) - }) - - it('should connect with the given client id', function (done) { - var client = connect({clientId: 'testclient'}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - serverClient.disconnect() - client.end(function (err) { - done(err) - }) - }) - }) - }) - - it('should connect with the client id and unclean state', function (done) { - var client = connect({clientId: 'testclient', clean: false}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - assert.isFalse(packet.clean) - client.end(false, function (err) { - serverClient.disconnect() - done(err) - }) - }) - }) - }) - - it('should require a clientId with clean=false', function (done) { - try { - var client = connect({ clean: false }) - client.on('error', function (err) { - done(err) - }) - } catch (err) { - assert.strictEqual(err.message, 'Missing clientId for unclean clients') - done() - } - }) - - it('should default to localhost', function (done) { - var client = connect({clientId: 'testclient'}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - serverClient.disconnect() - done() - }) - }) - }) - - it('should emit connect', function (done) { - var client = connect() - client.once('connect', function () { - client.end(true, done) - }) - client.once('error', done) - }) - - it('should provide connack packet with connect event', function (done) { - var connack = version === 5 ? {reasonCode: 0} : {returnCode: 0} - server.once('client', function (serverClient) { - connack.sessionPresent = true - serverClient.connack(connack) - server.once('client', function (serverClient) { - connack.sessionPresent = false - serverClient.connack(connack) - }) - }) - - var client = connect() - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, true) - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, false) - client.end() - done() - }) - }) - }) - - it('should mark the client as connected', function (done) { - var client = connect() - client.once('connect', function () { - client.end() - if (client.connected) { - done() - } else { - done(new Error('Not marked as connected')) - } - }) - }) - - it('should emit error on invalid clientId', function (done) { - var client = connect({clientId: 'invalid'}) - client.once('connect', function () { - done(new Error('Should not emit connect')) - }) - client.once('error', function (error) { - var value = version === 5 ? 128 : 2 - assert.strictEqual(error.code, value) // code for clientID identifer rejected - client.end() - done() - }) - }) - - it('should emit error event if the socket refuses the connection', function (done) { - // fake a port - var client = connect({ port: 4557 }) - - client.on('error', function (e) { - assert.equal(e.code, 'ECONNREFUSED') - client.end() - done() - }) - }) - - it('should have different client ids', function (done) { - // bug identified in this test: the client.end callback is invoked twice, once when the `end` - // method completes closing the stores and invokes the callback, and another time when the - // stream is closed. When the stream is closed, for some reason the closeStores method is called - // a second time. - var client1 = connect() - var client2 = connect() - - assert.notStrictEqual(client1.options.clientId, client2.options.clientId) - client1.end(true, () => { - client2.end(true, () => { - done() - }) - }) - }) - }) - - describe('handling offline states', function () { - it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { - var client = connect({reconnectPeriod: 20}) - - client.on('connect', function () { - this.stream.end() - }) - - client.on('offline', function () { - client.end(true, done) - }) - }) - - it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { - // fake a port - var client = connect({ reconnectPeriod: 20, port: 4557 }) - - client.on('error', function () {}) - - client.on('offline', function () { - client.end(true, done) - }) - }) - }) - - describe('topic validations when subscribing', function () { - it('should be ok for well-formated topics', function (done) { - var client = connect() - client.subscribe( - [ - '+', '+/event', 'event/+', '#', 'event/#', 'system/event/+', - 'system/+/event', 'system/registry/event/#', 'system/+/event/#', - 'system/registry/event/new_device', 'system/+/+/new_device' - ], - function (err) { - client.end(function () { - if (err) { - return done(new Error(err)) - } - done() - }) - } - ) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - var client = connect() - client.subscribe(['#/event', 'event#', 'event+'], function (err) { - client.end(false, function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an empty array for duplicate subs', function (done) { - var client = connect() - client.subscribe('event', function (err, granted1) { - if (err) { - return done(err) - } - client.subscribe('event', function (err, granted2) { - if (err) { - return done(err) - } - assert.isArray(granted2) - assert.isEmpty(granted2) - done() - }) - }) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - var client = connect() - client.subscribe('#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic event#', function (done) { - var client = connect() - client.subscribe('event#', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic system/#/event', function (done) { - var client = connect() - client.subscribe('system/#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for empty topic list', function (done) { - var client = connect() - client.subscribe([], function (err) { - client.end() - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - - it('should return an error (via callbacks) for topic system/+/#/event', function (done) { - var client = connect() - client.subscribe('system/+/#/event', function (err) { - client.end(true, function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - }) - - describe('offline messages', function () { - it('should queue message until connected', function (done) { - var client = connect() - - client.publish('test', 'test') - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 3) - - client.once('connect', function () { - assert.strictEqual(client.queue.length, 0) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not queue qos 0 messages if queueQoSZero is false', function (done) { - var client = connect({queueQoSZero: false}) - - client.publish('test', 'test', {qos: 0}) - assert.strictEqual(client.queue.length, 0) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should queue qos != 0 messages', function (done) { - var client = connect({queueQoSZero: false}) - - client.publish('test', 'test', {qos: 1}) - client.publish('test', 'test', {qos: 2}) - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 2) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not interrupt messages', function (done) { - var client = null - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var publishCount = 0 - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function () { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (packet.qos !== 0) { - serverClient.puback({messageId: packet.messageId}) - } - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - break - case 3: - assert.strictEqual(packet.payload.toString(), 'payload4') - server2.close() - done() - break - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore, - queueQoSZero: true - }) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'connack') { - setImmediate( - function () { - client.publish('test', 'payload3', {qos: 1}) - client.publish('test', 'payload4', {qos: 0}) - } - ) - } - }) - client.publish('test', 'payload1', {qos: 2}) - client.publish('test', 'payload2', {qos: 2}) - }) - }) - - it('should call cb if an outgoing QoS 0 message is not sent', function (done) { - var client = connect({queueQoSZero: false}) - var called = false - - client.publish('test', 'test', {qos: 0}, function () { - called = true - }) - - client.on('connect', function () { - assert.isTrue(called) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should delay ending up until all inflight messages are delivered', function (done) { - var client = connect() - var subscribeCalled = false - - client.on('connect', function () { - client.subscribe('test', function () { - subscribeCalled = true - }) - client.publish('test', 'test', function () { - client.end(false, function () { - assert.strictEqual(subscribeCalled, true) - done() - }) - }) - }) - }) - - it('wait QoS 1 publish messages', function (done) { - var client = connect() - var messageReceived = false - - client.on('connect', function () { - client.subscribe('test') - client.publish('test', 'test', { qos: 1 }, function () { - client.end(false, function () { - assert.strictEqual(messageReceived, true) - done() - }) - }) - client.on('message', function () { - messageReceived = true - }) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - }) - }) - - it('does not wait acks when force-closing', function (done) { - // non-running broker - var client = connect('mqtt://localhost:8993') - client.publish('test', 'test', { qos: 1 }) - client.end(true, done) - }) - - it('should call cb if store.put fails', function (done) { - const store = new Store() - store.put = function (packet, cb) { - process.nextTick(cb, new Error('oops there is an error')) - } - var client = connect({ incomingStore: store, outgoingStore: store }) - client.publish('test', 'test', { qos: 2 }, function (err) { - if (err) { - client.end(true, done) - } - }) - }) - }) - - describe('publishing', function () { - it('should publish a message (offline)', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - // don't wait on connect to send publish - client.publish(topic, payload) - - server.on('client', onClient) - - function onClient (serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (online)', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - // block on connect before sending publish - client.on('connect', function () { - client.publish(topic, payload) - }) - - server.on('client', onClient) - - function onClient (serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (retain, offline)', function (done) { - var client = connect({ queueQoSZero: true }) - var payload = 'test' - var topic = 'test' - var called = false - - client.publish(topic, payload, { retain: true }, function () { - called = true - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, true) - assert.strictEqual(called, true) - client.end(true, done) - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var payload = 'test_payload' - var topic = 'testTopic' - - client.on('packetsend', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - } else { - done(new Error('packet.cmd was not publish!')) - } - }) - - client.publish(topic, payload) - }) - - it('should accept options', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - var opts = { - retain: true, - qos: 1 - } - - client.once('connect', function () { - client.publish(topic, payload, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, false, 'incorrect dup') - client.end(done) - }) - }) - }) - - it('should publish with the default options for an empty parameter', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - var defaultOpts = {qos: 0, retain: false, dup: false} - - client.once('connect', function () { - client.publish(topic, payload, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') - client.end(true, done) - }) - }) - }) - - it('should mark a message as duplicate when "dup" option is set', function (done) { - var client = connect() - var payload = 'duplicated-test' - var topic = 'test' - var opts = { - retain: true, - qos: 1, - dup: true - } - - client.once('connect', function () { - client.publish(topic, payload, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') - client.end(done) - }) - }) - }) - - it('should fire a callback (qos 0)', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('a', 'b', function () { - client.end() - done() - }) - }) - }) - - it('should fire a callback (qos 1)', function (done) { - var client = connect() - var opts = { qos: 1 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end() - done() - }) - }) - }) - - it('should fire a callback (qos 2)', function (done) { - var client = connect() - var opts = { qos: 2 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end() - done() - }) - }) - }) - - it('should support UTF-8 characters in topic', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('中国', 'hello', function () { - client.end() - done() - }) - }) - }) - - it('should support UTF-8 characters in payload', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('hello', '中国', function () { - client.end() - done() - }) - }) - }) - - it('should publish 10 QoS 2 and receive them', function (done) { - var client = connect() - var count = 0 - - client.on('connect', function () { - client.subscribe('test') - client.publish('test', 'test', { qos: 2 }) - }) - - client.on('message', function () { - if (count >= 10) { - client.end() - done() - } else { - client.publish('test', 'test', { qos: 2 }) - } - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end() - done('error went offline... didnt see this happen') - }) - - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - - serverClient.on('pubrel', function () { - count++ - }) - }) - }) - - function testQosHandleMessage (qos, done) { - var client = connect() - - var messageEventCount = 0 - var handleMessageCount = 0 - - client.handleMessage = function (packet, callback) { - setTimeout(function () { - handleMessageCount++ - // next message event should not emit until handleMessage completes - assert.strictEqual(handleMessageCount, messageEventCount) - if (handleMessageCount === 10) { - setTimeout(function () { - client.end(true, done) - }) - } - callback() - }, 100) - } - - client.on('message', function (topic, message, packet) { - messageEventCount++ - }) - - client.on('connect', function () { - client.subscribe('test') - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end(true, function () { - done('error went offline... didnt see this happen') - }) - }) - - serverClient.on('subscribe', function () { - for (var i = 0; i < 10; i++) { - serverClient.publish({ - messageId: i, - topic: 'test', - payload: 'test' + i, - qos: qos - }) - } - }) - }) - } - - var qosTests = [ 0, 1, 2 ] - qosTests.forEach(function (QoS) { - it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { - testQosHandleMessage(QoS, done) - }) - }) - - it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { - var client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client._sendPacket = sinon.spy() - - client._handlePublish({ - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }, function (err) { - assert.exists(err) - }) - - assert.strictEqual(client._sendPacket.callCount, 0) - client.end() - client.on('connect', function () { done() }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePublish` method', function (done) { - var client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - try { - client._handlePublish({ - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - - it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePublish({ - messageId: 1, - topic: 'test', - payload: 'test', - qos: 1 - }, function () { - client.end() - done() - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePublish({ - messageId: 1, - topic: 'test', - payload: 'test', - qos: 2 - }, function () { - client.end() - done() - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del (packet, cb) { - process.nextTick(function () { - cb(new Error('Error')) - }) - } - - get (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({ incomingStore: store }) - - client._handlePubrel({ - messageId: 1, - qos: 2 - }, function () { - client.end(true, done) - }) - }) - - it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { - var delComplete = false - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del (packet, cb) { - process.nextTick(function () { - delComplete = true - cb(null) - }) - } - - get (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePubrel({ - messageId: 1, - qos: 2 - }, function () { - assert.isTrue(delComplete) - client.end(true, done) - }) - }) - - it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function (done) { - var store = new Store() - var client = connect({incomingStore: store}) - - var messageId = Math.floor(65535 * Math.random()) - var topic = 'testTopic' - var payload = 'testPayload' - var qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, {qos: 2}) - - store.put({ - messageId: messageId, - topic: topic, - payload: payload, - qos: qos, - cmd: 'publish' - }, function () { - // cleans up the client - client._sendPacket = sinon.spy() - client._handlePubrel({cmd: 'pubrel', messageId: messageId}, function (err) { - assert.exists(err) - assert.strictEqual(client._sendPacket.callCount, 0) - client.end(true, done) - }) - }) - }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePubrel` method', function (done) { - var store = new Store() - var client = connect({incomingStore: store}) - - var messageId = Math.floor(65535 * Math.random()) - var topic = 'test' - var payload = 'test' - var qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, {qos: 2}) - - store.put({ - messageId: messageId, - topic: topic, - payload: payload, - qos: qos, - cmd: 'publish' - }, function () { - try { - client._handlePubrel({cmd: 'pubrel', messageId: messageId}) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - }) - }) - - it('should keep message order', function (done) { - var publishCount = 0 - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () {}) - - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({messageId: packet.messageId}) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - server2.close() - done() - break - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', {qos: 1}) - client.publish('topic', 'payload2', {qos: 1}) - client.end(true) - } else { - client.publish('topic', 'payload3', {qos: 1}) - } - }) - client.on('close', function () { - if (!reconnect) { - client.reconnect({ - clean: false, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - } - }) - }) - }) - - function testCallbackStorePutByQoS (qos, clean, expected, done) { - var client = connect({ - clean: clean, - clientId: 'testId' - }) - - var callbacks = [] - - function cbStorePut () { - callbacks.push('storeput') - } - - client.on('connect', function () { - client.publish('test', 'test', {qos: qos, cbStorePut: cbStorePut}, function (err) { - if (err) done(err) - callbacks.push('publish') - assert.deepEqual(callbacks, expected) - client.end(true, done) - }) - }) - } - - var callbackStorePutByQoSParameters = [ - {args: [0, true], expected: ['publish']}, - {args: [0, false], expected: ['publish']}, - {args: [1, true], expected: ['storeput', 'publish']}, - {args: [1, false], expected: ['storeput', 'publish']}, - {args: [2, true], expected: ['storeput', 'publish']}, - {args: [2, false], expected: ['storeput', 'publish']} - ] - - callbackStorePutByQoSParameters.forEach(function (test) { - if (test.args[0] === 0) { // QoS 0 - it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } else { // QoS 1 and 2 - it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } - }) - }) - - describe('unsubscribing', function () { - it('should send an unsubscribe packet (offline)', function (done) { - var client = connect() - - client.unsubscribe('test') - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, 'test') - client.end(done) - }) - }) - }) - - it('should send an unsubscribe packet', function (done) { - var client = connect() - var topic = 'topic' - - client.once('connect', function () { - client.unsubscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - client.end(done) - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - client.end(true, done) - } - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - client.end(true, done) - } - }) - }) - - it('should accept an array of unsubs', function (done) { - var client = connect() - var topics = ['topic1', 'topic2'] - - client.once('connect', function () { - client.unsubscribe(topics) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.deepStrictEqual(packet.unsubscriptions, topics) - client.end(done) - }) - }) - }) - - it('should fire a callback on unsuback', function (done) { - var client = connect() - var topic = 'topic' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - serverClient.unsuback(packet) - }) - }) - }) - - it('should unsubscribe from a chinese topic', function (done) { - var client = connect() - var topic = '中国' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(err => { - done(err) - }) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - }) - }) - }) - }) - - describe('keepalive', function () { - var clock - - beforeEach(function () { - clock = sinon.useFakeTimers() - }) - - afterEach(function () { - clock.restore() - }) - - it('should checkPing at keepalive interval', function (done) { - var interval = 3 - var client = connect({ keepalive: interval }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 1) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 2) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 3) - - client.end(true, done) - }) - }) - - it('should not checkPing if publishing at a higher rate than keepalive', function (done) { - var intervalMs = 3000 - var client = connect({keepalive: intervalMs / 1000}) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 0) - client.end(true, done) - }) - }) - - it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function (done) { - var intervalMs = 3000 - var client = connect({ - keepalive: intervalMs / 1000, - reschedulePings: false - }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 1) - client.end(true, done) - }) - }) - }) - - describe('pinging', function () { - it('should set a ping timer', function (done) { - var client = connect({keepalive: 3}) - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should not set a ping timer keepalive=0', function (done) { - var client = connect({keepalive: 0}) - client.on('connect', function () { - assert.notExists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should reconnect if pingresp is not sent', function (done) { - var client = connect({keepalive: 1, reconnectPeriod: 100}) - - // Fake no pingresp being send by stubbing the _handlePingresp function - client._handlePingresp = function () {} - - client.once('connect', function () { - client.once('connect', function () { - client.end(true, done) - }) - }) - }) - - it('should not reconnect if pingresp is successful', function (done) { - var client = connect({keepalive: 100}) - client.once('close', function () { - done(new Error('Client closed connection')) - }) - setTimeout(done, 1000) - }) - - it('should defer the next ping when sending a control packet', function (done) { - var client = connect({keepalive: 1}) - - client.once('connect', function () { - client._checkPing = sinon.spy() - - client.publish('foo', 'bar') - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - done() - }, 75) - }, 75) - }, 75) - }) - }) - }) - - describe('subscribing', function () { - it('should send a subscribe message (offline)', function (done) { - var client = connect() - - client.subscribe('test') - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - done() - }) - }) - }) - - it('should send a subscribe message', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - done() - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - done() - } - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - done() - } - }) - }) - - it('should accept an array of subscriptions', function (done) { - var client = connect() - var subs = ['test1', 'test2'] - - client.once('connect', function () { - client.subscribe(subs) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] - var expected = subs.map(function (i) { - var result = {topic: i, qos: 0} - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - return result - }) - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept a hash of subscriptions', function (done) { - var client = connect() - var topics = { - test1: {qos: 0}, - test2: {qos: 1} - } - - client.once('connect', function () { - client.subscribe(topics) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var k - var expected = [] - - for (k in topics) { - if (topics.hasOwnProperty(k)) { - var result = { - topic: k, - qos: topics[k].qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - expected.push(result) - } - } - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept an options parameter', function (done) { - var client = connect() - var topic = 'test' - var opts = {qos: 1} - - client.once('connect', function () { - client.subscribe(topic, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var expected = [{ - topic: topic, - qos: 1 - }] - - if (version === 5) { - expected[0].nl = false - expected[0].rap = false - expected[0].rh = 0 - } - - assert.deepStrictEqual(packet.subscriptions, expected) - done() - }) - }) - }) - - it('should subscribe with the default options for an empty options parameter', function (done) { - var client = connect() - var topic = 'test' - var defaultOpts = {qos: 0} - - client.once('connect', function () { - client.subscribe(topic, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: defaultOpts.qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - - assert.include(packet.subscriptions[0], result) - client.end(err => done(err)) - }) - }) - }) - - it('should fire a callback on suback', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.subscribe(topic, { qos: 2 }, function (err, granted) { - if (err) { - done(err) - } else { - assert.exists(granted, 'granted not given') - var expectedResult = {topic: 'test', qos: 2} - if (version === 5) { - expectedResult.nl = false - expectedResult.rap = false - expectedResult.rh = 0 - expectedResult.properties = undefined - } - assert.include(granted[0], expectedResult) - client.end(err => done(err)) - } - }) - }) - }) - - it('should fire a callback with error if disconnected (options provided)', function (done) { - var client = connect() - var topic = 'test' - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, {qos: 2}, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should fire a callback with error if disconnected (options not provided)', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should subscribe with a chinese topic', function (done) { - var client = connect() - var topic = '中国' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - client.end(done) - }) - }) - }) - }) - - describe('receiving messages', function () { - it('should fire the message event', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - // - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.qos, 1) - assert.strictEqual(packet.topic, testPacket.topic) - assert.strictEqual(packet.payload.toString(), testPacket.payload) - assert.strictEqual(packet.retain, true) - client.end(true, done) - } - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should support binary data', function (done) { - var client = connect({ encoding: 'binary' }) - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2)', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2) - repeated publish', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - var messageHandler = function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - - assert.strictEqual(spiedMessageHandler.callCount, 1) - client.end(true, done) - } - - var spiedMessageHandler = sinon.spy(messageHandler) - - client.subscribe(testPacket.topic) - client.on('message', spiedMessageHandler) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - // twice, should be ignored - serverClient.publish(testPacket) - }) - }) - }) - - it('should support a chinese topic', function (done) { - var client = connect({ encoding: 'binary' }) - var testPacket = { - topic: '国', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - }) - - describe('qos handling', function () { - it('should follow qos 0 semantics (trivial)', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 0}, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 0, - retain: false - }) - }) - }) - }) - - it('should follow qos 1 semantics', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 50 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 1}) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - messageId: mid, - qos: 1 - }) - }) - - serverClient.once('puback', function (packet) { - assert.strictEqual(packet.messageId, mid) - client.end(done) - }) - }) - }) - - it('should follow qos 2 semantics', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - var publishReceived = 0 - var pubrecReceived = 0 - var pubrelReceived = 0 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - }) - - client.on('packetreceive', (packet) => { - switch (packet.cmd) { - case 'connack': - case 'suback': - // expected, but not specifically part of QOS 2 semantics - break - case 'publish': - assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') - assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') - publishReceived += 1 - break - case 'pubrel': - assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') - pubrelReceived += 1 - break - default: - should.fail() - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.on('pubrec', function () { - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') - assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') - pubrecReceived += 1 - }) - - serverClient.once('pubcomp', function () { - client.removeAllListeners() - serverClient.removeAllListeners() - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') - assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') - client.end(true, done) - }) - }) - }) - - it('should should empty the incoming store after a qos 2 handshake is completed', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - }) - - client.on('packetreceive', (packet) => { - if (packet.cmd === 'pubrel') { - assert.strictEqual(client.incomingStore._inflights.size, 1) - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.once('pubcomp', function () { - assert.strictEqual(client.incomingStore._inflights.size, 0) - client.removeAllListeners() - client.end(true, done) - }) - }) - }) - - function testMultiplePubrel (shouldSendPubcompFail, done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - var pubcompCount = 0 - var pubrelCount = 0 - var handleMessageCount = 0 - var emitMessageCount = 0 - var origSendPacket = client._sendPacket - var shouldSendFail - - client.handleMessage = function (packet, callback) { - handleMessageCount++ - callback() - } - - client.on('message', function () { - emitMessageCount++ - }) - - client._sendPacket = function (packet, sendDone) { - shouldSendFail = packet.cmd === 'pubcomp' && shouldSendPubcompFail - if (sendDone) { - sendDone(shouldSendFail ? new Error('testing pubcomp failure') : undefined) - } - - // send the mocked response - switch (packet.cmd) { - case 'subscribe': - const suback = {cmd: 'suback', messageId: packet.messageId, granted: [2]} - client._handlePacket(suback, function (err) { - assert.isNotOk(err) - }) - break - case 'pubrec': - case 'pubcomp': - // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp - if (packet.cmd === 'pubcomp') { - pubcompCount++ - if (pubcompCount === 2) { - // end the test once the client has gone through two rounds of replying to pubrel messages - assert.strictEqual(pubrelCount, 2) - assert.strictEqual(handleMessageCount, 1) - assert.strictEqual(emitMessageCount, 1) - client._sendPacket = origSendPacket - client.end(true, done) - break - } - } - - // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received - const pubrel = {cmd: 'pubrel', messageId: mid} - pubrelCount++ - client._handlePacket(pubrel, function (err) { - if (shouldSendFail) { - assert.exists(err) - assert.instanceOf(err, Error) - } else { - assert.notExists(err) - } - }) - break - } - } - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - const publish = {cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid} - client._handlePacket(publish, function (err) { - assert.notExists(err) - }) - }) - } - - it('handle qos 2 messages exactly once when multiple pubrel received', function (done) { - testMultiplePubrel(false, done) - }) - - it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function (done) { - testMultiplePubrel(true, done) - }) - }) - - describe('auto reconnect', function () { - it('should mark the client disconnecting if #end called', function (done) { - var client = connect() - - client.end(true, err => { - assert.isTrue(client.disconnecting) - done(err) - }) - }) - - it('should reconnect after stream disconnect', function (done) { - var client = connect() - - var tryReconnect = true - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - client.end(true, done) - } - }) - }) - - it('should emit \'reconnect\' when reconnecting', function (done) { - var client = connect() - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - client.end(true, done) - } - }) - }) - - it('should emit \'offline\' after going offline', function (done) { - var client = connect() - - var tryReconnect = true - var offlineEvent = false - - client.on('offline', function () { - offlineEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(offlineEvent) - client.end(true, done) - } - }) - }) - - it('should not reconnect if it was ended by the user', function (done) { - var client = connect() - - client.on('connect', function () { - client.end() - done() // it will raise an exception if called two times - }) - }) - - it('should setup a reconnect timer on disconnect', function (done) { - var client = connect() - - client.once('connect', function () { - assert.notExists(client.reconnectTimer) - client.stream.end() - }) - - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, done) - }) - }) - - var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] - reconnectPeriodTests.forEach((test) => { - it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { - var end - var reconnectSlushTime = 200 - var client = connect({reconnectPeriod: test.period}) - var reconnect = false - var start = Date.now() - - client.on('connect', function () { - if (!reconnect) { - client.stream.end() - reconnect = true - } else { - end = Date.now() - client.end(() => { - let reconnectPeriodDuringTest = end - start - if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { - // give the connection a 200 ms slush window - done() - } else { - done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) - } - }) - } - }) - }) - }) - - it('should always cleanup successfully on reconnection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1}) - // bind client.end so that when it is called it is automatically passed in the done callback - setTimeout(client.end.bind(client, done), 50) - }) - - it('should resend in-flight QoS 1 publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - check() - }) - - function check () { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight publish messages if disconnecting', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - client.end(true, err => { - assert.isFalse(serverPublished) - assert.isFalse(clientCalledBack) - done(err) - }) - }) - }) - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - }) - }) - }) - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - - server.once('client', function (serverClient) { - // ignore errors - serverClient.on('error', function () {}) - serverClient.on('publish', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('pubrel', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function () { - clientCalledBack = true - check() - }) - - function check () { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight QoS 1 removed publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function (err) { - clientCalledBack = true - assert.exists(err, 'error should exist') - assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, (err) => { - done(err) - }) - }) - - it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function (err) { - clientCalledBack = true - assert.strictEqual(err.message, 'Message removed') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, done) - }) - - it('should resubscribe when reconnecting', function (done) { - var client = connect({ reconnectPeriod: 100 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - client.end(done) - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should not resubscribe when reconnecting if resubscribe is disabled', function (done) { - var client = connect({ reconnectPeriod: 100, resubscribe: false }) - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - client.end(true, done) - } - }) - }) - - it('should not resubscribe when reconnecting if suback is error', function (done) { - var tryReconnect = true - var reconnectEvent = false - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos | 0x80 - }) - }) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - var client = connect({ - port: ports.PORTAND49, - host: 'localhost', - reconnectPeriod: 100 - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - server2.close() - client.end(true, done) - } - }) - }) - }) - - it('should preserved incomingStore after disconnecting if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - if (reconnect) { - serverClient.pubrel({ messageId: 1 }) - } - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) - }) - serverClient.on('pubrec', function (packet) { - client.end(false, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - }) - }) - serverClient.on('pubcomp', function (packet) { - client.end(true, () => { - server2.close() - done() - }) - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.subscribe('test', {qos: 2}, function () { - }) - reconnect = true - } - }) - client.on('message', function (topic, message) { - assert.strictEqual(topic, 'topic') - assert.strictEqual(message.toString(), 'payload') - }) - }) - }) - - it('should clear outgoing if close from server', function (done) { - var reconnect = false - var client = {} - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - if (reconnect) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - serverClient.destroy() - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: true, - clientId: 'cid1', - keepalive: 1, - reconnectPeriod: 0 - }) - - client.on('connect', function () { - client.subscribe('test', {qos: 2}, function (e) { - if (!e) { - client.end() - } - }) - }) - - client.on('close', function () { - if (reconnect) { - server2.close() - done() - } else { - assert.strictEqual(Object.keys(client.outgoing).length, 0) - reconnect = true - client.reconnect() - } - }) - }) - }) - - it('should resend in-flight QoS 1 publish messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, () => { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 1}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 2}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (!reconnect) { - serverClient.pubrec({messageId: packet.messageId}) - } - }) - serverClient.on('pubrel', function () { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 2}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight publish messages by published order', function (done) { - var publishCount = 0 - var reconnect = false - var disconnectOnce = true - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () {}) - - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({messageId: packet.messageId}) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - server2.close() - client.end(true, done) - break - } - } else { - if (disconnectOnce) { - client.end(true, function () { - reconnect = true - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - }) - disconnectOnce = false - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.nextId = 65535 - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', {qos: 1}) - client.publish('topic', 'payload2', {qos: 1}) - client.publish('topic', 'payload3', {qos: 1}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should be able to pub/sub if reconnect() is called at close handler', function (done) { - var client = connect({ reconnectPeriod: 0 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - client.reconnect() - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - it('should be able to pub/sub if reconnect() is called at out of close handler', function (done) { - var client = connect({ reconnectPeriod: 0 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - setTimeout(function () { - client.reconnect() - }, 100) - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - context('with alternate server client', function () { - var cachedClientListeners - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - - beforeEach(function () { - cachedClientListeners = server.listeners('client') - server.removeAllListeners('client') - }) - - afterEach(function () { - server.removeAllListeners('client') - cachedClientListeners.forEach(function (listener) { - server.on('client', listener) - }) - }) - - it('should resubscribe even if disconnect is before suback', function (done) { - var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - var subscribeCount = 0 - var connectCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - connectCount++ - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, confirm that the only two - // subscribes have taken place, then cleanup and exit - if (connectCount >= 2) { - assert.strictEqual(subscribeCount, 2) - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - - it('should resubscribe exactly once', function (done) { - var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - var subscribeCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, only two subs - // subscribes have taken place, then cleanup and exit - if (subscribeCount === 2) { - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - }) - }) -} +'use strict' + +/** + * Testing dependencies + */ +var should = require('chai').should +var sinon = require('sinon') +var mqtt = require('../') +var xtend = require('xtend') +var Store = require('./../lib/store') +var assert = require('chai').assert +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder + +module.exports = function (server, config) { + var version = config.protocolVersion || 4 + + function connect (opts) { + opts = xtend(config, opts) + return mqtt.connect(opts) + } + + describe('closing', function () { + it('should emit close if stream closes', function (done) { + var client = connect() + + client.once('connect', function () { + client.stream.end() + }) + client.once('close', function () { + client.end() + done() + }) + }) + + it('should mark the client as disconnected', function (done) { + var client = connect() + + client.once('close', function () { + client.end() + if (!client.connected) { + done() + } else { + done(new Error('Not marked as disconnected')) + } + }) + client.once('connect', function () { + client.stream.end() + }) + }) + + it('should stop ping timer if stream closes', function (done) { + var client = connect() + + client.once('close', function () { + assert.notExists(client.pingTimer) + client.end(true, done) + }) + + client.once('connect', function () { + assert.exists(client.pingTimer) + client.stream.end() + }) + }) + + it('should emit close after end called', function (done) { + var client = connect() + + client.once('close', function () { + done() + }) + + client.once('connect', function () { + client.end() + }) + }) + + it('should emit end after end called and client must be disconnected', function (done) { + var client = connect() + + client.once('end', function () { + if (client.disconnected) { + return done() + } + done(new Error('client must be disconnected')) + }) + + client.once('connect', function () { + client.end() + }) + }) + + it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { + var store = new Store() + var client = connect({ incomingStore: store }) + + store.close = function (cb) { + cb(new Error('test')) + } + client.once('end', function () { + if (arguments.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', function () { + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { + var store = new Store() + var client = connect({ outgoingStore: store }) + + store.close = function (cb) { + cb(new Error('test')) + } + client.once('end', function () { + if (arguments.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', function () { + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should return `this` if end called twice', function (done) { + var client = connect() + + client.once('connect', function () { + client.end() + var value = client.end() + if (value === client) { + done() + } else { + done(new Error('Not returning client.')) + } + }) + }) + + it('should emit end only on first client end', function (done) { + var client = connect() + + client.once('end', function () { + var timeout = setTimeout(done.bind(null), 200) + client.once('end', function () { + clearTimeout(timeout) + done(new Error('end was emitted twice')) + }) + client.end() + }) + + client.once('connect', client.end.bind(client)) + }) + + it('should stop ping timer after end called', function (done) { + var client = connect() + + client.once('connect', function () { + assert.exists(client.pingTimer) + client.end(() => { + assert.notExists(client.pingTimer) + done() + }) + }) + }) + + it('should be able to end even on a failed connection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist'}) + + var timeout = setTimeout(function () { + done(new Error('Failed to end a disconnected client')) + }, 500) + + setTimeout(function () { + client.end(function () { + clearTimeout(timeout) + done() + }) + }, 200) + }) + + it('should emit end even on a failed connection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist'}) + + var timeout = setTimeout(function () { + done(new Error('Disconnected client has failed to emit end')) + }, 500) + + client.once('end', function () { + clearTimeout(timeout) + done() + }) + + // after 200ms manually invoke client.end + setTimeout(() => { + var boundEnd = client.end.bind(client) + boundEnd() + }, 200) + }) + + it.skip('should emit end only once for a reconnecting client', function (done) { + // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. + // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code + // there will be gists showing the difference between a successful test here and a failed test. For now we + // will add the retries syntax because of the flakiness. + var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20}) + setTimeout(done.bind(null), 1000) + var endCallback = function () { + assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') + } + + var spy = sinon.spy(endCallback) + client.on('end', spy) + setTimeout(() => { + client.end.bind(client) + client.end() + }, 300) + }) + }) + + describe('connecting', function () { + it('should connect to the broker', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function () { + done() + client.end() + }) + }) + + it('should send a default client id', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'mqttjs') + client.end(done) + serverClient.disconnect() + }) + }) + }) + + it('should send be clean by default', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.strictEqual(packet.clean, true) + serverClient.disconnect() + done() + }) + }) + }) + + it('should connect with the given client id', function (done) { + var client = connect({clientId: 'testclient'}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + serverClient.disconnect() + client.end(function (err) { + done(err) + }) + }) + }) + }) + + it('should connect with the client id and unclean state', function (done) { + var client = connect({clientId: 'testclient', clean: false}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + assert.isFalse(packet.clean) + client.end(false, function (err) { + serverClient.disconnect() + done(err) + }) + }) + }) + }) + + it('should require a clientId with clean=false', function (done) { + try { + var client = connect({ clean: false }) + client.on('error', function (err) { + done(err) + }) + } catch (err) { + assert.strictEqual(err.message, 'Missing clientId for unclean clients') + done() + } + }) + + it('should default to localhost', function (done) { + var client = connect({clientId: 'testclient'}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + serverClient.disconnect() + done() + }) + }) + }) + + it('should emit connect', function (done) { + var client = connect() + client.once('connect', function () { + client.end(true, done) + }) + client.once('error', done) + }) + + it('should provide connack packet with connect event', function (done) { + var connack = version === 5 ? {reasonCode: 0} : {returnCode: 0} + server.once('client', function (serverClient) { + connack.sessionPresent = true + serverClient.connack(connack) + server.once('client', function (serverClient) { + connack.sessionPresent = false + serverClient.connack(connack) + }) + }) + + var client = connect() + client.once('connect', function (packet) { + assert.strictEqual(packet.sessionPresent, true) + client.once('connect', function (packet) { + assert.strictEqual(packet.sessionPresent, false) + client.end() + done() + }) + }) + }) + + it('should mark the client as connected', function (done) { + var client = connect() + client.once('connect', function () { + client.end() + if (client.connected) { + done() + } else { + done(new Error('Not marked as connected')) + } + }) + }) + + it('should emit error on invalid clientId', function (done) { + var client = connect({clientId: 'invalid'}) + client.once('connect', function () { + done(new Error('Should not emit connect')) + }) + client.once('error', function (error) { + var value = version === 5 ? 128 : 2 + assert.strictEqual(error.code, value) // code for clientID identifer rejected + client.end() + done() + }) + }) + + it('should emit error event if the socket refuses the connection', function (done) { + // fake a port + var client = connect({ port: 4557 }) + + client.on('error', function (e) { + assert.equal(e.code, 'ECONNREFUSED') + client.end() + done() + }) + }) + + it('should have different client ids', function (done) { + // bug identified in this test: the client.end callback is invoked twice, once when the `end` + // method completes closing the stores and invokes the callback, and another time when the + // stream is closed. When the stream is closed, for some reason the closeStores method is called + // a second time. + var client1 = connect() + var client2 = connect() + + assert.notStrictEqual(client1.options.clientId, client2.options.clientId) + client1.end(true, () => { + client2.end(true, () => { + done() + }) + }) + }) + }) + + describe('handling offline states', function () { + it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { + var client = connect({reconnectPeriod: 20}) + + client.on('connect', function () { + this.stream.end() + }) + + client.on('offline', function () { + client.end(true, done) + }) + }) + + it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { + // fake a port + var client = connect({ reconnectPeriod: 20, port: 4557 }) + + client.on('error', function () {}) + + client.on('offline', function () { + client.end(true, done) + }) + }) + }) + + describe('topic validations when subscribing', function () { + it('should be ok for well-formated topics', function (done) { + var client = connect() + client.subscribe( + [ + '+', '+/event', 'event/+', '#', 'event/#', 'system/event/+', + 'system/+/event', 'system/registry/event/#', 'system/+/event/#', + 'system/registry/event/new_device', 'system/+/+/new_device' + ], + function (err) { + client.end(function () { + if (err) { + return done(new Error(err)) + } + done() + }) + } + ) + }) + + it('should return an error (via callbacks) for topic #/event', function (done) { + var client = connect() + client.subscribe(['#/event', 'event#', 'event+'], function (err) { + client.end(false, function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an empty array for duplicate subs', function (done) { + var client = connect() + client.subscribe('event', function (err, granted1) { + if (err) { + return done(err) + } + client.subscribe('event', function (err, granted2) { + if (err) { + return done(err) + } + assert.isArray(granted2) + assert.isEmpty(granted2) + done() + }) + }) + }) + + it('should return an error (via callbacks) for topic #/event', function (done) { + var client = connect() + client.subscribe('#/event', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic event#', function (done) { + var client = connect() + client.subscribe('event#', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic system/#/event', function (done) { + var client = connect() + client.subscribe('system/#/event', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for empty topic list', function (done) { + var client = connect() + client.subscribe([], function (err) { + client.end() + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + + it('should return an error (via callbacks) for topic system/+/#/event', function (done) { + var client = connect() + client.subscribe('system/+/#/event', function (err) { + client.end(true, function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + }) + + describe('offline messages', function () { + it('should queue message until connected', function (done) { + var client = connect() + + client.publish('test', 'test') + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 3) + + client.once('connect', function () { + assert.strictEqual(client.queue.length, 0) + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should not queue qos 0 messages if queueQoSZero is false', function (done) { + var client = connect({queueQoSZero: false}) + + client.publish('test', 'test', {qos: 0}) + assert.strictEqual(client.queue.length, 0) + client.on('connect', function () { + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should queue qos != 0 messages', function (done) { + var client = connect({queueQoSZero: false}) + + client.publish('test', 'test', {qos: 1}) + client.publish('test', 'test', {qos: 2}) + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 2) + client.on('connect', function () { + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should not interrupt messages', function (done) { + var client = null + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var publishCount = 0 + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function () { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (packet.qos !== 0) { + serverClient.puback({messageId: packet.messageId}) + } + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + break + case 3: + assert.strictEqual(packet.payload.toString(), 'payload4') + server2.close() + done() + break + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore, + queueQoSZero: true + }) + client.on('packetreceive', function (packet) { + if (packet.cmd === 'connack') { + setImmediate( + function () { + client.publish('test', 'payload3', {qos: 1}) + client.publish('test', 'payload4', {qos: 0}) + } + ) + } + }) + client.publish('test', 'payload1', {qos: 2}) + client.publish('test', 'payload2', {qos: 2}) + }) + }) + + it('should call cb if an outgoing QoS 0 message is not sent', function (done) { + var client = connect({queueQoSZero: false}) + var called = false + + client.publish('test', 'test', {qos: 0}, function () { + called = true + }) + + client.on('connect', function () { + assert.isTrue(called) + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should delay ending up until all inflight messages are delivered', function (done) { + var client = connect() + var subscribeCalled = false + + client.on('connect', function () { + client.subscribe('test', function () { + subscribeCalled = true + }) + client.publish('test', 'test', function () { + client.end(false, function () { + assert.strictEqual(subscribeCalled, true) + done() + }) + }) + }) + }) + + it('wait QoS 1 publish messages', function (done) { + var client = connect() + var messageReceived = false + + client.on('connect', function () { + client.subscribe('test') + client.publish('test', 'test', { qos: 1 }, function () { + client.end(false, function () { + assert.strictEqual(messageReceived, true) + done() + }) + }) + client.on('message', function () { + messageReceived = true + }) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.on('publish', function (packet) { + serverClient.publish(packet) + }) + }) + }) + }) + + it('does not wait acks when force-closing', function (done) { + // non-running broker + var client = connect('mqtt://localhost:8993') + client.publish('test', 'test', { qos: 1 }) + client.end(true, done) + }) + + it('should call cb if store.put fails', function (done) { + const store = new Store() + store.put = function (packet, cb) { + process.nextTick(cb, new Error('oops there is an error')) + } + var client = connect({ incomingStore: store, outgoingStore: store }) + client.publish('test', 'test', { qos: 2 }, function (err) { + if (err) { + client.end(true, done) + } + }) + }) + }) + + describe('publishing', function () { + it('should publish a message (offline)', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + // don't wait on connect to send publish + client.publish(topic, payload) + + server.on('client', onClient) + + function onClient (serverClient) { + serverClient.once('connect', function () { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (online)', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + // block on connect before sending publish + client.on('connect', function () { + client.publish(topic, payload) + }) + + server.on('client', onClient) + + function onClient (serverClient) { + serverClient.once('connect', function () { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (retain, offline)', function (done) { + var client = connect({ queueQoSZero: true }) + var payload = 'test' + var topic = 'test' + var called = false + + client.publish(topic, payload, { retain: true }, function () { + called = true + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, true) + assert.strictEqual(called, true) + client.end(true, done) + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var payload = 'test_payload' + var topic = 'testTopic' + + client.on('packetsend', function (packet) { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + } else { + done(new Error('packet.cmd was not publish!')) + } + }) + + client.publish(topic, payload) + }) + + it('should accept options', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + var opts = { + retain: true, + qos: 1 + } + + client.once('connect', function () { + client.publish(topic, payload, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, false, 'incorrect dup') + client.end(done) + }) + }) + }) + + it('should publish with the default options for an empty parameter', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + var defaultOpts = {qos: 0, retain: false, dup: false} + + client.once('connect', function () { + client.publish(topic, payload, {}) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') + client.end(true, done) + }) + }) + }) + + it('should mark a message as duplicate when "dup" option is set', function (done) { + var client = connect() + var payload = 'duplicated-test' + var topic = 'test' + var opts = { + retain: true, + qos: 1, + dup: true + } + + client.once('connect', function () { + client.publish(topic, payload, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') + client.end(done) + }) + }) + }) + + it('should fire a callback (qos 0)', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('a', 'b', function () { + client.end() + done() + }) + }) + }) + + it('should fire a callback (qos 1)', function (done) { + var client = connect() + var opts = { qos: 1 } + + client.once('connect', function () { + client.publish('a', 'b', opts, function () { + client.end() + done() + }) + }) + }) + + it('should fire a callback (qos 2)', function (done) { + var client = connect() + var opts = { qos: 2 } + + client.once('connect', function () { + client.publish('a', 'b', opts, function () { + client.end() + done() + }) + }) + }) + + it('should support UTF-8 characters in topic', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('中国', 'hello', function () { + client.end() + done() + }) + }) + }) + + it('should support UTF-8 characters in payload', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('hello', '中国', function () { + client.end() + done() + }) + }) + }) + + it('should publish 10 QoS 2 and receive them', function (done) { + var client = connect() + var count = 0 + + client.on('connect', function () { + client.subscribe('test') + client.publish('test', 'test', { qos: 2 }) + }) + + client.on('message', function () { + if (count >= 10) { + client.end() + done() + } else { + client.publish('test', 'test', { qos: 2 }) + } + }) + + server.once('client', function (serverClient) { + serverClient.on('offline', function () { + client.end() + done('error went offline... didnt see this happen') + }) + + serverClient.on('subscribe', function () { + serverClient.on('publish', function (packet) { + serverClient.publish(packet) + }) + }) + + serverClient.on('pubrel', function () { + count++ + }) + }) + }) + + function testQosHandleMessage (qos, done) { + var client = connect() + + var messageEventCount = 0 + var handleMessageCount = 0 + + client.handleMessage = function (packet, callback) { + setTimeout(function () { + handleMessageCount++ + // next message event should not emit until handleMessage completes + assert.strictEqual(handleMessageCount, messageEventCount) + if (handleMessageCount === 10) { + setTimeout(function () { + client.end(true, done) + }) + } + callback() + }, 100) + } + + client.on('message', function (topic, message, packet) { + messageEventCount++ + }) + + client.on('connect', function () { + client.subscribe('test') + }) + + server.once('client', function (serverClient) { + serverClient.on('offline', function () { + client.end(true, function () { + done('error went offline... didnt see this happen') + }) + }) + + serverClient.on('subscribe', function () { + for (var i = 0; i < 10; i++) { + serverClient.publish({ + messageId: i, + topic: 'test', + payload: 'test' + i, + qos: qos + }) + } + }) + }) + } + + var qosTests = [ 0, 1, 2 ] + qosTests.forEach(function (QoS) { + it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { + testQosHandleMessage(QoS, done) + }) + }) + + it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { + var client = connect() + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client._sendPacket = sinon.spy() + + client._handlePublish({ + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1 + }, function (err) { + assert.exists(err) + }) + + assert.strictEqual(client._sendPacket.callCount, 0) + client.end() + client.on('connect', function () { done() }) + }) + + it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePublish` method', function (done) { + var client = connect() + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + try { + client._handlePublish({ + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1 + }) + client.end(true, done) + } catch (err) { + client.end(true, () => { done(err) }) + } + }) + + it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePublish({ + messageId: 1, + topic: 'test', + payload: 'test', + qos: 1 + }, function () { + client.end() + done() + }) + }) + + it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePublish({ + messageId: 1, + topic: 'test', + payload: 'test', + qos: 2 + }, function () { + client.end() + done() + }) + }) + + it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + cb(new Error('Error')) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({ incomingStore: store }) + + client._handlePubrel({ + messageId: 1, + qos: 2 + }, function () { + client.end(true, done) + }) + }) + + it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { + var delComplete = false + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + delComplete = true + cb(null) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePubrel({ + messageId: 1, + qos: 2 + }, function () { + assert.isTrue(delComplete) + client.end(true, done) + }) + }) + + it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function (done) { + var store = new Store() + var client = connect({incomingStore: store}) + + var messageId = Math.floor(65535 * Math.random()) + var topic = 'testTopic' + var payload = 'testPayload' + var qos = 2 + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', function () { + client.subscribe(topic, {qos: 2}) + + store.put({ + messageId: messageId, + topic: topic, + payload: payload, + qos: qos, + cmd: 'publish' + }, function () { + // cleans up the client + client._sendPacket = sinon.spy() + client._handlePubrel({cmd: 'pubrel', messageId: messageId}, function (err) { + assert.exists(err) + assert.strictEqual(client._sendPacket.callCount, 0) + client.end(true, done) + }) + }) + }) + }) + + it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePubrel` method', function (done) { + var store = new Store() + var client = connect({incomingStore: store}) + + var messageId = Math.floor(65535 * Math.random()) + var topic = 'test' + var payload = 'test' + var qos = 2 + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', function () { + client.subscribe(topic, {qos: 2}) + + store.put({ + messageId: messageId, + topic: topic, + payload: payload, + qos: qos, + cmd: 'publish' + }, function () { + try { + client._handlePubrel({cmd: 'pubrel', messageId: messageId}) + client.end(true, done) + } catch (err) { + client.end(true, () => { done(err) }) + } + }) + }) + }) + + it('should keep message order', function (done) { + var publishCount = 0 + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', function () {}) + + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + server2.close() + done() + break + } + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload1', {qos: 1}) + client.publish('topic', 'payload2', {qos: 1}) + client.end(true) + } else { + client.publish('topic', 'payload3', {qos: 1}) + } + }) + client.on('close', function () { + if (!reconnect) { + client.reconnect({ + clean: false, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + } + }) + }) + }) + + function testCallbackStorePutByQoS (qos, clean, expected, done) { + var client = connect({ + clean: clean, + clientId: 'testId' + }) + + var callbacks = [] + + function cbStorePut () { + callbacks.push('storeput') + } + + client.on('connect', function () { + client.publish('test', 'test', {qos: qos, cbStorePut: cbStorePut}, function (err) { + if (err) done(err) + callbacks.push('publish') + assert.deepEqual(callbacks, expected) + client.end(true, done) + }) + }) + } + + var callbackStorePutByQoSParameters = [ + {args: [0, true], expected: ['publish']}, + {args: [0, false], expected: ['publish']}, + {args: [1, true], expected: ['storeput', 'publish']}, + {args: [1, false], expected: ['storeput', 'publish']}, + {args: [2, true], expected: ['storeput', 'publish']}, + {args: [2, false], expected: ['storeput', 'publish']} + ] + + callbackStorePutByQoSParameters.forEach(function (test) { + if (test.args[0] === 0) { // QoS 0 + it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } else { // QoS 1 and 2 + it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } + }) + }) + + describe('unsubscribing', function () { + it('should send an unsubscribe packet (offline)', function (done) { + var client = connect() + + client.unsubscribe('test') + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, 'test') + client.end(done) + }) + }) + }) + + it('should send an unsubscribe packet', function (done) { + var client = connect() + var topic = 'topic' + + client.once('connect', function () { + client.unsubscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, topic) + client.end(done) + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'subscribe') { + client.end(true, done) + } + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetreceive', function (packet) { + if (packet.cmd === 'suback') { + client.end(true, done) + } + }) + }) + + it('should accept an array of unsubs', function (done) { + var client = connect() + var topics = ['topic1', 'topic2'] + + client.once('connect', function () { + client.unsubscribe(topics) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.deepStrictEqual(packet.unsubscriptions, topics) + client.end(done) + }) + }) + }) + + it('should fire a callback on unsuback', function (done) { + var client = connect() + var topic = 'topic' + + client.once('connect', function () { + client.unsubscribe(topic, () => { + client.end(true, done) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + serverClient.unsuback(packet) + }) + }) + }) + + it('should unsubscribe from a chinese topic', function (done) { + var client = connect() + var topic = '中国' + + client.once('connect', function () { + client.unsubscribe(topic, () => { + client.end(err => { + done(err) + }) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, topic) + }) + }) + }) + }) + + describe('keepalive', function () { + var clock + + beforeEach(function () { + clock = sinon.useFakeTimers() + }) + + afterEach(function () { + clock.restore() + }) + + it('should checkPing at keepalive interval', function (done) { + var interval = 3 + var client = connect({ keepalive: interval }) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 1) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 2) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 3) + + client.end(true, done) + }) + }) + + it('should not checkPing if publishing at a higher rate than keepalive', function (done) { + var intervalMs = 3000 + var client = connect({keepalive: intervalMs / 1000}) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 0) + client.end(true, done) + }) + }) + + it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function (done) { + var intervalMs = 3000 + var client = connect({ + keepalive: intervalMs / 1000, + reschedulePings: false + }) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 1) + client.end(true, done) + }) + }) + }) + + describe('pinging', function () { + it('should set a ping timer', function (done) { + var client = connect({keepalive: 3}) + client.once('connect', function () { + assert.exists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should not set a ping timer keepalive=0', function (done) { + var client = connect({keepalive: 0}) + client.on('connect', function () { + assert.notExists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should reconnect if pingresp is not sent', function (done) { + var client = connect({keepalive: 1, reconnectPeriod: 100}) + + // Fake no pingresp being send by stubbing the _handlePingresp function + client._handlePingresp = function () {} + + client.once('connect', function () { + client.once('connect', function () { + client.end(true, done) + }) + }) + }) + + it('should not reconnect if pingresp is successful', function (done) { + var client = connect({keepalive: 100}) + client.once('close', function () { + done(new Error('Client closed connection')) + }) + setTimeout(done, 1000) + }) + + it('should defer the next ping when sending a control packet', function (done) { + var client = connect({keepalive: 1}) + + client.once('connect', function () { + client._checkPing = sinon.spy() + + client.publish('foo', 'bar') + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + done() + }, 75) + }, 75) + }, 75) + }) + }) + }) + + describe('subscribing', function () { + it('should send a subscribe message (offline)', function (done) { + var client = connect() + + client.subscribe('test') + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + done() + }) + }) + }) + + it('should send a subscribe message', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.subscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: 0 + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + done() + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'subscribe') { + done() + } + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetreceive', function (packet) { + if (packet.cmd === 'suback') { + done() + } + }) + }) + + it('should accept an array of subscriptions', function (done) { + var client = connect() + var subs = ['test1', 'test2'] + + client.once('connect', function () { + client.subscribe(subs) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] + var expected = subs.map(function (i) { + var result = {topic: i, qos: 0} + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + return result + }) + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept a hash of subscriptions', function (done) { + var client = connect() + var topics = { + test1: {qos: 0}, + test2: {qos: 1} + } + + client.once('connect', function () { + client.subscribe(topics) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var k + var expected = [] + + for (k in topics) { + if (topics.hasOwnProperty(k)) { + var result = { + topic: k, + qos: topics[k].qos + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + expected.push(result) + } + } + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept an options parameter', function (done) { + var client = connect() + var topic = 'test' + var opts = {qos: 1} + + client.once('connect', function () { + client.subscribe(topic, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var expected = [{ + topic: topic, + qos: 1 + }] + + if (version === 5) { + expected[0].nl = false + expected[0].rap = false + expected[0].rh = 0 + } + + assert.deepStrictEqual(packet.subscriptions, expected) + done() + }) + }) + }) + + it('should subscribe with the default options for an empty options parameter', function (done) { + var client = connect() + var topic = 'test' + var defaultOpts = {qos: 0} + + client.once('connect', function () { + client.subscribe(topic, {}) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: defaultOpts.qos + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + + assert.include(packet.subscriptions[0], result) + client.end(err => done(err)) + }) + }) + }) + + it('should fire a callback on suback', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.subscribe(topic, { qos: 2 }, function (err, granted) { + if (err) { + done(err) + } else { + assert.exists(granted, 'granted not given') + var expectedResult = {topic: 'test', qos: 2} + if (version === 5) { + expectedResult.nl = false + expectedResult.rap = false + expectedResult.rh = 0 + expectedResult.properties = undefined + } + assert.include(granted[0], expectedResult) + client.end(err => done(err)) + } + }) + }) + }) + + it('should fire a callback with error if disconnected (options provided)', function (done) { + var client = connect() + var topic = 'test' + client.once('connect', function () { + client.end(true, function () { + client.subscribe(topic, {qos: 2}, function (err, granted) { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should fire a callback with error if disconnected (options not provided)', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.end(true, function () { + client.subscribe(topic, function (err, granted) { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should subscribe with a chinese topic', function (done) { + var client = connect() + var topic = '中国' + + client.once('connect', function () { + client.subscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: 0 + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + client.end(done) + }) + }) + }) + }) + + describe('receiving messages', function () { + it('should fire the message event', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + // + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.on('packetreceive', function (packet) { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.qos, 1) + assert.strictEqual(packet.topic, testPacket.topic) + assert.strictEqual(packet.payload.toString(), testPacket.payload) + assert.strictEqual(packet.retain, true) + client.end(true, done) + } + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should support binary data', function (done) { + var client = connect({ encoding: 'binary' }) + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2)', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5 + } + + server.testPublish = testPacket + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2) - repeated publish', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5 + } + + server.testPublish = testPacket + + var messageHandler = function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + + assert.strictEqual(spiedMessageHandler.callCount, 1) + client.end(true, done) + } + + var spiedMessageHandler = sinon.spy(messageHandler) + + client.subscribe(testPacket.topic) + client.on('message', spiedMessageHandler) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + // twice, should be ignored + serverClient.publish(testPacket) + }) + }) + }) + + it('should support a chinese topic', function (done) { + var client = connect({ encoding: 'binary' }) + var testPacket = { + topic: '国', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + }) + + describe('qos handling', function () { + it('should follow qos 0 semantics (trivial)', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 0}, () => { + client.end(true, done) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 0, + retain: false + }) + }) + }) + }) + + it('should follow qos 1 semantics', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 50 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 1}) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + messageId: mid, + qos: 1 + }) + }) + + serverClient.once('puback', function (packet) { + assert.strictEqual(packet.messageId, mid) + client.end(done) + }) + }) + }) + + it('should follow qos 2 semantics', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + var publishReceived = 0 + var pubrecReceived = 0 + var pubrelReceived = 0 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + }) + + client.on('packetreceive', (packet) => { + switch (packet.cmd) { + case 'connack': + case 'suback': + // expected, but not specifically part of QOS 2 semantics + break + case 'publish': + assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') + assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') + publishReceived += 1 + break + case 'pubrel': + assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') + pubrelReceived += 1 + break + default: + should.fail() + } + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid + }) + }) + + serverClient.on('pubrec', function () { + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') + assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') + pubrecReceived += 1 + }) + + serverClient.once('pubcomp', function () { + client.removeAllListeners() + serverClient.removeAllListeners() + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') + assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') + client.end(true, done) + }) + }) + }) + + it('should should empty the incoming store after a qos 2 handshake is completed', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + }) + + client.on('packetreceive', (packet) => { + if (packet.cmd === 'pubrel') { + assert.strictEqual(client.incomingStore._inflights.size, 1) + } + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid + }) + }) + + serverClient.once('pubcomp', function () { + assert.strictEqual(client.incomingStore._inflights.size, 0) + client.removeAllListeners() + client.end(true, done) + }) + }) + }) + + function testMultiplePubrel (shouldSendPubcompFail, done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + var pubcompCount = 0 + var pubrelCount = 0 + var handleMessageCount = 0 + var emitMessageCount = 0 + var origSendPacket = client._sendPacket + var shouldSendFail + + client.handleMessage = function (packet, callback) { + handleMessageCount++ + callback() + } + + client.on('message', function () { + emitMessageCount++ + }) + + client._sendPacket = function (packet, sendDone) { + shouldSendFail = packet.cmd === 'pubcomp' && shouldSendPubcompFail + if (sendDone) { + sendDone(shouldSendFail ? new Error('testing pubcomp failure') : undefined) + } + + // send the mocked response + switch (packet.cmd) { + case 'subscribe': + const suback = {cmd: 'suback', messageId: packet.messageId, granted: [2]} + client._handlePacket(suback, function (err) { + assert.isNotOk(err) + }) + break + case 'pubrec': + case 'pubcomp': + // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp + if (packet.cmd === 'pubcomp') { + pubcompCount++ + if (pubcompCount === 2) { + // end the test once the client has gone through two rounds of replying to pubrel messages + assert.strictEqual(pubrelCount, 2) + assert.strictEqual(handleMessageCount, 1) + assert.strictEqual(emitMessageCount, 1) + client._sendPacket = origSendPacket + client.end(true, done) + break + } + } + + // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received + const pubrel = {cmd: 'pubrel', messageId: mid} + pubrelCount++ + client._handlePacket(pubrel, function (err) { + if (shouldSendFail) { + assert.exists(err) + assert.instanceOf(err, Error) + } else { + assert.notExists(err) + } + }) + break + } + } + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + const publish = {cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid} + client._handlePacket(publish, function (err) { + assert.notExists(err) + }) + }) + } + + it('handle qos 2 messages exactly once when multiple pubrel received', function (done) { + testMultiplePubrel(false, done) + }) + + it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function (done) { + testMultiplePubrel(true, done) + }) + }) + + describe('auto reconnect', function () { + it('should mark the client disconnecting if #end called', function (done) { + var client = connect() + + client.end(true, err => { + assert.isTrue(client.disconnecting) + done(err) + }) + }) + + it('should reconnect after stream disconnect', function (done) { + var client = connect() + + var tryReconnect = true + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + client.end(true, done) + } + }) + }) + + it('should emit \'reconnect\' when reconnecting', function (done) { + var client = connect() + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + client.end(true, done) + } + }) + }) + + it('should emit \'offline\' after going offline', function (done) { + var client = connect() + + var tryReconnect = true + var offlineEvent = false + + client.on('offline', function () { + offlineEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(offlineEvent) + client.end(true, done) + } + }) + }) + + it('should not reconnect if it was ended by the user', function (done) { + var client = connect() + + client.on('connect', function () { + client.end() + done() // it will raise an exception if called two times + }) + }) + + it('should setup a reconnect timer on disconnect', function (done) { + var client = connect() + + client.once('connect', function () { + assert.notExists(client.reconnectTimer) + client.stream.end() + }) + + client.once('close', function () { + assert.exists(client.reconnectTimer) + client.end(true, done) + }) + }) + + var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] + reconnectPeriodTests.forEach((test) => { + it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { + var end + var reconnectSlushTime = 200 + var client = connect({reconnectPeriod: test.period}) + var reconnect = false + var start = Date.now() + + client.on('connect', function () { + if (!reconnect) { + client.stream.end() + reconnect = true + } else { + end = Date.now() + client.end(() => { + let reconnectPeriodDuringTest = end - start + if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { + // give the connection a 200 ms slush window + done() + } else { + done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) + } + }) + } + }) + }) + }) + + it('should always cleanup successfully on reconnection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1}) + // bind client.end so that when it is called it is automatically passed in the done callback + setTimeout(client.end.bind(client, done), 50) + }) + + it('should resend in-flight QoS 1 publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, function () { + clientCalledBack = true + check() + }) + + function check () { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight publish messages if disconnecting', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + client.end(true, err => { + assert.isFalse(serverPublished) + assert.isFalse(clientCalledBack) + done(err) + }) + }) + }) + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + serverPublished = true + }) + }) + }) + client.publish('hello', 'world', { qos: 1 }, function () { + clientCalledBack = true + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + + server.once('client', function (serverClient) { + // ignore errors + serverClient.on('error', function () {}) + serverClient.on('publish', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('pubrel', function () { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, function () { + clientCalledBack = true + check() + }) + + function check () { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight QoS 1 removed publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, function (err) { + clientCalledBack = true + assert.exists(err, 'error should exist') + assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, (err) => { + done(err) + }) + }) + + it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, function (err) { + clientCalledBack = true + assert.strictEqual(err.message, 'Message removed') + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, done) + }) + + it('should resubscribe when reconnecting', function (done) { + var client = connect({ reconnectPeriod: 100 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + client.end(done) + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should not resubscribe when reconnecting if resubscribe is disabled', function (done) { + var client = connect({ reconnectPeriod: 100, resubscribe: false }) + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + should.fail() + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) + client.end(true, done) + } + }) + }) + + it('should not resubscribe when reconnecting if suback is error', function (done) { + var tryReconnect = true + var reconnectEvent = false + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', function (packet) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos | 0x80 + }) + }) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + }) + }) + + server2.listen(ports.PORTAND49, function () { + var client = connect({ + port: ports.PORTAND49, + host: 'localhost', + reconnectPeriod: 100 + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + should.fail() + }) + }) + }) + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) + server2.close() + client.end(true, done) + } + }) + }) + }) + + it('should preserved incomingStore after disconnecting if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + if (reconnect) { + serverClient.pubrel({ messageId: 1 }) + } + }) + serverClient.on('subscribe', function (packet) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) + }) + serverClient.on('pubrec', function (packet) { + client.end(false, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + }) + }) + serverClient.on('pubcomp', function (packet) { + client.end(true, () => { + server2.close() + done() + }) + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.subscribe('test', {qos: 2}, function () { + }) + reconnect = true + } + }) + client.on('message', function (topic, message) { + assert.strictEqual(topic, 'topic') + assert.strictEqual(message.toString(), 'payload') + }) + }) + }) + + it('should clear outgoing if close from server', function (done) { + var reconnect = false + var client = {} + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', function (packet) { + if (reconnect) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } else { + serverClient.destroy() + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: true, + clientId: 'cid1', + keepalive: 1, + reconnectPeriod: 0 + }) + + client.on('connect', function () { + client.subscribe('test', {qos: 2}, function (e) { + if (!e) { + client.end() + } + }) + }) + + client.on('close', function () { + if (reconnect) { + server2.close() + done() + } else { + assert.strictEqual(Object.keys(client.outgoing).length, 0) + reconnect = true + client.reconnect() + } + }) + }) + }) + + it('should resend in-flight QoS 1 publish messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, () => { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 1}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 2}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (!reconnect) { + serverClient.pubrec({messageId: packet.messageId}) + } + }) + serverClient.on('pubrel', function () { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 2}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight publish messages by published order', function (done) { + var publishCount = 0 + var reconnect = false + var disconnectOnce = true + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', function () {}) + + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + server2.close() + client.end(true, done) + break + } + } else { + if (disconnectOnce) { + client.end(true, function () { + reconnect = true + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + }) + disconnectOnce = false + } + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.nextId = 65535 + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload1', {qos: 1}) + client.publish('topic', 'payload2', {qos: 1}) + client.publish('topic', 'payload3', {qos: 1}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should be able to pub/sub if reconnect() is called at close handler', function (done) { + var client = connect({ reconnectPeriod: 0 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('close', function () { + if (tryReconnect) { + tryReconnect = false + client.reconnect() + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', function () { + client.end() + }) + } + }) + }) + + it('should be able to pub/sub if reconnect() is called at out of close handler', function (done) { + var client = connect({ reconnectPeriod: 0 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('close', function () { + if (tryReconnect) { + tryReconnect = false + setTimeout(function () { + client.reconnect() + }, 100) + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', function () { + client.end() + }) + } + }) + }) + + context('with alternate server client', function () { + var cachedClientListeners + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + + beforeEach(function () { + cachedClientListeners = server.listeners('client') + server.removeAllListeners('client') + }) + + afterEach(function () { + server.removeAllListeners('client') + cachedClientListeners.forEach(function (listener) { + server.on('client', listener) + }) + }) + + it('should resubscribe even if disconnect is before suback', function (done) { + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) + var subscribeCount = 0 + var connectCount = 0 + + server.on('client', function (serverClient) { + serverClient.on('connect', function () { + connectCount++ + serverClient.connack(connack) + }) + + serverClient.on('subscribe', function () { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, confirm that the only two + // subscribes have taken place, then cleanup and exit + if (connectCount >= 2) { + assert.strictEqual(subscribeCount, 2) + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + + it('should resubscribe exactly once', function (done) { + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) + var subscribeCount = 0 + + server.on('client', function (serverClient) { + serverClient.on('connect', function () { + serverClient.connack(connack) + }) + + serverClient.on('subscribe', function () { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, only two subs + // subscribes have taken place, then cleanup and exit + if (subscribeCount === 2) { + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + }) + }) +} diff --git a/test/abstract_store.js b/test/abstract_store.js index 02b3ec849..33b78106d 100644 --- a/test/abstract_store.js +++ b/test/abstract_store.js @@ -1,135 +1,135 @@ -'use strict' - -require('should') - -module.exports = function abstractStoreTest (build) { - var store - - beforeEach(function (done) { - build(function (err, _store) { - store = _store - done(err) - }) - }) - - afterEach(function (done) { - store.close(done) - }) - - it('should put and stream in-flight packets', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet) - done() - }) - }) - }) - - it('should support destroying the stream', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - var stream = store.createStream() - stream.on('close', done) - stream.destroy() - }) - }) - - it('should add and del in-flight packets', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del(packet, function () { - store - .createStream() - .on('data', function () { - done(new Error('this should never happen')) - }) - .on('end', done) - }) - }) - }) - - it('should replace a packet when doing put with the same messageId', function (done) { - var packet1 = { - cmd: 'publish', // added - topic: 'hello', - payload: 'world', - qos: 2, - messageId: 42 - } - var packet2 = { - cmd: 'pubrel', // added - qos: 2, - messageId: 42 - } - - store.put(packet1, function () { - store.put(packet2, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet2) - done() - }) - }) - }) - }) - - it('should return the original packet on del', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del({ messageId: 42 }, function (err, deleted) { - if (err) { - throw err - } - deleted.should.eql(packet) - done() - }) - }) - }) - - it('should get a packet with the same messageId', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.get({ messageId: 42 }, function (err, fromDb) { - if (err) { - throw err - } - fromDb.should.eql(packet) - done() - }) - }) - }) -} +'use strict' + +require('should') + +module.exports = function abstractStoreTest (build) { + var store + + beforeEach(function (done) { + build(function (err, _store) { + store = _store + done(err) + }) + }) + + afterEach(function (done) { + store.close(done) + }) + + it('should put and stream in-flight packets', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store + .createStream() + .on('data', function (data) { + data.should.eql(packet) + done() + }) + }) + }) + + it('should support destroying the stream', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + var stream = store.createStream() + stream.on('close', done) + stream.destroy() + }) + }) + + it('should add and del in-flight packets', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.del(packet, function () { + store + .createStream() + .on('data', function () { + done(new Error('this should never happen')) + }) + .on('end', done) + }) + }) + }) + + it('should replace a packet when doing put with the same messageId', function (done) { + var packet1 = { + cmd: 'publish', // added + topic: 'hello', + payload: 'world', + qos: 2, + messageId: 42 + } + var packet2 = { + cmd: 'pubrel', // added + qos: 2, + messageId: 42 + } + + store.put(packet1, function () { + store.put(packet2, function () { + store + .createStream() + .on('data', function (data) { + data.should.eql(packet2) + done() + }) + }) + }) + }) + + it('should return the original packet on del', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.del({ messageId: 42 }, function (err, deleted) { + if (err) { + throw err + } + deleted.should.eql(packet) + done() + }) + }) + }) + + it('should get a packet with the same messageId', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.get({ messageId: 42 }, function (err, fromDb) { + if (err) { + throw err + } + fromDb.should.eql(packet) + done() + }) + }) + }) +} diff --git a/test/browser/server.js b/test/browser/server.js index 75a9a8994..c4cf66b96 100644 --- a/test/browser/server.js +++ b/test/browser/server.js @@ -1,132 +1,132 @@ -'use strict' - -var handleClient -var WS = require('ws') -var WebSocketServer = WS.Server -var Connection = require('mqtt-connection') -var http = require('http') - -handleClient = function (client) { - var self = this - - if (!self.clients) { - self.clients = {} - } - - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) - } else { - client.connack({returnCode: 0}) - } - self.clients[packet.clientId] = client - client.subscriptions = [] - }) - - client.on('publish', function (packet) { - var i, k, c, s, publish - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - - for (k in self.clients) { - c = self.clients[k] - publish = false - - for (i = 0; i < c.subscriptions.length; i++) { - s = c.subscriptions[i] - - if (s.test(packet.topic)) { - publish = true - } - } - - if (publish) { - try { - c.publish({topic: packet.topic, payload: packet.payload}) - } catch (error) { - delete self.clients[k] - } - } - } - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - var qos - var topic - var reg - var granted = [] - - for (var i = 0; i < packet.subscriptions.length; i++) { - qos = packet.subscriptions[i].qos - topic = packet.subscriptions[i].topic - reg = new RegExp(topic.replace('+', '[^/]+').replace('#', '.+') + '$') - - granted.push(qos) - client.subscriptions.push(reg) - } - - client.suback({messageId: packet.messageId, granted: granted}) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -function start (startPort, done) { - var server = http.createServer() - var wss = new WebSocketServer({server: server}) - - wss.on('connection', function (ws) { - var stream, connection - - if (!(ws.protocol === 'mqtt' || - ws.protocol === 'mqttv3.1')) { - return ws.close() - } - - stream = WS.createWebSocketStream(ws) - connection = new Connection(stream) - handleClient.call(server, connection) - }) - server.listen(startPort, done) - server.on('request', function (req, res) { - res.statusCode = 404 - res.end('Not Found') - }) - return server -} - -if (require.main === module) { - start(process.env.PORT || process.env.AIRTAP_PORT, function (err) { - if (err) { - console.error(err) - return - } - console.log('tunnelled server started on port', process.env.PORT || process.env.AIRTAP_PORT) - }) -} +'use strict' + +var handleClient +var WS = require('ws') +var WebSocketServer = WS.Server +var Connection = require('mqtt-connection') +var http = require('http') + +handleClient = function (client) { + var self = this + + if (!self.clients) { + self.clients = {} + } + + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({returnCode: 2}) + } else { + client.connack({returnCode: 0}) + } + self.clients[packet.clientId] = client + client.subscriptions = [] + }) + + client.on('publish', function (packet) { + var i, k, c, s, publish + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + + for (k in self.clients) { + c = self.clients[k] + publish = false + + for (i = 0; i < c.subscriptions.length; i++) { + s = c.subscriptions[i] + + if (s.test(packet.topic)) { + publish = true + } + } + + if (publish) { + try { + c.publish({topic: packet.topic, payload: packet.payload}) + } catch (error) { + delete self.clients[k] + } + } + } + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + var qos + var topic + var reg + var granted = [] + + for (var i = 0; i < packet.subscriptions.length; i++) { + qos = packet.subscriptions[i].qos + topic = packet.subscriptions[i].topic + reg = new RegExp(topic.replace('+', '[^/]+').replace('#', '.+') + '$') + + granted.push(qos) + client.subscriptions.push(reg) + } + + client.suback({messageId: packet.messageId, granted: granted}) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +function start (startPort, done) { + var server = http.createServer() + var wss = new WebSocketServer({server: server}) + + wss.on('connection', function (ws) { + var stream, connection + + if (!(ws.protocol === 'mqtt' || + ws.protocol === 'mqttv3.1')) { + return ws.close() + } + + stream = WS.createWebSocketStream(ws) + connection = new Connection(stream) + handleClient.call(server, connection) + }) + server.listen(startPort, done) + server.on('request', function (req, res) { + res.statusCode = 404 + res.end('Not Found') + }) + return server +} + +if (require.main === module) { + start(process.env.PORT || process.env.AIRTAP_PORT, function (err) { + if (err) { + console.error(err) + return + } + console.log('tunnelled server started on port', process.env.PORT || process.env.AIRTAP_PORT) + }) +} diff --git a/test/browser/test.js b/test/browser/test.js index 8e9cd42e3..78fa93cc5 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -1,92 +1,92 @@ -'use strict' - -var mqtt = require('../../lib/connect') -var xtend = require('xtend') -var _URL = require('url') -var parsed = _URL.parse(document.URL) -var isHttps = parsed.protocol === 'https:' -var port = parsed.port || (isHttps ? 443 : 80) -var host = parsed.hostname -var protocol = isHttps ? 'wss' : 'ws' - -function clientTests (buildClient) { - var client - - beforeEach(function () { - client = buildClient() - client.on('offline', function () { - console.log('client offline') - }) - client.on('connect', function () { - console.log('client connect') - }) - client.on('reconnect', function () { - console.log('client reconnect') - }) - }) - - afterEach(function (done) { - client.once('close', function () { - done() - }) - client.end() - }) - - it('should connect', function (done) { - client.on('connect', function () { - done() - }) - }) - - it('should publish and subscribe', function (done) { - client.subscribe('hello', function () { - done() - }).publish('hello', 'world') - }) -} - -function suiteFactory (configName, opts) { - function setVersion (base) { - return xtend(base || {}, opts) - } - - var suiteName = 'MqttClient(' + configName + '=' + JSON.stringify(opts) + ')' - describe(suiteName, function () { - this.timeout(10000) - - describe('specifying nothing', function () { - clientTests(function () { - return mqtt.connect(setVersion()) - }) - }) - - if (parsed.hostname === 'localhost') { - describe('specifying a port', function () { - clientTests(function () { - return mqtt.connect(setVersion({ protocol: protocol, port: port })) - }) - }) - } - - describe('specifying a port and host', function () { - clientTests(function () { - return mqtt.connect(setVersion({ protocol: protocol, port: port, host: host })) - }) - }) - - describe('specifying a URL', function () { - clientTests(function () { - return mqtt.connect(protocol + '://' + host + ':' + port, setVersion()) - }) - }) - - describe('specifying a URL with a path', function () { - clientTests(function () { - return mqtt.connect(protocol + '://' + host + ':' + port + '/mqtt', setVersion()) - }) - }) - }) -} - -suiteFactory('v3', {protocolId: 'MQIsdp', protocolVersion: 3}) -suiteFactory('default', {}) +'use strict' + +var mqtt = require('../../lib/connect') +var xtend = require('xtend') +var _URL = require('url') +var parsed = _URL.parse(document.URL) +var isHttps = parsed.protocol === 'https:' +var port = parsed.port || (isHttps ? 443 : 80) +var host = parsed.hostname +var protocol = isHttps ? 'wss' : 'ws' + +function clientTests (buildClient) { + var client + + beforeEach(function () { + client = buildClient() + client.on('offline', function () { + console.log('client offline') + }) + client.on('connect', function () { + console.log('client connect') + }) + client.on('reconnect', function () { + console.log('client reconnect') + }) + }) + + afterEach(function (done) { + client.once('close', function () { + done() + }) + client.end() + }) + + it('should connect', function (done) { + client.on('connect', function () { + done() + }) + }) + + it('should publish and subscribe', function (done) { + client.subscribe('hello', function () { + done() + }).publish('hello', 'world') + }) +} + +function suiteFactory (configName, opts) { + function setVersion (base) { + return xtend(base || {}, opts) + } + + var suiteName = 'MqttClient(' + configName + '=' + JSON.stringify(opts) + ')' + describe(suiteName, function () { + this.timeout(10000) + + describe('specifying nothing', function () { + clientTests(function () { + return mqtt.connect(setVersion()) + }) + }) + + if (parsed.hostname === 'localhost') { + describe('specifying a port', function () { + clientTests(function () { + return mqtt.connect(setVersion({ protocol: protocol, port: port })) + }) + }) + } + + describe('specifying a port and host', function () { + clientTests(function () { + return mqtt.connect(setVersion({ protocol: protocol, port: port, host: host })) + }) + }) + + describe('specifying a URL', function () { + clientTests(function () { + return mqtt.connect(protocol + '://' + host + ':' + port, setVersion()) + }) + }) + + describe('specifying a URL with a path', function () { + clientTests(function () { + return mqtt.connect(protocol + '://' + host + ':' + port + '/mqtt', setVersion()) + }) + }) + }) +} + +suiteFactory('v3', {protocolId: 'MQIsdp', protocolVersion: 3}) +suiteFactory('default', {}) diff --git a/test/client.js b/test/client.js index 4ea052ab8..0b3c4228a 100644 --- a/test/client.js +++ b/test/client.js @@ -1,486 +1,486 @@ -'use strict' - -var mqtt = require('..') -var assert = require('chai').assert -const { fork } = require('child_process') -var path = require('path') -var abstractClientTests = require('./abstract_client') -var net = require('net') -var eos = require('end-of-stream') -var mqttPacket = require('mqtt-packet') -var Duplex = require('readable-stream').Duplex -var Connection = require('mqtt-connection') -var MqttServer = require('./server').MqttServer -var util = require('util') -var ports = require('./helpers/port_list') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var debug = require('debug')('TEST:client') - -describe('MqttClient', function () { - var client - var server = serverBuilder('mqtt') - var config = {protocol: 'mqtt', port: ports.PORT} - server.listen(ports.PORT) - - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) - - abstractClientTests(server, config) - - describe('creating', function () { - it('should allow instantiation of MqttClient without the \'new\' operator', function (done) { - try { - client = mqtt.MqttClient(function () { - throw Error('break') - }, {}) - client.end() - } catch (err) { - assert.strictEqual(err.message, 'break') - done() - } - }) - }) - - describe('message ids', function () { - it('should increment the message id', function () { - client = mqtt.connect(config) - var currentId = client._nextId() - - assert.equal(client._nextId(), currentId + 1) - client.end() - }) - - it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { - var server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - client = mqtt.connect({ - port: ports.PORTAND49, - host: 'localhost' - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'pubcomp') { - client.end() - server2.close() - done() - } - }) - }) - }) - - it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function (done) { - var parser = mqttPacket.parser() - var count = 0 - var max = 1000 - var duplex = new Duplex({ - read: function (n) {}, - write: function (chunk, enc, cb) { - parser.parse(chunk) - cb() // nothing to do - } - }) - client = new mqtt.MqttClient(function () { - return duplex - }, {}) - - client.on('message', function (t, p, packet) { - if (++count === max) { - done() - } - }) - - parser.on('packet', function (packet) { - var packets = [] - - if (packet.cmd === 'connect') { - duplex.push(mqttPacket.generate({ - cmd: 'connack', - sessionPresent: false, - returnCode: 0 - })) - - for (var i = 0; i < max; i++) { - packets.push(mqttPacket.generate({ - cmd: 'publish', - topic: Buffer.from('hello'), - payload: Buffer.from('world'), - retain: false, - dup: false, - messageId: i + 1, - qos: 1 - })) - } - - duplex.push(Buffer.concat(packets)) - } - }) - }) - }) - - describe('flushing', function () { - it('should attempt to complete pending unsub and send on ping timeout', function (done) { - this.timeout(10000) - var server3 = new MqttServer(function (client) { - client.on('connect', function (packet) { - client.connack({returnCode: 0}) - }) - }).listen(ports.PORTAND72) - - var pubCallbackCalled = false - var unsubscribeCallbackCalled = false - client = mqtt.connect({ - port: ports.PORTAND72, - host: 'localhost', - keepalive: 1, - connectTimeout: 350, - reconnectPeriod: 0 - }) - client.once('connect', () => { - client.publish('fakeTopic', 'fakeMessage', {qos: 1}, (err, result) => { - assert.exists(err) - pubCallbackCalled = true - }) - client.unsubscribe('fakeTopic', (err, result) => { - assert.exists(err) - unsubscribeCallbackCalled = true - }) - setTimeout(() => { - client.end(() => { - assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') - server3.close() - done() - }) - }, 5000) - }) - }) - }) - - describe('reconnecting', function () { - it('should attempt to reconnect once server is down', function (done) { - this.timeout(30000) - - var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) - innerServer.on('close', (code) => { - if (code) { - done(util.format('child process closed with code %d', code)) - } - }) - - innerServer.on('exit', (code) => { - if (code) { - done(util.format('child process exited with code %d', code)) - } - }) - - client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) - client.once('connect', function () { - innerServer.kill('SIGINT') // mocks server shutdown - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, done) - }) - }) - }) - - it('should reconnect if a connack is not received in an interval', function (done) { - this.timeout(2000) - - var server2 = net.createServer().listen(ports.PORTAND43) - - server2.on('connection', function (c) { - eos(c, function () { - server2.close() - }) - }) - - server2.on('listening', function () { - client = mqtt.connect({ - servers: [ - { port: ports.PORTAND43, host: 'localhost_fake' }, - { port: ports.PORT, host: 'localhost' } - ], - connectTimeout: 500 - }) - - server.once('client', function () { - client.end(true, (err) => { - done(err) - }) - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - - it('should not be cleared by the connack timer', function (done) { - this.timeout(4000) - - var server2 = net.createServer().listen(ports.PORTAND44) - - server2.on('connection', function (c) { - c.destroy() - }) - - server2.once('listening', function () { - var reconnects = 0 - var connectTimeout = 1000 - var reconnectPeriod = 100 - var expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) - client = mqtt.connect({ - port: ports.PORTAND44, - host: 'localhost', - connectTimeout: connectTimeout, - reconnectPeriod: reconnectPeriod - }) - - client.on('reconnect', function () { - reconnects++ - if (reconnects >= expectedReconnects) { - client.end(true, done) - } - }) - }) - }) - - it('should not keep requeueing the first message when offline', function (done) { - this.timeout(2500) - - var server2 = serverBuilder('mqtt').listen(ports.PORTAND45) - client = mqtt.connect({ - port: ports.PORTAND45, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - server2.on('client', function (serverClient) { - client.publish('hello', 'world', { qos: 1 }, function () { - serverClient.destroy() - server2.close(() => { - debug('now publishing message in an offline state') - client.publish('hello', 'world', { qos: 1 }) - }) - }) - }) - - setTimeout(function () { - if (client.queue.length === 0) { - debug('calling final client.end()') - client.end(true, (err) => done(err)) - } else { - debug('calling client.end()') - client.end(true) - } - }, 2000) - }) - - it('should not send the same subscribe multiple times on a flaky connection', function (done) { - this.timeout(3500) - - var KILL_COUNT = 4 - var killedConnections = 0 - var subIds = {} - client = mqtt.connect({ - port: ports.PORTAND46, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - var server2 = new MqttServer(function (serverClient) { - serverClient.on('error', function () {}) - debug('setting serverClient connect callback') - serverClient.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - debug('connack with returnCode 2') - serverClient.connack({returnCode: 2}) - } else { - debug('connack with returnCode 0') - serverClient.connack({returnCode: 0}) - } - }) - }).listen(ports.PORTAND46) - - server2.on('client', function (serverClient) { - debug('client received on server2.') - debug('subscribing to topic `topic`') - client.subscribe('topic', function () { - debug('once subscribed to topic, end client, destroy serverClient, and close server.') - serverClient.destroy() - server2.close(() => { client.end(true, done) }) - }) - - serverClient.on('subscribe', function (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few sub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - } else { - // Keep track of acks - if (!subIds[packet.messageId]) { - subIds[packet.messageId] = 0 - } - subIds[packet.messageId]++ - if (subIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) - client.end(true) - serverClient.end() - server2.destroy() - } - - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } - }) - }) - }) - - it('should not fill the queue of subscribes if it cannot connect', function (done) { - this.timeout(2500) - var server2 = net.createServer(function (stream) { - var serverClient = new Connection(stream) - - serverClient.on('error', function (e) { /* do nothing */ }) - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - serverClient.destroy() - }) - }) - - server2.listen(ports.PORTAND48, function () { - client = mqtt.connect({ - port: ports.PORTAND48, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - client.subscribe('hello') - - setTimeout(function () { - assert.equal(client.queue.length, 1) - client.end(true, () => { - done() - }) - }, 1000) - }) - }) - - it('should not send the same publish multiple times on a flaky connection', function (done) { - this.timeout(3500) - - var KILL_COUNT = 4 - var killedConnections = 0 - var pubIds = {} - client = mqtt.connect({ - port: ports.PORTAND47, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - var server2 = net.createServer(function (stream) { - var serverClient = new Connection(stream) - serverClient.on('error', function () {}) - serverClient.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - serverClient.connack({returnCode: 2}) - } else { - serverClient.connack({returnCode: 0}) - } - }) - - this.emit('client', serverClient) - }).listen(ports.PORTAND47) - - server2.on('client', function (serverClient) { - client.publish('topic', 'data', { qos: 1 }, function () { - serverClient.destroy() - server2.close() - client.end(true, done) - }) - - serverClient.on('publish', function onPublish (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few pub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - - // to avoid receiving inflight messages - serverClient.removeListener('publish', onPublish) - } else { - // Keep track of acks - if (!pubIds[packet.messageId]) { - pubIds[packet.messageId] = 0 - } - - pubIds[packet.messageId]++ - - if (pubIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) - client.end(true) - serverClient.destroy() - server2.destroy() - } - - serverClient.puback(packet) - } - }) - }) - }) - }) - - it('check emit error on checkDisconnection w/o callback', function (done) { - this.timeout(15000) - - var server118 = new MqttServer(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0 - }) - }) - client.on('publish', function (packet) { - setImmediate(function () { - packet.reasonCode = 0 - client.puback(packet) - }) - }) - }).listen(ports.PORTAND118) - - var opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - client = mqtt.connect(opts) - - // wait for the client to receive an error... - client.on('error', function (error) { - assert.equal(error.message, 'client disconnecting') - server118.close() - done() - }) - client.on('connect', function () { - client.end(function () { - client._checkDisconnecting() - }) - server118.close() - }) - }) -}) +'use strict' + +var mqtt = require('..') +var assert = require('chai').assert +const { fork } = require('child_process') +var path = require('path') +var abstractClientTests = require('./abstract_client') +var net = require('net') +var eos = require('end-of-stream') +var mqttPacket = require('mqtt-packet') +var Duplex = require('readable-stream').Duplex +var Connection = require('mqtt-connection') +var MqttServer = require('./server').MqttServer +var util = require('util') +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var debug = require('debug')('TEST:client') + +describe('MqttClient', function () { + var client + var server = serverBuilder('mqtt') + var config = {protocol: 'mqtt', port: ports.PORT} + server.listen(ports.PORT) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) + + describe('creating', function () { + it('should allow instantiation of MqttClient without the \'new\' operator', function (done) { + try { + client = mqtt.MqttClient(function () { + throw Error('break') + }, {}) + client.end() + } catch (err) { + assert.strictEqual(err.message, 'break') + done() + } + }) + }) + + describe('message ids', function () { + it('should increment the message id', function () { + client = mqtt.connect(config) + var currentId = client._nextId() + + assert.equal(client._nextId(), currentId + 1) + client.end() + }) + + it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + }) + }) + + server2.listen(ports.PORTAND49, function () { + client = mqtt.connect({ + port: ports.PORTAND49, + host: 'localhost' + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'pubcomp') { + client.end() + server2.close() + done() + } + }) + }) + }) + + it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function (done) { + var parser = mqttPacket.parser() + var count = 0 + var max = 1000 + var duplex = new Duplex({ + read: function (n) {}, + write: function (chunk, enc, cb) { + parser.parse(chunk) + cb() // nothing to do + } + }) + client = new mqtt.MqttClient(function () { + return duplex + }, {}) + + client.on('message', function (t, p, packet) { + if (++count === max) { + done() + } + }) + + parser.on('packet', function (packet) { + var packets = [] + + if (packet.cmd === 'connect') { + duplex.push(mqttPacket.generate({ + cmd: 'connack', + sessionPresent: false, + returnCode: 0 + })) + + for (var i = 0; i < max; i++) { + packets.push(mqttPacket.generate({ + cmd: 'publish', + topic: Buffer.from('hello'), + payload: Buffer.from('world'), + retain: false, + dup: false, + messageId: i + 1, + qos: 1 + })) + } + + duplex.push(Buffer.concat(packets)) + } + }) + }) + }) + + describe('flushing', function () { + it('should attempt to complete pending unsub and send on ping timeout', function (done) { + this.timeout(10000) + var server3 = new MqttServer(function (client) { + client.on('connect', function (packet) { + client.connack({returnCode: 0}) + }) + }).listen(ports.PORTAND72) + + var pubCallbackCalled = false + var unsubscribeCallbackCalled = false + client = mqtt.connect({ + port: ports.PORTAND72, + host: 'localhost', + keepalive: 1, + connectTimeout: 350, + reconnectPeriod: 0 + }) + client.once('connect', () => { + client.publish('fakeTopic', 'fakeMessage', {qos: 1}, (err, result) => { + assert.exists(err) + pubCallbackCalled = true + }) + client.unsubscribe('fakeTopic', (err, result) => { + assert.exists(err) + unsubscribeCallbackCalled = true + }) + setTimeout(() => { + client.end(() => { + assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') + server3.close() + done() + }) + }, 5000) + }) + }) + }) + + describe('reconnecting', function () { + it('should attempt to reconnect once server is down', function (done) { + this.timeout(30000) + + var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) + innerServer.on('close', (code) => { + if (code) { + done(util.format('child process closed with code %d', code)) + } + }) + + innerServer.on('exit', (code) => { + if (code) { + done(util.format('child process exited with code %d', code)) + } + }) + + client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) + client.once('connect', function () { + innerServer.kill('SIGINT') // mocks server shutdown + client.once('close', function () { + assert.exists(client.reconnectTimer) + client.end(true, done) + }) + }) + }) + + it('should reconnect if a connack is not received in an interval', function (done) { + this.timeout(2000) + + var server2 = net.createServer().listen(ports.PORTAND43) + + server2.on('connection', function (c) { + eos(c, function () { + server2.close() + }) + }) + + server2.on('listening', function () { + client = mqtt.connect({ + servers: [ + { port: ports.PORTAND43, host: 'localhost_fake' }, + { port: ports.PORT, host: 'localhost' } + ], + connectTimeout: 500 + }) + + server.once('client', function () { + client.end(true, (err) => { + done(err) + }) + }) + + client.once('connect', function () { + client.stream.destroy() + }) + }) + }) + + it('should not be cleared by the connack timer', function (done) { + this.timeout(4000) + + var server2 = net.createServer().listen(ports.PORTAND44) + + server2.on('connection', function (c) { + c.destroy() + }) + + server2.once('listening', function () { + var reconnects = 0 + var connectTimeout = 1000 + var reconnectPeriod = 100 + var expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) + client = mqtt.connect({ + port: ports.PORTAND44, + host: 'localhost', + connectTimeout: connectTimeout, + reconnectPeriod: reconnectPeriod + }) + + client.on('reconnect', function () { + reconnects++ + if (reconnects >= expectedReconnects) { + client.end(true, done) + } + }) + }) + }) + + it('should not keep requeueing the first message when offline', function (done) { + this.timeout(2500) + + var server2 = serverBuilder('mqtt').listen(ports.PORTAND45) + client = mqtt.connect({ + port: ports.PORTAND45, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + server2.on('client', function (serverClient) { + client.publish('hello', 'world', { qos: 1 }, function () { + serverClient.destroy() + server2.close(() => { + debug('now publishing message in an offline state') + client.publish('hello', 'world', { qos: 1 }) + }) + }) + }) + + setTimeout(function () { + if (client.queue.length === 0) { + debug('calling final client.end()') + client.end(true, (err) => done(err)) + } else { + debug('calling client.end()') + client.end(true) + } + }, 2000) + }) + + it('should not send the same subscribe multiple times on a flaky connection', function (done) { + this.timeout(3500) + + var KILL_COUNT = 4 + var killedConnections = 0 + var subIds = {} + client = mqtt.connect({ + port: ports.PORTAND46, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + var server2 = new MqttServer(function (serverClient) { + serverClient.on('error', function () {}) + debug('setting serverClient connect callback') + serverClient.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + debug('connack with returnCode 2') + serverClient.connack({returnCode: 2}) + } else { + debug('connack with returnCode 0') + serverClient.connack({returnCode: 0}) + } + }) + }).listen(ports.PORTAND46) + + server2.on('client', function (serverClient) { + debug('client received on server2.') + debug('subscribing to topic `topic`') + client.subscribe('topic', function () { + debug('once subscribed to topic, end client, destroy serverClient, and close server.') + serverClient.destroy() + server2.close(() => { client.end(true, done) }) + }) + + serverClient.on('subscribe', function (packet) { + if (killedConnections < KILL_COUNT) { + // Kill the first few sub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + } else { + // Keep track of acks + if (!subIds[packet.messageId]) { + subIds[packet.messageId] = 0 + } + subIds[packet.messageId]++ + if (subIds[packet.messageId] > 1) { + done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) + client.end(true) + serverClient.end() + server2.destroy() + } + + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } + }) + }) + }) + + it('should not fill the queue of subscribes if it cannot connect', function (done) { + this.timeout(2500) + var server2 = net.createServer(function (stream) { + var serverClient = new Connection(stream) + + serverClient.on('error', function (e) { /* do nothing */ }) + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.destroy() + }) + }) + + server2.listen(ports.PORTAND48, function () { + client = mqtt.connect({ + port: ports.PORTAND48, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + client.subscribe('hello') + + setTimeout(function () { + assert.equal(client.queue.length, 1) + client.end(true, () => { + done() + }) + }, 1000) + }) + }) + + it('should not send the same publish multiple times on a flaky connection', function (done) { + this.timeout(3500) + + var KILL_COUNT = 4 + var killedConnections = 0 + var pubIds = {} + client = mqtt.connect({ + port: ports.PORTAND47, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + var server2 = net.createServer(function (stream) { + var serverClient = new Connection(stream) + serverClient.on('error', function () {}) + serverClient.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + serverClient.connack({returnCode: 2}) + } else { + serverClient.connack({returnCode: 0}) + } + }) + + this.emit('client', serverClient) + }).listen(ports.PORTAND47) + + server2.on('client', function (serverClient) { + client.publish('topic', 'data', { qos: 1 }, function () { + serverClient.destroy() + server2.close() + client.end(true, done) + }) + + serverClient.on('publish', function onPublish (packet) { + if (killedConnections < KILL_COUNT) { + // Kill the first few pub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + + // to avoid receiving inflight messages + serverClient.removeListener('publish', onPublish) + } else { + // Keep track of acks + if (!pubIds[packet.messageId]) { + pubIds[packet.messageId] = 0 + } + + pubIds[packet.messageId]++ + + if (pubIds[packet.messageId] > 1) { + done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) + client.end(true) + serverClient.destroy() + server2.destroy() + } + + serverClient.puback(packet) + } + }) + }) + }) + }) + + it('check emit error on checkDisconnection w/o callback', function (done) { + this.timeout(15000) + + var server118 = new MqttServer(function (client) { + client.on('connect', function (packet) { + client.connack({ + reasonCode: 0 + }) + }) + client.on('publish', function (packet) { + setImmediate(function () { + packet.reasonCode = 0 + client.puback(packet) + }) + }) + }).listen(ports.PORTAND118) + + var opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5 + } + client = mqtt.connect(opts) + + // wait for the client to receive an error... + client.on('error', function (error) { + assert.equal(error.message, 'client disconnecting') + server118.close() + done() + }) + client.on('connect', function () { + client.end(function () { + client._checkDisconnecting() + }) + server118.close() + }) + }) +}) diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index fd2bb9979..0fe2ecb88 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -1,1053 +1,1053 @@ -'use strict' - -var mqtt = require('..') -var abstractClientTests = require('./abstract_client') -var MqttServer = require('./server').MqttServer -var assert = require('chai').assert -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var ports = require('./helpers/port_list') - -describe('MQTT 5.0', function () { - var server = serverBuilder('mqtt').listen(ports.PORTAND115) - var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } - - abstractClientTests(server, config) - - it('topic should be complemented on receive', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - assert.strictEqual(packet.properties.topicAliasMaximum, 3) - serverClient.connack({ - reasonCode: 0 - }) - // register topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // overwrite registered topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test2', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('message', function (topic, messagee, packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 3: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - server103.close() - client.end(true, done) - break - } - }) - }) - - it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoUseTopicAlias: true - } - var client = mqtt.connect(opts) - - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias by autoApplyTopicAlias - client.publish('test1', 'Message') - }) - }) - - it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoAssignTopicAlias: true - } - var client = mqtt.connect(opts) - - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 2) - break - case 2: - assert.strictEqual(packet.topic, 'test3') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 3: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 4: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 5: - assert.strictEqual(packet.topic, 'test4') - assert.strictEqual(packet.properties.topicAlias, 2) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message') - client.publish('test2', 'Message') - client.publish('test3', 'Message') - - // use topicAlias - client.publish('test1', 'Message') - client.publish('test3', 'Message') - - // renew LRU topicAlias - client.publish('test4', 'Message') - }) - }) - - it('topicAlias should be removed and topic restored on resend', function (done) { - this.timeout(15000) - - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore: incomingStore, - outgoingStore: outgoingStore, - clean: false, - reconnectPeriod: 100 - } - var client = mqtt.connect(opts) - - var connectCount = 0 - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 2: - assert.strictEqual(packet.topic, 'test1') - var alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - serverClient.puback({messageId: packet.messageId}) - break - case 3: - assert.strictEqual(packet.topic, 'test1') - var alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - serverClient.puback({messageId: packet.messageId}) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.once('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('topicAlias should be removed and topic restored on offline publish', function (done) { - this.timeout(15000) - - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore: incomingStore, - outgoingStore: outgoingStore, - clean: false, - reconnectPeriod: 100 - } - var client = mqtt.connect(opts) - - var connectCount = 0 - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - var alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - assert.strictEqual(packet.qos, 1) - serverClient.puback({messageId: packet.messageId}) - break - case 1: - assert.strictEqual(packet.topic, 'test1') - var alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - assert.strictEqual(packet.qos, 0) - break - case 2: - assert.strictEqual(packet.topic, 'test1') - var alias3 - if (packet.properties) { - alias3 = packet.properties.topicAlias - } - assert.strictEqual(alias3, undefined) - assert.strictEqual(packet.qos, 0) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.once('close', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 4 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - server103.close() - client.end(true, done) - }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 1 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - server103.close() - client.end(true, done) - }) - }) - }) - - it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 4 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if broker PUBLISH topicAlias:0', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 0 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: '', // use topic alias - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } // in range topic alias - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received unregistered Topic Alias') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if there is Auth Data with no Auth Method', function (done) { - this.timeout(5000) - var client - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} - console.log('client connecting') - client = mqtt.connect(opts) - client.on('error', function (error) { - console.log('error hit') - assert.strictEqual(error.message, 'Packet has no Authentication Method') - // client will not be connected, so we will call done. - assert.isTrue(client.disconnected, 'validate client is disconnected') - client.end(true, done) - }) - }) - - it('auth packet', function (done) { - this.timeout(15000) - server.once('client', function (serverClient) { - console.log('server received client') - serverClient.on('auth', function (packet) { - console.log('serverClient received auth: packet %o', packet) - serverClient.end(done) - }) - }) - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} - console.log('calling mqtt connect') - mqtt.connect(opts) - }) - - it('Maximum Packet Size', function (done) { - this.timeout(15000) - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'exceeding packets size connack') - client.end(true, done) - }) - }) - - it('Change values of some properties by server response', function (done) { - this.timeout(15000) - var server116 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - serverKeepAlive: 16, - maximumPacketSize: 95 - } - }) - }) - }).listen(ports.PORTAND116) - var opts = { - host: 'localhost', - port: ports.PORTAND116, - protocolVersion: 5, - properties: { - topicAliasMaximum: 10, - serverKeepAlive: 11, - maximumPacketSize: 100 - } - } - var client = mqtt.connect(opts) - client.on('connect', function () { - assert.strictEqual(client.options.keepalive, 16) - assert.strictEqual(client.options.properties.maximumPacketSize, 95) - server116.close() - client.end(true, done) - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { - this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server316 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - serverClient.on('subscribe', function () { - if (!tryReconnect) { - server316.close() - serverClient.end(done) - } - }) - }) - }).listen(ports.PORTAND316) - var opts = { - host: 'localhost', - port: ports.PORTAND316, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { - // this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server326 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - serverClient.on('subscribe', function (packet) { - if (!reconnectEvent) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - if (!tryReconnect) { - assert.strictEqual(packet.properties.userProperties.test, 'test') - serverClient.end(done) - server326.close() - } - } - }) - }).listen(ports.PORTAND326) - - var opts = { - host: 'localhost', - port: ports.PORTAND326, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - var serverThatSendsErrors = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - packet.reasonCode = 142 - delete packet.cmd - serverClient.puback(packet) - break - case 2: - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubcomp(packet) - }) - }) - - it('Subscribe properties', function (done) { - this.timeout(15000) - var opts = { - host: 'localhost', - port: ports.PORTAND119, - protocolVersion: 5 - } - var subOptions = { properties: { subscriptionIdentifier: 1234 } } - var server119 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('subscribe', function (packet) { - assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) - server119.close() - serverClient.end() - done() - }) - }).listen(ports.PORTAND119) - - var client = mqtt.connect(opts) - client.on('connect', function () { - client.subscribe('a/b', subOptions) - }) - }) - - it('puback handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 1}, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - serverThatSendsErrors.close() - client.end(true, done) - }) - }) - - it('pubrec handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND118) - var opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 2}, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - serverThatSendsErrors.close() - client.end(true, done) - }) - }) - - it('puback handling custom reason code', function (done) { - // this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - - serverClient.on('puback', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - serverClient.end(done) - serverClient.destroy() - serverThatSendsErrors.close() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('server side disconnect', function (done) { - this.timeout(15000) - var server327 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - serverClient.disconnect({reasonCode: 128}) - server327.close() - }) - }) - server327.listen(ports.PORTAND327) - var opts = { - host: 'localhost', - port: ports.PORTAND327, - protocolVersion: 5 - } - - var client = mqtt.connect(opts) - client.once('disconnect', function (disconnectPacket) { - assert.strictEqual(disconnectPacket.reasonCode, 128) - client.end(true, done) - }) - }) - - it('pubrec handling custom reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - - serverClient.on('pubrec', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - client.end(true, done) - serverClient.destroy() - serverThatSendsErrors.close() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('puback handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('pubrec handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('puback handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 124124 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for puback') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('pubrec handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 34535 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for pubrec') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) -}) +'use strict' + +var mqtt = require('..') +var abstractClientTests = require('./abstract_client') +var MqttServer = require('./server').MqttServer +var assert = require('chai').assert +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var ports = require('./helpers/port_list') + +describe('MQTT 5.0', function () { + var server = serverBuilder('mqtt').listen(ports.PORTAND115) + var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } + + abstractClientTests(server, config) + + it('topic should be complemented on receive', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + assert.strictEqual(packet.properties.topicAliasMaximum, 3) + serverClient.connack({ + reasonCode: 0 + }) + // register topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // overwrite registered topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test2', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('message', function (topic, messagee, packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 3: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }) + + it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoUseTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias by autoApplyTopicAlias + client.publish('test1', 'Message') + }) + }) + + it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoAssignTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 2) + break + case 2: + assert.strictEqual(packet.topic, 'test3') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 3: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 4: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 5: + assert.strictEqual(packet.topic, 'test4') + assert.strictEqual(packet.properties.topicAlias, 2) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message') + client.publish('test2', 'Message') + client.publish('test3', 'Message') + + // use topicAlias + client.publish('test1', 'Message') + client.publish('test3', 'Message') + + // renew LRU topicAlias + client.publish('test4', 'Message') + }) + }) + + it('topicAlias should be removed and topic restored on resend', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + serverClient.puback({messageId: packet.messageId}) + break + case 3: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + serverClient.puback({messageId: packet.messageId}) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('topicAlias should be removed and topic restored on offline publish', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + assert.strictEqual(packet.qos, 1) + serverClient.puback({messageId: packet.messageId}) + break + case 1: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + assert.strictEqual(packet.qos, 0) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias3 + if (packet.properties) { + alias3 = packet.properties.topicAlias + } + assert.strictEqual(alias3, undefined) + assert.strictEqual(packet.qos, 0) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('close', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 4 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 1 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 4 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH topicAlias:0', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 0 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: '', // use topic alias + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } // in range topic alias + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received unregistered Topic Alias') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if there is Auth Data with no Auth Method', function (done) { + this.timeout(5000) + var client + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} + console.log('client connecting') + client = mqtt.connect(opts) + client.on('error', function (error) { + console.log('error hit') + assert.strictEqual(error.message, 'Packet has no Authentication Method') + // client will not be connected, so we will call done. + assert.isTrue(client.disconnected, 'validate client is disconnected') + client.end(true, done) + }) + }) + + it('auth packet', function (done) { + this.timeout(15000) + server.once('client', function (serverClient) { + console.log('server received client') + serverClient.on('auth', function (packet) { + console.log('serverClient received auth: packet %o', packet) + serverClient.end(done) + }) + }) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} + console.log('calling mqtt connect') + mqtt.connect(opts) + }) + + it('Maximum Packet Size', function (done) { + this.timeout(15000) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'exceeding packets size connack') + client.end(true, done) + }) + }) + + it('Change values of some properties by server response', function (done) { + this.timeout(15000) + var server116 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + serverKeepAlive: 16, + maximumPacketSize: 95 + } + }) + }) + }).listen(ports.PORTAND116) + var opts = { + host: 'localhost', + port: ports.PORTAND116, + protocolVersion: 5, + properties: { + topicAliasMaximum: 10, + serverKeepAlive: 11, + maximumPacketSize: 100 + } + } + var client = mqtt.connect(opts) + client.on('connect', function () { + assert.strictEqual(client.options.keepalive, 16) + assert.strictEqual(client.options.properties.maximumPacketSize, 95) + server116.close() + client.end(true, done) + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { + this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server316 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + serverClient.on('subscribe', function () { + if (!tryReconnect) { + server316.close() + serverClient.end(done) + } + }) + }) + }).listen(ports.PORTAND316) + var opts = { + host: 'localhost', + port: ports.PORTAND316, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { + // this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server326 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + serverClient.on('subscribe', function (packet) { + if (!reconnectEvent) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } else { + if (!tryReconnect) { + assert.strictEqual(packet.properties.userProperties.test, 'test') + serverClient.end(done) + server326.close() + } + } + }) + }).listen(ports.PORTAND326) + + var opts = { + host: 'localhost', + port: ports.PORTAND326, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + var serverThatSendsErrors = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + packet.reasonCode = 142 + delete packet.cmd + serverClient.puback(packet) + break + case 2: + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubcomp(packet) + }) + }) + + it('Subscribe properties', function (done) { + this.timeout(15000) + var opts = { + host: 'localhost', + port: ports.PORTAND119, + protocolVersion: 5 + } + var subOptions = { properties: { subscriptionIdentifier: 1234 } } + var server119 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('subscribe', function (packet) { + assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) + server119.close() + serverClient.end() + done() + }) + }).listen(ports.PORTAND119) + + var client = mqtt.connect(opts) + client.on('connect', function () { + client.subscribe('a/b', subOptions) + }) + }) + + it('puback handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 1}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('pubrec handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND118) + var opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 2}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('puback handling custom reason code', function (done) { + // this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + + serverClient.on('puback', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + serverClient.end(done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('server side disconnect', function (done) { + this.timeout(15000) + var server327 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + serverClient.disconnect({reasonCode: 128}) + server327.close() + }) + }) + server327.listen(ports.PORTAND327) + var opts = { + host: 'localhost', + port: ports.PORTAND327, + protocolVersion: 5 + } + + var client = mqtt.connect(opts) + client.once('disconnect', function (disconnectPacket) { + assert.strictEqual(disconnectPacket.reasonCode, 128) + client.end(true, done) + }) + }) + + it('pubrec handling custom reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + + serverClient.on('pubrec', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + client.end(true, done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 124124 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for puback') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 34535 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for pubrec') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) +}) diff --git a/test/helpers/port_list.js b/test/helpers/port_list.js index dc77ef07a..d11b8df21 100644 --- a/test/helpers/port_list.js +++ b/test/helpers/port_list.js @@ -1,51 +1,51 @@ -var PORT = 9876 -var PORTAND40 = PORT + 40 -var PORTAND41 = PORT + 41 -var PORTAND42 = PORT + 42 -var PORTAND43 = PORT + 43 -var PORTAND44 = PORT + 44 -var PORTAND45 = PORT + 45 -var PORTAND46 = PORT + 46 -var PORTAND47 = PORT + 47 -var PORTAND48 = PORT + 48 -var PORTAND49 = PORT + 49 -var PORTAND50 = PORT + 50 -var PORTAND72 = PORT + 72 -var PORTAND103 = PORT + 103 -var PORTAND114 = PORT + 114 -var PORTAND115 = PORT + 115 -var PORTAND116 = PORT + 116 -var PORTAND117 = PORT + 117 -var PORTAND118 = PORT + 118 -var PORTAND119 = PORT + 119 -var PORTAND316 = PORT + 316 -var PORTAND326 = PORT + 326 -var PORTAND327 = PORT + 327 -var PORTAND400 = PORT + 400 - -module.exports = { - PORT, - PORTAND40, - PORTAND41, - PORTAND42, - PORTAND43, - PORTAND44, - PORTAND45, - PORTAND46, - PORTAND47, - PORTAND48, - PORTAND49, - PORTAND50, - PORTAND72, - PORTAND103, - PORTAND114, - PORTAND115, - PORTAND116, - PORTAND117, - PORTAND118, - PORTAND119, - PORTAND316, - PORTAND326, - PORTAND327, - PORTAND400 -} +var PORT = 9876 +var PORTAND40 = PORT + 40 +var PORTAND41 = PORT + 41 +var PORTAND42 = PORT + 42 +var PORTAND43 = PORT + 43 +var PORTAND44 = PORT + 44 +var PORTAND45 = PORT + 45 +var PORTAND46 = PORT + 46 +var PORTAND47 = PORT + 47 +var PORTAND48 = PORT + 48 +var PORTAND49 = PORT + 49 +var PORTAND50 = PORT + 50 +var PORTAND72 = PORT + 72 +var PORTAND103 = PORT + 103 +var PORTAND114 = PORT + 114 +var PORTAND115 = PORT + 115 +var PORTAND116 = PORT + 116 +var PORTAND117 = PORT + 117 +var PORTAND118 = PORT + 118 +var PORTAND119 = PORT + 119 +var PORTAND316 = PORT + 316 +var PORTAND326 = PORT + 326 +var PORTAND327 = PORT + 327 +var PORTAND400 = PORT + 400 + +module.exports = { + PORT, + PORTAND40, + PORTAND41, + PORTAND42, + PORTAND43, + PORTAND44, + PORTAND45, + PORTAND46, + PORTAND47, + PORTAND48, + PORTAND49, + PORTAND50, + PORTAND72, + PORTAND103, + PORTAND114, + PORTAND115, + PORTAND116, + PORTAND117, + PORTAND118, + PORTAND119, + PORTAND316, + PORTAND326, + PORTAND327, + PORTAND400 +} diff --git a/test/helpers/server.js b/test/helpers/server.js index 46bd79537..d29042d3d 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -1,53 +1,53 @@ -'use strict' - -var MqttServer = require('../server').MqttServer -var MqttSecureServer = require('../server').MqttSecureServer -var fs = require('fs') - -module.exports.init_server = function (PORT) { - var server = new MqttServer(function (client) { - client.on('connect', function () { - client.connack(0) - }) - - client.on('publish', function (packet) { - switch (packet.qos) { - case 1: - client.puback({messageId: packet.messageId}) - break - case 2: - client.pubrec({messageId: packet.messageId}) - break - default: - break - } - }) - - client.on('pubrel', function (packet) { - client.pubcomp({messageId: packet.messageId}) - }) - - client.on('pingreq', function () { - client.pingresp() - }) - - client.on('disconnect', function () { - client.stream.end() - }) - }) - server.listen(PORT) - return server -} - -module.exports.init_secure_server = function (port, key, cert) { - var server = new MqttSecureServer({ - key: fs.readFileSync(key), - cert: fs.readFileSync(cert) - }, function (client) { - client.on('connect', function () { - client.connack({returnCode: 0}) - }) - }) - server.listen(port) - return server -} +'use strict' + +var MqttServer = require('../server').MqttServer +var MqttSecureServer = require('../server').MqttSecureServer +var fs = require('fs') + +module.exports.init_server = function (PORT) { + var server = new MqttServer(function (client) { + client.on('connect', function () { + client.connack(0) + }) + + client.on('publish', function (packet) { + switch (packet.qos) { + case 1: + client.puback({messageId: packet.messageId}) + break + case 2: + client.pubrec({messageId: packet.messageId}) + break + default: + break + } + }) + + client.on('pubrel', function (packet) { + client.pubcomp({messageId: packet.messageId}) + }) + + client.on('pingreq', function () { + client.pingresp() + }) + + client.on('disconnect', function () { + client.stream.end() + }) + }) + server.listen(PORT) + return server +} + +module.exports.init_secure_server = function (port, key, cert) { + var server = new MqttSecureServer({ + key: fs.readFileSync(key), + cert: fs.readFileSync(cert) + }, function (client) { + client.on('connect', function () { + client.connack({returnCode: 0}) + }) + }) + server.listen(port) + return server +} diff --git a/test/helpers/server_process.js b/test/helpers/server_process.js index 1d1095cb3..d4c2681b4 100644 --- a/test/helpers/server_process.js +++ b/test/helpers/server_process.js @@ -1,9 +1,9 @@ -'use strict' - -var MqttServer = require('../server').MqttServer - -new MqttServer(function (client) { - client.on('connect', function () { - client.connack({ returnCode: 0 }) - }) -}).listen(3481, 'localhost') +'use strict' + +var MqttServer = require('../server').MqttServer + +new MqttServer(function (client) { + client.on('connect', function () { + client.connack({ returnCode: 0 }) + }) +}).listen(3481, 'localhost') diff --git a/test/message-id-provider.js b/test/message-id-provider.js index 2f84bdf35..667a8296f 100644 --- a/test/message-id-provider.js +++ b/test/message-id-provider.js @@ -1,91 +1,91 @@ -'use strict' -var assert = require('chai').assert -var DefaultMessageIdProvider = require('../lib/default-message-id-provider') -var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') - -describe('message id provider', function () { - describe('default', function () { - it('should return 1 once the internal counter reached limit', function () { - var provider = new DefaultMessageIdProvider() - provider.nextId = 65535 - - assert.equal(provider.allocate(), 65535) - assert.equal(provider.allocate(), 1) - }) - - it('should return 65535 for last message id once the internal counter reached limit', function () { - var provider = new DefaultMessageIdProvider() - provider.nextId = 65535 - - assert.equal(provider.allocate(), 65535) - assert.equal(provider.getLastAllocated(), 65535) - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - }) - it('should return true when register with non allocated messageId', function () { - var provider = new DefaultMessageIdProvider() - assert.equal(provider.register(10), true) - }) - }) - describe('unique', function () { - it('should return 1, 2, 3.., when allocate', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - }) - it('should skip registerd messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.register(2), true) - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 3) - }) - it('should return false register allocated messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.register(1), false) - assert.equal(provider.register(5), true) - assert.equal(provider.register(5), false) - }) - it('should retrun correct last messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.register(2), true) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.allocate(), 3) - assert.equal(provider.getLastAllocated(), 3) - }) - it('should be reusable deallocated messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - provider.deallocate(2) - assert.equal(provider.allocate(), 2) - }) - it('should allocate all messageId and then return null', function () { - var provider = new UniqueMessageIdProvider() - for (var i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.deallocate(10000) - assert.equal(provider.allocate(), 10000) - assert.equal(provider.allocate(), null) - }) - it('should all messageId reallocatable after clear', function () { - var provider = new UniqueMessageIdProvider() - var i - for (i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.clear() - for (i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - }) - }) -}) +'use strict' +var assert = require('chai').assert +var DefaultMessageIdProvider = require('../lib/default-message-id-provider') +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') + +describe('message id provider', function () { + describe('default', function () { + it('should return 1 once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.allocate(), 1) + }) + + it('should return 65535 for last message id once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.getLastAllocated(), 65535) + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + }) + it('should return true when register with non allocated messageId', function () { + var provider = new DefaultMessageIdProvider() + assert.equal(provider.register(10), true) + }) + }) + describe('unique', function () { + it('should return 1, 2, 3.., when allocate', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + }) + it('should skip registerd messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.register(2), true) + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 3) + }) + it('should return false register allocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.register(1), false) + assert.equal(provider.register(5), true) + assert.equal(provider.register(5), false) + }) + it('should retrun correct last messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.register(2), true) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.allocate(), 3) + assert.equal(provider.getLastAllocated(), 3) + }) + it('should be reusable deallocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + provider.deallocate(2) + assert.equal(provider.allocate(), 2) + }) + it('should allocate all messageId and then return null', function () { + var provider = new UniqueMessageIdProvider() + for (var i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.deallocate(10000) + assert.equal(provider.allocate(), 10000) + assert.equal(provider.allocate(), null) + }) + it('should all messageId reallocatable after clear', function () { + var provider = new UniqueMessageIdProvider() + var i + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.clear() + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + }) + }) +}) diff --git a/test/mqtt.js b/test/mqtt.js index f55d04a33..d3315b69e 100644 --- a/test/mqtt.js +++ b/test/mqtt.js @@ -1,230 +1,230 @@ -'use strict' - -var fs = require('fs') -var path = require('path') -var mqtt = require('../') - -describe('mqtt', function () { - describe('#connect', function () { - var sslOpts, sslOpts2 - it('should return an MqttClient when connect is called with mqtt:/ url', function () { - var c = mqtt.connect('mqtt://localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should throw an error when called with no protocol specified', function () { - (function () { - var c = mqtt.connect('foo.bar.com') - c.end() - }).should.throw('Missing protocol') - }) - - it('should throw an error when called with no protocol specified - with options', function () { - (function () { - var c = mqtt.connect('tcp://foo.bar.com', { protocol: null }) - c.end() - }).should.throw('Missing protocol') - }) - - it('should return an MqttClient with username option set', function () { - var c = mqtt.connect('mqtt://user:pass@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.options.should.have.property('password', 'pass') - c.end() - }) - - it('should return an MqttClient with username and password options set', function () { - var c = mqtt.connect('mqtt://user@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.end() - }) - - it('should return an MqttClient with the clientid with random value', function () { - var c = mqtt.connect('mqtt://user@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end() - }) - - it('should return an MqttClient with the clientid with empty string', function () { - var c = mqtt.connect('mqtt://user@localhost:1883?clientId=') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - c.end() - }) - - it('should return an MqttClient with the clientid option set', function () { - var c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end() - }) - - it('should return an MqttClient when connect is called with tcp:/ url', function () { - var c = mqtt.connect('tcp://localhost') - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient with correct host when called with a host and port', function () { - var c = mqtt.connect('tcp://user:pass@localhost:1883') - - c.options.should.have.property('hostname', 'localhost') - c.options.should.have.property('port', 1883) - c.end() - }) - - sslOpts = { - keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), - certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), - caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')] - } - - it('should return an MqttClient when connect is called with mqtts:/ url', function () { - var c = mqtt.connect('mqtts://localhost', sslOpts) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with ssl:/ url', function () { - var c = mqtt.connect('ssl://localhost', sslOpts) - - c.options.should.have.property('protocol', 'ssl') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with ws:/ url', function () { - var c = mqtt.connect('ws://localhost', sslOpts) - - c.options.should.have.property('protocol', 'ws') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with wss:/ url', function () { - var c = mqtt.connect('wss://localhost', sslOpts) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - sslOpts2 = { - key: fs.readFileSync(path.join(__dirname, 'helpers', 'private-key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem')), - ca: [fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem'))] - } - - it('should throw an error when it is called with cert and key set but no protocol specified', function () { - // to do rewrite wrap function - (function () { - var c = mqtt.connect(sslOpts2) - c.end() - }).should.throw('Missing secure protocol key') - }) - - it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', function () { - (function () { - sslOpts2.protocol = 'UNKNOWNPROTOCOL' - var c = mqtt.connect(sslOpts2) - c.end() - }).should.throw() - }) - - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function () { - sslOpts2.protocol = 'mqtt' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function () { - sslOpts2.protocol = 'mqtts' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function () { - sslOpts2.protocol = 'ws' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function () { - sslOpts2.protocol = 'wss' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return an MqttClient with the clientid with option of clientId as empty string', function () { - var c = mqtt.connect('mqtt://localhost:1883', { - clientId: '' - }) - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - }) - - it('should return an MqttClient with the clientid with option of clientId empty', function () { - var c = mqtt.connect('mqtt://localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end() - }) - - it('should return an MqttClient with the clientid with option of with specific clientId', function () { - var c = mqtt.connect('mqtt://localhost:1883', { - clientId: '123' - }) - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end() - }) - }) -}) +'use strict' + +var fs = require('fs') +var path = require('path') +var mqtt = require('../') + +describe('mqtt', function () { + describe('#connect', function () { + var sslOpts, sslOpts2 + it('should return an MqttClient when connect is called with mqtt:/ url', function () { + var c = mqtt.connect('mqtt://localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should throw an error when called with no protocol specified', function () { + (function () { + var c = mqtt.connect('foo.bar.com') + c.end() + }).should.throw('Missing protocol') + }) + + it('should throw an error when called with no protocol specified - with options', function () { + (function () { + var c = mqtt.connect('tcp://foo.bar.com', { protocol: null }) + c.end() + }).should.throw('Missing protocol') + }) + + it('should return an MqttClient with username option set', function () { + var c = mqtt.connect('mqtt://user:pass@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.options.should.have.property('password', 'pass') + c.end() + }) + + it('should return an MqttClient with username and password options set', function () { + var c = mqtt.connect('mqtt://user@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.end() + }) + + it('should return an MqttClient with the clientid with random value', function () { + var c = mqtt.connect('mqtt://user@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end() + }) + + it('should return an MqttClient with the clientid with empty string', function () { + var c = mqtt.connect('mqtt://user@localhost:1883?clientId=') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + c.end() + }) + + it('should return an MqttClient with the clientid option set', function () { + var c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end() + }) + + it('should return an MqttClient when connect is called with tcp:/ url', function () { + var c = mqtt.connect('tcp://localhost') + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient with correct host when called with a host and port', function () { + var c = mqtt.connect('tcp://user:pass@localhost:1883') + + c.options.should.have.property('hostname', 'localhost') + c.options.should.have.property('port', 1883) + c.end() + }) + + sslOpts = { + keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), + certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), + caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')] + } + + it('should return an MqttClient when connect is called with mqtts:/ url', function () { + var c = mqtt.connect('mqtts://localhost', sslOpts) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with ssl:/ url', function () { + var c = mqtt.connect('ssl://localhost', sslOpts) + + c.options.should.have.property('protocol', 'ssl') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with ws:/ url', function () { + var c = mqtt.connect('ws://localhost', sslOpts) + + c.options.should.have.property('protocol', 'ws') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with wss:/ url', function () { + var c = mqtt.connect('wss://localhost', sslOpts) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + sslOpts2 = { + key: fs.readFileSync(path.join(__dirname, 'helpers', 'private-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem')), + ca: [fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem'))] + } + + it('should throw an error when it is called with cert and key set but no protocol specified', function () { + // to do rewrite wrap function + (function () { + var c = mqtt.connect(sslOpts2) + c.end() + }).should.throw('Missing secure protocol key') + }) + + it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', function () { + (function () { + sslOpts2.protocol = 'UNKNOWNPROTOCOL' + var c = mqtt.connect(sslOpts2) + c.end() + }).should.throw() + }) + + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function () { + sslOpts2.protocol = 'mqtt' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function () { + sslOpts2.protocol = 'mqtts' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function () { + sslOpts2.protocol = 'ws' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function () { + sslOpts2.protocol = 'wss' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return an MqttClient with the clientid with option of clientId as empty string', function () { + var c = mqtt.connect('mqtt://localhost:1883', { + clientId: '' + }) + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + }) + + it('should return an MqttClient with the clientid with option of clientId empty', function () { + var c = mqtt.connect('mqtt://localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end() + }) + + it('should return an MqttClient with the clientid with option of with specific clientId', function () { + var c = mqtt.connect('mqtt://localhost:1883', { + clientId: '123' + }) + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end() + }) + }) +}) diff --git a/test/mqtt_store.js b/test/mqtt_store.js index 976a01aff..0eda04d8b 100644 --- a/test/mqtt_store.js +++ b/test/mqtt_store.js @@ -1,9 +1,9 @@ -'use strict' - -var mqtt = require('../lib/connect') - -describe('store in lib/connect/index.js (webpack entry point)', function () { - it('should create store', function (done) { - done(null, new mqtt.Store()) - }) -}) +'use strict' + +var mqtt = require('../lib/connect') + +describe('store in lib/connect/index.js (webpack entry point)', function () { + it('should create store', function (done) { + done(null, new mqtt.Store()) + }) +}) diff --git a/test/secure_client.js b/test/secure_client.js index 95b7a6197..8c4904465 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -1,188 +1,188 @@ -'use strict' - -var mqtt = require('..') -var path = require('path') -var abstractClientTests = require('./abstract_client') -var fs = require('fs') -var port = 9899 -var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') -var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') -var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') -var MqttSecureServer = require('./server').MqttSecureServer -var assert = require('chai').assert - -var serverListener = function (client) { - // this is the Server's MQTT Client - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) - } else { - server.emit('connect', client) - client.connack({returnCode: 0}) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - /* jshint -W027 */ - /* eslint default-case:0 */ - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - /* jshint +W027 */ - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -var server = new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) -}, serverListener).listen(port) - -describe('MqttSecureClient', function () { - var config = { protocol: 'mqtts', port: port, rejectUnauthorized: false } - abstractClientTests(server, config) - - describe('with secure parameters', function () { - it('should validate successfully the CA', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI', function (done) { - var client = mqtt.connect('mqtts://localhost:' + port, { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI with path', function (done) { - var client = mqtt.connect('mqtts://localhost:' + port + '/', { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate unsuccessfully the CA', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.once('error', function () { - done() - client.end() - client.on('error', function () {}) - }) - }) - - it('should emit close on TLS error', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.on('error', function () {}) - - // TODO node v0.8.x emits multiple close events - client.once('close', function () { - done() - }) - }) - - it('should support SNI on the TLS connection', function (done) { - var hostname, client - server.removeAllListeners('secureConnection') // clear eventHandler - server.once('secureConnection', function (tlsSocket) { // one time eventHandler - assert.equal(tlsSocket.servername, hostname) // validate SNI set - server.setupConnection(tlsSocket) - }) - - hostname = 'localhost' - client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true, - host: hostname - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - server.on('secureConnection', server.setupConnection) // reset eventHandler - done() - }) - }) - }) -}) +'use strict' + +var mqtt = require('..') +var path = require('path') +var abstractClientTests = require('./abstract_client') +var fs = require('fs') +var port = 9899 +var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') +var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') +var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') +var MqttSecureServer = require('./server').MqttSecureServer +var assert = require('chai').assert + +var serverListener = function (client) { + // this is the Server's MQTT Client + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({returnCode: 2}) + } else { + server.emit('connect', client) + client.connack({returnCode: 0}) + } + }) + + client.on('publish', function (packet) { + setImmediate(function () { + /* jshint -W027 */ + /* eslint default-case:0 */ + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + /* jshint +W027 */ + }) + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +var server = new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) +}, serverListener).listen(port) + +describe('MqttSecureClient', function () { + var config = { protocol: 'mqtts', port: port, rejectUnauthorized: false } + abstractClientTests(server, config) + + describe('with secure parameters', function () { + it('should validate successfully the CA', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate successfully the CA using URI', function (done) { + var client = mqtt.connect('mqtts://localhost:' + port, { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate successfully the CA using URI with path', function (done) { + var client = mqtt.connect('mqtts://localhost:' + port + '/', { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate unsuccessfully the CA', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true + }) + + client.once('error', function () { + done() + client.end() + client.on('error', function () {}) + }) + }) + + it('should emit close on TLS error', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true + }) + + client.on('error', function () {}) + + // TODO node v0.8.x emits multiple close events + client.once('close', function () { + done() + }) + }) + + it('should support SNI on the TLS connection', function (done) { + var hostname, client + server.removeAllListeners('secureConnection') // clear eventHandler + server.once('secureConnection', function (tlsSocket) { // one time eventHandler + assert.equal(tlsSocket.servername, hostname) // validate SNI set + server.setupConnection(tlsSocket) + }) + + hostname = 'localhost' + client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + host: hostname + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + server.on('secureConnection', server.setupConnection) // reset eventHandler + done() + }) + }) + }) +}) diff --git a/test/server.js b/test/server.js index ccfe2f4d1..3b009d4fb 100644 --- a/test/server.js +++ b/test/server.js @@ -1,94 +1,94 @@ -'use strict' - -var net = require('net') -var tls = require('tls') -var Connection = require('mqtt-connection') - -/** - * MqttServer - * - * @param {Function} listener - fired on client connection - */ -class MqttServer extends net.Server { - constructor (listener) { - super() - this.connectionList = [] - - var that = this - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - var connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - }) - - if (listener) { - this.on('client', listener) - } - } -} - -/** - * MqttServerNoWait (w/o waiting for initialization) - * - * @param {Function} listener - fired on client connection - */ -class MqttServerNoWait extends net.Server { - constructor (listener) { - super() - this.connectionList = [] - - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - var connection = new Connection(duplex) - // do not wait for connection to return to send it to the client. - this.emit('client', connection) - }) - - if (listener) { - this.on('client', listener) - } - } -} - -/** - * MqttSecureServer - * - * @param {Object} opts - server options - * @param {Function} listener - */ -class MqttSecureServer extends tls.Server { - constructor (opts, listener) { - if (typeof opts === 'function') { - listener = opts - opts = {} - } - - // sets a listener for the 'connection' event - super(opts) - this.connectionList = [] - - this.on('secureConnection', function (socket) { - this.connectionList.push(socket) - var that = this - var connection = new Connection(socket, function () { - that.emit('client', connection) - }) - }) - - if (listener) { - this.on('client', listener) - } - } - - setupConnection (duplex) { - var that = this - var connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - } -} - -exports.MqttServer = MqttServer -exports.MqttServerNoWait = MqttServerNoWait -exports.MqttSecureServer = MqttSecureServer +'use strict' + +var net = require('net') +var tls = require('tls') +var Connection = require('mqtt-connection') + +/** + * MqttServer + * + * @param {Function} listener - fired on client connection + */ +class MqttServer extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + var that = this + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) + }) + + if (listener) { + this.on('client', listener) + } + } +} + +/** + * MqttServerNoWait (w/o waiting for initialization) + * + * @param {Function} listener - fired on client connection + */ +class MqttServerNoWait extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex) + // do not wait for connection to return to send it to the client. + this.emit('client', connection) + }) + + if (listener) { + this.on('client', listener) + } + } +} + +/** + * MqttSecureServer + * + * @param {Object} opts - server options + * @param {Function} listener + */ +class MqttSecureServer extends tls.Server { + constructor (opts, listener) { + if (typeof opts === 'function') { + listener = opts + opts = {} + } + + // sets a listener for the 'connection' event + super(opts) + this.connectionList = [] + + this.on('secureConnection', function (socket) { + this.connectionList.push(socket) + var that = this + var connection = new Connection(socket, function () { + that.emit('client', connection) + }) + }) + + if (listener) { + this.on('client', listener) + } + } + + setupConnection (duplex) { + var that = this + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) + } +} + +exports.MqttServer = MqttServer +exports.MqttServerNoWait = MqttServerNoWait +exports.MqttSecureServer = MqttSecureServer diff --git a/test/server_helpers_for_client_tests.js b/test/server_helpers_for_client_tests.js index 9527d47e2..e7ea345c4 100644 --- a/test/server_helpers_for_client_tests.js +++ b/test/server_helpers_for_client_tests.js @@ -1,147 +1,147 @@ -'use strict' - -var MqttServer = require('./server').MqttServer -var MqttSecureServer = require('./server').MqttSecureServer -var debug = require('debug')('TEST:server_helpers') - -var path = require('path') -var fs = require('fs') -var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') -var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') - -var http = require('http') -var WebSocket = require('ws') -var MQTTConnection = require('mqtt-connection') - -/** - * This will build the client for the server to use during testing, and set up the - * server side client based on mqtt-connection for handling MQTT messages. - * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' - * @param {Function} handler - event handler - */ -function serverBuilder (protocol, handler) { - var defaultHandler = function (serverClient) { - serverClient.on('auth', function (packet) { - if (serverClient.writable) return false - var rc = 'reasonCode' - var connack = {} - connack[rc] = 0 - serverClient.connack(connack) - }) - serverClient.on('connect', function (packet) { - if (!serverClient.writable) return false - var rc = 'returnCode' - var connack = {} - if (serverClient.options && serverClient.options.protocolVersion === 5) { - rc = 'reasonCode' - if (packet.clientId === 'invalid') { - connack[rc] = 128 - } else { - connack[rc] = 0 - } - } else { - if (packet.clientId === 'invalid') { - connack[rc] = 2 - } else { - connack[rc] = 0 - } - } - if (packet.properties && packet.properties.authenticationMethod) { - return false - } else { - serverClient.connack(connack) - } - }) - - serverClient.on('publish', function (packet) { - if (!serverClient.writable) return false - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - serverClient.puback(packet) - break - case 2: - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - if (!serverClient.writable) return false - serverClient.pubcomp(packet) - }) - - serverClient.on('pubrec', function (packet) { - if (!serverClient.writable) return false - serverClient.pubrel(packet) - }) - - serverClient.on('pubcomp', function () { - // Nothing to be done - }) - - serverClient.on('subscribe', function (packet) { - if (!serverClient.writable) return false - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - serverClient.on('unsubscribe', function (packet) { - if (!serverClient.writable) return false - packet.granted = packet.unsubscriptions.map(function () { return 0 }) - serverClient.unsuback(packet) - }) - - serverClient.on('pingreq', function () { - if (!serverClient.writable) return false - serverClient.pingresp() - }) - - serverClient.on('end', function () { - debug('disconnected from server') - }) - } - - if (!handler) { - handler = defaultHandler - } - - switch (protocol) { - case 'mqtt': - return new MqttServer(handler) - case 'mqtts': - return new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) - }, - handler) - case 'ws': - var attachWebsocketServer = function (server) { - var webSocketServer = new WebSocket.Server({server: server, perMessageDeflate: false}) - - webSocketServer.on('connection', function (ws) { - var stream = WebSocket.createWebSocketStream(ws) - var connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - server.emit('client', connection) - stream.on('error', function () {}) - connection.on('error', function () {}) - connection.on('close', function () {}) - }) - } - - var httpServer = http.createServer() - attachWebsocketServer(httpServer) - httpServer.on('client', handler) - return httpServer - } -} - -exports.serverBuilder = serverBuilder +'use strict' + +var MqttServer = require('./server').MqttServer +var MqttSecureServer = require('./server').MqttSecureServer +var debug = require('debug')('TEST:server_helpers') + +var path = require('path') +var fs = require('fs') +var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') +var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') + +var http = require('http') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') + +/** + * This will build the client for the server to use during testing, and set up the + * server side client based on mqtt-connection for handling MQTT messages. + * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' + * @param {Function} handler - event handler + */ +function serverBuilder (protocol, handler) { + var defaultHandler = function (serverClient) { + serverClient.on('auth', function (packet) { + if (serverClient.writable) return false + var rc = 'reasonCode' + var connack = {} + connack[rc] = 0 + serverClient.connack(connack) + }) + serverClient.on('connect', function (packet) { + if (!serverClient.writable) return false + var rc = 'returnCode' + var connack = {} + if (serverClient.options && serverClient.options.protocolVersion === 5) { + rc = 'reasonCode' + if (packet.clientId === 'invalid') { + connack[rc] = 128 + } else { + connack[rc] = 0 + } + } else { + if (packet.clientId === 'invalid') { + connack[rc] = 2 + } else { + connack[rc] = 0 + } + } + if (packet.properties && packet.properties.authenticationMethod) { + return false + } else { + serverClient.connack(connack) + } + }) + + serverClient.on('publish', function (packet) { + if (!serverClient.writable) return false + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + serverClient.puback(packet) + break + case 2: + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + if (!serverClient.writable) return false + serverClient.pubcomp(packet) + }) + + serverClient.on('pubrec', function (packet) { + if (!serverClient.writable) return false + serverClient.pubrel(packet) + }) + + serverClient.on('pubcomp', function () { + // Nothing to be done + }) + + serverClient.on('subscribe', function (packet) { + if (!serverClient.writable) return false + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + serverClient.on('unsubscribe', function (packet) { + if (!serverClient.writable) return false + packet.granted = packet.unsubscriptions.map(function () { return 0 }) + serverClient.unsuback(packet) + }) + + serverClient.on('pingreq', function () { + if (!serverClient.writable) return false + serverClient.pingresp() + }) + + serverClient.on('end', function () { + debug('disconnected from server') + }) + } + + if (!handler) { + handler = defaultHandler + } + + switch (protocol) { + case 'mqtt': + return new MqttServer(handler) + case 'mqtts': + return new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) + }, + handler) + case 'ws': + var attachWebsocketServer = function (server) { + var webSocketServer = new WebSocket.Server({server: server, perMessageDeflate: false}) + + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + server.emit('client', connection) + stream.on('error', function () {}) + connection.on('error', function () {}) + connection.on('close', function () {}) + }) + } + + var httpServer = http.createServer() + attachWebsocketServer(httpServer) + httpServer.on('client', handler) + return httpServer + } +} + +exports.serverBuilder = serverBuilder diff --git a/test/store.js b/test/store.js index 1489b2138..5244cdf84 100644 --- a/test/store.js +++ b/test/store.js @@ -1,10 +1,10 @@ -'use strict' - -var Store = require('../lib/store') -var abstractTest = require('../test/abstract_store') - -describe('in-memory store', function () { - abstractTest(function (done) { - done(null, new Store()) - }) -}) +'use strict' + +var Store = require('../lib/store') +var abstractTest = require('../test/abstract_store') + +describe('in-memory store', function () { + abstractTest(function (done) { + done(null, new Store()) + }) +}) diff --git a/test/unique_message_id_provider_client.js b/test/unique_message_id_provider_client.js index 933d85b82..a23625a85 100644 --- a/test/unique_message_id_provider_client.js +++ b/test/unique_message_id_provider_client.js @@ -1,21 +1,21 @@ -'use strict' - -var abstractClientTests = require('./abstract_client') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') -var ports = require('./helpers/port_list') - -describe('UniqueMessageIdProviderMqttClient', function () { - var server = serverBuilder('mqtt') - var config = {protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider()} - server.listen(ports.PORTAND400) - - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) - - abstractClientTests(server, config) -}) +'use strict' + +var abstractClientTests = require('./abstract_client') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') +var ports = require('./helpers/port_list') + +describe('UniqueMessageIdProviderMqttClient', function () { + var server = serverBuilder('mqtt') + var config = {protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider()} + server.listen(ports.PORTAND400) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) +}) diff --git a/test/util.js b/test/util.js index 0dd559cb9..ab2661804 100644 --- a/test/util.js +++ b/test/util.js @@ -1,15 +1,15 @@ -'use strict' - -var Transform = require('readable-stream').Transform - -module.exports.testStream = function () { - return new Transform({ - transform (buf, enc, cb) { - var that = this - setImmediate(function () { - that.push(buf) - cb() - }) - } - }) -} +'use strict' + +var Transform = require('readable-stream').Transform + +module.exports.testStream = function () { + return new Transform({ + transform (buf, enc, cb) { + var that = this + setImmediate(function () { + that.push(buf) + cb() + }) + } + }) +} diff --git a/test/websocket_client.js b/test/websocket_client.js index a7f59897a..9eb7007c2 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -1,191 +1,191 @@ -'use strict' - -var http = require('http') -var WebSocket = require('ws') -var MQTTConnection = require('mqtt-connection') -var abstractClientTests = require('./abstract_client') -var ports = require('./helpers/port_list') -var MqttServerNoWait = require('./server').MqttServerNoWait -var mqtt = require('../') -var xtend = require('xtend') -var assert = require('assert') -var port = 9999 -var httpServer = http.createServer() - -function attachWebsocketServer (httpServer) { - var webSocketServer = new WebSocket.Server({server: httpServer, perMessageDeflate: false}) - - webSocketServer.on('connection', function (ws) { - var stream = WebSocket.createWebSocketStream(ws) - var connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - httpServer.emit('client', connection) - stream.on('error', function () {}) - connection.on('error', function () {}) - }) - - return httpServer -} - -function attachClientEventHandlers (client) { - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({ returnCode: 2 }) - } else { - httpServer.emit('connect', client) - client.connack({returnCode: 0}) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -attachWebsocketServer(httpServer) - -httpServer.on('client', attachClientEventHandlers).listen(port) - -describe('Websocket Client', function () { - var baseConfig = { protocol: 'ws', port: port } - - function makeOptions (custom) { - // xtend returns a new object. Does not mutate arguments - return xtend(baseConfig, custom || {}) - } - - it('should use mqtt as the protocol by default', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqtt') - }) - mqtt.connect(makeOptions()).on('connect', function () { - this.end(true, done) - }) - }) - - it('should be able to transform the url (for e.g. to sign it)', function (done) { - var baseUrl = 'ws://localhost:9999/mqtt' - var sig = '?AUTH=token' - var expected = baseUrl + sig - var actual - var opts = makeOptions({ - path: '/mqtt', - transformWsUrl: function (url, opt, client) { - assert.equal(url, baseUrl) - assert.strictEqual(opt, opts) - assert.strictEqual(client.options, opts) - assert.strictEqual(typeof opt.transformWsUrl, 'function') - assert(client instanceof mqtt.MqttClient) - url += sig - actual = url - return url - }}) - mqtt.connect(opts) - .on('connect', function () { - assert.equal(this.stream.url, expected) - assert.equal(actual, expected) - this.end(true, done) - }) - }) - - it('should use mqttv3.1 as the protocol if using v3.1', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqttv3.1') - }) - - var opts = makeOptions({ - protocolId: 'MQIsdp', - protocolVersion: 3 - }) - - mqtt.connect(opts).on('connect', function () { - this.end(true, done) - }) - }) - - describe('reconnecting', () => { - it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { - var serverPort42Connected = false - var handler = function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - }) - } - this.timeout(15000) - var actualURL41 = 'wss://localhost:9917/' - var actualURL42 = 'ws://localhost:9918/' - var serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) - var serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) - - serverPort42.on('listening', function () { - let client = mqtt.connect({ - protocol: 'wss', - servers: [ - { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, - { port: ports.PORTAND41, host: 'localhost' } - ], - keepalive: 50 - }) - serverPort41.once('client', function (c) { - assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') - assert(serverPort42Connected) - c.stream.destroy() - client.end(true, done) - serverPort41.close() - }) - serverPort42.once('client', function (c) { - serverPort42Connected = true - assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') - c.stream.destroy() - serverPort42.close() - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - }) - - abstractClientTests(httpServer, makeOptions()) -}) +'use strict' + +var http = require('http') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') +var abstractClientTests = require('./abstract_client') +var ports = require('./helpers/port_list') +var MqttServerNoWait = require('./server').MqttServerNoWait +var mqtt = require('../') +var xtend = require('xtend') +var assert = require('assert') +var port = 9999 +var httpServer = http.createServer() + +function attachWebsocketServer (httpServer) { + var webSocketServer = new WebSocket.Server({server: httpServer, perMessageDeflate: false}) + + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + httpServer.emit('client', connection) + stream.on('error', function () {}) + connection.on('error', function () {}) + }) + + return httpServer +} + +function attachClientEventHandlers (client) { + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({ returnCode: 2 }) + } else { + httpServer.emit('connect', client) + client.connack({returnCode: 0}) + } + }) + + client.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + }) + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +attachWebsocketServer(httpServer) + +httpServer.on('client', attachClientEventHandlers).listen(port) + +describe('Websocket Client', function () { + var baseConfig = { protocol: 'ws', port: port } + + function makeOptions (custom) { + // xtend returns a new object. Does not mutate arguments + return xtend(baseConfig, custom || {}) + } + + it('should use mqtt as the protocol by default', function (done) { + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqtt') + }) + mqtt.connect(makeOptions()).on('connect', function () { + this.end(true, done) + }) + }) + + it('should be able to transform the url (for e.g. to sign it)', function (done) { + var baseUrl = 'ws://localhost:9999/mqtt' + var sig = '?AUTH=token' + var expected = baseUrl + sig + var actual + var opts = makeOptions({ + path: '/mqtt', + transformWsUrl: function (url, opt, client) { + assert.equal(url, baseUrl) + assert.strictEqual(opt, opts) + assert.strictEqual(client.options, opts) + assert.strictEqual(typeof opt.transformWsUrl, 'function') + assert(client instanceof mqtt.MqttClient) + url += sig + actual = url + return url + }}) + mqtt.connect(opts) + .on('connect', function () { + assert.equal(this.stream.url, expected) + assert.equal(actual, expected) + this.end(true, done) + }) + }) + + it('should use mqttv3.1 as the protocol if using v3.1', function (done) { + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqttv3.1') + }) + + var opts = makeOptions({ + protocolId: 'MQIsdp', + protocolVersion: 3 + }) + + mqtt.connect(opts).on('connect', function () { + this.end(true, done) + }) + }) + + describe('reconnecting', () => { + it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { + var serverPort42Connected = false + var handler = function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + }) + } + this.timeout(15000) + var actualURL41 = 'wss://localhost:9917/' + var actualURL42 = 'ws://localhost:9918/' + var serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) + var serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) + + serverPort42.on('listening', function () { + let client = mqtt.connect({ + protocol: 'wss', + servers: [ + { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, + { port: ports.PORTAND41, host: 'localhost' } + ], + keepalive: 50 + }) + serverPort41.once('client', function (c) { + assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') + assert(serverPort42Connected) + c.stream.destroy() + client.end(true, done) + serverPort41.close() + }) + serverPort42.once('client', function (c) { + serverPort42Connected = true + assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') + c.stream.destroy() + serverPort42.close() + }) + + client.once('connect', function () { + client.stream.destroy() + }) + }) + }) + }) + + abstractClientTests(httpServer, makeOptions()) +}) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index a8cf962d6..0e76c4fd3 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -149,7 +149,7 @@ export interface IClientPublishOptions { * MQTT 5.0 properties object */ properties?: { - payloadFormatIndicator?: number, + payloadFormatIndicator?: boolean, messageExpiryInterval?: number, topicAlias?: string, responseTopic?: string, From e6672e80a48db6273af6bde338035d473ee3305a Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Fri, 22 Oct 2021 10:13:10 -0700 Subject: [PATCH 088/110] Revert "fix: types (#1341)" (#1344) This reverts commit 59fab369d2738edcf62306a67375763d737bc4ad. --- .gitignore | 1 + .npmrc | 1 + README.md | 1666 +++--- benchmarks/bombing.js | 52 +- benchmarks/throughputCounter.js | 44 +- bin/mqtt.js | 54 +- bin/pub.js | 292 +- bin/sub.js | 246 +- example.js | 22 +- examples/client/secure-client.js | 48 +- examples/client/simple-both.js | 26 +- examples/client/simple-publish.js | 14 +- examples/client/simple-subscribe.js | 18 +- examples/tls client/mqttclient.js | 96 +- examples/ws/client.js | 106 +- examples/wss/client_with_proxy.js | 116 +- lib/client.js | 3676 ++++++------ lib/connect/ali.js | 256 +- lib/connect/index.js | 328 +- lib/connect/tcp.js | 42 +- lib/connect/tls.js | 90 +- lib/connect/ws.js | 512 +- lib/connect/wx.js | 268 +- lib/default-message-id-provider.js | 138 +- lib/store.js | 256 +- lib/topic-alias-send.js | 186 +- lib/unique-message-id-provider.js | 130 +- lib/validations.js | 104 +- mqtt.js | 42 +- package.json | 226 +- test/abstract_client.js | 6354 ++++++++++----------- test/abstract_store.js | 270 +- test/browser/server.js | 264 +- test/browser/test.js | 184 +- test/client.js | 972 ++-- test/client_mqtt5.js | 2106 +++---- test/helpers/port_list.js | 102 +- test/helpers/server.js | 106 +- test/helpers/server_process.js | 18 +- test/message-id-provider.js | 182 +- test/mqtt.js | 460 +- test/mqtt_store.js | 18 +- test/secure_client.js | 376 +- test/server.js | 188 +- test/server_helpers_for_client_tests.js | 294 +- test/store.js | 20 +- test/unique_message_id_provider_client.js | 42 +- test/util.js | 30 +- test/websocket_client.js | 382 +- types/lib/client-options.d.ts | 2 +- 50 files changed, 10714 insertions(+), 10712 deletions(-) diff --git a/.gitignore b/.gitignore index 6a69f7d7f..5c315db7f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ coverage test/typescript/.idea/* test/typescript/*.js test/typescript/*.map +package-lock.json # VS Code stuff **/typings/** **/.vscode/** diff --git a/.npmrc b/.npmrc index e69de29bb..c1ca392fe 100644 --- a/.npmrc +++ b/.npmrc @@ -0,0 +1 @@ +package-lock = false diff --git a/README.md b/README.md index 2b8a19b3e..cebd1ca8a 100644 --- a/README.md +++ b/README.md @@ -1,833 +1,833 @@ -![mqtt.js](https://raw.githubusercontent.com/mqttjs/MQTT.js/137ee0e3940c1f01049a30248c70f24dc6e6f829/MQTT.js.png) -======= - -![Github Test Status](https://github.com/mqttjs/MQTT.js/workflows/MQTT.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/mqttjs/MQTT.js/branch/master/graph/badge.svg)](https://codecov.io/gh/mqttjs/MQTT.js) - -MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written -in JavaScript for node.js and the browser. - -* [__MQTT.js vNext__](#vnext) -* [Upgrade notes](#notes) -* [Installation](#install) -* [Example](#example) -* [Command Line Tools](#cli) -* [API](#api) -* [Browser](#browser) -* [Weapp](#weapp) -* [About QoS](#qos) -* [TypeScript](#typescript) -* [Contributing](#contributing) -* [License](#license) - -MQTT.js is an OPEN Open Source Project, see the [Contributing](#contributing) section to find out what this means. - -[![JavaScript Style -Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) - - -## Discussion on the next major version of MQTT.js -There are discussions happening on the future of MQTT.js and the next major version (vNext). We invite the community to provide their thoughts and feedback in [this GitHub discussion](https://github.com/mqttjs/MQTT.js/discussions/1324) - - -## Important notes for existing users - -__v4.0.0__ (Released 04/2020) removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to -debug logging, along with some feature additions. - -As a __breaking change__, by default a error handler is built into the MQTT.js client, so if any -errors are emitted and the user has not created an event handler on the client for errors, the client will -not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been -added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. - -__v3.0.0__ adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. - -__Note:__ MQTT v5 support is experimental as it has not been implemented by brokers yet. - -__v2.0.0__ removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending -packets. It also removes all the deprecated functionality in v1.0.0, -mainly `mqtt.createConnection` and `mqtt.Server`. From v2.0.0, -subscriptions are restored upon reconnection if `clean: true`. -v1.x.x is now in *LTS*, and it will keep being supported as long as -there are v0.8, v0.10 and v0.12 users. - -As a __breaking change__, the `encoding` option in the old client is -removed, and now everything is UTF-8 with the exception of the -`password` in the CONNECT message and `payload` in the PUBLISH message, -which are `Buffer`. - -Another __breaking change__ is that MQTT.js now defaults to MQTT v3.1.1, -so to support old brokers, please read the [client options doc](#client). - -__v1.0.0__ improves the overall architecture of the project, which is now -split into three components: MQTT.js keeps the Client, -[mqtt-connection](http://npm.im/mqtt-connection) includes the barebone -Connection code for server-side usage, and [mqtt-packet](http://npm.im/mqtt-packet) -includes the protocol parser and generator. The new Client improves -performance by a 30% factor, embeds Websocket support -([MOWS](http://npm.im/mows) is now deprecated), and it has a better -support for QoS 1 and 2. The previous API is still supported but -deprecated, as such, it is not documented in this README. - - -## Installation - -```sh -npm install mqtt --save -``` - - -## Example - -For the sake of simplicity, let's put the subscriber and the publisher in the same file: - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('mqtt://test.mosquitto.org') - -client.on('connect', function () { - client.subscribe('presence', function (err) { - if (!err) { - client.publish('presence', 'Hello mqtt') - } - }) -}) - -client.on('message', function (topic, message) { - // message is Buffer - console.log(message.toString()) - client.end() -}) -``` - -output: -``` -Hello mqtt -``` - -If you want to run your own MQTT broker, you can use -[Mosquitto](http://mosquitto.org) or -[Aedes-cli](https://github.com/moscajs/aedes-cli), and launch it. - -You can also use a test instance: test.mosquitto.org. - -If you do not want to install a separate broker, you can try using the -[Aedes](https://github.com/moscajs/aedes). - -to use MQTT.js in the browser see the [browserify](#browserify) section - - -## Promise support - -If you want to use the new [async-await](https://blog.risingstack.com/async-await-node-js-7-nightly/) functionality in JavaScript, or just prefer using Promises instead of callbacks, [async-mqtt](https://github.com/mqttjs/async-mqtt) is a wrapper over MQTT.js which uses promises instead of callbacks when possible. - - -## Command Line Tools - -MQTT.js bundles a command to interact with a broker. -In order to have it available on your path, you should install MQTT.js -globally: - -```sh -npm install mqtt -g -``` - -Then, on one terminal - -``` -mqtt sub -t 'hello' -h 'test.mosquitto.org' -v -``` - -On another - -``` -mqtt pub -t 'hello' -h 'test.mosquitto.org' -m 'from MQTT.js' -``` - -See `mqtt help ` for the command help. - - -## Debug Logs - -MQTT.js uses the [debug](https://www.npmjs.com/package/debug#cmd) package for debugging purposes. To enable debug logs, add the following environment variable on runtime : -```ps -# (example using PowerShell, the VS Code default) -$env:DEBUG='mqttjs*' - -``` - - -## About Reconnection - -An important part of any websocket connection is what to do when a connection -drops off and the client needs to reconnect. MQTT has built-in reconnection -support that can be configured to behave in ways that suit the application. - -#### Refresh Authentication Options / Signed Urls with `transformWsUrl` (Websocket Only) - -When an mqtt connection drops and needs to reconnect, it's common to require -that any authentication associated with the connection is kept current with -the underlying auth mechanism. For instance some applications may pass an auth -token with connection options on the initial connection, while other cloud -services may require a url be signed with each connection. - -By the time the reconnect happens in the application lifecycle, the original -auth data may have expired. - -To address this we can use a hook called `transformWsUrl` to manipulate -either of the connection url or the client options at the time of a reconnect. - -Example (update clientId & username on each reconnect): -``` - const transformWsUrl = (url, options, client) => { - client.options.username = `token=${this.get_current_auth_token()}`; - client.options.clientId = `${this.get_updated_clientId()}`; - - return `${this.get_signed_cloud_url(url)`; - } - - const connection = await mqtt.connectAsync(, { - ..., - transformWsUrl: transformUrl, - }); - -``` -Now every time a new WebSocket connection is opened (hopefully not too often), -we will get a fresh signed url or fresh auth token data. - -Note: Currently this hook does _not_ support promises, meaning that in order to -use the latest auth token, you must have some outside mechanism running that -handles application-level authentication refreshing so that the websocket -connection can simply grab the latest valid token or signed url. - - -#### Enabling Reconnection with `reconnectPeriod` option - -To ensure that the mqtt client automatically tries to reconnect when the -connection is dropped, you must set the client option `reconnectPeriod` to a -value greater than 0. A value of 0 will disable reconnection and then terminate -the final connection when it drops. - -The default value is 1000 ms which means it will try to reconnect 1 second -after losing the connection. - - -## About Topic Alias Management - -### Enabling automatic Topic Alias using -If the client sets the option `autoUseTopicAlias:true` then MQTT.js uses existing topic alias automatically. - -example scenario: -``` -1. PUBLISH topic:'t1', ta:1 (register) -2. PUBLISH topic:'t1' -> topic:'', ta:1 (auto use existing map entry) -3. PUBLISH topic:'t2', ta:1 (register overwrite) -4. PUBLISH topic:'t2' -> topic:'', ta:1 (auto use existing map entry based on the receent map) -5. PUBLISH topic:'t1' (t1 is no longer mapped to ta:1) -``` - -User doesn't need to manage which topic is mapped to which topic alias. -If the user want to register topic alias, then publish topic with topic alias. -If the user want to use topic alias, then publish topic without topic alias. If there is a mapped topic alias then added it as a property and update the topic to empty string. - -### Enabling automatic Topic Alias assign - -If the client sets the option `autoAssignTopicAlias:true` then MQTT.js uses existing topic alias automatically. -If no topic alias exists, then assign a new vacant topic alias automatically. If topic alias is fully used, then LRU(Least Recently Used) topic-alias entry is overwritten. - -example scenario: -``` -The broker returns CONNACK (TopicAliasMaximum:3) -1. PUBLISH topic:'t1' -> 't1', ta:1 (auto assign t1:1 and register) -2. PUBLISH topic:'t1' -> '' , ta:1 (auto use existing map entry) -3. PUBLISH topic:'t2' -> 't2', ta:2 (auto assign t1:2 and register. 2 was vacant) -4. PUBLISH topic:'t3' -> 't3', ta:3 (auto assign t1:3 and register. 3 was vacant) -5. PUBLISH topic:'t4' -> 't4', ta:1 (LRU entry is overwritten) -``` - -Also user can manually register topic-alias pair using PUBLISH topic:'some', ta:X. It works well with automatic topic alias assign. - - -## API - - * mqtt.connect() - * mqtt.Client() - * mqtt.Client#publish() - * mqtt.Client#subscribe() - * mqtt.Client#unsubscribe() - * mqtt.Client#end() - * mqtt.Client#removeOutgoingMessage() - * mqtt.Client#reconnect() - * mqtt.Client#handleMessage() - * mqtt.Client#connected - * mqtt.Client#reconnecting - * mqtt.Client#getLastMessageId() - * mqtt.Store() - * mqtt.Store#put() - * mqtt.Store#del() - * mqtt.Store#createStream() - * mqtt.Store#close() - -------------------------------------------------------- - -### mqtt.connect([url], options) - -Connects to the broker specified by the given url and options and -returns a [Client](#client). - -The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', -'tls', 'ws', 'wss'. The URL can also be an object as returned by -[`URL.parse()`](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost), -in that case the two objects are merged, i.e. you can pass a single -object with both the URL and the connect options. - -You can also specify a `servers` options with content: `[{ host: -'localhost', port: 1883 }, ... ]`, in that case that array is iterated -at every connect. - -For all MQTT-related options, see the [Client](#client) -constructor. - -------------------------------------------------------- - -### mqtt.Client(streamBuilder, options) - -The `Client` class wraps a client connection to an -MQTT broker over an arbitrary transport method (TCP, TLS, -WebSocket, ecc). - -`Client` automatically handles the following: - -* Regular server pings -* QoS flow -* Automatic reconnections -* Start publishing before being connected - -The arguments are: - -* `streamBuilder` is a function that returns a subclass of the `Stream` class that supports -the `connect` event. Typically a `net.Socket`. -* `options` is the client connection options (see: the [connect packet](https://github.com/mcollina/mqtt-packet#connect)). Defaults: - * `wsOptions`: is the WebSocket connection options. Default is `{}`. - It's specific for WebSockets. For possible options have a look at: https://github.com/websockets/ws/blob/master/doc/ws.md. - * `keepalive`: `60` seconds, set to `0` to disable - * `reschedulePings`: reschedule ping messages after sending packets (default `true`) - * `clientId`: `'mqttjs_' + Math.random().toString(16).substr(2, 8)` - * `protocolId`: `'MQTT'` - * `protocolVersion`: `4` - * `clean`: `true`, set to false to receive QoS 1 and 2 messages while - offline - * `reconnectPeriod`: `1000` milliseconds, interval between two - reconnections. Disable auto reconnect by setting to `0`. - * `connectTimeout`: `30 * 1000` milliseconds, time to wait before a - CONNACK is received - * `username`: the username required by your broker, if any - * `password`: the password required by your broker, if any - * `incomingStore`: a [Store](#store) for the incoming packets - * `outgoingStore`: a [Store](#store) for the outgoing packets - * `queueQoSZero`: if connection is broken, queue outgoing QoS zero messages (default `true`) - * `customHandleAcks`: MQTT 5 feature of custom handling puback and pubrec packets. Its callback: - ```js - customHandleAcks: function(topic, message, packet, done) {/*some logic wit colling done(error, reasonCode)*/} - ``` - * `autoUseTopicAlias`: enabling automatic Topic Alias using functionality - * `autoAssignTopicAlias`: enabling automatic Topic Alias assign functionality - * `properties`: properties MQTT 5.0. - `object` that supports the following properties: - * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, - * `receiveMaximum`: representing the Receive Maximum value `number`, - * `maximumPacketSize`: representing the Maximum Packet Size the Client is willing to accept `number`, - * `topicAliasMaximum`: representing the Topic Alias Maximum value indicates the highest value that the Client will accept as a Topic Alias sent by the Server `number`, - * `requestResponseInformation`: The Client uses this value to request the Server to return Response Information in the CONNACK `boolean`, - * `requestProblemInformation`: The Client uses this value to indicate whether the Reason String or User Properties are sent in the case of failures `boolean`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - * `authenticationMethod`: the name of the authentication method used for extended authentication `string`, - * `authenticationData`: Binary Data containing authentication data `binary` - * `authPacket`: settings for auth packet `object` - * `will`: a message that will sent by the broker automatically when - the client disconnect badly. The format is: - * `topic`: the topic to publish - * `payload`: the message to publish - * `qos`: the QoS - * `retain`: the retain flag - * `properties`: properties of will by MQTT 5.0: - * `willDelayInterval`: representing the Will Delay Interval in seconds `number`, - * `payloadFormatIndicator`: Will Message is UTF-8 Encoded Character Data or not `boolean`, - * `messageExpiryInterval`: value is the lifetime of the Will Message in seconds and is sent as the Publication Expiry Interval when the Server publishes the Will Message `number`, - * `contentType`: describing the content of the Will Message `string`, - * `responseTopic`: String which is used as the Topic Name for a response message `string`, - * `correlationData`: The Correlation Data is used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` - * `transformWsUrl` : optional `(url, options, client) => url` function - For ws/wss protocols only. Can be used to implement signing - urls which upon reconnect can have become expired. - * `resubscribe` : if connection is broken and reconnects, - subscribed topics are automatically subscribed again (default `true`) - * `messageIdProvider`: custom messageId provider. when `new UniqueMessageIdProvider()` is set, then non conflict messageId is provided. - -In case mqtts (mqtt over tls) is required, the `options` object is -passed through to -[`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback). -If you are using a **self-signed certificate**, pass the `rejectUnauthorized: false` option. -Beware that you are exposing yourself to man in the middle attacks, so it is a configuration -that is not recommended for production environments. - -If you are connecting to a broker that supports only MQTT 3.1 (not -3.1.1 compliant), you should pass these additional options: - -```js -{ - protocolId: 'MQIsdp', - protocolVersion: 3 -} -``` - -This is confirmed on RabbitMQ 3.2.4, and on Mosquitto < 1.3. Mosquitto -version 1.3 and 1.4 works fine without those. - -#### Event `'connect'` - -`function (connack) {}` - -Emitted on successful (re)connection (i.e. connack rc=0). -* `connack` received connack packet. When `clean` connection option is `false` and server has a previous session -for `clientId` connection option, then `connack.sessionPresent` flag is `true`. When that is the case, -you may rely on stored session and prefer not to send subscribe commands for the client. - -#### Event `'reconnect'` - -`function () {}` - -Emitted when a reconnect starts. - -#### Event `'close'` - -`function () {}` - -Emitted after a disconnection. - -#### Event `'disconnect'` - -`function (packet) {}` - -Emitted after receiving disconnect packet from broker. MQTT 5.0 feature. - -#### Event `'offline'` - -`function () {}` - -Emitted when the client goes offline. - -#### Event `'error'` - -`function (error) {}` - -Emitted when the client cannot connect (i.e. connack rc != 0) or when a -parsing error occurs. - -The following TLS errors will be emitted as an `error` event: - -* `ECONNREFUSED` -* `ECONNRESET` -* `EADDRINUSE` -* `ENOTFOUND` - -#### Event `'end'` - -`function () {}` - -Emitted when mqtt.Client#end() is called. -If a callback was passed to `mqtt.Client#end()`, this event is emitted once the -callback returns. - -#### Event `'message'` - -`function (topic, message, packet) {}` - -Emitted when the client receives a publish packet -* `topic` topic of the received packet -* `message` payload of the received packet -* `packet` received packet, as defined in - [mqtt-packet](https://github.com/mcollina/mqtt-packet#publish) - -#### Event `'packetsend'` - -`function (packet) {}` - -Emitted when the client sends any packet. This includes .published() packets -as well as packets used by MQTT for managing subscriptions and connections -* `packet` received packet, as defined in - [mqtt-packet](https://github.com/mcollina/mqtt-packet) - -#### Event `'packetreceive'` - -`function (packet) {}` - -Emitted when the client receives any packet. This includes packets from -subscribed topics as well as packets used by MQTT for managing subscriptions -and connections -* `packet` received packet, as defined in - [mqtt-packet](https://github.com/mcollina/mqtt-packet) - -------------------------------------------------------- - -### mqtt.Client#publish(topic, message, [options], [callback]) - -Publish a message to a topic - -* `topic` is the topic to publish to, `String` -* `message` is the message to publish, `Buffer` or `String` -* `options` is the options to publish with, including: - * `qos` QoS level, `Number`, default `0` - * `retain` retain flag, `Boolean`, default `false` - * `dup` mark as duplicate flag, `Boolean`, default `false` - * `properties`: MQTT 5.0 properties `object` - * `payloadFormatIndicator`: Payload is UTF-8 Encoded Character Data or not `boolean`, - * `messageExpiryInterval`: the lifetime of the Application Message in seconds `number`, - * `topicAlias`: value that is used to identify the Topic instead of using the Topic Name `number`, - * `responseTopic`: String which is used as the Topic Name for a response message `string`, - * `correlationData`: used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - * `subscriptionIdentifier`: representing the identifier of the subscription `number`, - * `contentType`: String describing the content of the Application Message `string` - * `cbStorePut` - `function ()`, fired when message is put into `outgoingStore` if QoS is `1` or `2`. -* `callback` - `function (err)`, fired when the QoS handling completes, - or at the next tick if QoS 0. An error occurs if client is disconnecting. - -------------------------------------------------------- - -### mqtt.Client#subscribe(topic/topic array/topic object, [options], [callback]) - -Subscribe to a topic or topics - -* `topic` is a `String` topic to subscribe to or an `Array` of - topics to subscribe to. It can also be an object, it has as object - keys the topic name and as value the QoS, like `{'test1': {qos: 0}, 'test2': {qos: 1}}`. - MQTT `topic` wildcard characters are supported (`+` - for single level and `#` - for multi level) -* `options` is the options to subscribe with, including: - * `qos` QoS subscription level, default 0 - * `nl` No Local MQTT 5.0 flag (If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the ClientID of the publishing connection) - * `rap` Retain as Published MQTT 5.0 flag (If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If false, Application Messages forwarded using this subscription have the RETAIN flag set to 0.) - * `rh` Retain Handling MQTT 5.0 (This option specifies whether retained messages are sent when the subscription is established.) - * `properties`: `object` - * `subscriptionIdentifier`: representing the identifier of the subscription `number`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` -* `callback` - `function (err, granted)` - callback fired on suback where: - * `err` a subscription error or an error that occurs when client is disconnecting - * `granted` is an array of `{topic, qos}` where: - * `topic` is a subscribed to topic - * `qos` is the granted QoS level on it - -------------------------------------------------------- - -### mqtt.Client#unsubscribe(topic/topic array, [options], [callback]) - -Unsubscribe from a topic or topics - -* `topic` is a `String` topic or an array of topics to unsubscribe from -* `options`: options of unsubscribe. - * `properties`: `object` - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` -* `callback` - `function (err)`, fired on unsuback. An error occurs if client is disconnecting. - -------------------------------------------------------- - -### mqtt.Client#end([force], [options], [callback]) - -Close the client, accepts the following options: - -* `force`: passing it to true will close the client right away, without - waiting for the in-flight messages to be acked. This parameter is - optional. -* `options`: options of disconnect. - * `reasonCode`: Disconnect Reason Code `number` - * `properties`: `object` - * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, - * `reasonString`: representing the reason for the disconnect `string`, - * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, - * `serverReference`: String which can be used by the Client to identify another Server to use `string` -* `callback`: will be called when the client is closed. This parameter is - optional. - -------------------------------------------------------- - -### mqtt.Client#removeOutgoingMessage(mId) - -Remove a message from the outgoingStore. -The outgoing callback will be called with Error('Message removed') if the message is removed. - -After this function is called, the messageId is released and becomes reusable. - -* `mId`: The messageId of the message in the outgoingStore. - -------------------------------------------------------- - -### mqtt.Client#reconnect() - -Connect again using the same options as connect() - -------------------------------------------------------- - -### mqtt.Client#handleMessage(packet, callback) - -Handle messages with backpressure support, one at a time. -Override at will, but __always call `callback`__, or the client -will hang. - -------------------------------------------------------- - -### mqtt.Client#connected - -Boolean : set to `true` if the client is connected. `false` otherwise. - -------------------------------------------------------- - -### mqtt.Client#getLastMessageId() - -Number : get last message id. This is for sent messages only. - -------------------------------------------------------- - -### mqtt.Client#reconnecting - -Boolean : set to `true` if the client is trying to reconnect to the server. `false` otherwise. - -------------------------------------------------------- - -### mqtt.Store(options) - -In-memory implementation of the message store. - -* `options` is the store options: - * `clean`: `true`, clean inflight messages when close is called (default `true`) - -Other implementations of `mqtt.Store`: - -* [mqtt-level-store](http://npm.im/mqtt-level-store) which uses - [Level-browserify](http://npm.im/level-browserify) to store the inflight - data, making it usable both in Node and the Browser. -* [mqtt-nedb-store](https://github.com/behrad/mqtt-nedb-store) which - uses [nedb](https://www.npmjs.com/package/nedb) to store the inflight - data. -* [mqtt-localforage-store](http://npm.im/mqtt-localforage-store) which uses - [localForage](http://npm.im/localforage) to store the inflight - data, making it usable in the Browser without browserify. - -------------------------------------------------------- - -### mqtt.Store#put(packet, callback) - -Adds a packet to the store, a packet is -anything that has a `messageId` property. -The callback is called when the packet has been stored. - -------------------------------------------------------- - -### mqtt.Store#createStream() - -Creates a stream with all the packets in the store. - -------------------------------------------------------- - -### mqtt.Store#del(packet, cb) - -Removes a packet from the store, a packet is -anything that has a `messageId` property. -The callback is called when the packet has been removed. - -------------------------------------------------------- - -### mqtt.Store#close(cb) - -Closes the Store. - - -## Browser - - -### Via CDN - -The MQTT.js bundle is available through http://unpkg.com, specifically -at https://unpkg.com/mqtt/dist/mqtt.min.js. -See http://unpkg.com for the full documentation on version ranges. - - -## WeChat Mini Program -Support [WeChat Mini Program](https://mp.weixin.qq.com/). See [Doc](https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-socket.html). - - -## Example(js) - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('wxs://test.mosquitto.org') -``` - -## Example(ts) - -```ts -import { connect } from 'mqtt'; -const client = connect('wxs://test.mosquitto.org'); -``` - -## Ali Mini Program -Surport [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). See [Doc](https://docs.alipay.com/mini/developer/getting-started). - - -## Example(js) - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('alis://test.mosquitto.org') -``` - -## Example(ts) - -```ts -import { connect } from 'mqtt'; -const client = connect('alis://test.mosquitto.org'); -``` - - -### Browserify - -In order to use MQTT.js as a browserify module you can either require it in your browserify bundles or build it as a stand alone module. The exported module is AMD/CommonJs compatible and it will add an object in the global space. - -```bash -mkdir tmpdir -cd tmpdir -npm install mqtt -npm install browserify -npm install tinyify -cd node_modules/mqtt/ -npm install . -npx browserify mqtt.js -s mqtt >browserMqtt.js // use script tag -# show size for compressed browser transfer -gzip -### Webpack - -Just like browserify, export MQTT.js as library. The exported module would be `var mqtt = xxx` and it will add an object in the global space. You could also export module in other [formats (AMD/CommonJS/others)](http://webpack.github.io/docs/configuration.html#output-librarytarget) by setting **output.libraryTarget** in webpack configuration. - -```javascript -npm install -g webpack // install webpack - -cd node_modules/mqtt -npm install . // install dev dependencies -webpack mqtt.js ./browserMqtt.js --output-library mqtt -``` - -you can then use mqtt.js in the browser with the same api than node's one. - -```html - - - test Ws mqtt.js - - - - - - -``` - -### React -``` -npm install -g webpack // Install webpack globally -npm install mqtt // Install MQTT library -cd node_modules/mqtt -npm install . // Install dev deps at current dir -webpack mqtt.js --output-library mqtt // Build - -// now you can import the library with ES6 import, commonJS not tested -``` - - -```javascript -import React from 'react'; -import mqtt from 'mqtt'; - -export default () => { - const [connectionStatus, setConnectionStatus] = React.useState(false); - const [messages, setMessages] = React.useState([]); - - useEffect(() => { - const client = mqtt.connect(SOME_URL); - client.on('connect', () => setConnectionStatus(true)); - client.on('message', (topic, payload, packet) => { - setMessages(messages.concat(payload.toString())); - }); - }, []); - - return ( - <> - {messages.map((message) => ( -

{message}

- ) - - ) -} -``` - -Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/moscajs/aedes/blob/master/docs/Examples.md#mqtt-server-over-websocket-using-server-factory) to setup [Aedes](https://github.com/moscajs/aedes)). - - -## About QoS - -Here is how QoS works: - -* QoS 0 : received **at most once** : The packet is sent, and that's it. There is no validation about whether it has been received. -* QoS 1 : received **at least once** : The packet is sent and stored as long as the client has not received a confirmation from the server. MQTT ensures that it *will* be received, but there can be duplicates. -* QoS 2 : received **exactly once** : Same as QoS 1 but there is no duplicates. - -About data consumption, obviously, QoS 2 > QoS 1 > QoS 0, if that's a concern to you. - - -## Usage with TypeScript -This repo bundles TypeScript definition files for use in TypeScript projects and to support tools that can read `.d.ts` files. - -### Pre-requisites -Before you can begin using these TypeScript definitions with your project, you need to make sure your project meets a few of these requirements: - * TypeScript >= 2.1 - * Set tsconfig.json: `{"compilerOptions" : {"moduleResolution" : "node"}, ...}` - * Includes the TypeScript definitions for node. You can use npm to install this by typing the following into a terminal window: - `npm install --save-dev @types/node` - - -## Contributing - -MQTT.js is an **OPEN Open Source Project**. This means that: - -> Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. - -See the [CONTRIBUTING.md](https://github.com/mqttjs/MQTT.js/blob/master/CONTRIBUTING.md) file for more details. - -### Contributors - -MQTT.js is only possible due to the excellent work of the following contributors: - - - - - - -
Adam RuddGitHub/adamvrTwitter/@adam_vr
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
Maxime AgorGitHub/4rzaelTwitter/@4rzael
Siarhei BuntsevichGitHub/scarry1992
- - -## License - -MIT +![mqtt.js](https://raw.githubusercontent.com/mqttjs/MQTT.js/137ee0e3940c1f01049a30248c70f24dc6e6f829/MQTT.js.png) +======= + +![Github Test Status](https://github.com/mqttjs/MQTT.js/workflows/MQTT.js%20CI/badge.svg) [![codecov](https://codecov.io/gh/mqttjs/MQTT.js/branch/master/graph/badge.svg)](https://codecov.io/gh/mqttjs/MQTT.js) + +MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written +in JavaScript for node.js and the browser. + +* [__MQTT.js vNext__](#vnext) +* [Upgrade notes](#notes) +* [Installation](#install) +* [Example](#example) +* [Command Line Tools](#cli) +* [API](#api) +* [Browser](#browser) +* [Weapp](#weapp) +* [About QoS](#qos) +* [TypeScript](#typescript) +* [Contributing](#contributing) +* [License](#license) + +MQTT.js is an OPEN Open Source Project, see the [Contributing](#contributing) section to find out what this means. + +[![JavaScript Style +Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) + + +## Discussion on the next major version of MQTT.js +There are discussions happening on the future of MQTT.js and the next major version (vNext). We invite the community to provide their thoughts and feedback in [this GitHub discussion](https://github.com/mqttjs/MQTT.js/discussions/1324) + + +## Important notes for existing users + +__v4.0.0__ (Released 04/2020) removes support for all end of life node versions, and now supports node v12 and v14. It also adds improvements to +debug logging, along with some feature additions. + +As a __breaking change__, by default a error handler is built into the MQTT.js client, so if any +errors are emitted and the user has not created an event handler on the client for errors, the client will +not break as a result of unhandled errors. Additionally, typical TLS errors like `ECONNREFUSED`, `ECONNRESET` have been +added to a list of TLS errors that will be emitted from the MQTT.js client, and so can be handled as connection errors. + +__v3.0.0__ adds support for MQTT 5, support for node v10.x, and many fixes to improve reliability. + +__Note:__ MQTT v5 support is experimental as it has not been implemented by brokers yet. + +__v2.0.0__ removes support for node v0.8, v0.10 and v0.12, and it is 3x faster in sending +packets. It also removes all the deprecated functionality in v1.0.0, +mainly `mqtt.createConnection` and `mqtt.Server`. From v2.0.0, +subscriptions are restored upon reconnection if `clean: true`. +v1.x.x is now in *LTS*, and it will keep being supported as long as +there are v0.8, v0.10 and v0.12 users. + +As a __breaking change__, the `encoding` option in the old client is +removed, and now everything is UTF-8 with the exception of the +`password` in the CONNECT message and `payload` in the PUBLISH message, +which are `Buffer`. + +Another __breaking change__ is that MQTT.js now defaults to MQTT v3.1.1, +so to support old brokers, please read the [client options doc](#client). + +__v1.0.0__ improves the overall architecture of the project, which is now +split into three components: MQTT.js keeps the Client, +[mqtt-connection](http://npm.im/mqtt-connection) includes the barebone +Connection code for server-side usage, and [mqtt-packet](http://npm.im/mqtt-packet) +includes the protocol parser and generator. The new Client improves +performance by a 30% factor, embeds Websocket support +([MOWS](http://npm.im/mows) is now deprecated), and it has a better +support for QoS 1 and 2. The previous API is still supported but +deprecated, as such, it is not documented in this README. + + +## Installation + +```sh +npm install mqtt --save +``` + + +## Example + +For the sake of simplicity, let's put the subscriber and the publisher in the same file: + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('mqtt://test.mosquitto.org') + +client.on('connect', function () { + client.subscribe('presence', function (err) { + if (!err) { + client.publish('presence', 'Hello mqtt') + } + }) +}) + +client.on('message', function (topic, message) { + // message is Buffer + console.log(message.toString()) + client.end() +}) +``` + +output: +``` +Hello mqtt +``` + +If you want to run your own MQTT broker, you can use +[Mosquitto](http://mosquitto.org) or +[Aedes-cli](https://github.com/moscajs/aedes-cli), and launch it. + +You can also use a test instance: test.mosquitto.org. + +If you do not want to install a separate broker, you can try using the +[Aedes](https://github.com/moscajs/aedes). + +to use MQTT.js in the browser see the [browserify](#browserify) section + + +## Promise support + +If you want to use the new [async-await](https://blog.risingstack.com/async-await-node-js-7-nightly/) functionality in JavaScript, or just prefer using Promises instead of callbacks, [async-mqtt](https://github.com/mqttjs/async-mqtt) is a wrapper over MQTT.js which uses promises instead of callbacks when possible. + + +## Command Line Tools + +MQTT.js bundles a command to interact with a broker. +In order to have it available on your path, you should install MQTT.js +globally: + +```sh +npm install mqtt -g +``` + +Then, on one terminal + +``` +mqtt sub -t 'hello' -h 'test.mosquitto.org' -v +``` + +On another + +``` +mqtt pub -t 'hello' -h 'test.mosquitto.org' -m 'from MQTT.js' +``` + +See `mqtt help ` for the command help. + + +## Debug Logs + +MQTT.js uses the [debug](https://www.npmjs.com/package/debug#cmd) package for debugging purposes. To enable debug logs, add the following environment variable on runtime : +```ps +# (example using PowerShell, the VS Code default) +$env:DEBUG='mqttjs*' + +``` + + +## About Reconnection + +An important part of any websocket connection is what to do when a connection +drops off and the client needs to reconnect. MQTT has built-in reconnection +support that can be configured to behave in ways that suit the application. + +#### Refresh Authentication Options / Signed Urls with `transformWsUrl` (Websocket Only) + +When an mqtt connection drops and needs to reconnect, it's common to require +that any authentication associated with the connection is kept current with +the underlying auth mechanism. For instance some applications may pass an auth +token with connection options on the initial connection, while other cloud +services may require a url be signed with each connection. + +By the time the reconnect happens in the application lifecycle, the original +auth data may have expired. + +To address this we can use a hook called `transformWsUrl` to manipulate +either of the connection url or the client options at the time of a reconnect. + +Example (update clientId & username on each reconnect): +``` + const transformWsUrl = (url, options, client) => { + client.options.username = `token=${this.get_current_auth_token()}`; + client.options.clientId = `${this.get_updated_clientId()}`; + + return `${this.get_signed_cloud_url(url)`; + } + + const connection = await mqtt.connectAsync(, { + ..., + transformWsUrl: transformUrl, + }); + +``` +Now every time a new WebSocket connection is opened (hopefully not too often), +we will get a fresh signed url or fresh auth token data. + +Note: Currently this hook does _not_ support promises, meaning that in order to +use the latest auth token, you must have some outside mechanism running that +handles application-level authentication refreshing so that the websocket +connection can simply grab the latest valid token or signed url. + + +#### Enabling Reconnection with `reconnectPeriod` option + +To ensure that the mqtt client automatically tries to reconnect when the +connection is dropped, you must set the client option `reconnectPeriod` to a +value greater than 0. A value of 0 will disable reconnection and then terminate +the final connection when it drops. + +The default value is 1000 ms which means it will try to reconnect 1 second +after losing the connection. + + +## About Topic Alias Management + +### Enabling automatic Topic Alias using +If the client sets the option `autoUseTopicAlias:true` then MQTT.js uses existing topic alias automatically. + +example scenario: +``` +1. PUBLISH topic:'t1', ta:1 (register) +2. PUBLISH topic:'t1' -> topic:'', ta:1 (auto use existing map entry) +3. PUBLISH topic:'t2', ta:1 (register overwrite) +4. PUBLISH topic:'t2' -> topic:'', ta:1 (auto use existing map entry based on the receent map) +5. PUBLISH topic:'t1' (t1 is no longer mapped to ta:1) +``` + +User doesn't need to manage which topic is mapped to which topic alias. +If the user want to register topic alias, then publish topic with topic alias. +If the user want to use topic alias, then publish topic without topic alias. If there is a mapped topic alias then added it as a property and update the topic to empty string. + +### Enabling automatic Topic Alias assign + +If the client sets the option `autoAssignTopicAlias:true` then MQTT.js uses existing topic alias automatically. +If no topic alias exists, then assign a new vacant topic alias automatically. If topic alias is fully used, then LRU(Least Recently Used) topic-alias entry is overwritten. + +example scenario: +``` +The broker returns CONNACK (TopicAliasMaximum:3) +1. PUBLISH topic:'t1' -> 't1', ta:1 (auto assign t1:1 and register) +2. PUBLISH topic:'t1' -> '' , ta:1 (auto use existing map entry) +3. PUBLISH topic:'t2' -> 't2', ta:2 (auto assign t1:2 and register. 2 was vacant) +4. PUBLISH topic:'t3' -> 't3', ta:3 (auto assign t1:3 and register. 3 was vacant) +5. PUBLISH topic:'t4' -> 't4', ta:1 (LRU entry is overwritten) +``` + +Also user can manually register topic-alias pair using PUBLISH topic:'some', ta:X. It works well with automatic topic alias assign. + + +## API + + * mqtt.connect() + * mqtt.Client() + * mqtt.Client#publish() + * mqtt.Client#subscribe() + * mqtt.Client#unsubscribe() + * mqtt.Client#end() + * mqtt.Client#removeOutgoingMessage() + * mqtt.Client#reconnect() + * mqtt.Client#handleMessage() + * mqtt.Client#connected + * mqtt.Client#reconnecting + * mqtt.Client#getLastMessageId() + * mqtt.Store() + * mqtt.Store#put() + * mqtt.Store#del() + * mqtt.Store#createStream() + * mqtt.Store#close() + +------------------------------------------------------- + +### mqtt.connect([url], options) + +Connects to the broker specified by the given url and options and +returns a [Client](#client). + +The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', +'tls', 'ws', 'wss'. The URL can also be an object as returned by +[`URL.parse()`](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost), +in that case the two objects are merged, i.e. you can pass a single +object with both the URL and the connect options. + +You can also specify a `servers` options with content: `[{ host: +'localhost', port: 1883 }, ... ]`, in that case that array is iterated +at every connect. + +For all MQTT-related options, see the [Client](#client) +constructor. + +------------------------------------------------------- + +### mqtt.Client(streamBuilder, options) + +The `Client` class wraps a client connection to an +MQTT broker over an arbitrary transport method (TCP, TLS, +WebSocket, ecc). + +`Client` automatically handles the following: + +* Regular server pings +* QoS flow +* Automatic reconnections +* Start publishing before being connected + +The arguments are: + +* `streamBuilder` is a function that returns a subclass of the `Stream` class that supports +the `connect` event. Typically a `net.Socket`. +* `options` is the client connection options (see: the [connect packet](https://github.com/mcollina/mqtt-packet#connect)). Defaults: + * `wsOptions`: is the WebSocket connection options. Default is `{}`. + It's specific for WebSockets. For possible options have a look at: https://github.com/websockets/ws/blob/master/doc/ws.md. + * `keepalive`: `60` seconds, set to `0` to disable + * `reschedulePings`: reschedule ping messages after sending packets (default `true`) + * `clientId`: `'mqttjs_' + Math.random().toString(16).substr(2, 8)` + * `protocolId`: `'MQTT'` + * `protocolVersion`: `4` + * `clean`: `true`, set to false to receive QoS 1 and 2 messages while + offline + * `reconnectPeriod`: `1000` milliseconds, interval between two + reconnections. Disable auto reconnect by setting to `0`. + * `connectTimeout`: `30 * 1000` milliseconds, time to wait before a + CONNACK is received + * `username`: the username required by your broker, if any + * `password`: the password required by your broker, if any + * `incomingStore`: a [Store](#store) for the incoming packets + * `outgoingStore`: a [Store](#store) for the outgoing packets + * `queueQoSZero`: if connection is broken, queue outgoing QoS zero messages (default `true`) + * `customHandleAcks`: MQTT 5 feature of custom handling puback and pubrec packets. Its callback: + ```js + customHandleAcks: function(topic, message, packet, done) {/*some logic wit colling done(error, reasonCode)*/} + ``` + * `autoUseTopicAlias`: enabling automatic Topic Alias using functionality + * `autoAssignTopicAlias`: enabling automatic Topic Alias assign functionality + * `properties`: properties MQTT 5.0. + `object` that supports the following properties: + * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, + * `receiveMaximum`: representing the Receive Maximum value `number`, + * `maximumPacketSize`: representing the Maximum Packet Size the Client is willing to accept `number`, + * `topicAliasMaximum`: representing the Topic Alias Maximum value indicates the highest value that the Client will accept as a Topic Alias sent by the Server `number`, + * `requestResponseInformation`: The Client uses this value to request the Server to return Response Information in the CONNACK `boolean`, + * `requestProblemInformation`: The Client uses this value to indicate whether the Reason String or User Properties are sent in the case of failures `boolean`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, + * `authenticationMethod`: the name of the authentication method used for extended authentication `string`, + * `authenticationData`: Binary Data containing authentication data `binary` + * `authPacket`: settings for auth packet `object` + * `will`: a message that will sent by the broker automatically when + the client disconnect badly. The format is: + * `topic`: the topic to publish + * `payload`: the message to publish + * `qos`: the QoS + * `retain`: the retain flag + * `properties`: properties of will by MQTT 5.0: + * `willDelayInterval`: representing the Will Delay Interval in seconds `number`, + * `payloadFormatIndicator`: Will Message is UTF-8 Encoded Character Data or not `boolean`, + * `messageExpiryInterval`: value is the lifetime of the Will Message in seconds and is sent as the Publication Expiry Interval when the Server publishes the Will Message `number`, + * `contentType`: describing the content of the Will Message `string`, + * `responseTopic`: String which is used as the Topic Name for a response message `string`, + * `correlationData`: The Correlation Data is used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` + * `transformWsUrl` : optional `(url, options, client) => url` function + For ws/wss protocols only. Can be used to implement signing + urls which upon reconnect can have become expired. + * `resubscribe` : if connection is broken and reconnects, + subscribed topics are automatically subscribed again (default `true`) + * `messageIdProvider`: custom messageId provider. when `new UniqueMessageIdProvider()` is set, then non conflict messageId is provided. + +In case mqtts (mqtt over tls) is required, the `options` object is +passed through to +[`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback). +If you are using a **self-signed certificate**, pass the `rejectUnauthorized: false` option. +Beware that you are exposing yourself to man in the middle attacks, so it is a configuration +that is not recommended for production environments. + +If you are connecting to a broker that supports only MQTT 3.1 (not +3.1.1 compliant), you should pass these additional options: + +```js +{ + protocolId: 'MQIsdp', + protocolVersion: 3 +} +``` + +This is confirmed on RabbitMQ 3.2.4, and on Mosquitto < 1.3. Mosquitto +version 1.3 and 1.4 works fine without those. + +#### Event `'connect'` + +`function (connack) {}` + +Emitted on successful (re)connection (i.e. connack rc=0). +* `connack` received connack packet. When `clean` connection option is `false` and server has a previous session +for `clientId` connection option, then `connack.sessionPresent` flag is `true`. When that is the case, +you may rely on stored session and prefer not to send subscribe commands for the client. + +#### Event `'reconnect'` + +`function () {}` + +Emitted when a reconnect starts. + +#### Event `'close'` + +`function () {}` + +Emitted after a disconnection. + +#### Event `'disconnect'` + +`function (packet) {}` + +Emitted after receiving disconnect packet from broker. MQTT 5.0 feature. + +#### Event `'offline'` + +`function () {}` + +Emitted when the client goes offline. + +#### Event `'error'` + +`function (error) {}` + +Emitted when the client cannot connect (i.e. connack rc != 0) or when a +parsing error occurs. + +The following TLS errors will be emitted as an `error` event: + +* `ECONNREFUSED` +* `ECONNRESET` +* `EADDRINUSE` +* `ENOTFOUND` + +#### Event `'end'` + +`function () {}` + +Emitted when mqtt.Client#end() is called. +If a callback was passed to `mqtt.Client#end()`, this event is emitted once the +callback returns. + +#### Event `'message'` + +`function (topic, message, packet) {}` + +Emitted when the client receives a publish packet +* `topic` topic of the received packet +* `message` payload of the received packet +* `packet` received packet, as defined in + [mqtt-packet](https://github.com/mcollina/mqtt-packet#publish) + +#### Event `'packetsend'` + +`function (packet) {}` + +Emitted when the client sends any packet. This includes .published() packets +as well as packets used by MQTT for managing subscriptions and connections +* `packet` received packet, as defined in + [mqtt-packet](https://github.com/mcollina/mqtt-packet) + +#### Event `'packetreceive'` + +`function (packet) {}` + +Emitted when the client receives any packet. This includes packets from +subscribed topics as well as packets used by MQTT for managing subscriptions +and connections +* `packet` received packet, as defined in + [mqtt-packet](https://github.com/mcollina/mqtt-packet) + +------------------------------------------------------- + +### mqtt.Client#publish(topic, message, [options], [callback]) + +Publish a message to a topic + +* `topic` is the topic to publish to, `String` +* `message` is the message to publish, `Buffer` or `String` +* `options` is the options to publish with, including: + * `qos` QoS level, `Number`, default `0` + * `retain` retain flag, `Boolean`, default `false` + * `dup` mark as duplicate flag, `Boolean`, default `false` + * `properties`: MQTT 5.0 properties `object` + * `payloadFormatIndicator`: Payload is UTF-8 Encoded Character Data or not `boolean`, + * `messageExpiryInterval`: the lifetime of the Application Message in seconds `number`, + * `topicAlias`: value that is used to identify the Topic instead of using the Topic Name `number`, + * `responseTopic`: String which is used as the Topic Name for a response message `string`, + * `correlationData`: used by the sender of the Request Message to identify which request the Response Message is for when it is received `binary`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, + * `subscriptionIdentifier`: representing the identifier of the subscription `number`, + * `contentType`: String describing the content of the Application Message `string` + * `cbStorePut` - `function ()`, fired when message is put into `outgoingStore` if QoS is `1` or `2`. +* `callback` - `function (err)`, fired when the QoS handling completes, + or at the next tick if QoS 0. An error occurs if client is disconnecting. + +------------------------------------------------------- + +### mqtt.Client#subscribe(topic/topic array/topic object, [options], [callback]) + +Subscribe to a topic or topics + +* `topic` is a `String` topic to subscribe to or an `Array` of + topics to subscribe to. It can also be an object, it has as object + keys the topic name and as value the QoS, like `{'test1': {qos: 0}, 'test2': {qos: 1}}`. + MQTT `topic` wildcard characters are supported (`+` - for single level and `#` - for multi level) +* `options` is the options to subscribe with, including: + * `qos` QoS subscription level, default 0 + * `nl` No Local MQTT 5.0 flag (If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the ClientID of the publishing connection) + * `rap` Retain as Published MQTT 5.0 flag (If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If false, Application Messages forwarded using this subscription have the RETAIN flag set to 0.) + * `rh` Retain Handling MQTT 5.0 (This option specifies whether retained messages are sent when the subscription is established.) + * `properties`: `object` + * `subscriptionIdentifier`: representing the identifier of the subscription `number`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` +* `callback` - `function (err, granted)` + callback fired on suback where: + * `err` a subscription error or an error that occurs when client is disconnecting + * `granted` is an array of `{topic, qos}` where: + * `topic` is a subscribed to topic + * `qos` is the granted QoS level on it + +------------------------------------------------------- + +### mqtt.Client#unsubscribe(topic/topic array, [options], [callback]) + +Unsubscribe from a topic or topics + +* `topic` is a `String` topic or an array of topics to unsubscribe from +* `options`: options of unsubscribe. + * `properties`: `object` + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object` +* `callback` - `function (err)`, fired on unsuback. An error occurs if client is disconnecting. + +------------------------------------------------------- + +### mqtt.Client#end([force], [options], [callback]) + +Close the client, accepts the following options: + +* `force`: passing it to true will close the client right away, without + waiting for the in-flight messages to be acked. This parameter is + optional. +* `options`: options of disconnect. + * `reasonCode`: Disconnect Reason Code `number` + * `properties`: `object` + * `sessionExpiryInterval`: representing the Session Expiry Interval in seconds `number`, + * `reasonString`: representing the reason for the disconnect `string`, + * `userProperties`: The User Property is allowed to appear multiple times to represent multiple name, value pairs `object`, + * `serverReference`: String which can be used by the Client to identify another Server to use `string` +* `callback`: will be called when the client is closed. This parameter is + optional. + +------------------------------------------------------- + +### mqtt.Client#removeOutgoingMessage(mId) + +Remove a message from the outgoingStore. +The outgoing callback will be called with Error('Message removed') if the message is removed. + +After this function is called, the messageId is released and becomes reusable. + +* `mId`: The messageId of the message in the outgoingStore. + +------------------------------------------------------- + +### mqtt.Client#reconnect() + +Connect again using the same options as connect() + +------------------------------------------------------- + +### mqtt.Client#handleMessage(packet, callback) + +Handle messages with backpressure support, one at a time. +Override at will, but __always call `callback`__, or the client +will hang. + +------------------------------------------------------- + +### mqtt.Client#connected + +Boolean : set to `true` if the client is connected. `false` otherwise. + +------------------------------------------------------- + +### mqtt.Client#getLastMessageId() + +Number : get last message id. This is for sent messages only. + +------------------------------------------------------- + +### mqtt.Client#reconnecting + +Boolean : set to `true` if the client is trying to reconnect to the server. `false` otherwise. + +------------------------------------------------------- + +### mqtt.Store(options) + +In-memory implementation of the message store. + +* `options` is the store options: + * `clean`: `true`, clean inflight messages when close is called (default `true`) + +Other implementations of `mqtt.Store`: + +* [mqtt-level-store](http://npm.im/mqtt-level-store) which uses + [Level-browserify](http://npm.im/level-browserify) to store the inflight + data, making it usable both in Node and the Browser. +* [mqtt-nedb-store](https://github.com/behrad/mqtt-nedb-store) which + uses [nedb](https://www.npmjs.com/package/nedb) to store the inflight + data. +* [mqtt-localforage-store](http://npm.im/mqtt-localforage-store) which uses + [localForage](http://npm.im/localforage) to store the inflight + data, making it usable in the Browser without browserify. + +------------------------------------------------------- + +### mqtt.Store#put(packet, callback) + +Adds a packet to the store, a packet is +anything that has a `messageId` property. +The callback is called when the packet has been stored. + +------------------------------------------------------- + +### mqtt.Store#createStream() + +Creates a stream with all the packets in the store. + +------------------------------------------------------- + +### mqtt.Store#del(packet, cb) + +Removes a packet from the store, a packet is +anything that has a `messageId` property. +The callback is called when the packet has been removed. + +------------------------------------------------------- + +### mqtt.Store#close(cb) + +Closes the Store. + + +## Browser + + +### Via CDN + +The MQTT.js bundle is available through http://unpkg.com, specifically +at https://unpkg.com/mqtt/dist/mqtt.min.js. +See http://unpkg.com for the full documentation on version ranges. + + +## WeChat Mini Program +Support [WeChat Mini Program](https://mp.weixin.qq.com/). See [Doc](https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-socket.html). + + +## Example(js) + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('wxs://test.mosquitto.org') +``` + +## Example(ts) + +```ts +import { connect } from 'mqtt'; +const client = connect('wxs://test.mosquitto.org'); +``` + +## Ali Mini Program +Surport [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). See [Doc](https://docs.alipay.com/mini/developer/getting-started). + + +## Example(js) + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('alis://test.mosquitto.org') +``` + +## Example(ts) + +```ts +import { connect } from 'mqtt'; +const client = connect('alis://test.mosquitto.org'); +``` + + +### Browserify + +In order to use MQTT.js as a browserify module you can either require it in your browserify bundles or build it as a stand alone module. The exported module is AMD/CommonJs compatible and it will add an object in the global space. + +```bash +mkdir tmpdir +cd tmpdir +npm install mqtt +npm install browserify +npm install tinyify +cd node_modules/mqtt/ +npm install . +npx browserify mqtt.js -s mqtt >browserMqtt.js // use script tag +# show size for compressed browser transfer +gzip +### Webpack + +Just like browserify, export MQTT.js as library. The exported module would be `var mqtt = xxx` and it will add an object in the global space. You could also export module in other [formats (AMD/CommonJS/others)](http://webpack.github.io/docs/configuration.html#output-librarytarget) by setting **output.libraryTarget** in webpack configuration. + +```javascript +npm install -g webpack // install webpack + +cd node_modules/mqtt +npm install . // install dev dependencies +webpack mqtt.js ./browserMqtt.js --output-library mqtt +``` + +you can then use mqtt.js in the browser with the same api than node's one. + +```html + + + test Ws mqtt.js + + + + + + +``` + +### React +``` +npm install -g webpack // Install webpack globally +npm install mqtt // Install MQTT library +cd node_modules/mqtt +npm install . // Install dev deps at current dir +webpack mqtt.js --output-library mqtt // Build + +// now you can import the library with ES6 import, commonJS not tested +``` + + +```javascript +import React from 'react'; +import mqtt from 'mqtt'; + +export default () => { + const [connectionStatus, setConnectionStatus] = React.useState(false); + const [messages, setMessages] = React.useState([]); + + useEffect(() => { + const client = mqtt.connect(SOME_URL); + client.on('connect', () => setConnectionStatus(true)); + client.on('message', (topic, payload, packet) => { + setMessages(messages.concat(payload.toString())); + }); + }, []); + + return ( + <> + {messages.map((message) => ( +

{message}

+ ) + + ) +} +``` + +Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/moscajs/aedes/blob/master/docs/Examples.md#mqtt-server-over-websocket-using-server-factory) to setup [Aedes](https://github.com/moscajs/aedes)). + + +## About QoS + +Here is how QoS works: + +* QoS 0 : received **at most once** : The packet is sent, and that's it. There is no validation about whether it has been received. +* QoS 1 : received **at least once** : The packet is sent and stored as long as the client has not received a confirmation from the server. MQTT ensures that it *will* be received, but there can be duplicates. +* QoS 2 : received **exactly once** : Same as QoS 1 but there is no duplicates. + +About data consumption, obviously, QoS 2 > QoS 1 > QoS 0, if that's a concern to you. + + +## Usage with TypeScript +This repo bundles TypeScript definition files for use in TypeScript projects and to support tools that can read `.d.ts` files. + +### Pre-requisites +Before you can begin using these TypeScript definitions with your project, you need to make sure your project meets a few of these requirements: + * TypeScript >= 2.1 + * Set tsconfig.json: `{"compilerOptions" : {"moduleResolution" : "node"}, ...}` + * Includes the TypeScript definitions for node. You can use npm to install this by typing the following into a terminal window: + `npm install --save-dev @types/node` + + +## Contributing + +MQTT.js is an **OPEN Open Source Project**. This means that: + +> Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. + +See the [CONTRIBUTING.md](https://github.com/mqttjs/MQTT.js/blob/master/CONTRIBUTING.md) file for more details. + +### Contributors + +MQTT.js is only possible due to the excellent work of the following contributors: + + + + + + +
Adam RuddGitHub/adamvrTwitter/@adam_vr
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
Maxime AgorGitHub/4rzaelTwitter/@4rzael
Siarhei BuntsevichGitHub/scarry1992
+ + +## License + +MIT diff --git a/benchmarks/bombing.js b/benchmarks/bombing.js index a08fd206b..adef01445 100755 --- a/benchmarks/bombing.js +++ b/benchmarks/bombing.js @@ -1,26 +1,26 @@ -#! /usr/bin/env node - -var mqtt = require('../') -var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 }) - -var sent = 0 -var interval = 5000 - -function count () { - console.log('sent/s', sent / interval * 1000) - sent = 0 -} - -setInterval(count, interval) - -function publish () { - sent++ - client.publish('test', 'payload', publish) -} - -client.on('connect', publish) - -client.on('error', function () { - console.log('reconnect!') - client.stream.end() -}) +#! /usr/bin/env node + +var mqtt = require('../') +var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 }) + +var sent = 0 +var interval = 5000 + +function count () { + console.log('sent/s', sent / interval * 1000) + sent = 0 +} + +setInterval(count, interval) + +function publish () { + sent++ + client.publish('test', 'payload', publish) +} + +client.on('connect', publish) + +client.on('error', function () { + console.log('reconnect!') + client.stream.end() +}) diff --git a/benchmarks/throughputCounter.js b/benchmarks/throughputCounter.js index 90c15fc9d..0b778ef2c 100755 --- a/benchmarks/throughputCounter.js +++ b/benchmarks/throughputCounter.js @@ -1,22 +1,22 @@ -#! /usr/bin/env node - -var mqtt = require('../') - -var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 }) -var counter = 0 -var interval = 5000 - -function count () { - console.log('received/s', counter / interval * 1000) - counter = 0 -} - -setInterval(count, interval) - -client.on('connect', function () { - count() - this.subscribe('test') - this.on('message', function () { - counter++ - }) -}) +#! /usr/bin/env node + +var mqtt = require('../') + +var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 }) +var counter = 0 +var interval = 5000 + +function count () { + console.log('received/s', counter / interval * 1000) + counter = 0 +} + +setInterval(count, interval) + +client.on('connect', function () { + count() + this.subscribe('test') + this.on('message', function () { + counter++ + }) +}) diff --git a/bin/mqtt.js b/bin/mqtt.js index 4a277306e..022b33a64 100755 --- a/bin/mqtt.js +++ b/bin/mqtt.js @@ -1,27 +1,27 @@ -#!/usr/bin/env node -'use strict' - -/* - * Copyright (c) 2015-2015 MQTT.js contributors. - * Copyright (c) 2011-2014 Adam Rudd. - * - * See LICENSE for more information - */ -var path = require('path') -var commist = require('commist')() -var helpMe = require('help-me')({ - dir: path.join(path.dirname(require.main.filename), '/../doc'), - ext: '.txt' -}) - -commist.register('publish', require('./pub')) -commist.register('subscribe', require('./sub')) -commist.register('version', function () { - console.log('MQTT.js version:', require('./../package.json').version) -}) -commist.register('help', helpMe.toStdout) - -if (commist.parse(process.argv.slice(2)) !== null) { - console.log('No such command:', process.argv[2], '\n') - helpMe.toStdout() -} +#!/usr/bin/env node +'use strict' + +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ +var path = require('path') +var commist = require('commist')() +var helpMe = require('help-me')({ + dir: path.join(path.dirname(require.main.filename), '/../doc'), + ext: '.txt' +}) + +commist.register('publish', require('./pub')) +commist.register('subscribe', require('./sub')) +commist.register('version', function () { + console.log('MQTT.js version:', require('./../package.json').version) +}) +commist.register('help', helpMe.toStdout) + +if (commist.parse(process.argv.slice(2)) !== null) { + console.log('No such command:', process.argv[2], '\n') + helpMe.toStdout() +} diff --git a/bin/pub.js b/bin/pub.js index aefa4b7b6..94b066b40 100755 --- a/bin/pub.js +++ b/bin/pub.js @@ -1,146 +1,146 @@ -#!/usr/bin/env node - -'use strict' - -var mqtt = require('../') -var pump = require('pump') -var path = require('path') -var fs = require('fs') -var concat = require('concat-stream') -var Writable = require('readable-stream').Writable -var helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') -}) -var minimist = require('minimist') -var split2 = require('split2') - -function send (args) { - var client = mqtt.connect(args) - client.on('connect', function () { - client.publish(args.topic, args.message, args, function (err) { - if (err) { - console.warn(err) - } - client.end() - }) - }) - client.on('error', function (err) { - console.warn(err) - client.end() - }) -} - -function multisend (args) { - var client = mqtt.connect(args) - var sender = new Writable({ - objectMode: true - }) - sender._write = function (line, enc, cb) { - client.publish(args.topic, line.trim(), args, cb) - } - - client.on('connect', function () { - pump(process.stdin, split2(), sender, function (err) { - client.end() - if (err) { - throw err - } - }) - }) -} - -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'message', 'clientId', 'i', 'id'], - boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - message: 'm', - qos: 'q', - clientId: ['i', 'id'], - retain: 'r', - username: 'u', - password: 'P', - stdin: 's', - multiline: 'M', - protocol: ['C', 'l'], - help: 'H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - topic: '', - message: '' - } - }) - - if (args.help) { - return helpMe.toStdout('publish') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - args.topic = (args.topic || args._.shift()).toString() - args.message = (args.message || args._.shift()).toString() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('publish') - } - - if (args.stdin) { - if (args.multiline) { - multisend(args) - } else { - process.stdin.pipe(concat(function (data) { - args.message = data - send(args) - })) - } - } else { - send(args) - } -} - -module.exports = start - -if (require.main === module) { - start(process.argv.slice(2)) -} +#!/usr/bin/env node + +'use strict' + +var mqtt = require('../') +var pump = require('pump') +var path = require('path') +var fs = require('fs') +var concat = require('concat-stream') +var Writable = require('readable-stream').Writable +var helpMe = require('help-me')({ + dir: path.join(__dirname, '..', 'doc') +}) +var minimist = require('minimist') +var split2 = require('split2') + +function send (args) { + var client = mqtt.connect(args) + client.on('connect', function () { + client.publish(args.topic, args.message, args, function (err) { + if (err) { + console.warn(err) + } + client.end() + }) + }) + client.on('error', function (err) { + console.warn(err) + client.end() + }) +} + +function multisend (args) { + var client = mqtt.connect(args) + var sender = new Writable({ + objectMode: true + }) + sender._write = function (line, enc, cb) { + client.publish(args.topic, line.trim(), args, cb) + } + + client.on('connect', function () { + pump(process.stdin, split2(), sender, function (err) { + client.end() + if (err) { + throw err + } + }) + }) +} + +function start (args) { + args = minimist(args, { + string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'message', 'clientId', 'i', 'id'], + boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + message: 'm', + qos: 'q', + clientId: ['i', 'id'], + retain: 'r', + username: 'u', + password: 'P', + stdin: 's', + multiline: 'M', + protocol: ['C', 'l'], + help: 'H', + ca: 'cafile' + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + topic: '', + message: '' + } + }) + + if (args.help) { + return helpMe.toStdout('publish') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + args.topic = (args.topic || args._.shift()).toString() + args.message = (args.message || args._.shift()).toString() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('publish') + } + + if (args.stdin) { + if (args.multiline) { + multisend(args) + } else { + process.stdin.pipe(concat(function (data) { + args.message = data + send(args) + })) + } + } else { + send(args) + } +} + +module.exports = start + +if (require.main === module) { + start(process.argv.slice(2)) +} diff --git a/bin/sub.js b/bin/sub.js index 4c94ceb54..14bc57458 100755 --- a/bin/sub.js +++ b/bin/sub.js @@ -1,123 +1,123 @@ -#!/usr/bin/env node - -var mqtt = require('../') -var path = require('path') -var fs = require('fs') -var helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') -}) -var minimist = require('minimist') - -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'clientId', 'i', 'id'], - boolean: ['stdin', 'help', 'clean', 'insecure'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - qos: 'q', - clean: 'c', - keepalive: 'k', - clientId: ['i', 'id'], - username: 'u', - password: 'P', - protocol: ['C', 'l'], - verbose: 'v', - help: '-H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - clean: true, - keepAlive: 30 // 30 sec - } - }) - - if (args.help) { - return helpMe.toStdout('subscribe') - } - - args.topic = args.topic || args._.shift() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('subscribe') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - args.keepAlive = args['keep-alive'] - - var client = mqtt.connect(args) - - client.on('connect', function () { - client.subscribe(args.topic, { qos: args.qos }, function (err, result) { - if (err) { - console.error(err) - process.exit(1) - } - - result.forEach(function (sub) { - if (sub.qos > 2) { - console.error('subscription negated to', sub.topic, 'with code', sub.qos) - process.exit(1) - } - }) - }) - }) - - client.on('message', function (topic, payload) { - if (args.verbose) { - console.log(topic, payload.toString()) - } else { - console.log(payload.toString()) - } - }) - - client.on('error', function (err) { - console.warn(err) - client.end() - }) -} - -module.exports = start - -if (require.main === module) { - start(process.argv.slice(2)) -} +#!/usr/bin/env node + +var mqtt = require('../') +var path = require('path') +var fs = require('fs') +var helpMe = require('help-me')({ + dir: path.join(__dirname, '..', 'doc') +}) +var minimist = require('minimist') + +function start (args) { + args = minimist(args, { + string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'clientId', 'i', 'id'], + boolean: ['stdin', 'help', 'clean', 'insecure'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + qos: 'q', + clean: 'c', + keepalive: 'k', + clientId: ['i', 'id'], + username: 'u', + password: 'P', + protocol: ['C', 'l'], + verbose: 'v', + help: '-H', + ca: 'cafile' + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + clean: true, + keepAlive: 30 // 30 sec + } + }) + + if (args.help) { + return helpMe.toStdout('subscribe') + } + + args.topic = args.topic || args._.shift() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('subscribe') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + args.keepAlive = args['keep-alive'] + + var client = mqtt.connect(args) + + client.on('connect', function () { + client.subscribe(args.topic, { qos: args.qos }, function (err, result) { + if (err) { + console.error(err) + process.exit(1) + } + + result.forEach(function (sub) { + if (sub.qos > 2) { + console.error('subscription negated to', sub.topic, 'with code', sub.qos) + process.exit(1) + } + }) + }) + }) + + client.on('message', function (topic, payload) { + if (args.verbose) { + console.log(topic, payload.toString()) + } else { + console.log(payload.toString()) + } + }) + + client.on('error', function (err) { + console.warn(err) + client.end() + }) +} + +module.exports = start + +if (require.main === module) { + start(process.argv.slice(2)) +} diff --git a/example.js b/example.js index 91b0bfde6..ba14bf949 100644 --- a/example.js +++ b/example.js @@ -1,11 +1,11 @@ -var mqtt = require('./') -var client = mqtt.connect('mqtt://test.mosquitto.org') - -client.subscribe('presence') -client.publish('presence', 'Hello mqtt') - -client.on('message', function (topic, message) { - console.log(message.toString()) -}) - -client.end() +var mqtt = require('./') +var client = mqtt.connect('mqtt://test.mosquitto.org') + +client.subscribe('presence') +client.publish('presence', 'Hello mqtt') + +client.on('message', function (topic, message) { + console.log(message.toString()) +}) + +client.end() diff --git a/examples/client/secure-client.js b/examples/client/secure-client.js index fefe65d73..bf9b6f092 100644 --- a/examples/client/secure-client.js +++ b/examples/client/secure-client.js @@ -1,24 +1,24 @@ -'use strict' - -var mqtt = require('../..') -var path = require('path') -var fs = require('fs') -var KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-key.pem')) -var CERT = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-cert.pem')) - -var PORT = 8443 - -var options = { - port: PORT, - key: KEY, - cert: CERT, - rejectUnauthorized: false -} - -var client = mqtt.connect(options) - -client.subscribe('messages') -client.publish('messages', 'Current time is: ' + new Date()) -client.on('message', function (topic, message) { - console.log(message) -}) +'use strict' + +var mqtt = require('../..') +var path = require('path') +var fs = require('fs') +var KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-key.pem')) +var CERT = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-cert.pem')) + +var PORT = 8443 + +var options = { + port: PORT, + key: KEY, + cert: CERT, + rejectUnauthorized: false +} + +var client = mqtt.connect(options) + +client.subscribe('messages') +client.publish('messages', 'Current time is: ' + new Date()) +client.on('message', function (topic, message) { + console.log(message) +}) diff --git a/examples/client/simple-both.js b/examples/client/simple-both.js index 58a048465..8e9268b5f 100644 --- a/examples/client/simple-both.js +++ b/examples/client/simple-both.js @@ -1,13 +1,13 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -// or var client = mqtt.connect({ port: 1883, host: '192.168.1.100', keepalive: 10000}); - -client.subscribe('presence') -client.publish('presence', 'bin hier') -client.on('message', function (topic, message) { - console.log(message) -}) -client.end() +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +// or var client = mqtt.connect({ port: 1883, host: '192.168.1.100', keepalive: 10000}); + +client.subscribe('presence') +client.publish('presence', 'bin hier') +client.on('message', function (topic, message) { + console.log(message) +}) +client.end() diff --git a/examples/client/simple-publish.js b/examples/client/simple-publish.js index 4f8274c4a..a8b0f89b6 100644 --- a/examples/client/simple-publish.js +++ b/examples/client/simple-publish.js @@ -1,7 +1,7 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -client.publish('presence', 'hello!') -client.end() +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +client.publish('presence', 'hello!') +client.end() diff --git a/examples/client/simple-subscribe.js b/examples/client/simple-subscribe.js index f2c6d2c4a..7989b9c22 100644 --- a/examples/client/simple-subscribe.js +++ b/examples/client/simple-subscribe.js @@ -1,9 +1,9 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -client.subscribe('presence') -client.on('message', function (topic, message) { - console.log(message) -}) +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +client.subscribe('presence') +client.on('message', function (topic, message) { + console.log(message) +}) diff --git a/examples/tls client/mqttclient.js b/examples/tls client/mqttclient.js index d9bb4693a..392fcb39c 100644 --- a/examples/tls client/mqttclient.js +++ b/examples/tls client/mqttclient.js @@ -1,48 +1,48 @@ -'use strict' - -/** ************************** IMPORTANT NOTE *********************************** - - The certificate used on this example has been generated for a host named stark. - So as host we SHOULD use stark if we want the server to be authorized. - For testing this we should add on the computer running this example a line on - the hosts file: - /etc/hosts [UNIX] - OR - \System32\drivers\etc\hosts [Windows] - - The line to add on the file should be as follows: - stark - *******************************************************************************/ - -var mqtt = require('mqtt') -var fs = require('fs') -var path = require('path') -var KEY = fs.readFileSync(path.join(__dirname, '/tls-key.pem')) -var CERT = fs.readFileSync(path.join(__dirname, '/tls-cert.pem')) -var TRUSTED_CA_LIST = fs.readFileSync(path.join(__dirname, '/crt.ca.cg.pem')) - -var PORT = 1883 -var HOST = 'stark' - -var options = { - port: PORT, - host: HOST, - key: KEY, - cert: CERT, - rejectUnauthorized: true, - // The CA list will be used to determine if server is authorized - ca: TRUSTED_CA_LIST, - protocol: 'mqtts' -} - -var client = mqtt.connect(options) - -client.subscribe('messages') -client.publish('messages', 'Current time is: ' + new Date()) -client.on('message', function (topic, message) { - console.log(message) -}) - -client.on('connect', function () { - console.log('Connected') -}) +'use strict' + +/** ************************** IMPORTANT NOTE *********************************** + + The certificate used on this example has been generated for a host named stark. + So as host we SHOULD use stark if we want the server to be authorized. + For testing this we should add on the computer running this example a line on + the hosts file: + /etc/hosts [UNIX] + OR + \System32\drivers\etc\hosts [Windows] + + The line to add on the file should be as follows: + stark + *******************************************************************************/ + +var mqtt = require('mqtt') +var fs = require('fs') +var path = require('path') +var KEY = fs.readFileSync(path.join(__dirname, '/tls-key.pem')) +var CERT = fs.readFileSync(path.join(__dirname, '/tls-cert.pem')) +var TRUSTED_CA_LIST = fs.readFileSync(path.join(__dirname, '/crt.ca.cg.pem')) + +var PORT = 1883 +var HOST = 'stark' + +var options = { + port: PORT, + host: HOST, + key: KEY, + cert: CERT, + rejectUnauthorized: true, + // The CA list will be used to determine if server is authorized + ca: TRUSTED_CA_LIST, + protocol: 'mqtts' +} + +var client = mqtt.connect(options) + +client.subscribe('messages') +client.publish('messages', 'Current time is: ' + new Date()) +client.on('message', function (topic, message) { + console.log(message) +}) + +client.on('connect', function () { + console.log('Connected') +}) diff --git a/examples/ws/client.js b/examples/ws/client.js index 9349c2971..61524d345 100644 --- a/examples/ws/client.js +++ b/examples/ws/client.js @@ -1,53 +1,53 @@ -'use strict' - -var mqtt = require('../../') - -var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) - -// This sample should be run in tandem with the aedes_server.js file. -// Simply run it: -// $ node aedes_server.js -// -// Then run this file in a separate console: -// $ node websocket_sample.js -// -var host = 'ws://localhost:8080' - -var options = { - keepalive: 30, - clientId: clientId, - protocolId: 'MQTT', - protocolVersion: 4, - clean: true, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - will: { - topic: 'WillMsg', - payload: 'Connection Closed abnormally..!', - qos: 0, - retain: false - }, - rejectUnauthorized: false -} - -console.log('connecting mqtt client') -var client = mqtt.connect(host, options) - -client.on('error', function (err) { - console.log(err) - client.end() -}) - -client.on('connect', function () { - console.log('client connected:' + clientId) - client.subscribe('topic', { qos: 0 }) - client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) -}) - -client.on('message', function (topic, message, packet) { - console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) -}) - -client.on('close', function () { - console.log(clientId + ' disconnected') -}) +'use strict' + +var mqtt = require('../../') + +var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) + +// This sample should be run in tandem with the aedes_server.js file. +// Simply run it: +// $ node aedes_server.js +// +// Then run this file in a separate console: +// $ node websocket_sample.js +// +var host = 'ws://localhost:8080' + +var options = { + keepalive: 30, + clientId: clientId, + protocolId: 'MQTT', + protocolVersion: 4, + clean: true, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + will: { + topic: 'WillMsg', + payload: 'Connection Closed abnormally..!', + qos: 0, + retain: false + }, + rejectUnauthorized: false +} + +console.log('connecting mqtt client') +var client = mqtt.connect(host, options) + +client.on('error', function (err) { + console.log(err) + client.end() +}) + +client.on('connect', function () { + console.log('client connected:' + clientId) + client.subscribe('topic', { qos: 0 }) + client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) +}) + +client.on('message', function (topic, message, packet) { + console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) +}) + +client.on('close', function () { + console.log(clientId + ' disconnected') +}) diff --git a/examples/wss/client_with_proxy.js b/examples/wss/client_with_proxy.js index 657fe3700..4a0d9f3c9 100644 --- a/examples/wss/client_with_proxy.js +++ b/examples/wss/client_with_proxy.js @@ -1,58 +1,58 @@ -'use strict' - -var mqtt = require('mqtt') -var url = require('url') -var HttpsProxyAgent = require('https-proxy-agent') -/* -host: host of the endpoint you want to connect e.g. my.mqqt.host.com -path: path to you endpoint e.g. '/foo/bar/mqtt' -*/ -var endpoint = 'wss://' -/* create proxy agent -proxy: your proxy e.g. proxy.foo.bar.com -port: http proxy port e.g. 8080 -*/ -var proxy = process.env.http_proxy || 'http://:' -var parsed = url.parse(endpoint) -var proxyOpts = url.parse(proxy) -// true for wss -proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true -var agent = new HttpsProxyAgent(proxyOpts) -var wsOptions = { - agent: agent - // other wsOptions - // foo:'bar' -} -var mqttOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - clientId: 'testClient', - wsOptions: wsOptions -} - -var client = mqtt.connect(parsed, mqttOptions) - -client.on('connect', function () { - console.log('connected') -}) - -client.on('error', function (a) { - console.log('error!' + a) -}) - -client.on('offline', function (a) { - console.log('lost connection!' + a) -}) - -client.on('close', function (a) { - console.log('connection closed!' + a) -}) - -client.on('message', function (topic, message) { - console.log(message.toString()) -}) +'use strict' + +var mqtt = require('mqtt') +var url = require('url') +var HttpsProxyAgent = require('https-proxy-agent') +/* +host: host of the endpoint you want to connect e.g. my.mqqt.host.com +path: path to you endpoint e.g. '/foo/bar/mqtt' +*/ +var endpoint = 'wss://' +/* create proxy agent +proxy: your proxy e.g. proxy.foo.bar.com +port: http proxy port e.g. 8080 +*/ +var proxy = process.env.http_proxy || 'http://:' +var parsed = url.parse(endpoint) +var proxyOpts = url.parse(proxy) +// true for wss +proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true +var agent = new HttpsProxyAgent(proxyOpts) +var wsOptions = { + agent: agent + // other wsOptions + // foo:'bar' +} +var mqttOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + clientId: 'testClient', + wsOptions: wsOptions +} + +var client = mqtt.connect(parsed, mqttOptions) + +client.on('connect', function () { + console.log('connected') +}) + +client.on('error', function (a) { + console.log('error!' + a) +}) + +client.on('offline', function (a) { + console.log('lost connection!' + a) +}) + +client.on('close', function (a) { + console.log('connection closed!' + a) +}) + +client.on('message', function (topic, message) { + console.log(message.toString()) +}) diff --git a/lib/client.js b/lib/client.js index 6eaeb35ac..540a11780 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,1838 +1,1838 @@ -'use strict' - -/** - * Module dependencies - */ -var EventEmitter = require('events').EventEmitter -var Store = require('./store') -var TopicAliasRecv = require('./topic-alias-recv') -var TopicAliasSend = require('./topic-alias-send') -var mqttPacket = require('mqtt-packet') -var DefaultMessageIdProvider = require('./default-message-id-provider') -var Writable = require('readable-stream').Writable -var inherits = require('inherits') -var reInterval = require('reinterval') -var clone = require('rfdc/default') -var validations = require('./validations') -var xtend = require('xtend') -var debug = require('debug')('mqttjs:client') -var nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } -var setImmediate = global.setImmediate || function (callback) { - // works in node v0.8 - nextTick(callback) -} -var defaultConnectOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - resubscribe: true -} - -var socketErrors = [ - 'ECONNREFUSED', - 'EADDRINUSE', - 'ECONNRESET', - 'ENOTFOUND' -] - -// Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND. - -var errors = { - 0: '', - 1: 'Unacceptable protocol version', - 2: 'Identifier rejected', - 3: 'Server unavailable', - 4: 'Bad username or password', - 5: 'Not authorized', - 16: 'No matching subscribers', - 17: 'No subscription existed', - 128: 'Unspecified error', - 129: 'Malformed Packet', - 130: 'Protocol Error', - 131: 'Implementation specific error', - 132: 'Unsupported Protocol Version', - 133: 'Client Identifier not valid', - 134: 'Bad User Name or Password', - 135: 'Not authorized', - 136: 'Server unavailable', - 137: 'Server busy', - 138: 'Banned', - 139: 'Server shutting down', - 140: 'Bad authentication method', - 141: 'Keep Alive timeout', - 142: 'Session taken over', - 143: 'Topic Filter invalid', - 144: 'Topic Name invalid', - 145: 'Packet identifier in use', - 146: 'Packet Identifier not found', - 147: 'Receive Maximum exceeded', - 148: 'Topic Alias invalid', - 149: 'Packet too large', - 150: 'Message rate too high', - 151: 'Quota exceeded', - 152: 'Administrative action', - 153: 'Payload format invalid', - 154: 'Retain not supported', - 155: 'QoS not supported', - 156: 'Use another server', - 157: 'Server moved', - 158: 'Shared Subscriptions not supported', - 159: 'Connection rate exceeded', - 160: 'Maximum connect time', - 161: 'Subscription Identifiers not supported', - 162: 'Wildcard Subscriptions not supported' -} - -function defaultId () { - return 'mqttjs_' + Math.random().toString(16).substr(2, 8) -} - -function applyTopicAlias (client, packet) { - if (client.options.protocolVersion === 5) { - if (packet.cmd === 'publish') { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - var topic = packet.topic.toString() - if (client.topicAliasSend) { - if (alias) { - if (topic.length !== 0) { - // register topic alias - debug('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) - if (!client.topicAliasSend.put(topic, alias)) { - debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } else { - if (topic.length !== 0) { - if (client.options.autoAssignTopicAlias) { - alias = client.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto assign(use) topic: %s - alias: %d', topic, alias) - } else { - alias = client.topicAliasSend.getLruAlias() - client.topicAliasSend.put(topic, alias) - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto assign topic: %s - alias: %d', topic, alias) - } - } else if (client.options.autoUseTopicAlias) { - alias = client.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) - } - } - } - } - } else if (alias) { - debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } -} - -function removeTopicAliasAndRecoverTopicName (client, packet) { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - - var topic = packet.topic.toString() - if (topic.length === 0) { - // restore topic from alias - if (typeof alias === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - topic = client.topicAliasSend.getTopicByAlias(alias) - if (typeof topic === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - packet.topic = topic - } - } - } - if (alias) { - delete packet.properties.topicAlias - } -} - -function sendPacket (client, packet, cb) { - debug('sendPacket :: packet: %O', packet) - debug('sendPacket :: emitting `packetsend`') - - client.emit('packetsend', packet) - - debug('sendPacket :: writing to stream') - var result = mqttPacket.writeToStream(packet, client.stream, client.options) - debug('sendPacket :: writeToStream result %s', result) - if (!result && cb) { - debug('sendPacket :: handle events on `drain` once through callback.') - client.stream.once('drain', cb) - } else if (cb) { - debug('sendPacket :: invoking cb') - cb() - } -} - -function flush (queue) { - if (queue) { - debug('flush: queue exists? %b', !!(queue)) - Object.keys(queue).forEach(function (messageId) { - if (typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - delete queue[messageId] - } - }) - } -} - -function flushVolatile (queue) { - if (queue) { - debug('flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') - Object.keys(queue).forEach(function (messageId) { - if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - delete queue[messageId] - } - }) - } -} - -function storeAndSend (client, packet, cb, cbStorePut) { - debug('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) - var storePacket = packet - var err - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - err = removeTopicAliasAndRecoverTopicName(client, storePacket) - if (err) { - return cb && cb(err) - } - } - client.outgoingStore.put(storePacket, function storedPacket (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - sendPacket(client, packet, cb) - }) -} - -function nop (error) { - debug('nop ::', error) -} - -/** - * MqttClient constructor - * - * @param {Stream} stream - stream - * @param {Object} [options] - connection options - * (see Connection#connect) - */ -function MqttClient (streamBuilder, options) { - var k - var that = this - - if (!(this instanceof MqttClient)) { - return new MqttClient(streamBuilder, options) - } - - this.options = options || {} - - // Defaults - for (k in defaultConnectOptions) { - if (typeof this.options[k] === 'undefined') { - this.options[k] = defaultConnectOptions[k] - } else { - this.options[k] = options[k] - } - } - - debug('MqttClient :: options.protocol', options.protocol) - debug('MqttClient :: options.protocolVersion', options.protocolVersion) - debug('MqttClient :: options.username', options.username) - debug('MqttClient :: options.keepalive', options.keepalive) - debug('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) - debug('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) - debug('MqttClient :: options.topicAliasMaximum', options.topicAliasMaximum) - - this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() - - debug('MqttClient :: clientId', this.options.clientId) - - this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } - - this.streamBuilder = streamBuilder - - this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider - - // Inflight message storages - this.outgoingStore = options.outgoingStore || new Store() - this.incomingStore = options.incomingStore || new Store() - - // Should QoS zero messages be queued when the connection is broken? - this.queueQoSZero = options.queueQoSZero === undefined ? true : options.queueQoSZero - - // map of subscribed topics to support reconnection - this._resubscribeTopics = {} - - // map of a subscribe messageId and a topic - this.messageIdToTopic = {} - - // Ping timer, setup in _setupPingTimer - this.pingTimer = null - // Is the client connected? - this.connected = false - // Are we disconnecting? - this.disconnecting = false - // Packet queue - this.queue = [] - // connack timer - this.connackTimer = null - // Reconnect timer - this.reconnectTimer = null - // Is processing store? - this._storeProcessing = false - // Packet Ids are put into the store during store processing - this._packetIdsDuringStoreProcessing = {} - // Store processing queue - this._storeProcessingQueue = [] - - // Inflight callbacks - this.outgoing = {} - - // True if connection is first time. - this._firstConnection = true - - if (options.topicAliasMaximum > 0) { - if (options.topicAliasMaximum > 0xffff) { - debug('MqttClient :: options.topicAliasMaximum is out of range') - } else { - this.topicAliasRecv = new TopicAliasRecv(options.topicAliasMaximum) - } - } - - // Send queued packets - this.on('connect', function () { - var queue = this.queue - - function deliver () { - var entry = queue.shift() - debug('deliver :: entry %o', entry) - var packet = null - - if (!entry) { - that._resubscribe() - return - } - - packet = entry.packet - debug('deliver :: call _sendPacket for %o', packet) - var send = true - if (packet.messageId && packet.messageId !== 0) { - if (!that.messageIdProvider.register(packet.messageId)) { - send = false - } - } - if (send) { - that._sendPacket( - packet, - function (err) { - if (entry.cb) { - entry.cb(err) - } - deliver() - } - ) - } else { - debug('messageId: %d has already used. The message is skipped and removed.', packet.messageId) - deliver() - } - } - - debug('connect :: sending queued packets') - deliver() - }) - - this.on('close', function () { - debug('close :: connected set to `false`') - this.connected = false - - debug('close :: clearing connackTimer') - clearTimeout(this.connackTimer) - - debug('close :: clearing ping timer') - if (that.pingTimer !== null) { - that.pingTimer.clear() - that.pingTimer = null - } - - if (this.topicAliasRecv) { - this.topicAliasRecv.clear() - } - - debug('close :: calling _setupReconnect') - this._setupReconnect() - }) - EventEmitter.call(this) - - debug('MqttClient :: setting up stream') - this._setupStream() -} -inherits(MqttClient, EventEmitter) - -/** - * setup the event handlers in the inner stream. - * - * @api private - */ -MqttClient.prototype._setupStream = function () { - var connectPacket - var that = this - var writable = new Writable() - var parser = mqttPacket.parser(this.options) - var completeParse = null - var packets = [] - - debug('_setupStream :: calling method to clear reconnect') - this._clearReconnect() - - debug('_setupStream :: using streamBuilder provided to client to create stream') - this.stream = this.streamBuilder(this) - - parser.on('packet', function (packet) { - debug('parser :: on packet push to packets array.') - packets.push(packet) - }) - - function nextTickWork () { - if (packets.length) { - nextTick(work) - } else { - var done = completeParse - completeParse = null - done() - } - } - - function work () { - debug('work :: getting next packet in queue') - var packet = packets.shift() - - if (packet) { - debug('work :: packet pulled from queue') - that._handlePacket(packet, nextTickWork) - } else { - debug('work :: no packets in queue') - var done = completeParse - completeParse = null - debug('work :: done flag is %s', !!(done)) - if (done) done() - } - } - - writable._write = function (buf, enc, done) { - completeParse = done - debug('writable stream :: parsing buffer') - parser.parse(buf) - work() - } - - function streamErrorHandler (error) { - debug('streamErrorHandler :: error', error.message) - if (socketErrors.includes(error.code)) { - // handle error - debug('streamErrorHandler :: emitting error') - that.emit('error', error) - } else { - nop(error) - } - } - - debug('_setupStream :: pipe stream to writable stream') - this.stream.pipe(writable) - - // Suppress connection errors - this.stream.on('error', streamErrorHandler) - - // Echo stream close - this.stream.on('close', function () { - debug('(%s)stream :: on close', that.options.clientId) - flushVolatile(that.outgoing) - debug('stream: emit close to MqttClient') - that.emit('close') - }) - - // Send a connect packet - debug('_setupStream: sending packet `connect`') - connectPacket = Object.create(this.options) - connectPacket.cmd = 'connect' - if (this.topicAliasRecv) { - if (!connectPacket.properties) { - connectPacket.properties = {} - } - if (this.topicAliasRecv) { - connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max - } - } - // avoid message queue - sendPacket(this, connectPacket) - - // Echo connection errors - parser.on('error', this.emit.bind(this, 'error')) - - // auth - if (this.options.properties) { - if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { - that.end(() => - this.emit('error', new Error('Packet has no Authentication Method') - )) - return this - } - if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { - var authPacket = xtend({cmd: 'auth', reasonCode: 0}, this.options.authPacket) - sendPacket(this, authPacket) - } - } - - // many drain listeners are needed for qos 1 callbacks if the connection is intermittent - this.stream.setMaxListeners(1000) - - clearTimeout(this.connackTimer) - this.connackTimer = setTimeout(function () { - debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') - that._cleanUp(true) - }, this.options.connectTimeout) -} - -MqttClient.prototype._handlePacket = function (packet, done) { - var options = this.options - - if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { - this.emit('error', new Error('exceeding packets size ' + packet.cmd)) - this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) - return this - } - debug('_handlePacket :: emitting packetreceive') - this.emit('packetreceive', packet) - - switch (packet.cmd) { - case 'publish': - this._handlePublish(packet, done) - break - case 'puback': - case 'pubrec': - case 'pubcomp': - case 'suback': - case 'unsuback': - this._handleAck(packet) - done() - break - case 'pubrel': - this._handlePubrel(packet, done) - break - case 'connack': - this._handleConnack(packet) - done() - break - case 'pingresp': - this._handlePingresp(packet) - done() - break - case 'disconnect': - this._handleDisconnect(packet) - done() - break - default: - // do nothing - // maybe we should do an error handling - // or just log it - break - } -} - -MqttClient.prototype._checkDisconnecting = function (callback) { - if (this.disconnecting) { - if (callback) { - callback(new Error('client disconnecting')) - } else { - this.emit('error', new Error('client disconnecting')) - } - } - return this.disconnecting -} - -/** - * publish - publish to - * - * @param {String} topic - topic to publish to - * @param {String, Buffer} message - message to publish - * @param {Object} [opts] - publish options, includes: - * {Number} qos - qos level to publish on - * {Boolean} retain - whether or not to retain the message - * {Boolean} dup - whether or not mark a message as duplicate - * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` - * @param {Function} [callback] - function(err){} - * called when publish succeeds or fails - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.publish('topic', 'message'); - * @example - * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); - * @example client.publish('topic', 'message', console.log); - */ -MqttClient.prototype.publish = function (topic, message, opts, callback) { - debug('publish :: message `%s` to topic `%s`', message, topic) - var packet - var options = this.options - - // .publish(topic, payload, cb); - if (typeof opts === 'function') { - callback = opts - opts = null - } - - // default opts - var defaultOpts = {qos: 0, retain: false, dup: false} - opts = xtend(defaultOpts, opts) - - if (this._checkDisconnecting(callback)) { - return this - } - - var that = this - var publishProc = function () { - var messageId = 0 - if (opts.qos === 1 || opts.qos === 2) { - messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - } - packet = { - cmd: 'publish', - topic: topic, - payload: message, - qos: opts.qos, - retain: opts.retain, - messageId: messageId, - dup: opts.dup - } - - if (options.protocolVersion === 5) { - packet.properties = opts.properties - } - - debug('publish :: qos', opts.qos) - switch (opts.qos) { - case 1: - case 2: - // Add to callbacks - that.outgoing[packet.messageId] = { - volatile: false, - cb: callback || nop - } - debug('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, undefined, opts.cbStorePut) - break - default: - debug('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, callback, opts.cbStorePut) - break - } - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': publishProc, - 'cbStorePut': opts.cbStorePut, - 'callback': callback - } - ) - } else { - publishProc() - } - return this -} - -/** - * subscribe - subscribe to - * - * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} - * @param {Object} [opts] - optional subscription options, includes: - * {Number} qos - subscribe qos level - * @param {Function} [callback] - function(err, granted){} where: - * {Error} err - subscription error (none at the moment!) - * {Array} granted - array of {topic: 't', qos: 0} - * @returns {MqttClient} this - for chaining - * @api public - * @example client.subscribe('topic'); - * @example client.subscribe('topic', {qos: 1}); - * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); - * @example client.subscribe('topic', console.log); - */ -MqttClient.prototype.subscribe = function () { - var that = this - var args = new Array(arguments.length) - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - var subs = [] - var obj = args.shift() - var resubscribe = obj.resubscribe - var callback = args.pop() || nop - var opts = args.pop() - var version = this.options.protocolVersion - - delete obj.resubscribe - - if (typeof obj === 'string') { - obj = [obj] - } - - if (typeof callback !== 'function') { - opts = callback - callback = nop - } - - var invalidTopic = validations.validateTopics(obj) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (this._checkDisconnecting(callback)) { - debug('subscribe: discconecting true') - return this - } - - var defaultOpts = { - qos: 0 - } - if (version === 5) { - defaultOpts.nl = false - defaultOpts.rap = false - defaultOpts.rh = 0 - } - opts = xtend(defaultOpts, opts) - - if (Array.isArray(obj)) { - obj.forEach(function (topic) { - debug('subscribe: array topic %s', topic) - if (!that._resubscribeTopics.hasOwnProperty(topic) || - that._resubscribeTopics[topic].qos < opts.qos || - resubscribe) { - var currentOpts = { - topic: topic, - qos: opts.qos - } - if (version === 5) { - currentOpts.nl = opts.nl - currentOpts.rap = opts.rap - currentOpts.rh = opts.rh - currentOpts.properties = opts.properties - } - debug('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) - subs.push(currentOpts) - } - }) - } else { - Object - .keys(obj) - .forEach(function (k) { - debug('subscribe: object topic %s', k) - if (!that._resubscribeTopics.hasOwnProperty(k) || - that._resubscribeTopics[k].qos < obj[k].qos || - resubscribe) { - var currentOpts = { - topic: k, - qos: obj[k].qos - } - if (version === 5) { - currentOpts.nl = obj[k].nl - currentOpts.rap = obj[k].rap - currentOpts.rh = obj[k].rh - currentOpts.properties = opts.properties - } - debug('subscribe: pushing `%s` to subs list', currentOpts) - subs.push(currentOpts) - } - }) - } - - if (!subs.length) { - callback(null, []) - return this - } - - var subscribeProc = function () { - var messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - - var packet = { - cmd: 'subscribe', - subscriptions: subs, - qos: 1, - retain: false, - dup: false, - messageId: messageId - } - - if (opts.properties) { - packet.properties = opts.properties - } - - // subscriptions to resubscribe to in case of disconnect - if (that.options.resubscribe) { - debug('subscribe :: resubscribe true') - var topics = [] - subs.forEach(function (sub) { - if (that.options.reconnectPeriod > 0) { - var topic = { qos: sub.qos } - if (version === 5) { - topic.nl = sub.nl || false - topic.rap = sub.rap || false - topic.rh = sub.rh || 0 - topic.properties = sub.properties - } - that._resubscribeTopics[sub.topic] = topic - topics.push(sub.topic) - } - }) - that.messageIdToTopic[packet.messageId] = topics - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: function (err, packet) { - if (!err) { - var granted = packet.granted - for (var i = 0; i < granted.length; i += 1) { - subs[i].qos = granted[i] - } - } - - callback(err, subs) - } - } - debug('subscribe :: call _sendPacket') - that._sendPacket(packet) - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': subscribeProc, - 'callback': callback - } - ) - } else { - subscribeProc() - } - - return this -} - -/** - * unsubscribe - unsubscribe from topic(s) - * - * @param {String, Array} topic - topics to unsubscribe from - * @param {Object} [opts] - optional subscription options, includes: - * {Object} properties - properties of unsubscribe packet - * @param {Function} [callback] - callback fired on unsuback - * @returns {MqttClient} this - for chaining - * @api public - * @example client.unsubscribe('topic'); - * @example client.unsubscribe('topic', console.log); - */ -MqttClient.prototype.unsubscribe = function () { - var that = this - var args = new Array(arguments.length) - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - var topic = args.shift() - var callback = args.pop() || nop - var opts = args.pop() - if (typeof topic === 'string') { - topic = [topic] - } - - if (typeof callback !== 'function') { - opts = callback - callback = nop - } - - var invalidTopic = validations.validateTopics(topic) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (that._checkDisconnecting(callback)) { - return this - } - - var unsubscribeProc = function () { - var messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - var packet = { - cmd: 'unsubscribe', - qos: 1, - messageId: messageId - } - - if (typeof topic === 'string') { - packet.unsubscriptions = [topic] - } else if (Array.isArray(topic)) { - packet.unsubscriptions = topic - } - - if (that.options.resubscribe) { - packet.unsubscriptions.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } - - if (typeof opts === 'object' && opts.properties) { - packet.properties = opts.properties - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: callback - } - - debug('unsubscribe: call _sendPacket') - that._sendPacket(packet) - - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': unsubscribeProc, - 'callback': callback - } - ) - } else { - unsubscribeProc() - } - - return this -} - -/** - * end - close connection - * - * @returns {MqttClient} this - for chaining - * @param {Boolean} force - do not wait for all in-flight messages to be acked - * @param {Object} opts - added to the disconnect packet - * @param {Function} cb - called when the client has been closed - * - * @api public - */ -MqttClient.prototype.end = function (force, opts, cb) { - var that = this - - debug('end :: (%s)', this.options.clientId) - - if (force == null || typeof force !== 'boolean') { - cb = opts || nop - opts = force - force = false - if (typeof opts !== 'object') { - cb = opts - opts = null - if (typeof cb !== 'function') { - cb = nop - } - } - } - - if (typeof opts !== 'object') { - cb = opts - opts = null - } - - debug('end :: cb? %s', !!cb) - cb = cb || nop - - function closeStores () { - debug('end :: closeStores: closing incoming and outgoing stores') - that.disconnected = true - that.incomingStore.close(function (e1) { - that.outgoingStore.close(function (e2) { - debug('end :: closeStores: emitting end') - that.emit('end') - if (cb) { - let err = e1 || e2 - debug('end :: closeStores: invoking callback with args') - cb(err) - } - }) - }) - if (that._deferredReconnect) { - that._deferredReconnect() - } - } - - function finish () { - // defer closesStores of an I/O cycle, - // just to make sure things are - // ok for websockets - debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) - that._cleanUp(force, () => { - debug('end :: finish :: calling process.nextTick on closeStores') - // var boundProcess = nextTick.bind(null, closeStores) - nextTick(closeStores.bind(that)) - }, opts) - } - - if (this.disconnecting) { - cb() - return this - } - - this._clearReconnect() - - this.disconnecting = true - - if (!force && Object.keys(this.outgoing).length > 0) { - // wait 10ms, just to be sure we received all of it - debug('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) - this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) - } else { - debug('end :: (%s) :: immediately calling finish', that.options.clientId) - finish() - } - - return this -} - -/** - * removeOutgoingMessage - remove a message in outgoing store - * the outgoing callback will be called withe Error('Message removed') if the message is removed - * - * @param {Number} messageId - messageId to remove message - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.removeOutgoingMessage(client.getLastAllocated()); - */ -MqttClient.prototype.removeOutgoingMessage = function (messageId) { - var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null - delete this.outgoing[messageId] - this.outgoingStore.del({messageId: messageId}, function () { - cb(new Error('Message removed')) - }) - return this -} - -/** - * reconnect - connect again using the same options as connect() - * - * @param {Object} [opts] - optional reconnect options, includes: - * {Store} incomingStore - a store for the incoming packets - * {Store} outgoingStore - a store for the outgoing packets - * if opts is not given, current stores are used - * @returns {MqttClient} this - for chaining - * - * @api public - */ -MqttClient.prototype.reconnect = function (opts) { - debug('client reconnect') - var that = this - var f = function () { - if (opts) { - that.options.incomingStore = opts.incomingStore - that.options.outgoingStore = opts.outgoingStore - } else { - that.options.incomingStore = null - that.options.outgoingStore = null - } - that.incomingStore = that.options.incomingStore || new Store() - that.outgoingStore = that.options.outgoingStore || new Store() - that.disconnecting = false - that.disconnected = false - that._deferredReconnect = null - that._reconnect() - } - - if (this.disconnecting && !this.disconnected) { - this._deferredReconnect = f - } else { - f() - } - return this -} - -/** - * _reconnect - implement reconnection - * @api privateish - */ -MqttClient.prototype._reconnect = function () { - debug('_reconnect: emitting reconnect to client') - this.emit('reconnect') - if (this.connected) { - this.end(() => { this._setupStream() }) - debug('client already connected. disconnecting first.') - } else { - debug('_reconnect: calling _setupStream') - this._setupStream() - } -} - -/** - * _setupReconnect - setup reconnect timer - */ -MqttClient.prototype._setupReconnect = function () { - var that = this - - if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { - if (!this.reconnecting) { - debug('_setupReconnect :: emit `offline` state') - this.emit('offline') - debug('_setupReconnect :: set `reconnecting` to `true`') - this.reconnecting = true - } - debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) - that.reconnectTimer = setInterval(function () { - debug('reconnectTimer :: reconnect triggered!') - that._reconnect() - }, that.options.reconnectPeriod) - } else { - debug('_setupReconnect :: doing nothing...') - } -} - -/** - * _clearReconnect - clear the reconnect timer - */ -MqttClient.prototype._clearReconnect = function () { - debug('_clearReconnect : clearing reconnect timer') - if (this.reconnectTimer) { - clearInterval(this.reconnectTimer) - this.reconnectTimer = null - } -} - -/** - * _cleanUp - clean up on connection end - * @api private - */ -MqttClient.prototype._cleanUp = function (forced, done) { - var opts = arguments[2] - if (done) { - debug('_cleanUp :: done callback provided for on stream close') - this.stream.on('close', done) - } - - debug('_cleanUp :: forced? %s', forced) - if (forced) { - if ((this.options.reconnectPeriod === 0) && this.options.clean) { - flush(this.outgoing) - } - debug('_cleanUp :: (%s) :: destroying stream', this.options.clientId) - this.stream.destroy() - } else { - var packet = xtend({ cmd: 'disconnect' }, opts) - debug('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) - this._sendPacket( - packet, - setImmediate.bind( - null, - this.stream.end.bind(this.stream) - ) - ) - } - - if (!this.disconnecting) { - debug('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') - this._clearReconnect() - this._setupReconnect() - } - - if (this.pingTimer !== null) { - debug('_cleanUp :: clearing pingTimer') - this.pingTimer.clear() - this.pingTimer = null - } - - if (done && !this.connected) { - debug('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) - this.stream.removeListener('close', done) - done() - } -} - -/** - * _sendPacket - send or queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @api private - */ -MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { - debug('_sendPacket :: (%s) :: start', this.options.clientId) - cbStorePut = cbStorePut || nop - cb = cb || nop - - var err = applyTopicAlias(this, packet) - if (err) { - cb(err) - return - } - - if (!this.connected) { - debug('_sendPacket :: client not connected. Storing packet offline.') - this._storePacket(packet, cb, cbStorePut) - return - } - - // When sending a packet, reschedule the ping timer - this._shiftPingInterval() - - switch (packet.cmd) { - case 'publish': - break - case 'pubrel': - storeAndSend(this, packet, cb, cbStorePut) - return - default: - sendPacket(this, packet, cb) - return - } - - switch (packet.qos) { - case 2: - case 1: - storeAndSend(this, packet, cb, cbStorePut) - break - /** - * no need of case here since it will be caught by default - * and jshint comply that before default it must be a break - * anyway it will result in -1 evaluation - */ - case 0: - /* falls through */ - default: - sendPacket(this, packet, cb) - break - } - debug('_sendPacket :: (%s) :: end', this.options.clientId) -} - -/** - * _storePacket - queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @api private - */ -MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { - debug('_storePacket :: packet: %o', packet) - debug('_storePacket :: cb? %s', !!cb) - cbStorePut = cbStorePut || nop - - var storePacket = packet - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - var err = removeTopicAliasAndRecoverTopicName(this, storePacket) - if (err) { - return cb && cb(err) - } - } - // check that the packet is not a qos of 0, or that the command is not a publish - if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { - this.queue.push({ packet: storePacket, cb: cb }) - } else if (storePacket.qos > 0) { - cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null - this.outgoingStore.put(storePacket, function (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - }) - } else if (cb) { - cb(new Error('No connection to broker')) - } -} - -/** - * _setupPingTimer - setup the ping timer - * - * @api private - */ -MqttClient.prototype._setupPingTimer = function () { - debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) - var that = this - - if (!this.pingTimer && this.options.keepalive) { - this.pingResp = true - this.pingTimer = reInterval(function () { - that._checkPing() - }, this.options.keepalive * 1000) - } -} - -/** - * _shiftPingInterval - reschedule the ping interval - * - * @api private - */ -MqttClient.prototype._shiftPingInterval = function () { - if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { - this.pingTimer.reschedule(this.options.keepalive * 1000) - } -} -/** - * _checkPing - check if a pingresp has come back, and ping the server again - * - * @api private - */ -MqttClient.prototype._checkPing = function () { - debug('_checkPing :: checking ping...') - if (this.pingResp) { - debug('_checkPing :: ping response received. Clearing flag and sending `pingreq`') - this.pingResp = false - this._sendPacket({ cmd: 'pingreq' }) - } else { - // do a forced cleanup since socket will be in bad shape - debug('_checkPing :: calling _cleanUp with force true') - this._cleanUp(true) - } -} - -/** - * _handlePingresp - handle a pingresp - * - * @api private - */ -MqttClient.prototype._handlePingresp = function () { - this.pingResp = true -} - -/** - * _handleConnack - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handleConnack = function (packet) { - debug('_handleConnack') - var options = this.options - var version = options.protocolVersion - var rc = version === 5 ? packet.reasonCode : packet.returnCode - - clearTimeout(this.connackTimer) - delete this.topicAliasSend - - if (packet.properties) { - if (packet.properties.topicAliasMaximum) { - if (packet.properties.topicAliasMaximum > 0xffff) { - this.emit('error', new Error('topicAliasMaximum from broker is out of range')) - return - } - if (packet.properties.topicAliasMaximum > 0) { - this.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) - } - } - if (packet.properties.serverKeepAlive && options.keepalive) { - options.keepalive = packet.properties.serverKeepAlive - this._shiftPingInterval() - } - if (packet.properties.maximumPacketSize) { - if (!options.properties) { options.properties = {} } - options.properties.maximumPacketSize = packet.properties.maximumPacketSize - } - } - - if (rc === 0) { - this.reconnecting = false - this._onConnect(packet) - } else if (rc > 0) { - var err = new Error('Connection refused: ' + errors[rc]) - err.code = rc - this.emit('error', err) - } -} - -/** - * _handlePublish - * - * @param {Object} packet - * @api private - */ -/* -those late 2 case should be rewrite to comply with coding style: - -case 1: -case 0: - // do not wait sending a puback - // no callback passed - if (1 === qos) { - this._sendPacket({ - cmd: 'puback', - messageId: messageId - }); - } - // emit the message event for both qos 1 and 0 - this.emit('message', topic, message, packet); - this.handleMessage(packet, done); - break; -default: - // do nothing but every switch mus have a default - // log or throw an error about unknown qos - break; - -for now i just suppressed the warnings -*/ -MqttClient.prototype._handlePublish = function (packet, done) { - debug('_handlePublish: packet %o', packet) - done = typeof done !== 'undefined' ? done : nop - var topic = packet.topic.toString() - var message = packet.payload - var qos = packet.qos - var messageId = packet.messageId - var that = this - var options = this.options - var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] - if (this.options.protocolVersion === 5) { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - if (typeof alias !== 'undefined') { - if (topic.length === 0) { - if (alias > 0 && alias <= 0xffff) { - var gotTopic = this.topicAliasRecv.getTopicByAlias(alias) - if (gotTopic) { - topic = gotTopic - debug('_handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) - } else { - debug('_handlePublish :: unregistered topic alias. alias: %d', alias) - this.emit('error', new Error('Received unregistered Topic Alias')) - return - } - } else { - debug('_handlePublish :: topic alias out of range. alias: %d', alias) - this.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } else { - if (this.topicAliasRecv.put(topic, alias)) { - debug('_handlePublish :: registered topic: %s - alias: %d', topic, alias) - } else { - debug('_handlePublish :: topic alias out of range. alias: %d', alias) - this.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } - } - } - debug('_handlePublish: qos %d', qos) - switch (qos) { - case 2: { - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return that.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for pubrec')) } - if (code) { - that._sendPacket({cmd: 'pubrec', messageId: messageId, reasonCode: code}, done) - } else { - that.incomingStore.put(packet, function () { - that._sendPacket({cmd: 'pubrec', messageId: messageId}, done) - }) - } - }) - break - } - case 1: { - // emit the message event - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return that.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for puback')) } - if (!code) { that.emit('message', topic, message, packet) } - that.handleMessage(packet, function (err) { - if (err) { - return done && done(err) - } - that._sendPacket({cmd: 'puback', messageId: messageId, reasonCode: code}, done) - }) - }) - break - } - case 0: - // emit the message event - this.emit('message', topic, message, packet) - this.handleMessage(packet, done) - break - default: - // do nothing - debug('_handlePublish: unknown QoS. Doing nothing.') - // log or throw an error about unknown qos - break - } -} - -/** - * Handle messages with backpressure support, one at a time. - * Override at will. - * - * @param Packet packet the packet - * @param Function callback call when finished - * @api public - */ -MqttClient.prototype.handleMessage = function (packet, callback) { - callback() -} - -/** - * _handleAck - * - * @param {Object} packet - * @api private - */ - -MqttClient.prototype._handleAck = function (packet) { - /* eslint no-fallthrough: "off" */ - var messageId = packet.messageId - var type = packet.cmd - var response = null - var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null - var that = this - var err - - if (!cb) { - debug('_handleAck :: Server sent an ack in error. Ignoring.') - // Server sent an ack in error, ignore it. - return - } - - // Process - debug('_handleAck :: packet type', type) - switch (type) { - case 'pubcomp': - // same thing as puback for QoS 2 - case 'puback': - var pubackRC = packet.reasonCode - // Callback - we're done - if (pubackRC && pubackRC > 0 && pubackRC !== 16) { - err = new Error('Publish error: ' + errors[pubackRC]) - err.code = pubackRC - cb(err, packet) - } - delete this.outgoing[messageId] - this.outgoingStore.del(packet, cb) - this.messageIdProvider.deallocate(messageId) - this._invokeStoreProcessingQueue() - break - case 'pubrec': - response = { - cmd: 'pubrel', - qos: 2, - messageId: messageId - } - var pubrecRC = packet.reasonCode - - if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { - err = new Error('Publish error: ' + errors[pubrecRC]) - err.code = pubrecRC - cb(err, packet) - } else { - this._sendPacket(response) - } - break - case 'suback': - delete this.outgoing[messageId] - this.messageIdProvider.deallocate(messageId) - for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { - if ((packet.granted[grantedI] & 0x80) !== 0) { - // suback with Failure status - var topics = this.messageIdToTopic[messageId] - if (topics) { - topics.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } - } - } - this._invokeStoreProcessingQueue() - cb(null, packet) - break - case 'unsuback': - delete this.outgoing[messageId] - this.messageIdProvider.deallocate(messageId) - this._invokeStoreProcessingQueue() - cb(null) - break - default: - that.emit('error', new Error('unrecognized packet type')) - } - - if (this.disconnecting && - Object.keys(this.outgoing).length === 0) { - this.emit('outgoingEmpty') - } -} - -/** - * _handlePubrel - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handlePubrel = function (packet, callback) { - debug('handling pubrel packet') - callback = typeof callback !== 'undefined' ? callback : nop - var messageId = packet.messageId - var that = this - - var comp = {cmd: 'pubcomp', messageId: messageId} - - that.incomingStore.get(packet, function (err, pub) { - if (!err) { - that.emit('message', pub.topic, pub.payload, pub) - that.handleMessage(pub, function (err) { - if (err) { - return callback(err) - } - that.incomingStore.del(pub, nop) - that._sendPacket(comp, callback) - }) - } else { - that._sendPacket(comp, callback) - } - }) -} - -/** - * _handleDisconnect - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handleDisconnect = function (packet) { - this.emit('disconnect', packet) -} - -/** - * _nextId - * @return unsigned int - */ -MqttClient.prototype._nextId = function () { - return this.messageIdProvider.allocate() -} - -/** - * getLastMessageId - * @return unsigned int - */ -MqttClient.prototype.getLastMessageId = function () { - return this.messageIdProvider.getLastAllocated() -} - -/** - * _resubscribe - * @api private - */ -MqttClient.prototype._resubscribe = function () { - debug('_resubscribe') - var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) - if (!this._firstConnection && - (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && - _resubscribeTopicsKeys.length > 0) { - if (this.options.resubscribe) { - if (this.options.protocolVersion === 5) { - debug('_resubscribe: protocolVersion 5') - for (var topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { - var resubscribeTopic = {} - resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] - resubscribeTopic.resubscribe = true - this.subscribe(resubscribeTopic, {properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties}) - } - } else { - this._resubscribeTopics.resubscribe = true - this.subscribe(this._resubscribeTopics) - } - } else { - this._resubscribeTopics = {} - } - } - - this._firstConnection = false -} - -/** - * _onConnect - * - * @api private - */ -MqttClient.prototype._onConnect = function (packet) { - if (this.disconnected) { - this.emit('connect', packet) - return - } - - var that = this - - this.connackPacket = packet - this.messageIdProvider.clear() - this._setupPingTimer() - - this.connected = true - - function startStreamProcess () { - var outStore = that.outgoingStore.createStream() - - function clearStoreProcessing () { - that._storeProcessing = false - that._packetIdsDuringStoreProcessing = {} - } - - that.once('close', remove) - outStore.on('error', function (err) { - clearStoreProcessing() - that._flushStoreProcessingQueue() - that.removeListener('close', remove) - that.emit('error', err) - }) - - function remove () { - outStore.destroy() - outStore = null - that._flushStoreProcessingQueue() - clearStoreProcessing() - } - - function storeDeliver () { - // edge case, we wrapped this twice - if (!outStore) { - return - } - that._storeProcessing = true - - var packet = outStore.read(1) - - var cb - - if (!packet) { - // read when data is available in the future - outStore.once('readable', storeDeliver) - return - } - - // Skip already processed store packets - if (that._packetIdsDuringStoreProcessing[packet.messageId]) { - storeDeliver() - return - } - - // Avoid unnecessary stream read operations when disconnected - if (!that.disconnecting && !that.reconnectTimer) { - cb = that.outgoing[packet.messageId] ? that.outgoing[packet.messageId].cb : null - that.outgoing[packet.messageId] = { - volatile: false, - cb: function (err, status) { - // Ensure that the original callback passed in to publish gets invoked - if (cb) { - cb(err, status) - } - - storeDeliver() - } - } - that._packetIdsDuringStoreProcessing[packet.messageId] = true - if (that.messageIdProvider.register(packet.messageId)) { - that._sendPacket(packet) - } else { - debug('messageId: %d has already used.', packet.messageId) - } - } else if (outStore.destroy) { - outStore.destroy() - } - } - - outStore.on('end', function () { - var allProcessed = true - for (var id in that._packetIdsDuringStoreProcessing) { - if (!that._packetIdsDuringStoreProcessing[id]) { - allProcessed = false - break - } - } - if (allProcessed) { - clearStoreProcessing() - that.removeListener('close', remove) - that._invokeAllStoreProcessingQueue() - that.emit('connect', packet) - } else { - startStreamProcess() - } - }) - storeDeliver() - } - // start flowing - startStreamProcess() -} - -MqttClient.prototype._invokeStoreProcessingQueue = function () { - if (this._storeProcessingQueue.length > 0) { - var f = this._storeProcessingQueue[0] - if (f && f.invoke()) { - this._storeProcessingQueue.shift() - return true - } - } - return false -} - -MqttClient.prototype._invokeAllStoreProcessingQueue = function () { - while (this._invokeStoreProcessingQueue()) {} -} - -MqttClient.prototype._flushStoreProcessingQueue = function () { - for (var f of this._storeProcessingQueue) { - if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) - if (f.callback) f.callback(new Error('Connection closed')) - } - this._storeProcessingQueue.splice(0) -} - -module.exports = MqttClient +'use strict' + +/** + * Module dependencies + */ +var EventEmitter = require('events').EventEmitter +var Store = require('./store') +var TopicAliasRecv = require('./topic-alias-recv') +var TopicAliasSend = require('./topic-alias-send') +var mqttPacket = require('mqtt-packet') +var DefaultMessageIdProvider = require('./default-message-id-provider') +var Writable = require('readable-stream').Writable +var inherits = require('inherits') +var reInterval = require('reinterval') +var clone = require('rfdc/default') +var validations = require('./validations') +var xtend = require('xtend') +var debug = require('debug')('mqttjs:client') +var nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } +var setImmediate = global.setImmediate || function (callback) { + // works in node v0.8 + nextTick(callback) +} +var defaultConnectOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true +} + +var socketErrors = [ + 'ECONNREFUSED', + 'EADDRINUSE', + 'ECONNRESET', + 'ENOTFOUND' +] + +// Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND. + +var errors = { + 0: '', + 1: 'Unacceptable protocol version', + 2: 'Identifier rejected', + 3: 'Server unavailable', + 4: 'Bad username or password', + 5: 'Not authorized', + 16: 'No matching subscribers', + 17: 'No subscription existed', + 128: 'Unspecified error', + 129: 'Malformed Packet', + 130: 'Protocol Error', + 131: 'Implementation specific error', + 132: 'Unsupported Protocol Version', + 133: 'Client Identifier not valid', + 134: 'Bad User Name or Password', + 135: 'Not authorized', + 136: 'Server unavailable', + 137: 'Server busy', + 138: 'Banned', + 139: 'Server shutting down', + 140: 'Bad authentication method', + 141: 'Keep Alive timeout', + 142: 'Session taken over', + 143: 'Topic Filter invalid', + 144: 'Topic Name invalid', + 145: 'Packet identifier in use', + 146: 'Packet Identifier not found', + 147: 'Receive Maximum exceeded', + 148: 'Topic Alias invalid', + 149: 'Packet too large', + 150: 'Message rate too high', + 151: 'Quota exceeded', + 152: 'Administrative action', + 153: 'Payload format invalid', + 154: 'Retain not supported', + 155: 'QoS not supported', + 156: 'Use another server', + 157: 'Server moved', + 158: 'Shared Subscriptions not supported', + 159: 'Connection rate exceeded', + 160: 'Maximum connect time', + 161: 'Subscription Identifiers not supported', + 162: 'Wildcard Subscriptions not supported' +} + +function defaultId () { + return 'mqttjs_' + Math.random().toString(16).substr(2, 8) +} + +function applyTopicAlias (client, packet) { + if (client.options.protocolVersion === 5) { + if (packet.cmd === 'publish') { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + var topic = packet.topic.toString() + if (client.topicAliasSend) { + if (alias) { + if (topic.length !== 0) { + // register topic alias + debug('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) + if (!client.topicAliasSend.put(topic, alias)) { + debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + return new Error('Sending Topic Alias out of range') + } + } + } else { + if (topic.length !== 0) { + if (client.options.autoAssignTopicAlias) { + alias = client.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto assign(use) topic: %s - alias: %d', topic, alias) + } else { + alias = client.topicAliasSend.getLruAlias() + client.topicAliasSend.put(topic, alias) + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto assign topic: %s - alias: %d', topic, alias) + } + } else if (client.options.autoUseTopicAlias) { + alias = client.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) + } + } + } + } + } else if (alias) { + debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + return new Error('Sending Topic Alias out of range') + } + } + } +} + +function removeTopicAliasAndRecoverTopicName (client, packet) { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + + var topic = packet.topic.toString() + if (topic.length === 0) { + // restore topic from alias + if (typeof alias === 'undefined') { + return new Error('Unregistered Topic Alias') + } else { + topic = client.topicAliasSend.getTopicByAlias(alias) + if (typeof topic === 'undefined') { + return new Error('Unregistered Topic Alias') + } else { + packet.topic = topic + } + } + } + if (alias) { + delete packet.properties.topicAlias + } +} + +function sendPacket (client, packet, cb) { + debug('sendPacket :: packet: %O', packet) + debug('sendPacket :: emitting `packetsend`') + + client.emit('packetsend', packet) + + debug('sendPacket :: writing to stream') + var result = mqttPacket.writeToStream(packet, client.stream, client.options) + debug('sendPacket :: writeToStream result %s', result) + if (!result && cb) { + debug('sendPacket :: handle events on `drain` once through callback.') + client.stream.once('drain', cb) + } else if (cb) { + debug('sendPacket :: invoking cb') + cb() + } +} + +function flush (queue) { + if (queue) { + debug('flush: queue exists? %b', !!(queue)) + Object.keys(queue).forEach(function (messageId) { + if (typeof queue[messageId].cb === 'function') { + queue[messageId].cb(new Error('Connection closed')) + delete queue[messageId] + } + }) + } +} + +function flushVolatile (queue) { + if (queue) { + debug('flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') + Object.keys(queue).forEach(function (messageId) { + if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { + queue[messageId].cb(new Error('Connection closed')) + delete queue[messageId] + } + }) + } +} + +function storeAndSend (client, packet, cb, cbStorePut) { + debug('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) + var storePacket = packet + var err + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + err = removeTopicAliasAndRecoverTopicName(client, storePacket) + if (err) { + return cb && cb(err) + } + } + client.outgoingStore.put(storePacket, function storedPacket (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + sendPacket(client, packet, cb) + }) +} + +function nop (error) { + debug('nop ::', error) +} + +/** + * MqttClient constructor + * + * @param {Stream} stream - stream + * @param {Object} [options] - connection options + * (see Connection#connect) + */ +function MqttClient (streamBuilder, options) { + var k + var that = this + + if (!(this instanceof MqttClient)) { + return new MqttClient(streamBuilder, options) + } + + this.options = options || {} + + // Defaults + for (k in defaultConnectOptions) { + if (typeof this.options[k] === 'undefined') { + this.options[k] = defaultConnectOptions[k] + } else { + this.options[k] = options[k] + } + } + + debug('MqttClient :: options.protocol', options.protocol) + debug('MqttClient :: options.protocolVersion', options.protocolVersion) + debug('MqttClient :: options.username', options.username) + debug('MqttClient :: options.keepalive', options.keepalive) + debug('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) + debug('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) + debug('MqttClient :: options.topicAliasMaximum', options.topicAliasMaximum) + + this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() + + debug('MqttClient :: clientId', this.options.clientId) + + this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } + + this.streamBuilder = streamBuilder + + this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider + + // Inflight message storages + this.outgoingStore = options.outgoingStore || new Store() + this.incomingStore = options.incomingStore || new Store() + + // Should QoS zero messages be queued when the connection is broken? + this.queueQoSZero = options.queueQoSZero === undefined ? true : options.queueQoSZero + + // map of subscribed topics to support reconnection + this._resubscribeTopics = {} + + // map of a subscribe messageId and a topic + this.messageIdToTopic = {} + + // Ping timer, setup in _setupPingTimer + this.pingTimer = null + // Is the client connected? + this.connected = false + // Are we disconnecting? + this.disconnecting = false + // Packet queue + this.queue = [] + // connack timer + this.connackTimer = null + // Reconnect timer + this.reconnectTimer = null + // Is processing store? + this._storeProcessing = false + // Packet Ids are put into the store during store processing + this._packetIdsDuringStoreProcessing = {} + // Store processing queue + this._storeProcessingQueue = [] + + // Inflight callbacks + this.outgoing = {} + + // True if connection is first time. + this._firstConnection = true + + if (options.topicAliasMaximum > 0) { + if (options.topicAliasMaximum > 0xffff) { + debug('MqttClient :: options.topicAliasMaximum is out of range') + } else { + this.topicAliasRecv = new TopicAliasRecv(options.topicAliasMaximum) + } + } + + // Send queued packets + this.on('connect', function () { + var queue = this.queue + + function deliver () { + var entry = queue.shift() + debug('deliver :: entry %o', entry) + var packet = null + + if (!entry) { + that._resubscribe() + return + } + + packet = entry.packet + debug('deliver :: call _sendPacket for %o', packet) + var send = true + if (packet.messageId && packet.messageId !== 0) { + if (!that.messageIdProvider.register(packet.messageId)) { + send = false + } + } + if (send) { + that._sendPacket( + packet, + function (err) { + if (entry.cb) { + entry.cb(err) + } + deliver() + } + ) + } else { + debug('messageId: %d has already used. The message is skipped and removed.', packet.messageId) + deliver() + } + } + + debug('connect :: sending queued packets') + deliver() + }) + + this.on('close', function () { + debug('close :: connected set to `false`') + this.connected = false + + debug('close :: clearing connackTimer') + clearTimeout(this.connackTimer) + + debug('close :: clearing ping timer') + if (that.pingTimer !== null) { + that.pingTimer.clear() + that.pingTimer = null + } + + if (this.topicAliasRecv) { + this.topicAliasRecv.clear() + } + + debug('close :: calling _setupReconnect') + this._setupReconnect() + }) + EventEmitter.call(this) + + debug('MqttClient :: setting up stream') + this._setupStream() +} +inherits(MqttClient, EventEmitter) + +/** + * setup the event handlers in the inner stream. + * + * @api private + */ +MqttClient.prototype._setupStream = function () { + var connectPacket + var that = this + var writable = new Writable() + var parser = mqttPacket.parser(this.options) + var completeParse = null + var packets = [] + + debug('_setupStream :: calling method to clear reconnect') + this._clearReconnect() + + debug('_setupStream :: using streamBuilder provided to client to create stream') + this.stream = this.streamBuilder(this) + + parser.on('packet', function (packet) { + debug('parser :: on packet push to packets array.') + packets.push(packet) + }) + + function nextTickWork () { + if (packets.length) { + nextTick(work) + } else { + var done = completeParse + completeParse = null + done() + } + } + + function work () { + debug('work :: getting next packet in queue') + var packet = packets.shift() + + if (packet) { + debug('work :: packet pulled from queue') + that._handlePacket(packet, nextTickWork) + } else { + debug('work :: no packets in queue') + var done = completeParse + completeParse = null + debug('work :: done flag is %s', !!(done)) + if (done) done() + } + } + + writable._write = function (buf, enc, done) { + completeParse = done + debug('writable stream :: parsing buffer') + parser.parse(buf) + work() + } + + function streamErrorHandler (error) { + debug('streamErrorHandler :: error', error.message) + if (socketErrors.includes(error.code)) { + // handle error + debug('streamErrorHandler :: emitting error') + that.emit('error', error) + } else { + nop(error) + } + } + + debug('_setupStream :: pipe stream to writable stream') + this.stream.pipe(writable) + + // Suppress connection errors + this.stream.on('error', streamErrorHandler) + + // Echo stream close + this.stream.on('close', function () { + debug('(%s)stream :: on close', that.options.clientId) + flushVolatile(that.outgoing) + debug('stream: emit close to MqttClient') + that.emit('close') + }) + + // Send a connect packet + debug('_setupStream: sending packet `connect`') + connectPacket = Object.create(this.options) + connectPacket.cmd = 'connect' + if (this.topicAliasRecv) { + if (!connectPacket.properties) { + connectPacket.properties = {} + } + if (this.topicAliasRecv) { + connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max + } + } + // avoid message queue + sendPacket(this, connectPacket) + + // Echo connection errors + parser.on('error', this.emit.bind(this, 'error')) + + // auth + if (this.options.properties) { + if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { + that.end(() => + this.emit('error', new Error('Packet has no Authentication Method') + )) + return this + } + if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { + var authPacket = xtend({cmd: 'auth', reasonCode: 0}, this.options.authPacket) + sendPacket(this, authPacket) + } + } + + // many drain listeners are needed for qos 1 callbacks if the connection is intermittent + this.stream.setMaxListeners(1000) + + clearTimeout(this.connackTimer) + this.connackTimer = setTimeout(function () { + debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') + that._cleanUp(true) + }, this.options.connectTimeout) +} + +MqttClient.prototype._handlePacket = function (packet, done) { + var options = this.options + + if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { + this.emit('error', new Error('exceeding packets size ' + packet.cmd)) + this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) + return this + } + debug('_handlePacket :: emitting packetreceive') + this.emit('packetreceive', packet) + + switch (packet.cmd) { + case 'publish': + this._handlePublish(packet, done) + break + case 'puback': + case 'pubrec': + case 'pubcomp': + case 'suback': + case 'unsuback': + this._handleAck(packet) + done() + break + case 'pubrel': + this._handlePubrel(packet, done) + break + case 'connack': + this._handleConnack(packet) + done() + break + case 'pingresp': + this._handlePingresp(packet) + done() + break + case 'disconnect': + this._handleDisconnect(packet) + done() + break + default: + // do nothing + // maybe we should do an error handling + // or just log it + break + } +} + +MqttClient.prototype._checkDisconnecting = function (callback) { + if (this.disconnecting) { + if (callback) { + callback(new Error('client disconnecting')) + } else { + this.emit('error', new Error('client disconnecting')) + } + } + return this.disconnecting +} + +/** + * publish - publish to + * + * @param {String} topic - topic to publish to + * @param {String, Buffer} message - message to publish + * @param {Object} [opts] - publish options, includes: + * {Number} qos - qos level to publish on + * {Boolean} retain - whether or not to retain the message + * {Boolean} dup - whether or not mark a message as duplicate + * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` + * @param {Function} [callback] - function(err){} + * called when publish succeeds or fails + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.publish('topic', 'message'); + * @example + * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); + * @example client.publish('topic', 'message', console.log); + */ +MqttClient.prototype.publish = function (topic, message, opts, callback) { + debug('publish :: message `%s` to topic `%s`', message, topic) + var packet + var options = this.options + + // .publish(topic, payload, cb); + if (typeof opts === 'function') { + callback = opts + opts = null + } + + // default opts + var defaultOpts = {qos: 0, retain: false, dup: false} + opts = xtend(defaultOpts, opts) + + if (this._checkDisconnecting(callback)) { + return this + } + + var that = this + var publishProc = function () { + var messageId = 0 + if (opts.qos === 1 || opts.qos === 2) { + messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + } + packet = { + cmd: 'publish', + topic: topic, + payload: message, + qos: opts.qos, + retain: opts.retain, + messageId: messageId, + dup: opts.dup + } + + if (options.protocolVersion === 5) { + packet.properties = opts.properties + } + + debug('publish :: qos', opts.qos) + switch (opts.qos) { + case 1: + case 2: + // Add to callbacks + that.outgoing[packet.messageId] = { + volatile: false, + cb: callback || nop + } + debug('MqttClient:publish: packet cmd: %s', packet.cmd) + that._sendPacket(packet, undefined, opts.cbStorePut) + break + default: + debug('MqttClient:publish: packet cmd: %s', packet.cmd) + that._sendPacket(packet, callback, opts.cbStorePut) + break + } + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': publishProc, + 'cbStorePut': opts.cbStorePut, + 'callback': callback + } + ) + } else { + publishProc() + } + return this +} + +/** + * subscribe - subscribe to + * + * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} + * @param {Object} [opts] - optional subscription options, includes: + * {Number} qos - subscribe qos level + * @param {Function} [callback] - function(err, granted){} where: + * {Error} err - subscription error (none at the moment!) + * {Array} granted - array of {topic: 't', qos: 0} + * @returns {MqttClient} this - for chaining + * @api public + * @example client.subscribe('topic'); + * @example client.subscribe('topic', {qos: 1}); + * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); + * @example client.subscribe('topic', console.log); + */ +MqttClient.prototype.subscribe = function () { + var that = this + var args = new Array(arguments.length) + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i] + } + var subs = [] + var obj = args.shift() + var resubscribe = obj.resubscribe + var callback = args.pop() || nop + var opts = args.pop() + var version = this.options.protocolVersion + + delete obj.resubscribe + + if (typeof obj === 'string') { + obj = [obj] + } + + if (typeof callback !== 'function') { + opts = callback + callback = nop + } + + var invalidTopic = validations.validateTopics(obj) + if (invalidTopic !== null) { + setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) + return this + } + + if (this._checkDisconnecting(callback)) { + debug('subscribe: discconecting true') + return this + } + + var defaultOpts = { + qos: 0 + } + if (version === 5) { + defaultOpts.nl = false + defaultOpts.rap = false + defaultOpts.rh = 0 + } + opts = xtend(defaultOpts, opts) + + if (Array.isArray(obj)) { + obj.forEach(function (topic) { + debug('subscribe: array topic %s', topic) + if (!that._resubscribeTopics.hasOwnProperty(topic) || + that._resubscribeTopics[topic].qos < opts.qos || + resubscribe) { + var currentOpts = { + topic: topic, + qos: opts.qos + } + if (version === 5) { + currentOpts.nl = opts.nl + currentOpts.rap = opts.rap + currentOpts.rh = opts.rh + currentOpts.properties = opts.properties + } + debug('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) + subs.push(currentOpts) + } + }) + } else { + Object + .keys(obj) + .forEach(function (k) { + debug('subscribe: object topic %s', k) + if (!that._resubscribeTopics.hasOwnProperty(k) || + that._resubscribeTopics[k].qos < obj[k].qos || + resubscribe) { + var currentOpts = { + topic: k, + qos: obj[k].qos + } + if (version === 5) { + currentOpts.nl = obj[k].nl + currentOpts.rap = obj[k].rap + currentOpts.rh = obj[k].rh + currentOpts.properties = opts.properties + } + debug('subscribe: pushing `%s` to subs list', currentOpts) + subs.push(currentOpts) + } + }) + } + + if (!subs.length) { + callback(null, []) + return this + } + + var subscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + + var packet = { + cmd: 'subscribe', + subscriptions: subs, + qos: 1, + retain: false, + dup: false, + messageId: messageId + } + + if (opts.properties) { + packet.properties = opts.properties + } + + // subscriptions to resubscribe to in case of disconnect + if (that.options.resubscribe) { + debug('subscribe :: resubscribe true') + var topics = [] + subs.forEach(function (sub) { + if (that.options.reconnectPeriod > 0) { + var topic = { qos: sub.qos } + if (version === 5) { + topic.nl = sub.nl || false + topic.rap = sub.rap || false + topic.rh = sub.rh || 0 + topic.properties = sub.properties + } + that._resubscribeTopics[sub.topic] = topic + topics.push(sub.topic) + } + }) + that.messageIdToTopic[packet.messageId] = topics + } + + that.outgoing[packet.messageId] = { + volatile: true, + cb: function (err, packet) { + if (!err) { + var granted = packet.granted + for (var i = 0; i < granted.length; i += 1) { + subs[i].qos = granted[i] + } + } + + callback(err, subs) + } + } + debug('subscribe :: call _sendPacket') + that._sendPacket(packet) + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': subscribeProc, + 'callback': callback + } + ) + } else { + subscribeProc() + } + + return this +} + +/** + * unsubscribe - unsubscribe from topic(s) + * + * @param {String, Array} topic - topics to unsubscribe from + * @param {Object} [opts] - optional subscription options, includes: + * {Object} properties - properties of unsubscribe packet + * @param {Function} [callback] - callback fired on unsuback + * @returns {MqttClient} this - for chaining + * @api public + * @example client.unsubscribe('topic'); + * @example client.unsubscribe('topic', console.log); + */ +MqttClient.prototype.unsubscribe = function () { + var that = this + var args = new Array(arguments.length) + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i] + } + var topic = args.shift() + var callback = args.pop() || nop + var opts = args.pop() + if (typeof topic === 'string') { + topic = [topic] + } + + if (typeof callback !== 'function') { + opts = callback + callback = nop + } + + var invalidTopic = validations.validateTopics(topic) + if (invalidTopic !== null) { + setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) + return this + } + + if (that._checkDisconnecting(callback)) { + return this + } + + var unsubscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + var packet = { + cmd: 'unsubscribe', + qos: 1, + messageId: messageId + } + + if (typeof topic === 'string') { + packet.unsubscriptions = [topic] + } else if (Array.isArray(topic)) { + packet.unsubscriptions = topic + } + + if (that.options.resubscribe) { + packet.unsubscriptions.forEach(function (topic) { + delete that._resubscribeTopics[topic] + }) + } + + if (typeof opts === 'object' && opts.properties) { + packet.properties = opts.properties + } + + that.outgoing[packet.messageId] = { + volatile: true, + cb: callback + } + + debug('unsubscribe: call _sendPacket') + that._sendPacket(packet) + + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': unsubscribeProc, + 'callback': callback + } + ) + } else { + unsubscribeProc() + } + + return this +} + +/** + * end - close connection + * + * @returns {MqttClient} this - for chaining + * @param {Boolean} force - do not wait for all in-flight messages to be acked + * @param {Object} opts - added to the disconnect packet + * @param {Function} cb - called when the client has been closed + * + * @api public + */ +MqttClient.prototype.end = function (force, opts, cb) { + var that = this + + debug('end :: (%s)', this.options.clientId) + + if (force == null || typeof force !== 'boolean') { + cb = opts || nop + opts = force + force = false + if (typeof opts !== 'object') { + cb = opts + opts = null + if (typeof cb !== 'function') { + cb = nop + } + } + } + + if (typeof opts !== 'object') { + cb = opts + opts = null + } + + debug('end :: cb? %s', !!cb) + cb = cb || nop + + function closeStores () { + debug('end :: closeStores: closing incoming and outgoing stores') + that.disconnected = true + that.incomingStore.close(function (e1) { + that.outgoingStore.close(function (e2) { + debug('end :: closeStores: emitting end') + that.emit('end') + if (cb) { + let err = e1 || e2 + debug('end :: closeStores: invoking callback with args') + cb(err) + } + }) + }) + if (that._deferredReconnect) { + that._deferredReconnect() + } + } + + function finish () { + // defer closesStores of an I/O cycle, + // just to make sure things are + // ok for websockets + debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) + that._cleanUp(force, () => { + debug('end :: finish :: calling process.nextTick on closeStores') + // var boundProcess = nextTick.bind(null, closeStores) + nextTick(closeStores.bind(that)) + }, opts) + } + + if (this.disconnecting) { + cb() + return this + } + + this._clearReconnect() + + this.disconnecting = true + + if (!force && Object.keys(this.outgoing).length > 0) { + // wait 10ms, just to be sure we received all of it + debug('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) + this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) + } else { + debug('end :: (%s) :: immediately calling finish', that.options.clientId) + finish() + } + + return this +} + +/** + * removeOutgoingMessage - remove a message in outgoing store + * the outgoing callback will be called withe Error('Message removed') if the message is removed + * + * @param {Number} messageId - messageId to remove message + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.removeOutgoingMessage(client.getLastAllocated()); + */ +MqttClient.prototype.removeOutgoingMessage = function (messageId) { + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null + delete this.outgoing[messageId] + this.outgoingStore.del({messageId: messageId}, function () { + cb(new Error('Message removed')) + }) + return this +} + +/** + * reconnect - connect again using the same options as connect() + * + * @param {Object} [opts] - optional reconnect options, includes: + * {Store} incomingStore - a store for the incoming packets + * {Store} outgoingStore - a store for the outgoing packets + * if opts is not given, current stores are used + * @returns {MqttClient} this - for chaining + * + * @api public + */ +MqttClient.prototype.reconnect = function (opts) { + debug('client reconnect') + var that = this + var f = function () { + if (opts) { + that.options.incomingStore = opts.incomingStore + that.options.outgoingStore = opts.outgoingStore + } else { + that.options.incomingStore = null + that.options.outgoingStore = null + } + that.incomingStore = that.options.incomingStore || new Store() + that.outgoingStore = that.options.outgoingStore || new Store() + that.disconnecting = false + that.disconnected = false + that._deferredReconnect = null + that._reconnect() + } + + if (this.disconnecting && !this.disconnected) { + this._deferredReconnect = f + } else { + f() + } + return this +} + +/** + * _reconnect - implement reconnection + * @api privateish + */ +MqttClient.prototype._reconnect = function () { + debug('_reconnect: emitting reconnect to client') + this.emit('reconnect') + if (this.connected) { + this.end(() => { this._setupStream() }) + debug('client already connected. disconnecting first.') + } else { + debug('_reconnect: calling _setupStream') + this._setupStream() + } +} + +/** + * _setupReconnect - setup reconnect timer + */ +MqttClient.prototype._setupReconnect = function () { + var that = this + + if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { + if (!this.reconnecting) { + debug('_setupReconnect :: emit `offline` state') + this.emit('offline') + debug('_setupReconnect :: set `reconnecting` to `true`') + this.reconnecting = true + } + debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) + that.reconnectTimer = setInterval(function () { + debug('reconnectTimer :: reconnect triggered!') + that._reconnect() + }, that.options.reconnectPeriod) + } else { + debug('_setupReconnect :: doing nothing...') + } +} + +/** + * _clearReconnect - clear the reconnect timer + */ +MqttClient.prototype._clearReconnect = function () { + debug('_clearReconnect : clearing reconnect timer') + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer) + this.reconnectTimer = null + } +} + +/** + * _cleanUp - clean up on connection end + * @api private + */ +MqttClient.prototype._cleanUp = function (forced, done) { + var opts = arguments[2] + if (done) { + debug('_cleanUp :: done callback provided for on stream close') + this.stream.on('close', done) + } + + debug('_cleanUp :: forced? %s', forced) + if (forced) { + if ((this.options.reconnectPeriod === 0) && this.options.clean) { + flush(this.outgoing) + } + debug('_cleanUp :: (%s) :: destroying stream', this.options.clientId) + this.stream.destroy() + } else { + var packet = xtend({ cmd: 'disconnect' }, opts) + debug('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) + this._sendPacket( + packet, + setImmediate.bind( + null, + this.stream.end.bind(this.stream) + ) + ) + } + + if (!this.disconnecting) { + debug('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') + this._clearReconnect() + this._setupReconnect() + } + + if (this.pingTimer !== null) { + debug('_cleanUp :: clearing pingTimer') + this.pingTimer.clear() + this.pingTimer = null + } + + if (done && !this.connected) { + debug('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) + this.stream.removeListener('close', done) + done() + } +} + +/** + * _sendPacket - send or queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @api private + */ +MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { + debug('_sendPacket :: (%s) :: start', this.options.clientId) + cbStorePut = cbStorePut || nop + cb = cb || nop + + var err = applyTopicAlias(this, packet) + if (err) { + cb(err) + return + } + + if (!this.connected) { + debug('_sendPacket :: client not connected. Storing packet offline.') + this._storePacket(packet, cb, cbStorePut) + return + } + + // When sending a packet, reschedule the ping timer + this._shiftPingInterval() + + switch (packet.cmd) { + case 'publish': + break + case 'pubrel': + storeAndSend(this, packet, cb, cbStorePut) + return + default: + sendPacket(this, packet, cb) + return + } + + switch (packet.qos) { + case 2: + case 1: + storeAndSend(this, packet, cb, cbStorePut) + break + /** + * no need of case here since it will be caught by default + * and jshint comply that before default it must be a break + * anyway it will result in -1 evaluation + */ + case 0: + /* falls through */ + default: + sendPacket(this, packet, cb) + break + } + debug('_sendPacket :: (%s) :: end', this.options.clientId) +} + +/** + * _storePacket - queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @api private + */ +MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { + debug('_storePacket :: packet: %o', packet) + debug('_storePacket :: cb? %s', !!cb) + cbStorePut = cbStorePut || nop + + var storePacket = packet + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + var err = removeTopicAliasAndRecoverTopicName(this, storePacket) + if (err) { + return cb && cb(err) + } + } + // check that the packet is not a qos of 0, or that the command is not a publish + if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { + this.queue.push({ packet: storePacket, cb: cb }) + } else if (storePacket.qos > 0) { + cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null + this.outgoingStore.put(storePacket, function (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + }) + } else if (cb) { + cb(new Error('No connection to broker')) + } +} + +/** + * _setupPingTimer - setup the ping timer + * + * @api private + */ +MqttClient.prototype._setupPingTimer = function () { + debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) + var that = this + + if (!this.pingTimer && this.options.keepalive) { + this.pingResp = true + this.pingTimer = reInterval(function () { + that._checkPing() + }, this.options.keepalive * 1000) + } +} + +/** + * _shiftPingInterval - reschedule the ping interval + * + * @api private + */ +MqttClient.prototype._shiftPingInterval = function () { + if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { + this.pingTimer.reschedule(this.options.keepalive * 1000) + } +} +/** + * _checkPing - check if a pingresp has come back, and ping the server again + * + * @api private + */ +MqttClient.prototype._checkPing = function () { + debug('_checkPing :: checking ping...') + if (this.pingResp) { + debug('_checkPing :: ping response received. Clearing flag and sending `pingreq`') + this.pingResp = false + this._sendPacket({ cmd: 'pingreq' }) + } else { + // do a forced cleanup since socket will be in bad shape + debug('_checkPing :: calling _cleanUp with force true') + this._cleanUp(true) + } +} + +/** + * _handlePingresp - handle a pingresp + * + * @api private + */ +MqttClient.prototype._handlePingresp = function () { + this.pingResp = true +} + +/** + * _handleConnack + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handleConnack = function (packet) { + debug('_handleConnack') + var options = this.options + var version = options.protocolVersion + var rc = version === 5 ? packet.reasonCode : packet.returnCode + + clearTimeout(this.connackTimer) + delete this.topicAliasSend + + if (packet.properties) { + if (packet.properties.topicAliasMaximum) { + if (packet.properties.topicAliasMaximum > 0xffff) { + this.emit('error', new Error('topicAliasMaximum from broker is out of range')) + return + } + if (packet.properties.topicAliasMaximum > 0) { + this.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) + } + } + if (packet.properties.serverKeepAlive && options.keepalive) { + options.keepalive = packet.properties.serverKeepAlive + this._shiftPingInterval() + } + if (packet.properties.maximumPacketSize) { + if (!options.properties) { options.properties = {} } + options.properties.maximumPacketSize = packet.properties.maximumPacketSize + } + } + + if (rc === 0) { + this.reconnecting = false + this._onConnect(packet) + } else if (rc > 0) { + var err = new Error('Connection refused: ' + errors[rc]) + err.code = rc + this.emit('error', err) + } +} + +/** + * _handlePublish + * + * @param {Object} packet + * @api private + */ +/* +those late 2 case should be rewrite to comply with coding style: + +case 1: +case 0: + // do not wait sending a puback + // no callback passed + if (1 === qos) { + this._sendPacket({ + cmd: 'puback', + messageId: messageId + }); + } + // emit the message event for both qos 1 and 0 + this.emit('message', topic, message, packet); + this.handleMessage(packet, done); + break; +default: + // do nothing but every switch mus have a default + // log or throw an error about unknown qos + break; + +for now i just suppressed the warnings +*/ +MqttClient.prototype._handlePublish = function (packet, done) { + debug('_handlePublish: packet %o', packet) + done = typeof done !== 'undefined' ? done : nop + var topic = packet.topic.toString() + var message = packet.payload + var qos = packet.qos + var messageId = packet.messageId + var that = this + var options = this.options + var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] + if (this.options.protocolVersion === 5) { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + if (typeof alias !== 'undefined') { + if (topic.length === 0) { + if (alias > 0 && alias <= 0xffff) { + var gotTopic = this.topicAliasRecv.getTopicByAlias(alias) + if (gotTopic) { + topic = gotTopic + debug('_handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: unregistered topic alias. alias: %d', alias) + this.emit('error', new Error('Received unregistered Topic Alias')) + return + } + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } else { + if (this.topicAliasRecv.put(topic, alias)) { + debug('_handlePublish :: registered topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } + } + } + debug('_handlePublish: qos %d', qos) + switch (qos) { + case 2: { + options.customHandleAcks(topic, message, packet, function (error, code) { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { return that.emit('error', error) } + if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for pubrec')) } + if (code) { + that._sendPacket({cmd: 'pubrec', messageId: messageId, reasonCode: code}, done) + } else { + that.incomingStore.put(packet, function () { + that._sendPacket({cmd: 'pubrec', messageId: messageId}, done) + }) + } + }) + break + } + case 1: { + // emit the message event + options.customHandleAcks(topic, message, packet, function (error, code) { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { return that.emit('error', error) } + if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for puback')) } + if (!code) { that.emit('message', topic, message, packet) } + that.handleMessage(packet, function (err) { + if (err) { + return done && done(err) + } + that._sendPacket({cmd: 'puback', messageId: messageId, reasonCode: code}, done) + }) + }) + break + } + case 0: + // emit the message event + this.emit('message', topic, message, packet) + this.handleMessage(packet, done) + break + default: + // do nothing + debug('_handlePublish: unknown QoS. Doing nothing.') + // log or throw an error about unknown qos + break + } +} + +/** + * Handle messages with backpressure support, one at a time. + * Override at will. + * + * @param Packet packet the packet + * @param Function callback call when finished + * @api public + */ +MqttClient.prototype.handleMessage = function (packet, callback) { + callback() +} + +/** + * _handleAck + * + * @param {Object} packet + * @api private + */ + +MqttClient.prototype._handleAck = function (packet) { + /* eslint no-fallthrough: "off" */ + var messageId = packet.messageId + var type = packet.cmd + var response = null + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null + var that = this + var err + + if (!cb) { + debug('_handleAck :: Server sent an ack in error. Ignoring.') + // Server sent an ack in error, ignore it. + return + } + + // Process + debug('_handleAck :: packet type', type) + switch (type) { + case 'pubcomp': + // same thing as puback for QoS 2 + case 'puback': + var pubackRC = packet.reasonCode + // Callback - we're done + if (pubackRC && pubackRC > 0 && pubackRC !== 16) { + err = new Error('Publish error: ' + errors[pubackRC]) + err.code = pubackRC + cb(err, packet) + } + delete this.outgoing[messageId] + this.outgoingStore.del(packet, cb) + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() + break + case 'pubrec': + response = { + cmd: 'pubrel', + qos: 2, + messageId: messageId + } + var pubrecRC = packet.reasonCode + + if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { + err = new Error('Publish error: ' + errors[pubrecRC]) + err.code = pubrecRC + cb(err, packet) + } else { + this._sendPacket(response) + } + break + case 'suback': + delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) + for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { + if ((packet.granted[grantedI] & 0x80) !== 0) { + // suback with Failure status + var topics = this.messageIdToTopic[messageId] + if (topics) { + topics.forEach(function (topic) { + delete that._resubscribeTopics[topic] + }) + } + } + } + this._invokeStoreProcessingQueue() + cb(null, packet) + break + case 'unsuback': + delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() + cb(null) + break + default: + that.emit('error', new Error('unrecognized packet type')) + } + + if (this.disconnecting && + Object.keys(this.outgoing).length === 0) { + this.emit('outgoingEmpty') + } +} + +/** + * _handlePubrel + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handlePubrel = function (packet, callback) { + debug('handling pubrel packet') + callback = typeof callback !== 'undefined' ? callback : nop + var messageId = packet.messageId + var that = this + + var comp = {cmd: 'pubcomp', messageId: messageId} + + that.incomingStore.get(packet, function (err, pub) { + if (!err) { + that.emit('message', pub.topic, pub.payload, pub) + that.handleMessage(pub, function (err) { + if (err) { + return callback(err) + } + that.incomingStore.del(pub, nop) + that._sendPacket(comp, callback) + }) + } else { + that._sendPacket(comp, callback) + } + }) +} + +/** + * _handleDisconnect + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handleDisconnect = function (packet) { + this.emit('disconnect', packet) +} + +/** + * _nextId + * @return unsigned int + */ +MqttClient.prototype._nextId = function () { + return this.messageIdProvider.allocate() +} + +/** + * getLastMessageId + * @return unsigned int + */ +MqttClient.prototype.getLastMessageId = function () { + return this.messageIdProvider.getLastAllocated() +} + +/** + * _resubscribe + * @api private + */ +MqttClient.prototype._resubscribe = function () { + debug('_resubscribe') + var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) + if (!this._firstConnection && + (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && + _resubscribeTopicsKeys.length > 0) { + if (this.options.resubscribe) { + if (this.options.protocolVersion === 5) { + debug('_resubscribe: protocolVersion 5') + for (var topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { + var resubscribeTopic = {} + resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] + resubscribeTopic.resubscribe = true + this.subscribe(resubscribeTopic, {properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties}) + } + } else { + this._resubscribeTopics.resubscribe = true + this.subscribe(this._resubscribeTopics) + } + } else { + this._resubscribeTopics = {} + } + } + + this._firstConnection = false +} + +/** + * _onConnect + * + * @api private + */ +MqttClient.prototype._onConnect = function (packet) { + if (this.disconnected) { + this.emit('connect', packet) + return + } + + var that = this + + this.connackPacket = packet + this.messageIdProvider.clear() + this._setupPingTimer() + + this.connected = true + + function startStreamProcess () { + var outStore = that.outgoingStore.createStream() + + function clearStoreProcessing () { + that._storeProcessing = false + that._packetIdsDuringStoreProcessing = {} + } + + that.once('close', remove) + outStore.on('error', function (err) { + clearStoreProcessing() + that._flushStoreProcessingQueue() + that.removeListener('close', remove) + that.emit('error', err) + }) + + function remove () { + outStore.destroy() + outStore = null + that._flushStoreProcessingQueue() + clearStoreProcessing() + } + + function storeDeliver () { + // edge case, we wrapped this twice + if (!outStore) { + return + } + that._storeProcessing = true + + var packet = outStore.read(1) + + var cb + + if (!packet) { + // read when data is available in the future + outStore.once('readable', storeDeliver) + return + } + + // Skip already processed store packets + if (that._packetIdsDuringStoreProcessing[packet.messageId]) { + storeDeliver() + return + } + + // Avoid unnecessary stream read operations when disconnected + if (!that.disconnecting && !that.reconnectTimer) { + cb = that.outgoing[packet.messageId] ? that.outgoing[packet.messageId].cb : null + that.outgoing[packet.messageId] = { + volatile: false, + cb: function (err, status) { + // Ensure that the original callback passed in to publish gets invoked + if (cb) { + cb(err, status) + } + + storeDeliver() + } + } + that._packetIdsDuringStoreProcessing[packet.messageId] = true + if (that.messageIdProvider.register(packet.messageId)) { + that._sendPacket(packet) + } else { + debug('messageId: %d has already used.', packet.messageId) + } + } else if (outStore.destroy) { + outStore.destroy() + } + } + + outStore.on('end', function () { + var allProcessed = true + for (var id in that._packetIdsDuringStoreProcessing) { + if (!that._packetIdsDuringStoreProcessing[id]) { + allProcessed = false + break + } + } + if (allProcessed) { + clearStoreProcessing() + that.removeListener('close', remove) + that._invokeAllStoreProcessingQueue() + that.emit('connect', packet) + } else { + startStreamProcess() + } + }) + storeDeliver() + } + // start flowing + startStreamProcess() +} + +MqttClient.prototype._invokeStoreProcessingQueue = function () { + if (this._storeProcessingQueue.length > 0) { + var f = this._storeProcessingQueue[0] + if (f && f.invoke()) { + this._storeProcessingQueue.shift() + return true + } + } + return false +} + +MqttClient.prototype._invokeAllStoreProcessingQueue = function () { + while (this._invokeStoreProcessingQueue()) {} +} + +MqttClient.prototype._flushStoreProcessingQueue = function () { + for (var f of this._storeProcessingQueue) { + if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) + if (f.callback) f.callback(new Error('Connection closed')) + } + this._storeProcessingQueue.splice(0) +} + +module.exports = MqttClient diff --git a/lib/connect/ali.js b/lib/connect/ali.js index 1cbb726a5..e7fe6a3c5 100644 --- a/lib/connect/ali.js +++ b/lib/connect/ali.js @@ -1,128 +1,128 @@ -'use strict' - -var Transform = require('readable-stream').Transform -var duplexify = require('duplexify') - -/* global FileReader */ -var my -var proxy -var stream -var isInitialized = false - -function buildProxy () { - var proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - my.sendSocketMessage({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function () { - next(new Error()) - } - }) - } - proxy._flush = function socketEnd (done) { - my.closeSocket({ - success: function () { - done() - } - }) - } - - return proxy -} - -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } -} - -function buildUrl (opts, client) { - var protocol = opts.protocol === 'alis' ? 'wss' : 'ws' - var url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function bindEventHandler () { - if (isInitialized) return - - isInitialized = true - - my.onSocketOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - my.onSocketMessage(function (res) { - if (typeof res.data === 'string') { - var buffer = Buffer.from(res.data, 'base64') - proxy.push(buffer) - } else { - var reader = new FileReader() - reader.addEventListener('load', function () { - var data = reader.result - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - reader.readAsArrayBuffer(res.data) - } - }) - - my.onSocketClose(function () { - stream.end() - stream.destroy() - }) - - my.onSocketError(function (res) { - stream.destroy(res) - }) -} - -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host - - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } - - var websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - setDefaultOpts(opts) - - var url = buildUrl(opts, client) - my = opts.my - my.connectSocket({ - url: url, - protocols: websocketSubProtocol - }) - - proxy = buildProxy() - stream = duplexify.obj() - - bindEventHandler() - - return stream -} - -module.exports = buildStream +'use strict' + +var Transform = require('readable-stream').Transform +var duplexify = require('duplexify') + +/* global FileReader */ +var my +var proxy +var stream +var isInitialized = false + +function buildProxy () { + var proxy = new Transform() + proxy._write = function (chunk, encoding, next) { + my.sendSocketMessage({ + data: chunk.buffer, + success: function () { + next() + }, + fail: function () { + next(new Error()) + } + }) + } + proxy._flush = function socketEnd (done) { + my.closeSocket({ + success: function () { + done() + } + }) + } + + return proxy +} + +function setDefaultOpts (opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } +} + +function buildUrl (opts, client) { + var protocol = opts.protocol === 'alis' ? 'wss' : 'ws' + var url = protocol + '://' + opts.hostname + opts.path + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path + } + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function bindEventHandler () { + if (isInitialized) return + + isInitialized = true + + my.onSocketOpen(function () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + my.onSocketMessage(function (res) { + if (typeof res.data === 'string') { + var buffer = Buffer.from(res.data, 'base64') + proxy.push(buffer) + } else { + var reader = new FileReader() + reader.addEventListener('load', function () { + var data = reader.result + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + reader.readAsArrayBuffer(res.data) + } + }) + + my.onSocketClose(function () { + stream.end() + stream.destroy() + }) + + my.onSocketError(function (res) { + stream.destroy(res) + }) +} + +function buildStream (client, opts) { + opts.hostname = opts.hostname || opts.host + + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } + + var websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + setDefaultOpts(opts) + + var url = buildUrl(opts, client) + my = opts.my + my.connectSocket({ + url: url, + protocols: websocketSubProtocol + }) + + proxy = buildProxy() + stream = duplexify.obj() + + bindEventHandler() + + return stream +} + +module.exports = buildStream diff --git a/lib/connect/index.js b/lib/connect/index.js index 9fc151c75..97e7b4c15 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -1,164 +1,164 @@ -'use strict' - -var MqttClient = require('../client') -var Store = require('../store') -var url = require('url') -var xtend = require('xtend') -var debug = require('debug')('mqttjs') - -var protocols = {} - -// eslint-disable-next-line camelcase -if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ !== 'function') { - protocols.mqtt = require('./tcp') - protocols.tcp = require('./tcp') - protocols.ssl = require('./tls') - protocols.tls = require('./tls') - protocols.mqtts = require('./tls') -} else { - protocols.wx = require('./wx') - protocols.wxs = require('./wx') - - protocols.ali = require('./ali') - protocols.alis = require('./ali') -} - -protocols.ws = require('./ws') -protocols.wss = require('./ws') - -/** - * Parse the auth attribute and merge username and password in the options object. - * - * @param {Object} [opts] option object - */ -function parseAuthOptions (opts) { - var matches - if (opts.auth) { - matches = opts.auth.match(/^(.+):(.+)$/) - if (matches) { - opts.username = matches[1] - opts.password = matches[2] - } else { - opts.username = opts.auth - } - } -} - -/** - * connect - connect to an MQTT broker. - * - * @param {String} [brokerUrl] - url of the broker, optional - * @param {Object} opts - see MqttClient#constructor - */ -function connect (brokerUrl, opts) { - debug('connecting to an MQTT broker...') - if ((typeof brokerUrl === 'object') && !opts) { - opts = brokerUrl - brokerUrl = null - } - - opts = opts || {} - - if (brokerUrl) { - var parsed = url.parse(brokerUrl, true) - if (parsed.port != null) { - parsed.port = Number(parsed.port) - } - - opts = xtend(parsed, opts) - - if (opts.protocol === null) { - throw new Error('Missing protocol') - } - - opts.protocol = opts.protocol.replace(/:$/, '') - } - - // merge in the auth options if supplied - parseAuthOptions(opts) - - // support clientId passed in the query string of the url - if (opts.query && typeof opts.query.clientId === 'string') { - opts.clientId = opts.query.clientId - } - - if (opts.cert && opts.key) { - if (opts.protocol) { - if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { - switch (opts.protocol) { - case 'mqtt': - opts.protocol = 'mqtts' - break - case 'ws': - opts.protocol = 'wss' - break - case 'wx': - opts.protocol = 'wxs' - break - case 'ali': - opts.protocol = 'alis' - break - default: - throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!') - } - } - } else { - // A cert and key was provided, however no protocol was specified, so we will throw an error. - throw new Error('Missing secure protocol key') - } - } - - if (!protocols[opts.protocol]) { - var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 - opts.protocol = [ - 'mqtt', - 'mqtts', - 'ws', - 'wss', - 'wx', - 'wxs', - 'ali', - 'alis' - ].filter(function (key, index) { - if (isSecure && index % 2 === 0) { - // Skip insecure protocols when requesting a secure one. - return false - } - return (typeof protocols[key] === 'function') - })[0] - } - - if (opts.clean === false && !opts.clientId) { - throw new Error('Missing clientId for unclean clients') - } - - if (opts.protocol) { - opts.defaultProtocol = opts.protocol - } - - function wrapper (client) { - if (opts.servers) { - if (!client._reconnectCount || client._reconnectCount === opts.servers.length) { - client._reconnectCount = 0 - } - - opts.host = opts.servers[client._reconnectCount].host - opts.port = opts.servers[client._reconnectCount].port - opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol) - opts.hostname = opts.host - - client._reconnectCount++ - } - - debug('calling streambuilder for', opts.protocol) - return protocols[opts.protocol](client, opts) - } - var client = new MqttClient(wrapper, opts) - client.on('error', function () { /* Automatically set up client error handling */ }) - return client -} - -module.exports = connect -module.exports.connect = connect -module.exports.MqttClient = MqttClient -module.exports.Store = Store +'use strict' + +var MqttClient = require('../client') +var Store = require('../store') +var url = require('url') +var xtend = require('xtend') +var debug = require('debug')('mqttjs') + +var protocols = {} + +// eslint-disable-next-line camelcase +if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ !== 'function') { + protocols.mqtt = require('./tcp') + protocols.tcp = require('./tcp') + protocols.ssl = require('./tls') + protocols.tls = require('./tls') + protocols.mqtts = require('./tls') +} else { + protocols.wx = require('./wx') + protocols.wxs = require('./wx') + + protocols.ali = require('./ali') + protocols.alis = require('./ali') +} + +protocols.ws = require('./ws') +protocols.wss = require('./ws') + +/** + * Parse the auth attribute and merge username and password in the options object. + * + * @param {Object} [opts] option object + */ +function parseAuthOptions (opts) { + var matches + if (opts.auth) { + matches = opts.auth.match(/^(.+):(.+)$/) + if (matches) { + opts.username = matches[1] + opts.password = matches[2] + } else { + opts.username = opts.auth + } + } +} + +/** + * connect - connect to an MQTT broker. + * + * @param {String} [brokerUrl] - url of the broker, optional + * @param {Object} opts - see MqttClient#constructor + */ +function connect (brokerUrl, opts) { + debug('connecting to an MQTT broker...') + if ((typeof brokerUrl === 'object') && !opts) { + opts = brokerUrl + brokerUrl = null + } + + opts = opts || {} + + if (brokerUrl) { + var parsed = url.parse(brokerUrl, true) + if (parsed.port != null) { + parsed.port = Number(parsed.port) + } + + opts = xtend(parsed, opts) + + if (opts.protocol === null) { + throw new Error('Missing protocol') + } + + opts.protocol = opts.protocol.replace(/:$/, '') + } + + // merge in the auth options if supplied + parseAuthOptions(opts) + + // support clientId passed in the query string of the url + if (opts.query && typeof opts.query.clientId === 'string') { + opts.clientId = opts.query.clientId + } + + if (opts.cert && opts.key) { + if (opts.protocol) { + if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { + switch (opts.protocol) { + case 'mqtt': + opts.protocol = 'mqtts' + break + case 'ws': + opts.protocol = 'wss' + break + case 'wx': + opts.protocol = 'wxs' + break + case 'ali': + opts.protocol = 'alis' + break + default: + throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!') + } + } + } else { + // A cert and key was provided, however no protocol was specified, so we will throw an error. + throw new Error('Missing secure protocol key') + } + } + + if (!protocols[opts.protocol]) { + var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 + opts.protocol = [ + 'mqtt', + 'mqtts', + 'ws', + 'wss', + 'wx', + 'wxs', + 'ali', + 'alis' + ].filter(function (key, index) { + if (isSecure && index % 2 === 0) { + // Skip insecure protocols when requesting a secure one. + return false + } + return (typeof protocols[key] === 'function') + })[0] + } + + if (opts.clean === false && !opts.clientId) { + throw new Error('Missing clientId for unclean clients') + } + + if (opts.protocol) { + opts.defaultProtocol = opts.protocol + } + + function wrapper (client) { + if (opts.servers) { + if (!client._reconnectCount || client._reconnectCount === opts.servers.length) { + client._reconnectCount = 0 + } + + opts.host = opts.servers[client._reconnectCount].host + opts.port = opts.servers[client._reconnectCount].port + opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol) + opts.hostname = opts.host + + client._reconnectCount++ + } + + debug('calling streambuilder for', opts.protocol) + return protocols[opts.protocol](client, opts) + } + var client = new MqttClient(wrapper, opts) + client.on('error', function () { /* Automatically set up client error handling */ }) + return client +} + +module.exports = connect +module.exports.connect = connect +module.exports.MqttClient = MqttClient +module.exports.Store = Store diff --git a/lib/connect/tcp.js b/lib/connect/tcp.js index 3fe2c0922..9912102eb 100644 --- a/lib/connect/tcp.js +++ b/lib/connect/tcp.js @@ -1,21 +1,21 @@ -'use strict' -var net = require('net') -var debug = require('debug')('mqttjs:tcp') - -/* - variables port and host can be removed since - you have all required information in opts object -*/ -function streamBuilder (client, opts) { - var port, host - opts.port = opts.port || 1883 - opts.hostname = opts.hostname || opts.host || 'localhost' - - port = opts.port - host = opts.hostname - - debug('port %d and host %s', port, host) - return net.createConnection(port, host) -} - -module.exports = streamBuilder +'use strict' +var net = require('net') +var debug = require('debug')('mqttjs:tcp') + +/* + variables port and host can be removed since + you have all required information in opts object +*/ +function streamBuilder (client, opts) { + var port, host + opts.port = opts.port || 1883 + opts.hostname = opts.hostname || opts.host || 'localhost' + + port = opts.port + host = opts.hostname + + debug('port %d and host %s', port, host) + return net.createConnection(port, host) +} + +module.exports = streamBuilder diff --git a/lib/connect/tls.js b/lib/connect/tls.js index 226bff8b3..aac296666 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -1,45 +1,45 @@ -'use strict' -var tls = require('tls') -var debug = require('debug')('mqttjs:tls') - -function buildBuilder (mqttClient, opts) { - var connection - opts.port = opts.port || 8883 - opts.host = opts.hostname || opts.host || 'localhost' - opts.servername = opts.host - - opts.rejectUnauthorized = opts.rejectUnauthorized !== false - - delete opts.path - - debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) - - connection = tls.connect(opts) - /* eslint no-use-before-define: [2, "nofunc"] */ - connection.on('secureConnect', function () { - if (opts.rejectUnauthorized && !connection.authorized) { - connection.emit('error', new Error('TLS not authorized')) - } else { - connection.removeListener('error', handleTLSerrors) - } - }) - - function handleTLSerrors (err) { - // How can I get verify this error is a tls error? - if (opts.rejectUnauthorized) { - mqttClient.emit('error', err) - } - - // close this connection to match the behaviour of net - // otherwise all we get is an error from the connection - // and close event doesn't fire. This is a work around - // to enable the reconnect code to work the same as with - // net.createConnection - connection.end() - } - - connection.on('error', handleTLSerrors) - return connection -} - -module.exports = buildBuilder +'use strict' +var tls = require('tls') +var debug = require('debug')('mqttjs:tls') + +function buildBuilder (mqttClient, opts) { + var connection + opts.port = opts.port || 8883 + opts.host = opts.hostname || opts.host || 'localhost' + opts.servername = opts.host + + opts.rejectUnauthorized = opts.rejectUnauthorized !== false + + delete opts.path + + debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) + + connection = tls.connect(opts) + /* eslint no-use-before-define: [2, "nofunc"] */ + connection.on('secureConnect', function () { + if (opts.rejectUnauthorized && !connection.authorized) { + connection.emit('error', new Error('TLS not authorized')) + } else { + connection.removeListener('error', handleTLSerrors) + } + }) + + function handleTLSerrors (err) { + // How can I get verify this error is a tls error? + if (opts.rejectUnauthorized) { + mqttClient.emit('error', err) + } + + // close this connection to match the behaviour of net + // otherwise all we get is an error from the connection + // and close event doesn't fire. This is a work around + // to enable the reconnect code to work the same as with + // net.createConnection + connection.end() + } + + connection.on('error', handleTLSerrors) + return connection +} + +module.exports = buildBuilder diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 18646a5a1..5c1d2c691 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,256 +1,256 @@ -'use strict' - -const WS = require('ws') -const debug = require('debug')('mqttjs:ws') -const duplexify = require('duplexify') -const Transform = require('readable-stream').Transform - -let WSS_OPTIONS = [ - 'rejectUnauthorized', - 'ca', - 'cert', - 'key', - 'pfx', - 'passphrase' -] -// eslint-disable-next-line camelcase -const IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' -function buildUrl (opts, client) { - let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function setDefaultOpts (opts) { - let options = opts - if (!opts.hostname) { - options.hostname = 'localhost' - } - if (!opts.port) { - if (opts.protocol === 'wss') { - options.port = 443 - } else { - options.port = 80 - } - } - if (!opts.path) { - options.path = '/' - } - - if (!opts.wsOptions) { - options.wsOptions = {} - } - if (!IS_BROWSER && opts.protocol === 'wss') { - // Add cert/key/ca etc options - WSS_OPTIONS.forEach(function (prop) { - if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) { - options.wsOptions[prop] = opts[prop] - } - }) - } - - return options -} - -function setDefaultBrowserOpts (opts) { - let options = setDefaultOpts(opts) - - if (!options.hostname) { - options.hostname = options.host - } - - if (!options.hostname) { - // Throwing an error in a Web Worker if no `hostname` is given, because we - // can not determine the `hostname` automatically. If connecting to - // localhost, please supply the `hostname` as an argument. - if (typeof (document) === 'undefined') { - throw new Error('Could not determine host. Specify host manually.') - } - const parsed = new URL(document.URL) - options.hostname = parsed.hostname - - if (!options.port) { - options.port = parsed.port - } - } - - // objectMode should be defined for logic - if (options.objectMode === undefined) { - options.objectMode = !(options.binary === true || options.binary === undefined) - } - - return options -} - -function createWebSocket (client, url, opts) { - debug('createWebSocket') - debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) - let socket = new WS(url, [websocketSubProtocol], opts.wsOptions) - return socket -} - -function createBrowserWebSocket (client, opts) { - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - let url = buildUrl(opts, client) - /* global WebSocket */ - let socket = new WebSocket(url, [websocketSubProtocol]) - socket.binaryType = 'arraybuffer' - return socket -} - -function streamBuilder (client, opts) { - debug('streamBuilder') - let options = setDefaultOpts(opts) - const url = buildUrl(options, client) - let socket = createWebSocket(client, url, options) - let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) - webSocketStream.url = url - socket.on('close', () => { webSocketStream.destroy() }) - return webSocketStream -} - -function browserStreamBuilder (client, opts) { - debug('browserStreamBuilder') - let stream - let options = setDefaultBrowserOpts(opts) - // sets the maximum socket buffer size before throttling - const bufferSize = options.browserBufferSize || 1024 * 512 - - const bufferTimeout = opts.browserBufferTimeout || 1000 - - const coerceToBuffer = !opts.objectMode - - let socket = createBrowserWebSocket(client, opts) - - let proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) - - if (!opts.objectMode) { - proxy._writev = writev - } - proxy.on('close', () => { socket.close() }) - - const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') - - // was already open when passed in - if (socket.readyState === socket.OPEN) { - stream = proxy - } else { - stream = stream = duplexify(undefined, undefined, opts) - if (!opts.objectMode) { - stream._writev = writev - } - - if (eventListenerSupport) { - socket.addEventListener('open', onopen) - } else { - socket.onopen = onopen - } - } - - stream.socket = socket - - if (eventListenerSupport) { - socket.addEventListener('close', onclose) - socket.addEventListener('error', onerror) - socket.addEventListener('message', onmessage) - } else { - socket.onclose = onclose - socket.onerror = onerror - socket.onmessage = onmessage - } - - // methods for browserStreamBuilder - - function buildProxy (options, socketWrite, socketEnd) { - let proxy = new Transform({ - objectModeMode: options.objectMode - }) - - proxy._write = socketWrite - proxy._flush = socketEnd - - return proxy - } - - function onopen () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - } - - function onclose () { - stream.end() - stream.destroy() - } - - function onerror (err) { - stream.destroy(err) - } - - function onmessage (event) { - let data = event.data - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - } - - // this is to be enabled only if objectMode is false - function writev (chunks, cb) { - const buffers = new Array(chunks.length) - for (let i = 0; i < chunks.length; i++) { - if (typeof chunks[i].chunk === 'string') { - buffers[i] = Buffer.from(chunks[i], 'utf8') - } else { - buffers[i] = chunks[i].chunk - } - } - - this._write(Buffer.concat(buffers), 'binary', cb) - } - - function socketWriteBrowser (chunk, enc, next) { - if (socket.bufferedAmount > bufferSize) { - // throttle data until buffered amount is reduced. - setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) - } - - if (coerceToBuffer && typeof chunk === 'string') { - chunk = Buffer.from(chunk, 'utf8') - } - - try { - socket.send(chunk) - } catch (err) { - return next(err) - } - - next() - } - - function socketEndBrowser (done) { - socket.close() - done() - } - - // end methods for browserStreamBuilder - - return stream -} - -if (IS_BROWSER) { - module.exports = browserStreamBuilder -} else { - module.exports = streamBuilder -} +'use strict' + +const WS = require('ws') +const debug = require('debug')('mqttjs:ws') +const duplexify = require('duplexify') +const Transform = require('readable-stream').Transform + +let WSS_OPTIONS = [ + 'rejectUnauthorized', + 'ca', + 'cert', + 'key', + 'pfx', + 'passphrase' +] +// eslint-disable-next-line camelcase +const IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' +function buildUrl (opts, client) { + let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function setDefaultOpts (opts) { + let options = opts + if (!opts.hostname) { + options.hostname = 'localhost' + } + if (!opts.port) { + if (opts.protocol === 'wss') { + options.port = 443 + } else { + options.port = 80 + } + } + if (!opts.path) { + options.path = '/' + } + + if (!opts.wsOptions) { + options.wsOptions = {} + } + if (!IS_BROWSER && opts.protocol === 'wss') { + // Add cert/key/ca etc options + WSS_OPTIONS.forEach(function (prop) { + if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) { + options.wsOptions[prop] = opts[prop] + } + }) + } + + return options +} + +function setDefaultBrowserOpts (opts) { + let options = setDefaultOpts(opts) + + if (!options.hostname) { + options.hostname = options.host + } + + if (!options.hostname) { + // Throwing an error in a Web Worker if no `hostname` is given, because we + // can not determine the `hostname` automatically. If connecting to + // localhost, please supply the `hostname` as an argument. + if (typeof (document) === 'undefined') { + throw new Error('Could not determine host. Specify host manually.') + } + const parsed = new URL(document.URL) + options.hostname = parsed.hostname + + if (!options.port) { + options.port = parsed.port + } + } + + // objectMode should be defined for logic + if (options.objectMode === undefined) { + options.objectMode = !(options.binary === true || options.binary === undefined) + } + + return options +} + +function createWebSocket (client, url, opts) { + debug('createWebSocket') + debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) + let socket = new WS(url, [websocketSubProtocol], opts.wsOptions) + return socket +} + +function createBrowserWebSocket (client, opts) { + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + let url = buildUrl(opts, client) + /* global WebSocket */ + let socket = new WebSocket(url, [websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + return socket +} + +function streamBuilder (client, opts) { + debug('streamBuilder') + let options = setDefaultOpts(opts) + const url = buildUrl(options, client) + let socket = createWebSocket(client, url, options) + let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) + webSocketStream.url = url + socket.on('close', () => { webSocketStream.destroy() }) + return webSocketStream +} + +function browserStreamBuilder (client, opts) { + debug('browserStreamBuilder') + let stream + let options = setDefaultBrowserOpts(opts) + // sets the maximum socket buffer size before throttling + const bufferSize = options.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + let socket = createBrowserWebSocket(client, opts) + + let proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) + + if (!opts.objectMode) { + proxy._writev = writev + } + proxy.on('close', () => { socket.close() }) + + const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') + + // was already open when passed in + if (socket.readyState === socket.OPEN) { + stream = proxy + } else { + stream = stream = duplexify(undefined, undefined, opts) + if (!opts.objectMode) { + stream._writev = writev + } + + if (eventListenerSupport) { + socket.addEventListener('open', onopen) + } else { + socket.onopen = onopen + } + } + + stream.socket = socket + + if (eventListenerSupport) { + socket.addEventListener('close', onclose) + socket.addEventListener('error', onerror) + socket.addEventListener('message', onmessage) + } else { + socket.onclose = onclose + socket.onerror = onerror + socket.onmessage = onmessage + } + + // methods for browserStreamBuilder + + function buildProxy (options, socketWrite, socketEnd) { + let proxy = new Transform({ + objectModeMode: options.objectMode + }) + + proxy._write = socketWrite + proxy._flush = socketEnd + + return proxy + } + + function onopen () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + } + + function onclose () { + stream.end() + stream.destroy() + } + + function onerror (err) { + stream.destroy(err) + } + + function onmessage (event) { + let data = event.data + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + } + + // this is to be enabled only if objectMode is false + function writev (chunks, cb) { + const buffers = new Array(chunks.length) + for (let i = 0; i < chunks.length; i++) { + if (typeof chunks[i].chunk === 'string') { + buffers[i] = Buffer.from(chunks[i], 'utf8') + } else { + buffers[i] = chunks[i].chunk + } + } + + this._write(Buffer.concat(buffers), 'binary', cb) + } + + function socketWriteBrowser (chunk, enc, next) { + if (socket.bufferedAmount > bufferSize) { + // throttle data until buffered amount is reduced. + setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) + } + + if (coerceToBuffer && typeof chunk === 'string') { + chunk = Buffer.from(chunk, 'utf8') + } + + try { + socket.send(chunk) + } catch (err) { + return next(err) + } + + next() + } + + function socketEndBrowser (done) { + socket.close() + done() + } + + // end methods for browserStreamBuilder + + return stream +} + +if (IS_BROWSER) { + module.exports = browserStreamBuilder +} else { + module.exports = streamBuilder +} diff --git a/lib/connect/wx.js b/lib/connect/wx.js index 2b675079a..b9c7a0705 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -1,134 +1,134 @@ -'use strict' - -var Transform = require('readable-stream').Transform -var duplexify = require('duplexify') - -/* global wx */ -var socketTask -var proxy -var stream - -function buildProxy () { - var proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - socketTask.send({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function (errMsg) { - next(new Error(errMsg)) - } - }) - } - proxy._flush = function socketEnd (done) { - socketTask.close({ - success: function () { - done() - } - }) - } - - return proxy -} - -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } -} - -function buildUrl (opts, client) { - var protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' - var url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function bindEventHandler () { - socketTask.onOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - socketTask.onMessage(function (res) { - var data = res.data - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - - socketTask.onClose(function () { - stream.end() - stream.destroy() - }) - - socketTask.onError(function (res) { - stream.destroy(new Error(res.errMsg)) - }) -} - -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host - - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } - - var websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - setDefaultOpts(opts) - - var url = buildUrl(opts, client) - socketTask = wx.connectSocket({ - url: url, - protocols: [websocketSubProtocol] - }) - - proxy = buildProxy() - stream = duplexify.obj() - stream._destroy = function (err, cb) { - socketTask.close({ - success: function () { - cb && cb(err) - } - }) - } - - var destroyRef = stream.destroy - stream.destroy = function () { - stream.destroy = destroyRef - - var self = this - setTimeout(function () { - socketTask.close({ - fail: function () { - self._destroy(new Error()) - } - }) - }, 0) - }.bind(stream) - - bindEventHandler() - - return stream -} - -module.exports = buildStream +'use strict' + +var Transform = require('readable-stream').Transform +var duplexify = require('duplexify') + +/* global wx */ +var socketTask +var proxy +var stream + +function buildProxy () { + var proxy = new Transform() + proxy._write = function (chunk, encoding, next) { + socketTask.send({ + data: chunk.buffer, + success: function () { + next() + }, + fail: function (errMsg) { + next(new Error(errMsg)) + } + }) + } + proxy._flush = function socketEnd (done) { + socketTask.close({ + success: function () { + done() + } + }) + } + + return proxy +} + +function setDefaultOpts (opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } +} + +function buildUrl (opts, client) { + var protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' + var url = protocol + '://' + opts.hostname + opts.path + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path + } + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function bindEventHandler () { + socketTask.onOpen(function () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + socketTask.onMessage(function (res) { + var data = res.data + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + + socketTask.onClose(function () { + stream.end() + stream.destroy() + }) + + socketTask.onError(function (res) { + stream.destroy(new Error(res.errMsg)) + }) +} + +function buildStream (client, opts) { + opts.hostname = opts.hostname || opts.host + + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } + + var websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + setDefaultOpts(opts) + + var url = buildUrl(opts, client) + socketTask = wx.connectSocket({ + url: url, + protocols: [websocketSubProtocol] + }) + + proxy = buildProxy() + stream = duplexify.obj() + stream._destroy = function (err, cb) { + socketTask.close({ + success: function () { + cb && cb(err) + } + }) + } + + var destroyRef = stream.destroy + stream.destroy = function () { + stream.destroy = destroyRef + + var self = this + setTimeout(function () { + socketTask.close({ + fail: function () { + self._destroy(new Error()) + } + }) + }, 0) + }.bind(stream) + + bindEventHandler() + + return stream +} + +module.exports = buildStream diff --git a/lib/default-message-id-provider.js b/lib/default-message-id-provider.js index d1bcc9ed0..c0a953f3f 100644 --- a/lib/default-message-id-provider.js +++ b/lib/default-message-id-provider.js @@ -1,69 +1,69 @@ -'use strict' - -/** - * DefaultMessageAllocator constructor - * @constructor - */ -function DefaultMessageIdProvider () { - if (!(this instanceof DefaultMessageIdProvider)) { - return new DefaultMessageIdProvider() - } - - /** - * MessageIDs starting with 1 - * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 - */ - this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) -} - -/** - * allocate - * - * Get the next messageId. - * @return unsigned int - */ -DefaultMessageIdProvider.prototype.allocate = function () { - // id becomes current state of this.nextId and increments afterwards - var id = this.nextId++ - // Ensure 16 bit unsigned int (max 65535, nextId got one higher) - if (this.nextId === 65536) { - this.nextId = 1 - } - return id -} - -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -DefaultMessageIdProvider.prototype.getLastAllocated = function () { - return (this.nextId === 1) ? 65535 : (this.nextId - 1) -} - -/** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ -DefaultMessageIdProvider.prototype.register = function (messageId) { - return true -} - -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -DefaultMessageIdProvider.prototype.deallocate = function (messageId) { -} - -/** - * clear - * Deallocate all messageIds. - */ -DefaultMessageIdProvider.prototype.clear = function () { -} - -module.exports = DefaultMessageIdProvider +'use strict' + +/** + * DefaultMessageAllocator constructor + * @constructor + */ +function DefaultMessageIdProvider () { + if (!(this instanceof DefaultMessageIdProvider)) { + return new DefaultMessageIdProvider() + } + + /** + * MessageIDs starting with 1 + * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 + */ + this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) +} + +/** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.allocate = function () { + // id becomes current state of this.nextId and increments afterwards + var id = this.nextId++ + // Ensure 16 bit unsigned int (max 65535, nextId got one higher) + if (this.nextId === 65536) { + this.nextId = 1 + } + return id +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.getLastAllocated = function () { + return (this.nextId === 1) ? 65535 : (this.nextId - 1) +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +DefaultMessageIdProvider.prototype.register = function (messageId) { + return true +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +DefaultMessageIdProvider.prototype.deallocate = function (messageId) { +} + +/** + * clear + * Deallocate all messageIds. + */ +DefaultMessageIdProvider.prototype.clear = function () { +} + +module.exports = DefaultMessageIdProvider diff --git a/lib/store.js b/lib/store.js index 37809750b..efbfabf09 100644 --- a/lib/store.js +++ b/lib/store.js @@ -1,128 +1,128 @@ -'use strict' - -/** - * Module dependencies - */ -var xtend = require('xtend') - -var Readable = require('readable-stream').Readable -var streamsOpts = { objectMode: true } -var defaultStoreOptions = { - clean: true -} - -/** - * In-memory implementation of the message store - * This can actually be saved into files. - * - * @param {Object} [options] - store options - */ -function Store (options) { - if (!(this instanceof Store)) { - return new Store(options) - } - - this.options = options || {} - - // Defaults - this.options = xtend(defaultStoreOptions, options) - - this._inflights = new Map() -} - -/** - * Adds a packet to the store, a packet is - * anything that has a messageId property. - * - */ -Store.prototype.put = function (packet, cb) { - this._inflights.set(packet.messageId, packet) - - if (cb) { - cb() - } - - return this -} - -/** - * Creates a stream with all the packets in the store - * - */ -Store.prototype.createStream = function () { - var stream = new Readable(streamsOpts) - var destroyed = false - var values = [] - var i = 0 - - this._inflights.forEach(function (value, key) { - values.push(value) - }) - - stream._read = function () { - if (!destroyed && i < values.length) { - this.push(values[i++]) - } else { - this.push(null) - } - } - - stream.destroy = function () { - if (destroyed) { - return - } - - var self = this - - destroyed = true - - setTimeout(function () { - self.emit('close') - }, 0) - } - - return stream -} - -/** - * deletes a packet from the store. - */ -Store.prototype.del = function (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - this._inflights.delete(packet.messageId) - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this -} - -/** - * get a packet from the store. - */ -Store.prototype.get = function (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this -} - -/** - * Close the store - */ -Store.prototype.close = function (cb) { - if (this.options.clean) { - this._inflights = null - } - if (cb) { - cb() - } -} - -module.exports = Store +'use strict' + +/** + * Module dependencies + */ +var xtend = require('xtend') + +var Readable = require('readable-stream').Readable +var streamsOpts = { objectMode: true } +var defaultStoreOptions = { + clean: true +} + +/** + * In-memory implementation of the message store + * This can actually be saved into files. + * + * @param {Object} [options] - store options + */ +function Store (options) { + if (!(this instanceof Store)) { + return new Store(options) + } + + this.options = options || {} + + // Defaults + this.options = xtend(defaultStoreOptions, options) + + this._inflights = new Map() +} + +/** + * Adds a packet to the store, a packet is + * anything that has a messageId property. + * + */ +Store.prototype.put = function (packet, cb) { + this._inflights.set(packet.messageId, packet) + + if (cb) { + cb() + } + + return this +} + +/** + * Creates a stream with all the packets in the store + * + */ +Store.prototype.createStream = function () { + var stream = new Readable(streamsOpts) + var destroyed = false + var values = [] + var i = 0 + + this._inflights.forEach(function (value, key) { + values.push(value) + }) + + stream._read = function () { + if (!destroyed && i < values.length) { + this.push(values[i++]) + } else { + this.push(null) + } + } + + stream.destroy = function () { + if (destroyed) { + return + } + + var self = this + + destroyed = true + + setTimeout(function () { + self.emit('close') + }, 0) + } + + return stream +} + +/** + * deletes a packet from the store. + */ +Store.prototype.del = function (packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + this._inflights.delete(packet.messageId) + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this +} + +/** + * get a packet from the store. + */ +Store.prototype.get = function (packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this +} + +/** + * Close the store + */ +Store.prototype.close = function (cb) { + if (this.options.clean) { + this._inflights = null + } + if (cb) { + cb() + } +} + +module.exports = Store diff --git a/lib/topic-alias-send.js b/lib/topic-alias-send.js index 71b10468a..f3abf2084 100644 --- a/lib/topic-alias-send.js +++ b/lib/topic-alias-send.js @@ -1,93 +1,93 @@ -'use strict' - -/** - * Module dependencies - */ -var LruMap = require('collections/lru-map') -var NumberAllocator = require('number-allocator').NumberAllocator - -/** - * Topic Alias sending manager - * This holds both topic to alias and alias to topic map - * @param {Number} [max] - topic alias maximum entries - */ -function TopicAliasSend (max) { - if (!(this instanceof TopicAliasSend)) { - return new TopicAliasSend(max) - } - - if (max > 0) { - this.aliasToTopic = new LruMap() - this.topicToAlias = {} - this.numberAllocator = new NumberAllocator(1, max) - this.max = max - this.length = 0 - } -} - -/** - * Insert or update topic - alias entry. - * @param {String} [topic] - topic - * @param {Number} [alias] - topic alias - * @returns {Boolean} - if success return true otherwise false - */ -TopicAliasSend.prototype.put = function (topic, alias) { - if (alias === 0 || alias > this.max) { - return false - } - const entry = this.aliasToTopic.get(alias) - if (entry) { - delete this.topicToAlias[entry.topic] - } - this.aliasToTopic.set(alias, {'topic': topic, 'alias': alias}) - this.topicToAlias[topic] = alias - this.numberAllocator.use(alias) - this.length = this.aliasToTopic.length - return true -} - -/** - * Get topic by alias - * @param {Number} [alias] - topic alias - * @returns {String} - if mapped topic exists return topic, otherwise return undefined - */ -TopicAliasSend.prototype.getTopicByAlias = function (alias) { - const entry = this.aliasToTopic.get(alias) - if (typeof entry === 'undefined') return entry - return entry.topic -} - -/** - * Get topic by alias - * @param {String} [topic] - topic - * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined - */ -TopicAliasSend.prototype.getAliasByTopic = function (topic) { - const alias = this.topicToAlias[topic] - if (typeof alias !== 'undefined') { - this.aliasToTopic.get(alias) // LRU update - } - return alias -} - -/** - * Clear all entries - */ -TopicAliasSend.prototype.clear = function () { - this.aliasToTopic.clear() - this.topicToAlias = {} - this.numberAllocator.clear() - this.length = 0 -} - -/** - * Get Least Recently Used (LRU) topic alias - * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias - */ -TopicAliasSend.prototype.getLruAlias = function () { - const alias = this.numberAllocator.firstVacant() - if (alias) return alias - return this.aliasToTopic.min().alias -} - -module.exports = TopicAliasSend +'use strict' + +/** + * Module dependencies + */ +var LruMap = require('collections/lru-map') +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * Topic Alias sending manager + * This holds both topic to alias and alias to topic map + * @param {Number} [max] - topic alias maximum entries + */ +function TopicAliasSend (max) { + if (!(this instanceof TopicAliasSend)) { + return new TopicAliasSend(max) + } + + if (max > 0) { + this.aliasToTopic = new LruMap() + this.topicToAlias = {} + this.numberAllocator = new NumberAllocator(1, max) + this.max = max + this.length = 0 + } +} + +/** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ +TopicAliasSend.prototype.put = function (topic, alias) { + if (alias === 0 || alias > this.max) { + return false + } + const entry = this.aliasToTopic.get(alias) + if (entry) { + delete this.topicToAlias[entry.topic] + } + this.aliasToTopic.set(alias, {'topic': topic, 'alias': alias}) + this.topicToAlias[topic] = alias + this.numberAllocator.use(alias) + this.length = this.aliasToTopic.length + return true +} + +/** + * Get topic by alias + * @param {Number} [alias] - topic alias + * @returns {String} - if mapped topic exists return topic, otherwise return undefined + */ +TopicAliasSend.prototype.getTopicByAlias = function (alias) { + const entry = this.aliasToTopic.get(alias) + if (typeof entry === 'undefined') return entry + return entry.topic +} + +/** + * Get topic by alias + * @param {String} [topic] - topic + * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined + */ +TopicAliasSend.prototype.getAliasByTopic = function (topic) { + const alias = this.topicToAlias[topic] + if (typeof alias !== 'undefined') { + this.aliasToTopic.get(alias) // LRU update + } + return alias +} + +/** + * Clear all entries + */ +TopicAliasSend.prototype.clear = function () { + this.aliasToTopic.clear() + this.topicToAlias = {} + this.numberAllocator.clear() + this.length = 0 +} + +/** + * Get Least Recently Used (LRU) topic alias + * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias + */ +TopicAliasSend.prototype.getLruAlias = function () { + const alias = this.numberAllocator.firstVacant() + if (alias) return alias + return this.aliasToTopic.min().alias +} + +module.exports = TopicAliasSend diff --git a/lib/unique-message-id-provider.js b/lib/unique-message-id-provider.js index 20e59977f..6ffd4bde6 100644 --- a/lib/unique-message-id-provider.js +++ b/lib/unique-message-id-provider.js @@ -1,65 +1,65 @@ -'use strict' - -var NumberAllocator = require('number-allocator').NumberAllocator - -/** - * UniqueMessageAllocator constructor - * @constructor - */ -function UniqueMessageIdProvider () { - if (!(this instanceof UniqueMessageIdProvider)) { - return new UniqueMessageIdProvider() - } - - this.numberAllocator = new NumberAllocator(1, 65535) -} - -/** - * allocate - * - * Get the next messageId. - * @return if messageId is fully allocated then return null, - * otherwise return the smallest usable unsigned int messageId. - */ -UniqueMessageIdProvider.prototype.allocate = function () { - this.lastId = this.numberAllocator.alloc() - return this.lastId -} - -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -UniqueMessageIdProvider.prototype.getLastAllocated = function () { - return this.lastId -} - -/** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ -UniqueMessageIdProvider.prototype.register = function (messageId) { - return this.numberAllocator.use(messageId) -} - -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -UniqueMessageIdProvider.prototype.deallocate = function (messageId) { - this.numberAllocator.free(messageId) -} - -/** - * clear - * Deallocate all messageIds. - */ -UniqueMessageIdProvider.prototype.clear = function () { - this.numberAllocator.clear() -} - -module.exports = UniqueMessageIdProvider +'use strict' + +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * UniqueMessageAllocator constructor + * @constructor + */ +function UniqueMessageIdProvider () { + if (!(this instanceof UniqueMessageIdProvider)) { + return new UniqueMessageIdProvider() + } + + this.numberAllocator = new NumberAllocator(1, 65535) +} + +/** + * allocate + * + * Get the next messageId. + * @return if messageId is fully allocated then return null, + * otherwise return the smallest usable unsigned int messageId. + */ +UniqueMessageIdProvider.prototype.allocate = function () { + this.lastId = this.numberAllocator.alloc() + return this.lastId +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +UniqueMessageIdProvider.prototype.getLastAllocated = function () { + return this.lastId +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +UniqueMessageIdProvider.prototype.register = function (messageId) { + return this.numberAllocator.use(messageId) +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +UniqueMessageIdProvider.prototype.deallocate = function (messageId) { + this.numberAllocator.free(messageId) +} + +/** + * clear + * Deallocate all messageIds. + */ +UniqueMessageIdProvider.prototype.clear = function () { + this.numberAllocator.clear() +} + +module.exports = UniqueMessageIdProvider diff --git a/lib/validations.js b/lib/validations.js index 452e3ba1a..1a3277901 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -1,52 +1,52 @@ -'use strict' - -/** - * Validate a topic to see if it's valid or not. - * A topic is valid if it follow below rules: - * - Rule #1: If any part of the topic is not `+` or `#`, then it must not contain `+` and '#' - * - Rule #2: Part `#` must be located at the end of the mailbox - * - * @param {String} topic - A topic - * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. - */ -function validateTopic (topic) { - var parts = topic.split('/') - - for (var i = 0; i < parts.length; i++) { - if (parts[i] === '+') { - continue - } - - if (parts[i] === '#') { - // for Rule #2 - return i === parts.length - 1 - } - - if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { - return false - } - } - - return true -} - -/** - * Validate an array of topics to see if any of them is valid or not - * @param {Array} topics - Array of topics - * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one - */ -function validateTopics (topics) { - if (topics.length === 0) { - return 'empty_topic_list' - } - for (var i = 0; i < topics.length; i++) { - if (!validateTopic(topics[i])) { - return topics[i] - } - } - return null -} - -module.exports = { - validateTopics: validateTopics -} +'use strict' + +/** + * Validate a topic to see if it's valid or not. + * A topic is valid if it follow below rules: + * - Rule #1: If any part of the topic is not `+` or `#`, then it must not contain `+` and '#' + * - Rule #2: Part `#` must be located at the end of the mailbox + * + * @param {String} topic - A topic + * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. + */ +function validateTopic (topic) { + var parts = topic.split('/') + + for (var i = 0; i < parts.length; i++) { + if (parts[i] === '+') { + continue + } + + if (parts[i] === '#') { + // for Rule #2 + return i === parts.length - 1 + } + + if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { + return false + } + } + + return true +} + +/** + * Validate an array of topics to see if any of them is valid or not + * @param {Array} topics - Array of topics + * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one + */ +function validateTopics (topics) { + if (topics.length === 0) { + return 'empty_topic_list' + } + for (var i = 0; i < topics.length; i++) { + if (!validateTopic(topics[i])) { + return topics[i] + } + } + return null +} + +module.exports = { + validateTopics: validateTopics +} diff --git a/mqtt.js b/mqtt.js index 56cd6f04e..c8b94fda1 100644 --- a/mqtt.js +++ b/mqtt.js @@ -1,21 +1,21 @@ -/* - * Copyright (c) 2015-2015 MQTT.js contributors. - * Copyright (c) 2011-2014 Adam Rudd. - * - * See LICENSE for more information - */ - -var MqttClient = require('./lib/client') -var connect = require('./lib/connect') -var Store = require('./lib/store') -var DefaultMessageIdProvider = require('./lib/default-message-id-provider') -var UniqueMessageIdProvider = require('./lib/unique-message-id-provider') - -module.exports.connect = connect - -// Expose MqttClient -module.exports.MqttClient = MqttClient -module.exports.Client = MqttClient -module.exports.Store = Store -module.exports.DefaultMessageIdProvider = DefaultMessageIdProvider -module.exports.UniqueMessageIdProvider = UniqueMessageIdProvider +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ + +var MqttClient = require('./lib/client') +var connect = require('./lib/connect') +var Store = require('./lib/store') +var DefaultMessageIdProvider = require('./lib/default-message-id-provider') +var UniqueMessageIdProvider = require('./lib/unique-message-id-provider') + +module.exports.connect = connect + +// Expose MqttClient +module.exports.MqttClient = MqttClient +module.exports.Client = MqttClient +module.exports.Store = Store +module.exports.DefaultMessageIdProvider = DefaultMessageIdProvider +module.exports.UniqueMessageIdProvider = UniqueMessageIdProvider diff --git a/package.json b/package.json index 0549681fe..712dc0350 100644 --- a/package.json +++ b/package.json @@ -1,113 +1,113 @@ -{ - "name": "mqtt", - "description": "A library for the MQTT protocol", - "version": "4.2.8", - "contributors": [ - "Adam Rudd ", - "Matteo Collina (https://github.com/mcollina)", - "Siarhei Buntsevich (https://github.com/scarry1992)", - "Yoseph Maguire (https://github.com/YoDaMa)" - ], - "keywords": [ - "mqtt", - "publish/subscribe", - "publish", - "subscribe" - ], - "license": "MIT", - "repository": { - "type": "git", - "url": "git://github.com/mqttjs/MQTT.js.git" - }, - "main": "mqtt.js", - "types": "types/index.d.ts", - "scripts": { - "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", - "pretest": "standard | snazzy", - "tslint": "tslint types/**/*.d.ts", - "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", - "typescript-compile-execute": "node test/typescript/*.js", - "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", - "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && uglifyjs dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", - "prepare": "npm run browser-build", - "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", - "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", - "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" - }, - "pre-commit": [ - "pretest", - "tslint" - ], - "bin": { - "mqtt_pub": "./bin/pub.js", - "mqtt_sub": "./bin/sub.js", - "mqtt": "./bin/mqtt.js" - }, - "files": [ - "dist/", - "CONTRIBUTING.md", - "doc", - "lib", - "bin", - "types", - "mqtt.js" - ], - "engines": { - "node": ">=10.0.0" - }, - "browser": { - "./mqtt.js": "./lib/connect/index.js", - "fs": false, - "tls": false, - "net": false - }, - "dependencies": { - "collections": "^5.1.12", - "commist": "^1.0.0", - "concat-stream": "^2.0.0", - "debug": "^4.1.1", - "duplexify": "^4.1.1", - "help-me": "^3.0.0", - "inherits": "^2.0.3", - "minimist": "^1.2.5", - "mqtt-packet": "^6.8.0", - "number-allocator": "^1.0.7", - "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", - "reinterval": "^1.1.0", - "split2": "^3.1.0", - "ws": "^7.5.0", - "xtend": "^4.0.2" - }, - "devDependencies": { - "@types/node": "^10.0.0", - "@types/ws": "^8.2.0", - "aedes": "^0.42.5", - "airtap": "^3.0.0", - "browserify": "^16.5.0", - "chai": "^4.2.0", - "codecov": "^3.0.4", - "end-of-stream": "^1.4.1", - "global": "^4.3.2", - "mkdirp": "^0.5.1", - "mocha": "^4.1.0", - "mqtt-connection": "^4.0.0", - "nyc": "^15.0.1", - "pre-commit": "^1.2.2", - "rimraf": "^3.0.2", - "should": "^13.2.1", - "sinon": "^9.0.0", - "snazzy": "^8.0.0", - "standard": "^11.0.1", - "tslint": "^5.11.0", - "tslint-config-standard": "^8.0.1", - "typescript": "^3.2.2", - "uglify-es": "^3.3.9" - }, - "standard": { - "env": [ - "mocha" - ] - } -} +{ + "name": "mqtt", + "description": "A library for the MQTT protocol", + "version": "4.2.8", + "contributors": [ + "Adam Rudd ", + "Matteo Collina (https://github.com/mcollina)", + "Siarhei Buntsevich (https://github.com/scarry1992)", + "Yoseph Maguire (https://github.com/YoDaMa)" + ], + "keywords": [ + "mqtt", + "publish/subscribe", + "publish", + "subscribe" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/mqttjs/MQTT.js.git" + }, + "main": "mqtt.js", + "types": "types/index.d.ts", + "scripts": { + "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", + "pretest": "standard | snazzy", + "tslint": "tslint types/**/*.d.ts", + "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", + "typescript-compile-execute": "node test/typescript/*.js", + "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", + "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && uglifyjs dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", + "prepare": "npm run browser-build", + "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", + "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", + "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" + }, + "pre-commit": [ + "pretest", + "tslint" + ], + "bin": { + "mqtt_pub": "./bin/pub.js", + "mqtt_sub": "./bin/sub.js", + "mqtt": "./bin/mqtt.js" + }, + "files": [ + "dist/", + "CONTRIBUTING.md", + "doc", + "lib", + "bin", + "types", + "mqtt.js" + ], + "engines": { + "node": ">=10.0.0" + }, + "browser": { + "./mqtt.js": "./lib/connect/index.js", + "fs": false, + "tls": false, + "net": false + }, + "dependencies": { + "collections": "^5.1.12", + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.7", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", + "reinterval": "^1.1.0", + "split2": "^3.1.0", + "ws": "^7.5.0", + "xtend": "^4.0.2" + }, + "devDependencies": { + "@types/node": "^10.0.0", + "@types/ws": "^8.2.0", + "aedes": "^0.42.5", + "airtap": "^3.0.0", + "browserify": "^16.5.0", + "chai": "^4.2.0", + "codecov": "^3.0.4", + "end-of-stream": "^1.4.1", + "global": "^4.3.2", + "mkdirp": "^0.5.1", + "mocha": "^4.1.0", + "mqtt-connection": "^4.0.0", + "nyc": "^15.0.1", + "pre-commit": "^1.2.2", + "rimraf": "^3.0.2", + "should": "^13.2.1", + "sinon": "^9.0.0", + "snazzy": "^8.0.0", + "standard": "^11.0.1", + "tslint": "^5.11.0", + "tslint-config-standard": "^8.0.1", + "typescript": "^3.2.2", + "uglify-es": "^3.3.9" + }, + "standard": { + "env": [ + "mocha" + ] + } +} diff --git a/test/abstract_client.js b/test/abstract_client.js index fc1f2096f..4c8b0fa77 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -1,3177 +1,3177 @@ -'use strict' - -/** - * Testing dependencies - */ -var should = require('chai').should -var sinon = require('sinon') -var mqtt = require('../') -var xtend = require('xtend') -var Store = require('./../lib/store') -var assert = require('chai').assert -var ports = require('./helpers/port_list') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder - -module.exports = function (server, config) { - var version = config.protocolVersion || 4 - - function connect (opts) { - opts = xtend(config, opts) - return mqtt.connect(opts) - } - - describe('closing', function () { - it('should emit close if stream closes', function (done) { - var client = connect() - - client.once('connect', function () { - client.stream.end() - }) - client.once('close', function () { - client.end() - done() - }) - }) - - it('should mark the client as disconnected', function (done) { - var client = connect() - - client.once('close', function () { - client.end() - if (!client.connected) { - done() - } else { - done(new Error('Not marked as disconnected')) - } - }) - client.once('connect', function () { - client.stream.end() - }) - }) - - it('should stop ping timer if stream closes', function (done) { - var client = connect() - - client.once('close', function () { - assert.notExists(client.pingTimer) - client.end(true, done) - }) - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.stream.end() - }) - }) - - it('should emit close after end called', function (done) { - var client = connect() - - client.once('close', function () { - done() - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should emit end after end called and client must be disconnected', function (done) { - var client = connect() - - client.once('end', function () { - if (client.disconnected) { - return done() - } - done(new Error('client must be disconnected')) - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { - var store = new Store() - var client = connect({ incomingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { - var store = new Store() - var client = connect({ outgoingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should return `this` if end called twice', function (done) { - var client = connect() - - client.once('connect', function () { - client.end() - var value = client.end() - if (value === client) { - done() - } else { - done(new Error('Not returning client.')) - } - }) - }) - - it('should emit end only on first client end', function (done) { - var client = connect() - - client.once('end', function () { - var timeout = setTimeout(done.bind(null), 200) - client.once('end', function () { - clearTimeout(timeout) - done(new Error('end was emitted twice')) - }) - client.end() - }) - - client.once('connect', client.end.bind(client)) - }) - - it('should stop ping timer after end called', function (done) { - var client = connect() - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end(() => { - assert.notExists(client.pingTimer) - done() - }) - }) - }) - - it('should be able to end even on a failed connection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist'}) - - var timeout = setTimeout(function () { - done(new Error('Failed to end a disconnected client')) - }, 500) - - setTimeout(function () { - client.end(function () { - clearTimeout(timeout) - done() - }) - }, 200) - }) - - it('should emit end even on a failed connection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist'}) - - var timeout = setTimeout(function () { - done(new Error('Disconnected client has failed to emit end')) - }, 500) - - client.once('end', function () { - clearTimeout(timeout) - done() - }) - - // after 200ms manually invoke client.end - setTimeout(() => { - var boundEnd = client.end.bind(client) - boundEnd() - }, 200) - }) - - it.skip('should emit end only once for a reconnecting client', function (done) { - // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. - // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code - // there will be gists showing the difference between a successful test here and a failed test. For now we - // will add the retries syntax because of the flakiness. - var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20}) - setTimeout(done.bind(null), 1000) - var endCallback = function () { - assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') - } - - var spy = sinon.spy(endCallback) - client.on('end', spy) - setTimeout(() => { - client.end.bind(client) - client.end() - }, 300) - }) - }) - - describe('connecting', function () { - it('should connect to the broker', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function () { - done() - client.end() - }) - }) - - it('should send a default client id', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'mqttjs') - client.end(done) - serverClient.disconnect() - }) - }) - }) - - it('should send be clean by default', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.strictEqual(packet.clean, true) - serverClient.disconnect() - done() - }) - }) - }) - - it('should connect with the given client id', function (done) { - var client = connect({clientId: 'testclient'}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - serverClient.disconnect() - client.end(function (err) { - done(err) - }) - }) - }) - }) - - it('should connect with the client id and unclean state', function (done) { - var client = connect({clientId: 'testclient', clean: false}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - assert.isFalse(packet.clean) - client.end(false, function (err) { - serverClient.disconnect() - done(err) - }) - }) - }) - }) - - it('should require a clientId with clean=false', function (done) { - try { - var client = connect({ clean: false }) - client.on('error', function (err) { - done(err) - }) - } catch (err) { - assert.strictEqual(err.message, 'Missing clientId for unclean clients') - done() - } - }) - - it('should default to localhost', function (done) { - var client = connect({clientId: 'testclient'}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - serverClient.disconnect() - done() - }) - }) - }) - - it('should emit connect', function (done) { - var client = connect() - client.once('connect', function () { - client.end(true, done) - }) - client.once('error', done) - }) - - it('should provide connack packet with connect event', function (done) { - var connack = version === 5 ? {reasonCode: 0} : {returnCode: 0} - server.once('client', function (serverClient) { - connack.sessionPresent = true - serverClient.connack(connack) - server.once('client', function (serverClient) { - connack.sessionPresent = false - serverClient.connack(connack) - }) - }) - - var client = connect() - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, true) - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, false) - client.end() - done() - }) - }) - }) - - it('should mark the client as connected', function (done) { - var client = connect() - client.once('connect', function () { - client.end() - if (client.connected) { - done() - } else { - done(new Error('Not marked as connected')) - } - }) - }) - - it('should emit error on invalid clientId', function (done) { - var client = connect({clientId: 'invalid'}) - client.once('connect', function () { - done(new Error('Should not emit connect')) - }) - client.once('error', function (error) { - var value = version === 5 ? 128 : 2 - assert.strictEqual(error.code, value) // code for clientID identifer rejected - client.end() - done() - }) - }) - - it('should emit error event if the socket refuses the connection', function (done) { - // fake a port - var client = connect({ port: 4557 }) - - client.on('error', function (e) { - assert.equal(e.code, 'ECONNREFUSED') - client.end() - done() - }) - }) - - it('should have different client ids', function (done) { - // bug identified in this test: the client.end callback is invoked twice, once when the `end` - // method completes closing the stores and invokes the callback, and another time when the - // stream is closed. When the stream is closed, for some reason the closeStores method is called - // a second time. - var client1 = connect() - var client2 = connect() - - assert.notStrictEqual(client1.options.clientId, client2.options.clientId) - client1.end(true, () => { - client2.end(true, () => { - done() - }) - }) - }) - }) - - describe('handling offline states', function () { - it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { - var client = connect({reconnectPeriod: 20}) - - client.on('connect', function () { - this.stream.end() - }) - - client.on('offline', function () { - client.end(true, done) - }) - }) - - it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { - // fake a port - var client = connect({ reconnectPeriod: 20, port: 4557 }) - - client.on('error', function () {}) - - client.on('offline', function () { - client.end(true, done) - }) - }) - }) - - describe('topic validations when subscribing', function () { - it('should be ok for well-formated topics', function (done) { - var client = connect() - client.subscribe( - [ - '+', '+/event', 'event/+', '#', 'event/#', 'system/event/+', - 'system/+/event', 'system/registry/event/#', 'system/+/event/#', - 'system/registry/event/new_device', 'system/+/+/new_device' - ], - function (err) { - client.end(function () { - if (err) { - return done(new Error(err)) - } - done() - }) - } - ) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - var client = connect() - client.subscribe(['#/event', 'event#', 'event+'], function (err) { - client.end(false, function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an empty array for duplicate subs', function (done) { - var client = connect() - client.subscribe('event', function (err, granted1) { - if (err) { - return done(err) - } - client.subscribe('event', function (err, granted2) { - if (err) { - return done(err) - } - assert.isArray(granted2) - assert.isEmpty(granted2) - done() - }) - }) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - var client = connect() - client.subscribe('#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic event#', function (done) { - var client = connect() - client.subscribe('event#', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic system/#/event', function (done) { - var client = connect() - client.subscribe('system/#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for empty topic list', function (done) { - var client = connect() - client.subscribe([], function (err) { - client.end() - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - - it('should return an error (via callbacks) for topic system/+/#/event', function (done) { - var client = connect() - client.subscribe('system/+/#/event', function (err) { - client.end(true, function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - }) - - describe('offline messages', function () { - it('should queue message until connected', function (done) { - var client = connect() - - client.publish('test', 'test') - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 3) - - client.once('connect', function () { - assert.strictEqual(client.queue.length, 0) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not queue qos 0 messages if queueQoSZero is false', function (done) { - var client = connect({queueQoSZero: false}) - - client.publish('test', 'test', {qos: 0}) - assert.strictEqual(client.queue.length, 0) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should queue qos != 0 messages', function (done) { - var client = connect({queueQoSZero: false}) - - client.publish('test', 'test', {qos: 1}) - client.publish('test', 'test', {qos: 2}) - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 2) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not interrupt messages', function (done) { - var client = null - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var publishCount = 0 - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function () { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (packet.qos !== 0) { - serverClient.puback({messageId: packet.messageId}) - } - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - break - case 3: - assert.strictEqual(packet.payload.toString(), 'payload4') - server2.close() - done() - break - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore, - queueQoSZero: true - }) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'connack') { - setImmediate( - function () { - client.publish('test', 'payload3', {qos: 1}) - client.publish('test', 'payload4', {qos: 0}) - } - ) - } - }) - client.publish('test', 'payload1', {qos: 2}) - client.publish('test', 'payload2', {qos: 2}) - }) - }) - - it('should call cb if an outgoing QoS 0 message is not sent', function (done) { - var client = connect({queueQoSZero: false}) - var called = false - - client.publish('test', 'test', {qos: 0}, function () { - called = true - }) - - client.on('connect', function () { - assert.isTrue(called) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should delay ending up until all inflight messages are delivered', function (done) { - var client = connect() - var subscribeCalled = false - - client.on('connect', function () { - client.subscribe('test', function () { - subscribeCalled = true - }) - client.publish('test', 'test', function () { - client.end(false, function () { - assert.strictEqual(subscribeCalled, true) - done() - }) - }) - }) - }) - - it('wait QoS 1 publish messages', function (done) { - var client = connect() - var messageReceived = false - - client.on('connect', function () { - client.subscribe('test') - client.publish('test', 'test', { qos: 1 }, function () { - client.end(false, function () { - assert.strictEqual(messageReceived, true) - done() - }) - }) - client.on('message', function () { - messageReceived = true - }) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - }) - }) - - it('does not wait acks when force-closing', function (done) { - // non-running broker - var client = connect('mqtt://localhost:8993') - client.publish('test', 'test', { qos: 1 }) - client.end(true, done) - }) - - it('should call cb if store.put fails', function (done) { - const store = new Store() - store.put = function (packet, cb) { - process.nextTick(cb, new Error('oops there is an error')) - } - var client = connect({ incomingStore: store, outgoingStore: store }) - client.publish('test', 'test', { qos: 2 }, function (err) { - if (err) { - client.end(true, done) - } - }) - }) - }) - - describe('publishing', function () { - it('should publish a message (offline)', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - // don't wait on connect to send publish - client.publish(topic, payload) - - server.on('client', onClient) - - function onClient (serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (online)', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - // block on connect before sending publish - client.on('connect', function () { - client.publish(topic, payload) - }) - - server.on('client', onClient) - - function onClient (serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (retain, offline)', function (done) { - var client = connect({ queueQoSZero: true }) - var payload = 'test' - var topic = 'test' - var called = false - - client.publish(topic, payload, { retain: true }, function () { - called = true - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, true) - assert.strictEqual(called, true) - client.end(true, done) - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var payload = 'test_payload' - var topic = 'testTopic' - - client.on('packetsend', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - } else { - done(new Error('packet.cmd was not publish!')) - } - }) - - client.publish(topic, payload) - }) - - it('should accept options', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - var opts = { - retain: true, - qos: 1 - } - - client.once('connect', function () { - client.publish(topic, payload, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, false, 'incorrect dup') - client.end(done) - }) - }) - }) - - it('should publish with the default options for an empty parameter', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - var defaultOpts = {qos: 0, retain: false, dup: false} - - client.once('connect', function () { - client.publish(topic, payload, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') - client.end(true, done) - }) - }) - }) - - it('should mark a message as duplicate when "dup" option is set', function (done) { - var client = connect() - var payload = 'duplicated-test' - var topic = 'test' - var opts = { - retain: true, - qos: 1, - dup: true - } - - client.once('connect', function () { - client.publish(topic, payload, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') - client.end(done) - }) - }) - }) - - it('should fire a callback (qos 0)', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('a', 'b', function () { - client.end() - done() - }) - }) - }) - - it('should fire a callback (qos 1)', function (done) { - var client = connect() - var opts = { qos: 1 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end() - done() - }) - }) - }) - - it('should fire a callback (qos 2)', function (done) { - var client = connect() - var opts = { qos: 2 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end() - done() - }) - }) - }) - - it('should support UTF-8 characters in topic', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('中国', 'hello', function () { - client.end() - done() - }) - }) - }) - - it('should support UTF-8 characters in payload', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('hello', '中国', function () { - client.end() - done() - }) - }) - }) - - it('should publish 10 QoS 2 and receive them', function (done) { - var client = connect() - var count = 0 - - client.on('connect', function () { - client.subscribe('test') - client.publish('test', 'test', { qos: 2 }) - }) - - client.on('message', function () { - if (count >= 10) { - client.end() - done() - } else { - client.publish('test', 'test', { qos: 2 }) - } - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end() - done('error went offline... didnt see this happen') - }) - - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - - serverClient.on('pubrel', function () { - count++ - }) - }) - }) - - function testQosHandleMessage (qos, done) { - var client = connect() - - var messageEventCount = 0 - var handleMessageCount = 0 - - client.handleMessage = function (packet, callback) { - setTimeout(function () { - handleMessageCount++ - // next message event should not emit until handleMessage completes - assert.strictEqual(handleMessageCount, messageEventCount) - if (handleMessageCount === 10) { - setTimeout(function () { - client.end(true, done) - }) - } - callback() - }, 100) - } - - client.on('message', function (topic, message, packet) { - messageEventCount++ - }) - - client.on('connect', function () { - client.subscribe('test') - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end(true, function () { - done('error went offline... didnt see this happen') - }) - }) - - serverClient.on('subscribe', function () { - for (var i = 0; i < 10; i++) { - serverClient.publish({ - messageId: i, - topic: 'test', - payload: 'test' + i, - qos: qos - }) - } - }) - }) - } - - var qosTests = [ 0, 1, 2 ] - qosTests.forEach(function (QoS) { - it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { - testQosHandleMessage(QoS, done) - }) - }) - - it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { - var client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client._sendPacket = sinon.spy() - - client._handlePublish({ - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }, function (err) { - assert.exists(err) - }) - - assert.strictEqual(client._sendPacket.callCount, 0) - client.end() - client.on('connect', function () { done() }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePublish` method', function (done) { - var client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - try { - client._handlePublish({ - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - - it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePublish({ - messageId: 1, - topic: 'test', - payload: 'test', - qos: 1 - }, function () { - client.end() - done() - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePublish({ - messageId: 1, - topic: 'test', - payload: 'test', - qos: 2 - }, function () { - client.end() - done() - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del (packet, cb) { - process.nextTick(function () { - cb(new Error('Error')) - }) - } - - get (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({ incomingStore: store }) - - client._handlePubrel({ - messageId: 1, - qos: 2 - }, function () { - client.end(true, done) - }) - }) - - it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { - var delComplete = false - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del (packet, cb) { - process.nextTick(function () { - delComplete = true - cb(null) - }) - } - - get (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePubrel({ - messageId: 1, - qos: 2 - }, function () { - assert.isTrue(delComplete) - client.end(true, done) - }) - }) - - it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function (done) { - var store = new Store() - var client = connect({incomingStore: store}) - - var messageId = Math.floor(65535 * Math.random()) - var topic = 'testTopic' - var payload = 'testPayload' - var qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, {qos: 2}) - - store.put({ - messageId: messageId, - topic: topic, - payload: payload, - qos: qos, - cmd: 'publish' - }, function () { - // cleans up the client - client._sendPacket = sinon.spy() - client._handlePubrel({cmd: 'pubrel', messageId: messageId}, function (err) { - assert.exists(err) - assert.strictEqual(client._sendPacket.callCount, 0) - client.end(true, done) - }) - }) - }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePubrel` method', function (done) { - var store = new Store() - var client = connect({incomingStore: store}) - - var messageId = Math.floor(65535 * Math.random()) - var topic = 'test' - var payload = 'test' - var qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, {qos: 2}) - - store.put({ - messageId: messageId, - topic: topic, - payload: payload, - qos: qos, - cmd: 'publish' - }, function () { - try { - client._handlePubrel({cmd: 'pubrel', messageId: messageId}) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - }) - }) - - it('should keep message order', function (done) { - var publishCount = 0 - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () {}) - - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({messageId: packet.messageId}) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - server2.close() - done() - break - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', {qos: 1}) - client.publish('topic', 'payload2', {qos: 1}) - client.end(true) - } else { - client.publish('topic', 'payload3', {qos: 1}) - } - }) - client.on('close', function () { - if (!reconnect) { - client.reconnect({ - clean: false, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - } - }) - }) - }) - - function testCallbackStorePutByQoS (qos, clean, expected, done) { - var client = connect({ - clean: clean, - clientId: 'testId' - }) - - var callbacks = [] - - function cbStorePut () { - callbacks.push('storeput') - } - - client.on('connect', function () { - client.publish('test', 'test', {qos: qos, cbStorePut: cbStorePut}, function (err) { - if (err) done(err) - callbacks.push('publish') - assert.deepEqual(callbacks, expected) - client.end(true, done) - }) - }) - } - - var callbackStorePutByQoSParameters = [ - {args: [0, true], expected: ['publish']}, - {args: [0, false], expected: ['publish']}, - {args: [1, true], expected: ['storeput', 'publish']}, - {args: [1, false], expected: ['storeput', 'publish']}, - {args: [2, true], expected: ['storeput', 'publish']}, - {args: [2, false], expected: ['storeput', 'publish']} - ] - - callbackStorePutByQoSParameters.forEach(function (test) { - if (test.args[0] === 0) { // QoS 0 - it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } else { // QoS 1 and 2 - it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } - }) - }) - - describe('unsubscribing', function () { - it('should send an unsubscribe packet (offline)', function (done) { - var client = connect() - - client.unsubscribe('test') - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, 'test') - client.end(done) - }) - }) - }) - - it('should send an unsubscribe packet', function (done) { - var client = connect() - var topic = 'topic' - - client.once('connect', function () { - client.unsubscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - client.end(done) - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - client.end(true, done) - } - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - client.end(true, done) - } - }) - }) - - it('should accept an array of unsubs', function (done) { - var client = connect() - var topics = ['topic1', 'topic2'] - - client.once('connect', function () { - client.unsubscribe(topics) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.deepStrictEqual(packet.unsubscriptions, topics) - client.end(done) - }) - }) - }) - - it('should fire a callback on unsuback', function (done) { - var client = connect() - var topic = 'topic' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - serverClient.unsuback(packet) - }) - }) - }) - - it('should unsubscribe from a chinese topic', function (done) { - var client = connect() - var topic = '中国' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(err => { - done(err) - }) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - }) - }) - }) - }) - - describe('keepalive', function () { - var clock - - beforeEach(function () { - clock = sinon.useFakeTimers() - }) - - afterEach(function () { - clock.restore() - }) - - it('should checkPing at keepalive interval', function (done) { - var interval = 3 - var client = connect({ keepalive: interval }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 1) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 2) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 3) - - client.end(true, done) - }) - }) - - it('should not checkPing if publishing at a higher rate than keepalive', function (done) { - var intervalMs = 3000 - var client = connect({keepalive: intervalMs / 1000}) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 0) - client.end(true, done) - }) - }) - - it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function (done) { - var intervalMs = 3000 - var client = connect({ - keepalive: intervalMs / 1000, - reschedulePings: false - }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 1) - client.end(true, done) - }) - }) - }) - - describe('pinging', function () { - it('should set a ping timer', function (done) { - var client = connect({keepalive: 3}) - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should not set a ping timer keepalive=0', function (done) { - var client = connect({keepalive: 0}) - client.on('connect', function () { - assert.notExists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should reconnect if pingresp is not sent', function (done) { - var client = connect({keepalive: 1, reconnectPeriod: 100}) - - // Fake no pingresp being send by stubbing the _handlePingresp function - client._handlePingresp = function () {} - - client.once('connect', function () { - client.once('connect', function () { - client.end(true, done) - }) - }) - }) - - it('should not reconnect if pingresp is successful', function (done) { - var client = connect({keepalive: 100}) - client.once('close', function () { - done(new Error('Client closed connection')) - }) - setTimeout(done, 1000) - }) - - it('should defer the next ping when sending a control packet', function (done) { - var client = connect({keepalive: 1}) - - client.once('connect', function () { - client._checkPing = sinon.spy() - - client.publish('foo', 'bar') - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - done() - }, 75) - }, 75) - }, 75) - }) - }) - }) - - describe('subscribing', function () { - it('should send a subscribe message (offline)', function (done) { - var client = connect() - - client.subscribe('test') - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - done() - }) - }) - }) - - it('should send a subscribe message', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - done() - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - done() - } - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - done() - } - }) - }) - - it('should accept an array of subscriptions', function (done) { - var client = connect() - var subs = ['test1', 'test2'] - - client.once('connect', function () { - client.subscribe(subs) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] - var expected = subs.map(function (i) { - var result = {topic: i, qos: 0} - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - return result - }) - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept a hash of subscriptions', function (done) { - var client = connect() - var topics = { - test1: {qos: 0}, - test2: {qos: 1} - } - - client.once('connect', function () { - client.subscribe(topics) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var k - var expected = [] - - for (k in topics) { - if (topics.hasOwnProperty(k)) { - var result = { - topic: k, - qos: topics[k].qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - expected.push(result) - } - } - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept an options parameter', function (done) { - var client = connect() - var topic = 'test' - var opts = {qos: 1} - - client.once('connect', function () { - client.subscribe(topic, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var expected = [{ - topic: topic, - qos: 1 - }] - - if (version === 5) { - expected[0].nl = false - expected[0].rap = false - expected[0].rh = 0 - } - - assert.deepStrictEqual(packet.subscriptions, expected) - done() - }) - }) - }) - - it('should subscribe with the default options for an empty options parameter', function (done) { - var client = connect() - var topic = 'test' - var defaultOpts = {qos: 0} - - client.once('connect', function () { - client.subscribe(topic, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: defaultOpts.qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - - assert.include(packet.subscriptions[0], result) - client.end(err => done(err)) - }) - }) - }) - - it('should fire a callback on suback', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.subscribe(topic, { qos: 2 }, function (err, granted) { - if (err) { - done(err) - } else { - assert.exists(granted, 'granted not given') - var expectedResult = {topic: 'test', qos: 2} - if (version === 5) { - expectedResult.nl = false - expectedResult.rap = false - expectedResult.rh = 0 - expectedResult.properties = undefined - } - assert.include(granted[0], expectedResult) - client.end(err => done(err)) - } - }) - }) - }) - - it('should fire a callback with error if disconnected (options provided)', function (done) { - var client = connect() - var topic = 'test' - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, {qos: 2}, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should fire a callback with error if disconnected (options not provided)', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should subscribe with a chinese topic', function (done) { - var client = connect() - var topic = '中国' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - client.end(done) - }) - }) - }) - }) - - describe('receiving messages', function () { - it('should fire the message event', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - // - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.qos, 1) - assert.strictEqual(packet.topic, testPacket.topic) - assert.strictEqual(packet.payload.toString(), testPacket.payload) - assert.strictEqual(packet.retain, true) - client.end(true, done) - } - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should support binary data', function (done) { - var client = connect({ encoding: 'binary' }) - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2)', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2) - repeated publish', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - var messageHandler = function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - - assert.strictEqual(spiedMessageHandler.callCount, 1) - client.end(true, done) - } - - var spiedMessageHandler = sinon.spy(messageHandler) - - client.subscribe(testPacket.topic) - client.on('message', spiedMessageHandler) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - // twice, should be ignored - serverClient.publish(testPacket) - }) - }) - }) - - it('should support a chinese topic', function (done) { - var client = connect({ encoding: 'binary' }) - var testPacket = { - topic: '国', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - }) - - describe('qos handling', function () { - it('should follow qos 0 semantics (trivial)', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 0}, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 0, - retain: false - }) - }) - }) - }) - - it('should follow qos 1 semantics', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 50 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 1}) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - messageId: mid, - qos: 1 - }) - }) - - serverClient.once('puback', function (packet) { - assert.strictEqual(packet.messageId, mid) - client.end(done) - }) - }) - }) - - it('should follow qos 2 semantics', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - var publishReceived = 0 - var pubrecReceived = 0 - var pubrelReceived = 0 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - }) - - client.on('packetreceive', (packet) => { - switch (packet.cmd) { - case 'connack': - case 'suback': - // expected, but not specifically part of QOS 2 semantics - break - case 'publish': - assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') - assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') - publishReceived += 1 - break - case 'pubrel': - assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') - pubrelReceived += 1 - break - default: - should.fail() - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.on('pubrec', function () { - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') - assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') - pubrecReceived += 1 - }) - - serverClient.once('pubcomp', function () { - client.removeAllListeners() - serverClient.removeAllListeners() - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') - assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') - client.end(true, done) - }) - }) - }) - - it('should should empty the incoming store after a qos 2 handshake is completed', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - }) - - client.on('packetreceive', (packet) => { - if (packet.cmd === 'pubrel') { - assert.strictEqual(client.incomingStore._inflights.size, 1) - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.once('pubcomp', function () { - assert.strictEqual(client.incomingStore._inflights.size, 0) - client.removeAllListeners() - client.end(true, done) - }) - }) - }) - - function testMultiplePubrel (shouldSendPubcompFail, done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - var pubcompCount = 0 - var pubrelCount = 0 - var handleMessageCount = 0 - var emitMessageCount = 0 - var origSendPacket = client._sendPacket - var shouldSendFail - - client.handleMessage = function (packet, callback) { - handleMessageCount++ - callback() - } - - client.on('message', function () { - emitMessageCount++ - }) - - client._sendPacket = function (packet, sendDone) { - shouldSendFail = packet.cmd === 'pubcomp' && shouldSendPubcompFail - if (sendDone) { - sendDone(shouldSendFail ? new Error('testing pubcomp failure') : undefined) - } - - // send the mocked response - switch (packet.cmd) { - case 'subscribe': - const suback = {cmd: 'suback', messageId: packet.messageId, granted: [2]} - client._handlePacket(suback, function (err) { - assert.isNotOk(err) - }) - break - case 'pubrec': - case 'pubcomp': - // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp - if (packet.cmd === 'pubcomp') { - pubcompCount++ - if (pubcompCount === 2) { - // end the test once the client has gone through two rounds of replying to pubrel messages - assert.strictEqual(pubrelCount, 2) - assert.strictEqual(handleMessageCount, 1) - assert.strictEqual(emitMessageCount, 1) - client._sendPacket = origSendPacket - client.end(true, done) - break - } - } - - // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received - const pubrel = {cmd: 'pubrel', messageId: mid} - pubrelCount++ - client._handlePacket(pubrel, function (err) { - if (shouldSendFail) { - assert.exists(err) - assert.instanceOf(err, Error) - } else { - assert.notExists(err) - } - }) - break - } - } - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - const publish = {cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid} - client._handlePacket(publish, function (err) { - assert.notExists(err) - }) - }) - } - - it('handle qos 2 messages exactly once when multiple pubrel received', function (done) { - testMultiplePubrel(false, done) - }) - - it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function (done) { - testMultiplePubrel(true, done) - }) - }) - - describe('auto reconnect', function () { - it('should mark the client disconnecting if #end called', function (done) { - var client = connect() - - client.end(true, err => { - assert.isTrue(client.disconnecting) - done(err) - }) - }) - - it('should reconnect after stream disconnect', function (done) { - var client = connect() - - var tryReconnect = true - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - client.end(true, done) - } - }) - }) - - it('should emit \'reconnect\' when reconnecting', function (done) { - var client = connect() - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - client.end(true, done) - } - }) - }) - - it('should emit \'offline\' after going offline', function (done) { - var client = connect() - - var tryReconnect = true - var offlineEvent = false - - client.on('offline', function () { - offlineEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(offlineEvent) - client.end(true, done) - } - }) - }) - - it('should not reconnect if it was ended by the user', function (done) { - var client = connect() - - client.on('connect', function () { - client.end() - done() // it will raise an exception if called two times - }) - }) - - it('should setup a reconnect timer on disconnect', function (done) { - var client = connect() - - client.once('connect', function () { - assert.notExists(client.reconnectTimer) - client.stream.end() - }) - - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, done) - }) - }) - - var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] - reconnectPeriodTests.forEach((test) => { - it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { - var end - var reconnectSlushTime = 200 - var client = connect({reconnectPeriod: test.period}) - var reconnect = false - var start = Date.now() - - client.on('connect', function () { - if (!reconnect) { - client.stream.end() - reconnect = true - } else { - end = Date.now() - client.end(() => { - let reconnectPeriodDuringTest = end - start - if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { - // give the connection a 200 ms slush window - done() - } else { - done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) - } - }) - } - }) - }) - }) - - it('should always cleanup successfully on reconnection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1}) - // bind client.end so that when it is called it is automatically passed in the done callback - setTimeout(client.end.bind(client, done), 50) - }) - - it('should resend in-flight QoS 1 publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - check() - }) - - function check () { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight publish messages if disconnecting', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - client.end(true, err => { - assert.isFalse(serverPublished) - assert.isFalse(clientCalledBack) - done(err) - }) - }) - }) - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - }) - }) - }) - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - - server.once('client', function (serverClient) { - // ignore errors - serverClient.on('error', function () {}) - serverClient.on('publish', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('pubrel', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function () { - clientCalledBack = true - check() - }) - - function check () { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight QoS 1 removed publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function (err) { - clientCalledBack = true - assert.exists(err, 'error should exist') - assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, (err) => { - done(err) - }) - }) - - it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function (err) { - clientCalledBack = true - assert.strictEqual(err.message, 'Message removed') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, done) - }) - - it('should resubscribe when reconnecting', function (done) { - var client = connect({ reconnectPeriod: 100 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - client.end(done) - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should not resubscribe when reconnecting if resubscribe is disabled', function (done) { - var client = connect({ reconnectPeriod: 100, resubscribe: false }) - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - client.end(true, done) - } - }) - }) - - it('should not resubscribe when reconnecting if suback is error', function (done) { - var tryReconnect = true - var reconnectEvent = false - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos | 0x80 - }) - }) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - var client = connect({ - port: ports.PORTAND49, - host: 'localhost', - reconnectPeriod: 100 - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - server2.close() - client.end(true, done) - } - }) - }) - }) - - it('should preserved incomingStore after disconnecting if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - if (reconnect) { - serverClient.pubrel({ messageId: 1 }) - } - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) - }) - serverClient.on('pubrec', function (packet) { - client.end(false, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - }) - }) - serverClient.on('pubcomp', function (packet) { - client.end(true, () => { - server2.close() - done() - }) - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.subscribe('test', {qos: 2}, function () { - }) - reconnect = true - } - }) - client.on('message', function (topic, message) { - assert.strictEqual(topic, 'topic') - assert.strictEqual(message.toString(), 'payload') - }) - }) - }) - - it('should clear outgoing if close from server', function (done) { - var reconnect = false - var client = {} - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - if (reconnect) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - serverClient.destroy() - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: true, - clientId: 'cid1', - keepalive: 1, - reconnectPeriod: 0 - }) - - client.on('connect', function () { - client.subscribe('test', {qos: 2}, function (e) { - if (!e) { - client.end() - } - }) - }) - - client.on('close', function () { - if (reconnect) { - server2.close() - done() - } else { - assert.strictEqual(Object.keys(client.outgoing).length, 0) - reconnect = true - client.reconnect() - } - }) - }) - }) - - it('should resend in-flight QoS 1 publish messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, () => { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 1}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 2}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (!reconnect) { - serverClient.pubrec({messageId: packet.messageId}) - } - }) - serverClient.on('pubrel', function () { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 2}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight publish messages by published order', function (done) { - var publishCount = 0 - var reconnect = false - var disconnectOnce = true - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () {}) - - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({messageId: packet.messageId}) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - server2.close() - client.end(true, done) - break - } - } else { - if (disconnectOnce) { - client.end(true, function () { - reconnect = true - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - }) - disconnectOnce = false - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.nextId = 65535 - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', {qos: 1}) - client.publish('topic', 'payload2', {qos: 1}) - client.publish('topic', 'payload3', {qos: 1}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should be able to pub/sub if reconnect() is called at close handler', function (done) { - var client = connect({ reconnectPeriod: 0 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - client.reconnect() - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - it('should be able to pub/sub if reconnect() is called at out of close handler', function (done) { - var client = connect({ reconnectPeriod: 0 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - setTimeout(function () { - client.reconnect() - }, 100) - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - context('with alternate server client', function () { - var cachedClientListeners - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - - beforeEach(function () { - cachedClientListeners = server.listeners('client') - server.removeAllListeners('client') - }) - - afterEach(function () { - server.removeAllListeners('client') - cachedClientListeners.forEach(function (listener) { - server.on('client', listener) - }) - }) - - it('should resubscribe even if disconnect is before suback', function (done) { - var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - var subscribeCount = 0 - var connectCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - connectCount++ - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, confirm that the only two - // subscribes have taken place, then cleanup and exit - if (connectCount >= 2) { - assert.strictEqual(subscribeCount, 2) - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - - it('should resubscribe exactly once', function (done) { - var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - var subscribeCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, only two subs - // subscribes have taken place, then cleanup and exit - if (subscribeCount === 2) { - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - }) - }) -} +'use strict' + +/** + * Testing dependencies + */ +var should = require('chai').should +var sinon = require('sinon') +var mqtt = require('../') +var xtend = require('xtend') +var Store = require('./../lib/store') +var assert = require('chai').assert +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder + +module.exports = function (server, config) { + var version = config.protocolVersion || 4 + + function connect (opts) { + opts = xtend(config, opts) + return mqtt.connect(opts) + } + + describe('closing', function () { + it('should emit close if stream closes', function (done) { + var client = connect() + + client.once('connect', function () { + client.stream.end() + }) + client.once('close', function () { + client.end() + done() + }) + }) + + it('should mark the client as disconnected', function (done) { + var client = connect() + + client.once('close', function () { + client.end() + if (!client.connected) { + done() + } else { + done(new Error('Not marked as disconnected')) + } + }) + client.once('connect', function () { + client.stream.end() + }) + }) + + it('should stop ping timer if stream closes', function (done) { + var client = connect() + + client.once('close', function () { + assert.notExists(client.pingTimer) + client.end(true, done) + }) + + client.once('connect', function () { + assert.exists(client.pingTimer) + client.stream.end() + }) + }) + + it('should emit close after end called', function (done) { + var client = connect() + + client.once('close', function () { + done() + }) + + client.once('connect', function () { + client.end() + }) + }) + + it('should emit end after end called and client must be disconnected', function (done) { + var client = connect() + + client.once('end', function () { + if (client.disconnected) { + return done() + } + done(new Error('client must be disconnected')) + }) + + client.once('connect', function () { + client.end() + }) + }) + + it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { + var store = new Store() + var client = connect({ incomingStore: store }) + + store.close = function (cb) { + cb(new Error('test')) + } + client.once('end', function () { + if (arguments.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', function () { + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { + var store = new Store() + var client = connect({ outgoingStore: store }) + + store.close = function (cb) { + cb(new Error('test')) + } + client.once('end', function () { + if (arguments.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', function () { + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should return `this` if end called twice', function (done) { + var client = connect() + + client.once('connect', function () { + client.end() + var value = client.end() + if (value === client) { + done() + } else { + done(new Error('Not returning client.')) + } + }) + }) + + it('should emit end only on first client end', function (done) { + var client = connect() + + client.once('end', function () { + var timeout = setTimeout(done.bind(null), 200) + client.once('end', function () { + clearTimeout(timeout) + done(new Error('end was emitted twice')) + }) + client.end() + }) + + client.once('connect', client.end.bind(client)) + }) + + it('should stop ping timer after end called', function (done) { + var client = connect() + + client.once('connect', function () { + assert.exists(client.pingTimer) + client.end(() => { + assert.notExists(client.pingTimer) + done() + }) + }) + }) + + it('should be able to end even on a failed connection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist'}) + + var timeout = setTimeout(function () { + done(new Error('Failed to end a disconnected client')) + }, 500) + + setTimeout(function () { + client.end(function () { + clearTimeout(timeout) + done() + }) + }, 200) + }) + + it('should emit end even on a failed connection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist'}) + + var timeout = setTimeout(function () { + done(new Error('Disconnected client has failed to emit end')) + }, 500) + + client.once('end', function () { + clearTimeout(timeout) + done() + }) + + // after 200ms manually invoke client.end + setTimeout(() => { + var boundEnd = client.end.bind(client) + boundEnd() + }, 200) + }) + + it.skip('should emit end only once for a reconnecting client', function (done) { + // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. + // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code + // there will be gists showing the difference between a successful test here and a failed test. For now we + // will add the retries syntax because of the flakiness. + var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20}) + setTimeout(done.bind(null), 1000) + var endCallback = function () { + assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') + } + + var spy = sinon.spy(endCallback) + client.on('end', spy) + setTimeout(() => { + client.end.bind(client) + client.end() + }, 300) + }) + }) + + describe('connecting', function () { + it('should connect to the broker', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function () { + done() + client.end() + }) + }) + + it('should send a default client id', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'mqttjs') + client.end(done) + serverClient.disconnect() + }) + }) + }) + + it('should send be clean by default', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.strictEqual(packet.clean, true) + serverClient.disconnect() + done() + }) + }) + }) + + it('should connect with the given client id', function (done) { + var client = connect({clientId: 'testclient'}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + serverClient.disconnect() + client.end(function (err) { + done(err) + }) + }) + }) + }) + + it('should connect with the client id and unclean state', function (done) { + var client = connect({clientId: 'testclient', clean: false}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + assert.isFalse(packet.clean) + client.end(false, function (err) { + serverClient.disconnect() + done(err) + }) + }) + }) + }) + + it('should require a clientId with clean=false', function (done) { + try { + var client = connect({ clean: false }) + client.on('error', function (err) { + done(err) + }) + } catch (err) { + assert.strictEqual(err.message, 'Missing clientId for unclean clients') + done() + } + }) + + it('should default to localhost', function (done) { + var client = connect({clientId: 'testclient'}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + serverClient.disconnect() + done() + }) + }) + }) + + it('should emit connect', function (done) { + var client = connect() + client.once('connect', function () { + client.end(true, done) + }) + client.once('error', done) + }) + + it('should provide connack packet with connect event', function (done) { + var connack = version === 5 ? {reasonCode: 0} : {returnCode: 0} + server.once('client', function (serverClient) { + connack.sessionPresent = true + serverClient.connack(connack) + server.once('client', function (serverClient) { + connack.sessionPresent = false + serverClient.connack(connack) + }) + }) + + var client = connect() + client.once('connect', function (packet) { + assert.strictEqual(packet.sessionPresent, true) + client.once('connect', function (packet) { + assert.strictEqual(packet.sessionPresent, false) + client.end() + done() + }) + }) + }) + + it('should mark the client as connected', function (done) { + var client = connect() + client.once('connect', function () { + client.end() + if (client.connected) { + done() + } else { + done(new Error('Not marked as connected')) + } + }) + }) + + it('should emit error on invalid clientId', function (done) { + var client = connect({clientId: 'invalid'}) + client.once('connect', function () { + done(new Error('Should not emit connect')) + }) + client.once('error', function (error) { + var value = version === 5 ? 128 : 2 + assert.strictEqual(error.code, value) // code for clientID identifer rejected + client.end() + done() + }) + }) + + it('should emit error event if the socket refuses the connection', function (done) { + // fake a port + var client = connect({ port: 4557 }) + + client.on('error', function (e) { + assert.equal(e.code, 'ECONNREFUSED') + client.end() + done() + }) + }) + + it('should have different client ids', function (done) { + // bug identified in this test: the client.end callback is invoked twice, once when the `end` + // method completes closing the stores and invokes the callback, and another time when the + // stream is closed. When the stream is closed, for some reason the closeStores method is called + // a second time. + var client1 = connect() + var client2 = connect() + + assert.notStrictEqual(client1.options.clientId, client2.options.clientId) + client1.end(true, () => { + client2.end(true, () => { + done() + }) + }) + }) + }) + + describe('handling offline states', function () { + it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { + var client = connect({reconnectPeriod: 20}) + + client.on('connect', function () { + this.stream.end() + }) + + client.on('offline', function () { + client.end(true, done) + }) + }) + + it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { + // fake a port + var client = connect({ reconnectPeriod: 20, port: 4557 }) + + client.on('error', function () {}) + + client.on('offline', function () { + client.end(true, done) + }) + }) + }) + + describe('topic validations when subscribing', function () { + it('should be ok for well-formated topics', function (done) { + var client = connect() + client.subscribe( + [ + '+', '+/event', 'event/+', '#', 'event/#', 'system/event/+', + 'system/+/event', 'system/registry/event/#', 'system/+/event/#', + 'system/registry/event/new_device', 'system/+/+/new_device' + ], + function (err) { + client.end(function () { + if (err) { + return done(new Error(err)) + } + done() + }) + } + ) + }) + + it('should return an error (via callbacks) for topic #/event', function (done) { + var client = connect() + client.subscribe(['#/event', 'event#', 'event+'], function (err) { + client.end(false, function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an empty array for duplicate subs', function (done) { + var client = connect() + client.subscribe('event', function (err, granted1) { + if (err) { + return done(err) + } + client.subscribe('event', function (err, granted2) { + if (err) { + return done(err) + } + assert.isArray(granted2) + assert.isEmpty(granted2) + done() + }) + }) + }) + + it('should return an error (via callbacks) for topic #/event', function (done) { + var client = connect() + client.subscribe('#/event', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic event#', function (done) { + var client = connect() + client.subscribe('event#', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic system/#/event', function (done) { + var client = connect() + client.subscribe('system/#/event', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for empty topic list', function (done) { + var client = connect() + client.subscribe([], function (err) { + client.end() + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + + it('should return an error (via callbacks) for topic system/+/#/event', function (done) { + var client = connect() + client.subscribe('system/+/#/event', function (err) { + client.end(true, function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + }) + + describe('offline messages', function () { + it('should queue message until connected', function (done) { + var client = connect() + + client.publish('test', 'test') + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 3) + + client.once('connect', function () { + assert.strictEqual(client.queue.length, 0) + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should not queue qos 0 messages if queueQoSZero is false', function (done) { + var client = connect({queueQoSZero: false}) + + client.publish('test', 'test', {qos: 0}) + assert.strictEqual(client.queue.length, 0) + client.on('connect', function () { + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should queue qos != 0 messages', function (done) { + var client = connect({queueQoSZero: false}) + + client.publish('test', 'test', {qos: 1}) + client.publish('test', 'test', {qos: 2}) + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 2) + client.on('connect', function () { + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should not interrupt messages', function (done) { + var client = null + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var publishCount = 0 + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function () { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (packet.qos !== 0) { + serverClient.puback({messageId: packet.messageId}) + } + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + break + case 3: + assert.strictEqual(packet.payload.toString(), 'payload4') + server2.close() + done() + break + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore, + queueQoSZero: true + }) + client.on('packetreceive', function (packet) { + if (packet.cmd === 'connack') { + setImmediate( + function () { + client.publish('test', 'payload3', {qos: 1}) + client.publish('test', 'payload4', {qos: 0}) + } + ) + } + }) + client.publish('test', 'payload1', {qos: 2}) + client.publish('test', 'payload2', {qos: 2}) + }) + }) + + it('should call cb if an outgoing QoS 0 message is not sent', function (done) { + var client = connect({queueQoSZero: false}) + var called = false + + client.publish('test', 'test', {qos: 0}, function () { + called = true + }) + + client.on('connect', function () { + assert.isTrue(called) + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should delay ending up until all inflight messages are delivered', function (done) { + var client = connect() + var subscribeCalled = false + + client.on('connect', function () { + client.subscribe('test', function () { + subscribeCalled = true + }) + client.publish('test', 'test', function () { + client.end(false, function () { + assert.strictEqual(subscribeCalled, true) + done() + }) + }) + }) + }) + + it('wait QoS 1 publish messages', function (done) { + var client = connect() + var messageReceived = false + + client.on('connect', function () { + client.subscribe('test') + client.publish('test', 'test', { qos: 1 }, function () { + client.end(false, function () { + assert.strictEqual(messageReceived, true) + done() + }) + }) + client.on('message', function () { + messageReceived = true + }) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.on('publish', function (packet) { + serverClient.publish(packet) + }) + }) + }) + }) + + it('does not wait acks when force-closing', function (done) { + // non-running broker + var client = connect('mqtt://localhost:8993') + client.publish('test', 'test', { qos: 1 }) + client.end(true, done) + }) + + it('should call cb if store.put fails', function (done) { + const store = new Store() + store.put = function (packet, cb) { + process.nextTick(cb, new Error('oops there is an error')) + } + var client = connect({ incomingStore: store, outgoingStore: store }) + client.publish('test', 'test', { qos: 2 }, function (err) { + if (err) { + client.end(true, done) + } + }) + }) + }) + + describe('publishing', function () { + it('should publish a message (offline)', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + // don't wait on connect to send publish + client.publish(topic, payload) + + server.on('client', onClient) + + function onClient (serverClient) { + serverClient.once('connect', function () { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (online)', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + // block on connect before sending publish + client.on('connect', function () { + client.publish(topic, payload) + }) + + server.on('client', onClient) + + function onClient (serverClient) { + serverClient.once('connect', function () { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (retain, offline)', function (done) { + var client = connect({ queueQoSZero: true }) + var payload = 'test' + var topic = 'test' + var called = false + + client.publish(topic, payload, { retain: true }, function () { + called = true + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, true) + assert.strictEqual(called, true) + client.end(true, done) + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var payload = 'test_payload' + var topic = 'testTopic' + + client.on('packetsend', function (packet) { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + } else { + done(new Error('packet.cmd was not publish!')) + } + }) + + client.publish(topic, payload) + }) + + it('should accept options', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + var opts = { + retain: true, + qos: 1 + } + + client.once('connect', function () { + client.publish(topic, payload, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, false, 'incorrect dup') + client.end(done) + }) + }) + }) + + it('should publish with the default options for an empty parameter', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + var defaultOpts = {qos: 0, retain: false, dup: false} + + client.once('connect', function () { + client.publish(topic, payload, {}) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') + client.end(true, done) + }) + }) + }) + + it('should mark a message as duplicate when "dup" option is set', function (done) { + var client = connect() + var payload = 'duplicated-test' + var topic = 'test' + var opts = { + retain: true, + qos: 1, + dup: true + } + + client.once('connect', function () { + client.publish(topic, payload, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') + client.end(done) + }) + }) + }) + + it('should fire a callback (qos 0)', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('a', 'b', function () { + client.end() + done() + }) + }) + }) + + it('should fire a callback (qos 1)', function (done) { + var client = connect() + var opts = { qos: 1 } + + client.once('connect', function () { + client.publish('a', 'b', opts, function () { + client.end() + done() + }) + }) + }) + + it('should fire a callback (qos 2)', function (done) { + var client = connect() + var opts = { qos: 2 } + + client.once('connect', function () { + client.publish('a', 'b', opts, function () { + client.end() + done() + }) + }) + }) + + it('should support UTF-8 characters in topic', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('中国', 'hello', function () { + client.end() + done() + }) + }) + }) + + it('should support UTF-8 characters in payload', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('hello', '中国', function () { + client.end() + done() + }) + }) + }) + + it('should publish 10 QoS 2 and receive them', function (done) { + var client = connect() + var count = 0 + + client.on('connect', function () { + client.subscribe('test') + client.publish('test', 'test', { qos: 2 }) + }) + + client.on('message', function () { + if (count >= 10) { + client.end() + done() + } else { + client.publish('test', 'test', { qos: 2 }) + } + }) + + server.once('client', function (serverClient) { + serverClient.on('offline', function () { + client.end() + done('error went offline... didnt see this happen') + }) + + serverClient.on('subscribe', function () { + serverClient.on('publish', function (packet) { + serverClient.publish(packet) + }) + }) + + serverClient.on('pubrel', function () { + count++ + }) + }) + }) + + function testQosHandleMessage (qos, done) { + var client = connect() + + var messageEventCount = 0 + var handleMessageCount = 0 + + client.handleMessage = function (packet, callback) { + setTimeout(function () { + handleMessageCount++ + // next message event should not emit until handleMessage completes + assert.strictEqual(handleMessageCount, messageEventCount) + if (handleMessageCount === 10) { + setTimeout(function () { + client.end(true, done) + }) + } + callback() + }, 100) + } + + client.on('message', function (topic, message, packet) { + messageEventCount++ + }) + + client.on('connect', function () { + client.subscribe('test') + }) + + server.once('client', function (serverClient) { + serverClient.on('offline', function () { + client.end(true, function () { + done('error went offline... didnt see this happen') + }) + }) + + serverClient.on('subscribe', function () { + for (var i = 0; i < 10; i++) { + serverClient.publish({ + messageId: i, + topic: 'test', + payload: 'test' + i, + qos: qos + }) + } + }) + }) + } + + var qosTests = [ 0, 1, 2 ] + qosTests.forEach(function (QoS) { + it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { + testQosHandleMessage(QoS, done) + }) + }) + + it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { + var client = connect() + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client._sendPacket = sinon.spy() + + client._handlePublish({ + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1 + }, function (err) { + assert.exists(err) + }) + + assert.strictEqual(client._sendPacket.callCount, 0) + client.end() + client.on('connect', function () { done() }) + }) + + it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePublish` method', function (done) { + var client = connect() + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + try { + client._handlePublish({ + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1 + }) + client.end(true, done) + } catch (err) { + client.end(true, () => { done(err) }) + } + }) + + it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePublish({ + messageId: 1, + topic: 'test', + payload: 'test', + qos: 1 + }, function () { + client.end() + done() + }) + }) + + it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePublish({ + messageId: 1, + topic: 'test', + payload: 'test', + qos: 2 + }, function () { + client.end() + done() + }) + }) + + it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + cb(new Error('Error')) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({ incomingStore: store }) + + client._handlePubrel({ + messageId: 1, + qos: 2 + }, function () { + client.end(true, done) + }) + }) + + it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { + var delComplete = false + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + delComplete = true + cb(null) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePubrel({ + messageId: 1, + qos: 2 + }, function () { + assert.isTrue(delComplete) + client.end(true, done) + }) + }) + + it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function (done) { + var store = new Store() + var client = connect({incomingStore: store}) + + var messageId = Math.floor(65535 * Math.random()) + var topic = 'testTopic' + var payload = 'testPayload' + var qos = 2 + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', function () { + client.subscribe(topic, {qos: 2}) + + store.put({ + messageId: messageId, + topic: topic, + payload: payload, + qos: qos, + cmd: 'publish' + }, function () { + // cleans up the client + client._sendPacket = sinon.spy() + client._handlePubrel({cmd: 'pubrel', messageId: messageId}, function (err) { + assert.exists(err) + assert.strictEqual(client._sendPacket.callCount, 0) + client.end(true, done) + }) + }) + }) + }) + + it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePubrel` method', function (done) { + var store = new Store() + var client = connect({incomingStore: store}) + + var messageId = Math.floor(65535 * Math.random()) + var topic = 'test' + var payload = 'test' + var qos = 2 + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', function () { + client.subscribe(topic, {qos: 2}) + + store.put({ + messageId: messageId, + topic: topic, + payload: payload, + qos: qos, + cmd: 'publish' + }, function () { + try { + client._handlePubrel({cmd: 'pubrel', messageId: messageId}) + client.end(true, done) + } catch (err) { + client.end(true, () => { done(err) }) + } + }) + }) + }) + + it('should keep message order', function (done) { + var publishCount = 0 + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', function () {}) + + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + server2.close() + done() + break + } + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload1', {qos: 1}) + client.publish('topic', 'payload2', {qos: 1}) + client.end(true) + } else { + client.publish('topic', 'payload3', {qos: 1}) + } + }) + client.on('close', function () { + if (!reconnect) { + client.reconnect({ + clean: false, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + } + }) + }) + }) + + function testCallbackStorePutByQoS (qos, clean, expected, done) { + var client = connect({ + clean: clean, + clientId: 'testId' + }) + + var callbacks = [] + + function cbStorePut () { + callbacks.push('storeput') + } + + client.on('connect', function () { + client.publish('test', 'test', {qos: qos, cbStorePut: cbStorePut}, function (err) { + if (err) done(err) + callbacks.push('publish') + assert.deepEqual(callbacks, expected) + client.end(true, done) + }) + }) + } + + var callbackStorePutByQoSParameters = [ + {args: [0, true], expected: ['publish']}, + {args: [0, false], expected: ['publish']}, + {args: [1, true], expected: ['storeput', 'publish']}, + {args: [1, false], expected: ['storeput', 'publish']}, + {args: [2, true], expected: ['storeput', 'publish']}, + {args: [2, false], expected: ['storeput', 'publish']} + ] + + callbackStorePutByQoSParameters.forEach(function (test) { + if (test.args[0] === 0) { // QoS 0 + it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } else { // QoS 1 and 2 + it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } + }) + }) + + describe('unsubscribing', function () { + it('should send an unsubscribe packet (offline)', function (done) { + var client = connect() + + client.unsubscribe('test') + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, 'test') + client.end(done) + }) + }) + }) + + it('should send an unsubscribe packet', function (done) { + var client = connect() + var topic = 'topic' + + client.once('connect', function () { + client.unsubscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, topic) + client.end(done) + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'subscribe') { + client.end(true, done) + } + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetreceive', function (packet) { + if (packet.cmd === 'suback') { + client.end(true, done) + } + }) + }) + + it('should accept an array of unsubs', function (done) { + var client = connect() + var topics = ['topic1', 'topic2'] + + client.once('connect', function () { + client.unsubscribe(topics) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.deepStrictEqual(packet.unsubscriptions, topics) + client.end(done) + }) + }) + }) + + it('should fire a callback on unsuback', function (done) { + var client = connect() + var topic = 'topic' + + client.once('connect', function () { + client.unsubscribe(topic, () => { + client.end(true, done) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + serverClient.unsuback(packet) + }) + }) + }) + + it('should unsubscribe from a chinese topic', function (done) { + var client = connect() + var topic = '中国' + + client.once('connect', function () { + client.unsubscribe(topic, () => { + client.end(err => { + done(err) + }) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, topic) + }) + }) + }) + }) + + describe('keepalive', function () { + var clock + + beforeEach(function () { + clock = sinon.useFakeTimers() + }) + + afterEach(function () { + clock.restore() + }) + + it('should checkPing at keepalive interval', function (done) { + var interval = 3 + var client = connect({ keepalive: interval }) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 1) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 2) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 3) + + client.end(true, done) + }) + }) + + it('should not checkPing if publishing at a higher rate than keepalive', function (done) { + var intervalMs = 3000 + var client = connect({keepalive: intervalMs / 1000}) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 0) + client.end(true, done) + }) + }) + + it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function (done) { + var intervalMs = 3000 + var client = connect({ + keepalive: intervalMs / 1000, + reschedulePings: false + }) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 1) + client.end(true, done) + }) + }) + }) + + describe('pinging', function () { + it('should set a ping timer', function (done) { + var client = connect({keepalive: 3}) + client.once('connect', function () { + assert.exists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should not set a ping timer keepalive=0', function (done) { + var client = connect({keepalive: 0}) + client.on('connect', function () { + assert.notExists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should reconnect if pingresp is not sent', function (done) { + var client = connect({keepalive: 1, reconnectPeriod: 100}) + + // Fake no pingresp being send by stubbing the _handlePingresp function + client._handlePingresp = function () {} + + client.once('connect', function () { + client.once('connect', function () { + client.end(true, done) + }) + }) + }) + + it('should not reconnect if pingresp is successful', function (done) { + var client = connect({keepalive: 100}) + client.once('close', function () { + done(new Error('Client closed connection')) + }) + setTimeout(done, 1000) + }) + + it('should defer the next ping when sending a control packet', function (done) { + var client = connect({keepalive: 1}) + + client.once('connect', function () { + client._checkPing = sinon.spy() + + client.publish('foo', 'bar') + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + done() + }, 75) + }, 75) + }, 75) + }) + }) + }) + + describe('subscribing', function () { + it('should send a subscribe message (offline)', function (done) { + var client = connect() + + client.subscribe('test') + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + done() + }) + }) + }) + + it('should send a subscribe message', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.subscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: 0 + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + done() + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'subscribe') { + done() + } + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetreceive', function (packet) { + if (packet.cmd === 'suback') { + done() + } + }) + }) + + it('should accept an array of subscriptions', function (done) { + var client = connect() + var subs = ['test1', 'test2'] + + client.once('connect', function () { + client.subscribe(subs) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] + var expected = subs.map(function (i) { + var result = {topic: i, qos: 0} + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + return result + }) + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept a hash of subscriptions', function (done) { + var client = connect() + var topics = { + test1: {qos: 0}, + test2: {qos: 1} + } + + client.once('connect', function () { + client.subscribe(topics) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var k + var expected = [] + + for (k in topics) { + if (topics.hasOwnProperty(k)) { + var result = { + topic: k, + qos: topics[k].qos + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + expected.push(result) + } + } + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept an options parameter', function (done) { + var client = connect() + var topic = 'test' + var opts = {qos: 1} + + client.once('connect', function () { + client.subscribe(topic, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var expected = [{ + topic: topic, + qos: 1 + }] + + if (version === 5) { + expected[0].nl = false + expected[0].rap = false + expected[0].rh = 0 + } + + assert.deepStrictEqual(packet.subscriptions, expected) + done() + }) + }) + }) + + it('should subscribe with the default options for an empty options parameter', function (done) { + var client = connect() + var topic = 'test' + var defaultOpts = {qos: 0} + + client.once('connect', function () { + client.subscribe(topic, {}) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: defaultOpts.qos + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + + assert.include(packet.subscriptions[0], result) + client.end(err => done(err)) + }) + }) + }) + + it('should fire a callback on suback', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.subscribe(topic, { qos: 2 }, function (err, granted) { + if (err) { + done(err) + } else { + assert.exists(granted, 'granted not given') + var expectedResult = {topic: 'test', qos: 2} + if (version === 5) { + expectedResult.nl = false + expectedResult.rap = false + expectedResult.rh = 0 + expectedResult.properties = undefined + } + assert.include(granted[0], expectedResult) + client.end(err => done(err)) + } + }) + }) + }) + + it('should fire a callback with error if disconnected (options provided)', function (done) { + var client = connect() + var topic = 'test' + client.once('connect', function () { + client.end(true, function () { + client.subscribe(topic, {qos: 2}, function (err, granted) { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should fire a callback with error if disconnected (options not provided)', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.end(true, function () { + client.subscribe(topic, function (err, granted) { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should subscribe with a chinese topic', function (done) { + var client = connect() + var topic = '中国' + + client.once('connect', function () { + client.subscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: 0 + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + client.end(done) + }) + }) + }) + }) + + describe('receiving messages', function () { + it('should fire the message event', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + // + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.on('packetreceive', function (packet) { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.qos, 1) + assert.strictEqual(packet.topic, testPacket.topic) + assert.strictEqual(packet.payload.toString(), testPacket.payload) + assert.strictEqual(packet.retain, true) + client.end(true, done) + } + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should support binary data', function (done) { + var client = connect({ encoding: 'binary' }) + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2)', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5 + } + + server.testPublish = testPacket + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2) - repeated publish', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5 + } + + server.testPublish = testPacket + + var messageHandler = function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + + assert.strictEqual(spiedMessageHandler.callCount, 1) + client.end(true, done) + } + + var spiedMessageHandler = sinon.spy(messageHandler) + + client.subscribe(testPacket.topic) + client.on('message', spiedMessageHandler) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + // twice, should be ignored + serverClient.publish(testPacket) + }) + }) + }) + + it('should support a chinese topic', function (done) { + var client = connect({ encoding: 'binary' }) + var testPacket = { + topic: '国', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + }) + + describe('qos handling', function () { + it('should follow qos 0 semantics (trivial)', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 0}, () => { + client.end(true, done) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 0, + retain: false + }) + }) + }) + }) + + it('should follow qos 1 semantics', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 50 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 1}) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + messageId: mid, + qos: 1 + }) + }) + + serverClient.once('puback', function (packet) { + assert.strictEqual(packet.messageId, mid) + client.end(done) + }) + }) + }) + + it('should follow qos 2 semantics', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + var publishReceived = 0 + var pubrecReceived = 0 + var pubrelReceived = 0 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + }) + + client.on('packetreceive', (packet) => { + switch (packet.cmd) { + case 'connack': + case 'suback': + // expected, but not specifically part of QOS 2 semantics + break + case 'publish': + assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') + assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') + publishReceived += 1 + break + case 'pubrel': + assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') + pubrelReceived += 1 + break + default: + should.fail() + } + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid + }) + }) + + serverClient.on('pubrec', function () { + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') + assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') + pubrecReceived += 1 + }) + + serverClient.once('pubcomp', function () { + client.removeAllListeners() + serverClient.removeAllListeners() + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') + assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') + client.end(true, done) + }) + }) + }) + + it('should should empty the incoming store after a qos 2 handshake is completed', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + }) + + client.on('packetreceive', (packet) => { + if (packet.cmd === 'pubrel') { + assert.strictEqual(client.incomingStore._inflights.size, 1) + } + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid + }) + }) + + serverClient.once('pubcomp', function () { + assert.strictEqual(client.incomingStore._inflights.size, 0) + client.removeAllListeners() + client.end(true, done) + }) + }) + }) + + function testMultiplePubrel (shouldSendPubcompFail, done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + var pubcompCount = 0 + var pubrelCount = 0 + var handleMessageCount = 0 + var emitMessageCount = 0 + var origSendPacket = client._sendPacket + var shouldSendFail + + client.handleMessage = function (packet, callback) { + handleMessageCount++ + callback() + } + + client.on('message', function () { + emitMessageCount++ + }) + + client._sendPacket = function (packet, sendDone) { + shouldSendFail = packet.cmd === 'pubcomp' && shouldSendPubcompFail + if (sendDone) { + sendDone(shouldSendFail ? new Error('testing pubcomp failure') : undefined) + } + + // send the mocked response + switch (packet.cmd) { + case 'subscribe': + const suback = {cmd: 'suback', messageId: packet.messageId, granted: [2]} + client._handlePacket(suback, function (err) { + assert.isNotOk(err) + }) + break + case 'pubrec': + case 'pubcomp': + // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp + if (packet.cmd === 'pubcomp') { + pubcompCount++ + if (pubcompCount === 2) { + // end the test once the client has gone through two rounds of replying to pubrel messages + assert.strictEqual(pubrelCount, 2) + assert.strictEqual(handleMessageCount, 1) + assert.strictEqual(emitMessageCount, 1) + client._sendPacket = origSendPacket + client.end(true, done) + break + } + } + + // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received + const pubrel = {cmd: 'pubrel', messageId: mid} + pubrelCount++ + client._handlePacket(pubrel, function (err) { + if (shouldSendFail) { + assert.exists(err) + assert.instanceOf(err, Error) + } else { + assert.notExists(err) + } + }) + break + } + } + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + const publish = {cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid} + client._handlePacket(publish, function (err) { + assert.notExists(err) + }) + }) + } + + it('handle qos 2 messages exactly once when multiple pubrel received', function (done) { + testMultiplePubrel(false, done) + }) + + it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function (done) { + testMultiplePubrel(true, done) + }) + }) + + describe('auto reconnect', function () { + it('should mark the client disconnecting if #end called', function (done) { + var client = connect() + + client.end(true, err => { + assert.isTrue(client.disconnecting) + done(err) + }) + }) + + it('should reconnect after stream disconnect', function (done) { + var client = connect() + + var tryReconnect = true + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + client.end(true, done) + } + }) + }) + + it('should emit \'reconnect\' when reconnecting', function (done) { + var client = connect() + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + client.end(true, done) + } + }) + }) + + it('should emit \'offline\' after going offline', function (done) { + var client = connect() + + var tryReconnect = true + var offlineEvent = false + + client.on('offline', function () { + offlineEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(offlineEvent) + client.end(true, done) + } + }) + }) + + it('should not reconnect if it was ended by the user', function (done) { + var client = connect() + + client.on('connect', function () { + client.end() + done() // it will raise an exception if called two times + }) + }) + + it('should setup a reconnect timer on disconnect', function (done) { + var client = connect() + + client.once('connect', function () { + assert.notExists(client.reconnectTimer) + client.stream.end() + }) + + client.once('close', function () { + assert.exists(client.reconnectTimer) + client.end(true, done) + }) + }) + + var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] + reconnectPeriodTests.forEach((test) => { + it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { + var end + var reconnectSlushTime = 200 + var client = connect({reconnectPeriod: test.period}) + var reconnect = false + var start = Date.now() + + client.on('connect', function () { + if (!reconnect) { + client.stream.end() + reconnect = true + } else { + end = Date.now() + client.end(() => { + let reconnectPeriodDuringTest = end - start + if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { + // give the connection a 200 ms slush window + done() + } else { + done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) + } + }) + } + }) + }) + }) + + it('should always cleanup successfully on reconnection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1}) + // bind client.end so that when it is called it is automatically passed in the done callback + setTimeout(client.end.bind(client, done), 50) + }) + + it('should resend in-flight QoS 1 publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, function () { + clientCalledBack = true + check() + }) + + function check () { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight publish messages if disconnecting', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + client.end(true, err => { + assert.isFalse(serverPublished) + assert.isFalse(clientCalledBack) + done(err) + }) + }) + }) + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + serverPublished = true + }) + }) + }) + client.publish('hello', 'world', { qos: 1 }, function () { + clientCalledBack = true + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + + server.once('client', function (serverClient) { + // ignore errors + serverClient.on('error', function () {}) + serverClient.on('publish', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('pubrel', function () { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, function () { + clientCalledBack = true + check() + }) + + function check () { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight QoS 1 removed publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, function (err) { + clientCalledBack = true + assert.exists(err, 'error should exist') + assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, (err) => { + done(err) + }) + }) + + it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, function (err) { + clientCalledBack = true + assert.strictEqual(err.message, 'Message removed') + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, done) + }) + + it('should resubscribe when reconnecting', function (done) { + var client = connect({ reconnectPeriod: 100 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + client.end(done) + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should not resubscribe when reconnecting if resubscribe is disabled', function (done) { + var client = connect({ reconnectPeriod: 100, resubscribe: false }) + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + should.fail() + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) + client.end(true, done) + } + }) + }) + + it('should not resubscribe when reconnecting if suback is error', function (done) { + var tryReconnect = true + var reconnectEvent = false + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', function (packet) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos | 0x80 + }) + }) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + }) + }) + + server2.listen(ports.PORTAND49, function () { + var client = connect({ + port: ports.PORTAND49, + host: 'localhost', + reconnectPeriod: 100 + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + should.fail() + }) + }) + }) + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) + server2.close() + client.end(true, done) + } + }) + }) + }) + + it('should preserved incomingStore after disconnecting if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + if (reconnect) { + serverClient.pubrel({ messageId: 1 }) + } + }) + serverClient.on('subscribe', function (packet) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) + }) + serverClient.on('pubrec', function (packet) { + client.end(false, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + }) + }) + serverClient.on('pubcomp', function (packet) { + client.end(true, () => { + server2.close() + done() + }) + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.subscribe('test', {qos: 2}, function () { + }) + reconnect = true + } + }) + client.on('message', function (topic, message) { + assert.strictEqual(topic, 'topic') + assert.strictEqual(message.toString(), 'payload') + }) + }) + }) + + it('should clear outgoing if close from server', function (done) { + var reconnect = false + var client = {} + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', function (packet) { + if (reconnect) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } else { + serverClient.destroy() + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: true, + clientId: 'cid1', + keepalive: 1, + reconnectPeriod: 0 + }) + + client.on('connect', function () { + client.subscribe('test', {qos: 2}, function (e) { + if (!e) { + client.end() + } + }) + }) + + client.on('close', function () { + if (reconnect) { + server2.close() + done() + } else { + assert.strictEqual(Object.keys(client.outgoing).length, 0) + reconnect = true + client.reconnect() + } + }) + }) + }) + + it('should resend in-flight QoS 1 publish messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, () => { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 1}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 2}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (!reconnect) { + serverClient.pubrec({messageId: packet.messageId}) + } + }) + serverClient.on('pubrel', function () { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 2}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight publish messages by published order', function (done) { + var publishCount = 0 + var reconnect = false + var disconnectOnce = true + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', function () {}) + + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + server2.close() + client.end(true, done) + break + } + } else { + if (disconnectOnce) { + client.end(true, function () { + reconnect = true + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + }) + disconnectOnce = false + } + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.nextId = 65535 + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload1', {qos: 1}) + client.publish('topic', 'payload2', {qos: 1}) + client.publish('topic', 'payload3', {qos: 1}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should be able to pub/sub if reconnect() is called at close handler', function (done) { + var client = connect({ reconnectPeriod: 0 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('close', function () { + if (tryReconnect) { + tryReconnect = false + client.reconnect() + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', function () { + client.end() + }) + } + }) + }) + + it('should be able to pub/sub if reconnect() is called at out of close handler', function (done) { + var client = connect({ reconnectPeriod: 0 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('close', function () { + if (tryReconnect) { + tryReconnect = false + setTimeout(function () { + client.reconnect() + }, 100) + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', function () { + client.end() + }) + } + }) + }) + + context('with alternate server client', function () { + var cachedClientListeners + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + + beforeEach(function () { + cachedClientListeners = server.listeners('client') + server.removeAllListeners('client') + }) + + afterEach(function () { + server.removeAllListeners('client') + cachedClientListeners.forEach(function (listener) { + server.on('client', listener) + }) + }) + + it('should resubscribe even if disconnect is before suback', function (done) { + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) + var subscribeCount = 0 + var connectCount = 0 + + server.on('client', function (serverClient) { + serverClient.on('connect', function () { + connectCount++ + serverClient.connack(connack) + }) + + serverClient.on('subscribe', function () { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, confirm that the only two + // subscribes have taken place, then cleanup and exit + if (connectCount >= 2) { + assert.strictEqual(subscribeCount, 2) + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + + it('should resubscribe exactly once', function (done) { + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) + var subscribeCount = 0 + + server.on('client', function (serverClient) { + serverClient.on('connect', function () { + serverClient.connack(connack) + }) + + serverClient.on('subscribe', function () { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, only two subs + // subscribes have taken place, then cleanup and exit + if (subscribeCount === 2) { + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + }) + }) +} diff --git a/test/abstract_store.js b/test/abstract_store.js index 33b78106d..02b3ec849 100644 --- a/test/abstract_store.js +++ b/test/abstract_store.js @@ -1,135 +1,135 @@ -'use strict' - -require('should') - -module.exports = function abstractStoreTest (build) { - var store - - beforeEach(function (done) { - build(function (err, _store) { - store = _store - done(err) - }) - }) - - afterEach(function (done) { - store.close(done) - }) - - it('should put and stream in-flight packets', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet) - done() - }) - }) - }) - - it('should support destroying the stream', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - var stream = store.createStream() - stream.on('close', done) - stream.destroy() - }) - }) - - it('should add and del in-flight packets', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del(packet, function () { - store - .createStream() - .on('data', function () { - done(new Error('this should never happen')) - }) - .on('end', done) - }) - }) - }) - - it('should replace a packet when doing put with the same messageId', function (done) { - var packet1 = { - cmd: 'publish', // added - topic: 'hello', - payload: 'world', - qos: 2, - messageId: 42 - } - var packet2 = { - cmd: 'pubrel', // added - qos: 2, - messageId: 42 - } - - store.put(packet1, function () { - store.put(packet2, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet2) - done() - }) - }) - }) - }) - - it('should return the original packet on del', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del({ messageId: 42 }, function (err, deleted) { - if (err) { - throw err - } - deleted.should.eql(packet) - done() - }) - }) - }) - - it('should get a packet with the same messageId', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.get({ messageId: 42 }, function (err, fromDb) { - if (err) { - throw err - } - fromDb.should.eql(packet) - done() - }) - }) - }) -} +'use strict' + +require('should') + +module.exports = function abstractStoreTest (build) { + var store + + beforeEach(function (done) { + build(function (err, _store) { + store = _store + done(err) + }) + }) + + afterEach(function (done) { + store.close(done) + }) + + it('should put and stream in-flight packets', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store + .createStream() + .on('data', function (data) { + data.should.eql(packet) + done() + }) + }) + }) + + it('should support destroying the stream', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + var stream = store.createStream() + stream.on('close', done) + stream.destroy() + }) + }) + + it('should add and del in-flight packets', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.del(packet, function () { + store + .createStream() + .on('data', function () { + done(new Error('this should never happen')) + }) + .on('end', done) + }) + }) + }) + + it('should replace a packet when doing put with the same messageId', function (done) { + var packet1 = { + cmd: 'publish', // added + topic: 'hello', + payload: 'world', + qos: 2, + messageId: 42 + } + var packet2 = { + cmd: 'pubrel', // added + qos: 2, + messageId: 42 + } + + store.put(packet1, function () { + store.put(packet2, function () { + store + .createStream() + .on('data', function (data) { + data.should.eql(packet2) + done() + }) + }) + }) + }) + + it('should return the original packet on del', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.del({ messageId: 42 }, function (err, deleted) { + if (err) { + throw err + } + deleted.should.eql(packet) + done() + }) + }) + }) + + it('should get a packet with the same messageId', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.get({ messageId: 42 }, function (err, fromDb) { + if (err) { + throw err + } + fromDb.should.eql(packet) + done() + }) + }) + }) +} diff --git a/test/browser/server.js b/test/browser/server.js index c4cf66b96..75a9a8994 100644 --- a/test/browser/server.js +++ b/test/browser/server.js @@ -1,132 +1,132 @@ -'use strict' - -var handleClient -var WS = require('ws') -var WebSocketServer = WS.Server -var Connection = require('mqtt-connection') -var http = require('http') - -handleClient = function (client) { - var self = this - - if (!self.clients) { - self.clients = {} - } - - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) - } else { - client.connack({returnCode: 0}) - } - self.clients[packet.clientId] = client - client.subscriptions = [] - }) - - client.on('publish', function (packet) { - var i, k, c, s, publish - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - - for (k in self.clients) { - c = self.clients[k] - publish = false - - for (i = 0; i < c.subscriptions.length; i++) { - s = c.subscriptions[i] - - if (s.test(packet.topic)) { - publish = true - } - } - - if (publish) { - try { - c.publish({topic: packet.topic, payload: packet.payload}) - } catch (error) { - delete self.clients[k] - } - } - } - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - var qos - var topic - var reg - var granted = [] - - for (var i = 0; i < packet.subscriptions.length; i++) { - qos = packet.subscriptions[i].qos - topic = packet.subscriptions[i].topic - reg = new RegExp(topic.replace('+', '[^/]+').replace('#', '.+') + '$') - - granted.push(qos) - client.subscriptions.push(reg) - } - - client.suback({messageId: packet.messageId, granted: granted}) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -function start (startPort, done) { - var server = http.createServer() - var wss = new WebSocketServer({server: server}) - - wss.on('connection', function (ws) { - var stream, connection - - if (!(ws.protocol === 'mqtt' || - ws.protocol === 'mqttv3.1')) { - return ws.close() - } - - stream = WS.createWebSocketStream(ws) - connection = new Connection(stream) - handleClient.call(server, connection) - }) - server.listen(startPort, done) - server.on('request', function (req, res) { - res.statusCode = 404 - res.end('Not Found') - }) - return server -} - -if (require.main === module) { - start(process.env.PORT || process.env.AIRTAP_PORT, function (err) { - if (err) { - console.error(err) - return - } - console.log('tunnelled server started on port', process.env.PORT || process.env.AIRTAP_PORT) - }) -} +'use strict' + +var handleClient +var WS = require('ws') +var WebSocketServer = WS.Server +var Connection = require('mqtt-connection') +var http = require('http') + +handleClient = function (client) { + var self = this + + if (!self.clients) { + self.clients = {} + } + + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({returnCode: 2}) + } else { + client.connack({returnCode: 0}) + } + self.clients[packet.clientId] = client + client.subscriptions = [] + }) + + client.on('publish', function (packet) { + var i, k, c, s, publish + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + + for (k in self.clients) { + c = self.clients[k] + publish = false + + for (i = 0; i < c.subscriptions.length; i++) { + s = c.subscriptions[i] + + if (s.test(packet.topic)) { + publish = true + } + } + + if (publish) { + try { + c.publish({topic: packet.topic, payload: packet.payload}) + } catch (error) { + delete self.clients[k] + } + } + } + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + var qos + var topic + var reg + var granted = [] + + for (var i = 0; i < packet.subscriptions.length; i++) { + qos = packet.subscriptions[i].qos + topic = packet.subscriptions[i].topic + reg = new RegExp(topic.replace('+', '[^/]+').replace('#', '.+') + '$') + + granted.push(qos) + client.subscriptions.push(reg) + } + + client.suback({messageId: packet.messageId, granted: granted}) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +function start (startPort, done) { + var server = http.createServer() + var wss = new WebSocketServer({server: server}) + + wss.on('connection', function (ws) { + var stream, connection + + if (!(ws.protocol === 'mqtt' || + ws.protocol === 'mqttv3.1')) { + return ws.close() + } + + stream = WS.createWebSocketStream(ws) + connection = new Connection(stream) + handleClient.call(server, connection) + }) + server.listen(startPort, done) + server.on('request', function (req, res) { + res.statusCode = 404 + res.end('Not Found') + }) + return server +} + +if (require.main === module) { + start(process.env.PORT || process.env.AIRTAP_PORT, function (err) { + if (err) { + console.error(err) + return + } + console.log('tunnelled server started on port', process.env.PORT || process.env.AIRTAP_PORT) + }) +} diff --git a/test/browser/test.js b/test/browser/test.js index 78fa93cc5..8e9cd42e3 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -1,92 +1,92 @@ -'use strict' - -var mqtt = require('../../lib/connect') -var xtend = require('xtend') -var _URL = require('url') -var parsed = _URL.parse(document.URL) -var isHttps = parsed.protocol === 'https:' -var port = parsed.port || (isHttps ? 443 : 80) -var host = parsed.hostname -var protocol = isHttps ? 'wss' : 'ws' - -function clientTests (buildClient) { - var client - - beforeEach(function () { - client = buildClient() - client.on('offline', function () { - console.log('client offline') - }) - client.on('connect', function () { - console.log('client connect') - }) - client.on('reconnect', function () { - console.log('client reconnect') - }) - }) - - afterEach(function (done) { - client.once('close', function () { - done() - }) - client.end() - }) - - it('should connect', function (done) { - client.on('connect', function () { - done() - }) - }) - - it('should publish and subscribe', function (done) { - client.subscribe('hello', function () { - done() - }).publish('hello', 'world') - }) -} - -function suiteFactory (configName, opts) { - function setVersion (base) { - return xtend(base || {}, opts) - } - - var suiteName = 'MqttClient(' + configName + '=' + JSON.stringify(opts) + ')' - describe(suiteName, function () { - this.timeout(10000) - - describe('specifying nothing', function () { - clientTests(function () { - return mqtt.connect(setVersion()) - }) - }) - - if (parsed.hostname === 'localhost') { - describe('specifying a port', function () { - clientTests(function () { - return mqtt.connect(setVersion({ protocol: protocol, port: port })) - }) - }) - } - - describe('specifying a port and host', function () { - clientTests(function () { - return mqtt.connect(setVersion({ protocol: protocol, port: port, host: host })) - }) - }) - - describe('specifying a URL', function () { - clientTests(function () { - return mqtt.connect(protocol + '://' + host + ':' + port, setVersion()) - }) - }) - - describe('specifying a URL with a path', function () { - clientTests(function () { - return mqtt.connect(protocol + '://' + host + ':' + port + '/mqtt', setVersion()) - }) - }) - }) -} - -suiteFactory('v3', {protocolId: 'MQIsdp', protocolVersion: 3}) -suiteFactory('default', {}) +'use strict' + +var mqtt = require('../../lib/connect') +var xtend = require('xtend') +var _URL = require('url') +var parsed = _URL.parse(document.URL) +var isHttps = parsed.protocol === 'https:' +var port = parsed.port || (isHttps ? 443 : 80) +var host = parsed.hostname +var protocol = isHttps ? 'wss' : 'ws' + +function clientTests (buildClient) { + var client + + beforeEach(function () { + client = buildClient() + client.on('offline', function () { + console.log('client offline') + }) + client.on('connect', function () { + console.log('client connect') + }) + client.on('reconnect', function () { + console.log('client reconnect') + }) + }) + + afterEach(function (done) { + client.once('close', function () { + done() + }) + client.end() + }) + + it('should connect', function (done) { + client.on('connect', function () { + done() + }) + }) + + it('should publish and subscribe', function (done) { + client.subscribe('hello', function () { + done() + }).publish('hello', 'world') + }) +} + +function suiteFactory (configName, opts) { + function setVersion (base) { + return xtend(base || {}, opts) + } + + var suiteName = 'MqttClient(' + configName + '=' + JSON.stringify(opts) + ')' + describe(suiteName, function () { + this.timeout(10000) + + describe('specifying nothing', function () { + clientTests(function () { + return mqtt.connect(setVersion()) + }) + }) + + if (parsed.hostname === 'localhost') { + describe('specifying a port', function () { + clientTests(function () { + return mqtt.connect(setVersion({ protocol: protocol, port: port })) + }) + }) + } + + describe('specifying a port and host', function () { + clientTests(function () { + return mqtt.connect(setVersion({ protocol: protocol, port: port, host: host })) + }) + }) + + describe('specifying a URL', function () { + clientTests(function () { + return mqtt.connect(protocol + '://' + host + ':' + port, setVersion()) + }) + }) + + describe('specifying a URL with a path', function () { + clientTests(function () { + return mqtt.connect(protocol + '://' + host + ':' + port + '/mqtt', setVersion()) + }) + }) + }) +} + +suiteFactory('v3', {protocolId: 'MQIsdp', protocolVersion: 3}) +suiteFactory('default', {}) diff --git a/test/client.js b/test/client.js index 0b3c4228a..4ea052ab8 100644 --- a/test/client.js +++ b/test/client.js @@ -1,486 +1,486 @@ -'use strict' - -var mqtt = require('..') -var assert = require('chai').assert -const { fork } = require('child_process') -var path = require('path') -var abstractClientTests = require('./abstract_client') -var net = require('net') -var eos = require('end-of-stream') -var mqttPacket = require('mqtt-packet') -var Duplex = require('readable-stream').Duplex -var Connection = require('mqtt-connection') -var MqttServer = require('./server').MqttServer -var util = require('util') -var ports = require('./helpers/port_list') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var debug = require('debug')('TEST:client') - -describe('MqttClient', function () { - var client - var server = serverBuilder('mqtt') - var config = {protocol: 'mqtt', port: ports.PORT} - server.listen(ports.PORT) - - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) - - abstractClientTests(server, config) - - describe('creating', function () { - it('should allow instantiation of MqttClient without the \'new\' operator', function (done) { - try { - client = mqtt.MqttClient(function () { - throw Error('break') - }, {}) - client.end() - } catch (err) { - assert.strictEqual(err.message, 'break') - done() - } - }) - }) - - describe('message ids', function () { - it('should increment the message id', function () { - client = mqtt.connect(config) - var currentId = client._nextId() - - assert.equal(client._nextId(), currentId + 1) - client.end() - }) - - it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { - var server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - client = mqtt.connect({ - port: ports.PORTAND49, - host: 'localhost' - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'pubcomp') { - client.end() - server2.close() - done() - } - }) - }) - }) - - it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function (done) { - var parser = mqttPacket.parser() - var count = 0 - var max = 1000 - var duplex = new Duplex({ - read: function (n) {}, - write: function (chunk, enc, cb) { - parser.parse(chunk) - cb() // nothing to do - } - }) - client = new mqtt.MqttClient(function () { - return duplex - }, {}) - - client.on('message', function (t, p, packet) { - if (++count === max) { - done() - } - }) - - parser.on('packet', function (packet) { - var packets = [] - - if (packet.cmd === 'connect') { - duplex.push(mqttPacket.generate({ - cmd: 'connack', - sessionPresent: false, - returnCode: 0 - })) - - for (var i = 0; i < max; i++) { - packets.push(mqttPacket.generate({ - cmd: 'publish', - topic: Buffer.from('hello'), - payload: Buffer.from('world'), - retain: false, - dup: false, - messageId: i + 1, - qos: 1 - })) - } - - duplex.push(Buffer.concat(packets)) - } - }) - }) - }) - - describe('flushing', function () { - it('should attempt to complete pending unsub and send on ping timeout', function (done) { - this.timeout(10000) - var server3 = new MqttServer(function (client) { - client.on('connect', function (packet) { - client.connack({returnCode: 0}) - }) - }).listen(ports.PORTAND72) - - var pubCallbackCalled = false - var unsubscribeCallbackCalled = false - client = mqtt.connect({ - port: ports.PORTAND72, - host: 'localhost', - keepalive: 1, - connectTimeout: 350, - reconnectPeriod: 0 - }) - client.once('connect', () => { - client.publish('fakeTopic', 'fakeMessage', {qos: 1}, (err, result) => { - assert.exists(err) - pubCallbackCalled = true - }) - client.unsubscribe('fakeTopic', (err, result) => { - assert.exists(err) - unsubscribeCallbackCalled = true - }) - setTimeout(() => { - client.end(() => { - assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') - server3.close() - done() - }) - }, 5000) - }) - }) - }) - - describe('reconnecting', function () { - it('should attempt to reconnect once server is down', function (done) { - this.timeout(30000) - - var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) - innerServer.on('close', (code) => { - if (code) { - done(util.format('child process closed with code %d', code)) - } - }) - - innerServer.on('exit', (code) => { - if (code) { - done(util.format('child process exited with code %d', code)) - } - }) - - client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) - client.once('connect', function () { - innerServer.kill('SIGINT') // mocks server shutdown - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, done) - }) - }) - }) - - it('should reconnect if a connack is not received in an interval', function (done) { - this.timeout(2000) - - var server2 = net.createServer().listen(ports.PORTAND43) - - server2.on('connection', function (c) { - eos(c, function () { - server2.close() - }) - }) - - server2.on('listening', function () { - client = mqtt.connect({ - servers: [ - { port: ports.PORTAND43, host: 'localhost_fake' }, - { port: ports.PORT, host: 'localhost' } - ], - connectTimeout: 500 - }) - - server.once('client', function () { - client.end(true, (err) => { - done(err) - }) - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - - it('should not be cleared by the connack timer', function (done) { - this.timeout(4000) - - var server2 = net.createServer().listen(ports.PORTAND44) - - server2.on('connection', function (c) { - c.destroy() - }) - - server2.once('listening', function () { - var reconnects = 0 - var connectTimeout = 1000 - var reconnectPeriod = 100 - var expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) - client = mqtt.connect({ - port: ports.PORTAND44, - host: 'localhost', - connectTimeout: connectTimeout, - reconnectPeriod: reconnectPeriod - }) - - client.on('reconnect', function () { - reconnects++ - if (reconnects >= expectedReconnects) { - client.end(true, done) - } - }) - }) - }) - - it('should not keep requeueing the first message when offline', function (done) { - this.timeout(2500) - - var server2 = serverBuilder('mqtt').listen(ports.PORTAND45) - client = mqtt.connect({ - port: ports.PORTAND45, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - server2.on('client', function (serverClient) { - client.publish('hello', 'world', { qos: 1 }, function () { - serverClient.destroy() - server2.close(() => { - debug('now publishing message in an offline state') - client.publish('hello', 'world', { qos: 1 }) - }) - }) - }) - - setTimeout(function () { - if (client.queue.length === 0) { - debug('calling final client.end()') - client.end(true, (err) => done(err)) - } else { - debug('calling client.end()') - client.end(true) - } - }, 2000) - }) - - it('should not send the same subscribe multiple times on a flaky connection', function (done) { - this.timeout(3500) - - var KILL_COUNT = 4 - var killedConnections = 0 - var subIds = {} - client = mqtt.connect({ - port: ports.PORTAND46, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - var server2 = new MqttServer(function (serverClient) { - serverClient.on('error', function () {}) - debug('setting serverClient connect callback') - serverClient.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - debug('connack with returnCode 2') - serverClient.connack({returnCode: 2}) - } else { - debug('connack with returnCode 0') - serverClient.connack({returnCode: 0}) - } - }) - }).listen(ports.PORTAND46) - - server2.on('client', function (serverClient) { - debug('client received on server2.') - debug('subscribing to topic `topic`') - client.subscribe('topic', function () { - debug('once subscribed to topic, end client, destroy serverClient, and close server.') - serverClient.destroy() - server2.close(() => { client.end(true, done) }) - }) - - serverClient.on('subscribe', function (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few sub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - } else { - // Keep track of acks - if (!subIds[packet.messageId]) { - subIds[packet.messageId] = 0 - } - subIds[packet.messageId]++ - if (subIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) - client.end(true) - serverClient.end() - server2.destroy() - } - - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } - }) - }) - }) - - it('should not fill the queue of subscribes if it cannot connect', function (done) { - this.timeout(2500) - var server2 = net.createServer(function (stream) { - var serverClient = new Connection(stream) - - serverClient.on('error', function (e) { /* do nothing */ }) - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - serverClient.destroy() - }) - }) - - server2.listen(ports.PORTAND48, function () { - client = mqtt.connect({ - port: ports.PORTAND48, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - client.subscribe('hello') - - setTimeout(function () { - assert.equal(client.queue.length, 1) - client.end(true, () => { - done() - }) - }, 1000) - }) - }) - - it('should not send the same publish multiple times on a flaky connection', function (done) { - this.timeout(3500) - - var KILL_COUNT = 4 - var killedConnections = 0 - var pubIds = {} - client = mqtt.connect({ - port: ports.PORTAND47, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - var server2 = net.createServer(function (stream) { - var serverClient = new Connection(stream) - serverClient.on('error', function () {}) - serverClient.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - serverClient.connack({returnCode: 2}) - } else { - serverClient.connack({returnCode: 0}) - } - }) - - this.emit('client', serverClient) - }).listen(ports.PORTAND47) - - server2.on('client', function (serverClient) { - client.publish('topic', 'data', { qos: 1 }, function () { - serverClient.destroy() - server2.close() - client.end(true, done) - }) - - serverClient.on('publish', function onPublish (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few pub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - - // to avoid receiving inflight messages - serverClient.removeListener('publish', onPublish) - } else { - // Keep track of acks - if (!pubIds[packet.messageId]) { - pubIds[packet.messageId] = 0 - } - - pubIds[packet.messageId]++ - - if (pubIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) - client.end(true) - serverClient.destroy() - server2.destroy() - } - - serverClient.puback(packet) - } - }) - }) - }) - }) - - it('check emit error on checkDisconnection w/o callback', function (done) { - this.timeout(15000) - - var server118 = new MqttServer(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0 - }) - }) - client.on('publish', function (packet) { - setImmediate(function () { - packet.reasonCode = 0 - client.puback(packet) - }) - }) - }).listen(ports.PORTAND118) - - var opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - client = mqtt.connect(opts) - - // wait for the client to receive an error... - client.on('error', function (error) { - assert.equal(error.message, 'client disconnecting') - server118.close() - done() - }) - client.on('connect', function () { - client.end(function () { - client._checkDisconnecting() - }) - server118.close() - }) - }) -}) +'use strict' + +var mqtt = require('..') +var assert = require('chai').assert +const { fork } = require('child_process') +var path = require('path') +var abstractClientTests = require('./abstract_client') +var net = require('net') +var eos = require('end-of-stream') +var mqttPacket = require('mqtt-packet') +var Duplex = require('readable-stream').Duplex +var Connection = require('mqtt-connection') +var MqttServer = require('./server').MqttServer +var util = require('util') +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var debug = require('debug')('TEST:client') + +describe('MqttClient', function () { + var client + var server = serverBuilder('mqtt') + var config = {protocol: 'mqtt', port: ports.PORT} + server.listen(ports.PORT) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) + + describe('creating', function () { + it('should allow instantiation of MqttClient without the \'new\' operator', function (done) { + try { + client = mqtt.MqttClient(function () { + throw Error('break') + }, {}) + client.end() + } catch (err) { + assert.strictEqual(err.message, 'break') + done() + } + }) + }) + + describe('message ids', function () { + it('should increment the message id', function () { + client = mqtt.connect(config) + var currentId = client._nextId() + + assert.equal(client._nextId(), currentId + 1) + client.end() + }) + + it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + }) + }) + + server2.listen(ports.PORTAND49, function () { + client = mqtt.connect({ + port: ports.PORTAND49, + host: 'localhost' + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'pubcomp') { + client.end() + server2.close() + done() + } + }) + }) + }) + + it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function (done) { + var parser = mqttPacket.parser() + var count = 0 + var max = 1000 + var duplex = new Duplex({ + read: function (n) {}, + write: function (chunk, enc, cb) { + parser.parse(chunk) + cb() // nothing to do + } + }) + client = new mqtt.MqttClient(function () { + return duplex + }, {}) + + client.on('message', function (t, p, packet) { + if (++count === max) { + done() + } + }) + + parser.on('packet', function (packet) { + var packets = [] + + if (packet.cmd === 'connect') { + duplex.push(mqttPacket.generate({ + cmd: 'connack', + sessionPresent: false, + returnCode: 0 + })) + + for (var i = 0; i < max; i++) { + packets.push(mqttPacket.generate({ + cmd: 'publish', + topic: Buffer.from('hello'), + payload: Buffer.from('world'), + retain: false, + dup: false, + messageId: i + 1, + qos: 1 + })) + } + + duplex.push(Buffer.concat(packets)) + } + }) + }) + }) + + describe('flushing', function () { + it('should attempt to complete pending unsub and send on ping timeout', function (done) { + this.timeout(10000) + var server3 = new MqttServer(function (client) { + client.on('connect', function (packet) { + client.connack({returnCode: 0}) + }) + }).listen(ports.PORTAND72) + + var pubCallbackCalled = false + var unsubscribeCallbackCalled = false + client = mqtt.connect({ + port: ports.PORTAND72, + host: 'localhost', + keepalive: 1, + connectTimeout: 350, + reconnectPeriod: 0 + }) + client.once('connect', () => { + client.publish('fakeTopic', 'fakeMessage', {qos: 1}, (err, result) => { + assert.exists(err) + pubCallbackCalled = true + }) + client.unsubscribe('fakeTopic', (err, result) => { + assert.exists(err) + unsubscribeCallbackCalled = true + }) + setTimeout(() => { + client.end(() => { + assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') + server3.close() + done() + }) + }, 5000) + }) + }) + }) + + describe('reconnecting', function () { + it('should attempt to reconnect once server is down', function (done) { + this.timeout(30000) + + var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) + innerServer.on('close', (code) => { + if (code) { + done(util.format('child process closed with code %d', code)) + } + }) + + innerServer.on('exit', (code) => { + if (code) { + done(util.format('child process exited with code %d', code)) + } + }) + + client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) + client.once('connect', function () { + innerServer.kill('SIGINT') // mocks server shutdown + client.once('close', function () { + assert.exists(client.reconnectTimer) + client.end(true, done) + }) + }) + }) + + it('should reconnect if a connack is not received in an interval', function (done) { + this.timeout(2000) + + var server2 = net.createServer().listen(ports.PORTAND43) + + server2.on('connection', function (c) { + eos(c, function () { + server2.close() + }) + }) + + server2.on('listening', function () { + client = mqtt.connect({ + servers: [ + { port: ports.PORTAND43, host: 'localhost_fake' }, + { port: ports.PORT, host: 'localhost' } + ], + connectTimeout: 500 + }) + + server.once('client', function () { + client.end(true, (err) => { + done(err) + }) + }) + + client.once('connect', function () { + client.stream.destroy() + }) + }) + }) + + it('should not be cleared by the connack timer', function (done) { + this.timeout(4000) + + var server2 = net.createServer().listen(ports.PORTAND44) + + server2.on('connection', function (c) { + c.destroy() + }) + + server2.once('listening', function () { + var reconnects = 0 + var connectTimeout = 1000 + var reconnectPeriod = 100 + var expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) + client = mqtt.connect({ + port: ports.PORTAND44, + host: 'localhost', + connectTimeout: connectTimeout, + reconnectPeriod: reconnectPeriod + }) + + client.on('reconnect', function () { + reconnects++ + if (reconnects >= expectedReconnects) { + client.end(true, done) + } + }) + }) + }) + + it('should not keep requeueing the first message when offline', function (done) { + this.timeout(2500) + + var server2 = serverBuilder('mqtt').listen(ports.PORTAND45) + client = mqtt.connect({ + port: ports.PORTAND45, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + server2.on('client', function (serverClient) { + client.publish('hello', 'world', { qos: 1 }, function () { + serverClient.destroy() + server2.close(() => { + debug('now publishing message in an offline state') + client.publish('hello', 'world', { qos: 1 }) + }) + }) + }) + + setTimeout(function () { + if (client.queue.length === 0) { + debug('calling final client.end()') + client.end(true, (err) => done(err)) + } else { + debug('calling client.end()') + client.end(true) + } + }, 2000) + }) + + it('should not send the same subscribe multiple times on a flaky connection', function (done) { + this.timeout(3500) + + var KILL_COUNT = 4 + var killedConnections = 0 + var subIds = {} + client = mqtt.connect({ + port: ports.PORTAND46, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + var server2 = new MqttServer(function (serverClient) { + serverClient.on('error', function () {}) + debug('setting serverClient connect callback') + serverClient.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + debug('connack with returnCode 2') + serverClient.connack({returnCode: 2}) + } else { + debug('connack with returnCode 0') + serverClient.connack({returnCode: 0}) + } + }) + }).listen(ports.PORTAND46) + + server2.on('client', function (serverClient) { + debug('client received on server2.') + debug('subscribing to topic `topic`') + client.subscribe('topic', function () { + debug('once subscribed to topic, end client, destroy serverClient, and close server.') + serverClient.destroy() + server2.close(() => { client.end(true, done) }) + }) + + serverClient.on('subscribe', function (packet) { + if (killedConnections < KILL_COUNT) { + // Kill the first few sub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + } else { + // Keep track of acks + if (!subIds[packet.messageId]) { + subIds[packet.messageId] = 0 + } + subIds[packet.messageId]++ + if (subIds[packet.messageId] > 1) { + done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) + client.end(true) + serverClient.end() + server2.destroy() + } + + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } + }) + }) + }) + + it('should not fill the queue of subscribes if it cannot connect', function (done) { + this.timeout(2500) + var server2 = net.createServer(function (stream) { + var serverClient = new Connection(stream) + + serverClient.on('error', function (e) { /* do nothing */ }) + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.destroy() + }) + }) + + server2.listen(ports.PORTAND48, function () { + client = mqtt.connect({ + port: ports.PORTAND48, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + client.subscribe('hello') + + setTimeout(function () { + assert.equal(client.queue.length, 1) + client.end(true, () => { + done() + }) + }, 1000) + }) + }) + + it('should not send the same publish multiple times on a flaky connection', function (done) { + this.timeout(3500) + + var KILL_COUNT = 4 + var killedConnections = 0 + var pubIds = {} + client = mqtt.connect({ + port: ports.PORTAND47, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + var server2 = net.createServer(function (stream) { + var serverClient = new Connection(stream) + serverClient.on('error', function () {}) + serverClient.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + serverClient.connack({returnCode: 2}) + } else { + serverClient.connack({returnCode: 0}) + } + }) + + this.emit('client', serverClient) + }).listen(ports.PORTAND47) + + server2.on('client', function (serverClient) { + client.publish('topic', 'data', { qos: 1 }, function () { + serverClient.destroy() + server2.close() + client.end(true, done) + }) + + serverClient.on('publish', function onPublish (packet) { + if (killedConnections < KILL_COUNT) { + // Kill the first few pub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + + // to avoid receiving inflight messages + serverClient.removeListener('publish', onPublish) + } else { + // Keep track of acks + if (!pubIds[packet.messageId]) { + pubIds[packet.messageId] = 0 + } + + pubIds[packet.messageId]++ + + if (pubIds[packet.messageId] > 1) { + done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) + client.end(true) + serverClient.destroy() + server2.destroy() + } + + serverClient.puback(packet) + } + }) + }) + }) + }) + + it('check emit error on checkDisconnection w/o callback', function (done) { + this.timeout(15000) + + var server118 = new MqttServer(function (client) { + client.on('connect', function (packet) { + client.connack({ + reasonCode: 0 + }) + }) + client.on('publish', function (packet) { + setImmediate(function () { + packet.reasonCode = 0 + client.puback(packet) + }) + }) + }).listen(ports.PORTAND118) + + var opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5 + } + client = mqtt.connect(opts) + + // wait for the client to receive an error... + client.on('error', function (error) { + assert.equal(error.message, 'client disconnecting') + server118.close() + done() + }) + client.on('connect', function () { + client.end(function () { + client._checkDisconnecting() + }) + server118.close() + }) + }) +}) diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index 0fe2ecb88..fd2bb9979 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -1,1053 +1,1053 @@ -'use strict' - -var mqtt = require('..') -var abstractClientTests = require('./abstract_client') -var MqttServer = require('./server').MqttServer -var assert = require('chai').assert -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var ports = require('./helpers/port_list') - -describe('MQTT 5.0', function () { - var server = serverBuilder('mqtt').listen(ports.PORTAND115) - var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } - - abstractClientTests(server, config) - - it('topic should be complemented on receive', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - assert.strictEqual(packet.properties.topicAliasMaximum, 3) - serverClient.connack({ - reasonCode: 0 - }) - // register topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // overwrite registered topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test2', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('message', function (topic, messagee, packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 3: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - server103.close() - client.end(true, done) - break - } - }) - }) - - it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoUseTopicAlias: true - } - var client = mqtt.connect(opts) - - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias by autoApplyTopicAlias - client.publish('test1', 'Message') - }) - }) - - it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoAssignTopicAlias: true - } - var client = mqtt.connect(opts) - - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 2) - break - case 2: - assert.strictEqual(packet.topic, 'test3') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 3: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 4: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 5: - assert.strictEqual(packet.topic, 'test4') - assert.strictEqual(packet.properties.topicAlias, 2) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message') - client.publish('test2', 'Message') - client.publish('test3', 'Message') - - // use topicAlias - client.publish('test1', 'Message') - client.publish('test3', 'Message') - - // renew LRU topicAlias - client.publish('test4', 'Message') - }) - }) - - it('topicAlias should be removed and topic restored on resend', function (done) { - this.timeout(15000) - - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore: incomingStore, - outgoingStore: outgoingStore, - clean: false, - reconnectPeriod: 100 - } - var client = mqtt.connect(opts) - - var connectCount = 0 - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 2: - assert.strictEqual(packet.topic, 'test1') - var alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - serverClient.puback({messageId: packet.messageId}) - break - case 3: - assert.strictEqual(packet.topic, 'test1') - var alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - serverClient.puback({messageId: packet.messageId}) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.once('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('topicAlias should be removed and topic restored on offline publish', function (done) { - this.timeout(15000) - - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore: incomingStore, - outgoingStore: outgoingStore, - clean: false, - reconnectPeriod: 100 - } - var client = mqtt.connect(opts) - - var connectCount = 0 - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - var alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - assert.strictEqual(packet.qos, 1) - serverClient.puback({messageId: packet.messageId}) - break - case 1: - assert.strictEqual(packet.topic, 'test1') - var alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - assert.strictEqual(packet.qos, 0) - break - case 2: - assert.strictEqual(packet.topic, 'test1') - var alias3 - if (packet.properties) { - alias3 = packet.properties.topicAlias - } - assert.strictEqual(alias3, undefined) - assert.strictEqual(packet.qos, 0) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.once('close', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 4 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - server103.close() - client.end(true, done) - }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 1 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - server103.close() - client.end(true, done) - }) - }) - }) - - it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 4 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if broker PUBLISH topicAlias:0', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 0 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: '', // use topic alias - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } // in range topic alias - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received unregistered Topic Alias') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if there is Auth Data with no Auth Method', function (done) { - this.timeout(5000) - var client - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} - console.log('client connecting') - client = mqtt.connect(opts) - client.on('error', function (error) { - console.log('error hit') - assert.strictEqual(error.message, 'Packet has no Authentication Method') - // client will not be connected, so we will call done. - assert.isTrue(client.disconnected, 'validate client is disconnected') - client.end(true, done) - }) - }) - - it('auth packet', function (done) { - this.timeout(15000) - server.once('client', function (serverClient) { - console.log('server received client') - serverClient.on('auth', function (packet) { - console.log('serverClient received auth: packet %o', packet) - serverClient.end(done) - }) - }) - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} - console.log('calling mqtt connect') - mqtt.connect(opts) - }) - - it('Maximum Packet Size', function (done) { - this.timeout(15000) - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'exceeding packets size connack') - client.end(true, done) - }) - }) - - it('Change values of some properties by server response', function (done) { - this.timeout(15000) - var server116 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - serverKeepAlive: 16, - maximumPacketSize: 95 - } - }) - }) - }).listen(ports.PORTAND116) - var opts = { - host: 'localhost', - port: ports.PORTAND116, - protocolVersion: 5, - properties: { - topicAliasMaximum: 10, - serverKeepAlive: 11, - maximumPacketSize: 100 - } - } - var client = mqtt.connect(opts) - client.on('connect', function () { - assert.strictEqual(client.options.keepalive, 16) - assert.strictEqual(client.options.properties.maximumPacketSize, 95) - server116.close() - client.end(true, done) - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { - this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server316 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - serverClient.on('subscribe', function () { - if (!tryReconnect) { - server316.close() - serverClient.end(done) - } - }) - }) - }).listen(ports.PORTAND316) - var opts = { - host: 'localhost', - port: ports.PORTAND316, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { - // this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server326 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - serverClient.on('subscribe', function (packet) { - if (!reconnectEvent) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - if (!tryReconnect) { - assert.strictEqual(packet.properties.userProperties.test, 'test') - serverClient.end(done) - server326.close() - } - } - }) - }).listen(ports.PORTAND326) - - var opts = { - host: 'localhost', - port: ports.PORTAND326, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - var serverThatSendsErrors = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - packet.reasonCode = 142 - delete packet.cmd - serverClient.puback(packet) - break - case 2: - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubcomp(packet) - }) - }) - - it('Subscribe properties', function (done) { - this.timeout(15000) - var opts = { - host: 'localhost', - port: ports.PORTAND119, - protocolVersion: 5 - } - var subOptions = { properties: { subscriptionIdentifier: 1234 } } - var server119 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('subscribe', function (packet) { - assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) - server119.close() - serverClient.end() - done() - }) - }).listen(ports.PORTAND119) - - var client = mqtt.connect(opts) - client.on('connect', function () { - client.subscribe('a/b', subOptions) - }) - }) - - it('puback handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 1}, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - serverThatSendsErrors.close() - client.end(true, done) - }) - }) - - it('pubrec handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND118) - var opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 2}, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - serverThatSendsErrors.close() - client.end(true, done) - }) - }) - - it('puback handling custom reason code', function (done) { - // this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - - serverClient.on('puback', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - serverClient.end(done) - serverClient.destroy() - serverThatSendsErrors.close() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('server side disconnect', function (done) { - this.timeout(15000) - var server327 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - serverClient.disconnect({reasonCode: 128}) - server327.close() - }) - }) - server327.listen(ports.PORTAND327) - var opts = { - host: 'localhost', - port: ports.PORTAND327, - protocolVersion: 5 - } - - var client = mqtt.connect(opts) - client.once('disconnect', function (disconnectPacket) { - assert.strictEqual(disconnectPacket.reasonCode, 128) - client.end(true, done) - }) - }) - - it('pubrec handling custom reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - - serverClient.on('pubrec', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - client.end(true, done) - serverClient.destroy() - serverThatSendsErrors.close() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('puback handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('pubrec handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('puback handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 124124 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for puback') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('pubrec handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 34535 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for pubrec') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) -}) +'use strict' + +var mqtt = require('..') +var abstractClientTests = require('./abstract_client') +var MqttServer = require('./server').MqttServer +var assert = require('chai').assert +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var ports = require('./helpers/port_list') + +describe('MQTT 5.0', function () { + var server = serverBuilder('mqtt').listen(ports.PORTAND115) + var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } + + abstractClientTests(server, config) + + it('topic should be complemented on receive', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + assert.strictEqual(packet.properties.topicAliasMaximum, 3) + serverClient.connack({ + reasonCode: 0 + }) + // register topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // overwrite registered topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test2', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('message', function (topic, messagee, packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 3: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }) + + it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoUseTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias by autoApplyTopicAlias + client.publish('test1', 'Message') + }) + }) + + it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoAssignTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 2) + break + case 2: + assert.strictEqual(packet.topic, 'test3') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 3: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 4: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 5: + assert.strictEqual(packet.topic, 'test4') + assert.strictEqual(packet.properties.topicAlias, 2) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message') + client.publish('test2', 'Message') + client.publish('test3', 'Message') + + // use topicAlias + client.publish('test1', 'Message') + client.publish('test3', 'Message') + + // renew LRU topicAlias + client.publish('test4', 'Message') + }) + }) + + it('topicAlias should be removed and topic restored on resend', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + serverClient.puback({messageId: packet.messageId}) + break + case 3: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + serverClient.puback({messageId: packet.messageId}) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('topicAlias should be removed and topic restored on offline publish', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + assert.strictEqual(packet.qos, 1) + serverClient.puback({messageId: packet.messageId}) + break + case 1: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + assert.strictEqual(packet.qos, 0) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias3 + if (packet.properties) { + alias3 = packet.properties.topicAlias + } + assert.strictEqual(alias3, undefined) + assert.strictEqual(packet.qos, 0) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('close', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 4 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 1 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 4 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH topicAlias:0', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 0 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: '', // use topic alias + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } // in range topic alias + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received unregistered Topic Alias') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if there is Auth Data with no Auth Method', function (done) { + this.timeout(5000) + var client + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} + console.log('client connecting') + client = mqtt.connect(opts) + client.on('error', function (error) { + console.log('error hit') + assert.strictEqual(error.message, 'Packet has no Authentication Method') + // client will not be connected, so we will call done. + assert.isTrue(client.disconnected, 'validate client is disconnected') + client.end(true, done) + }) + }) + + it('auth packet', function (done) { + this.timeout(15000) + server.once('client', function (serverClient) { + console.log('server received client') + serverClient.on('auth', function (packet) { + console.log('serverClient received auth: packet %o', packet) + serverClient.end(done) + }) + }) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} + console.log('calling mqtt connect') + mqtt.connect(opts) + }) + + it('Maximum Packet Size', function (done) { + this.timeout(15000) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'exceeding packets size connack') + client.end(true, done) + }) + }) + + it('Change values of some properties by server response', function (done) { + this.timeout(15000) + var server116 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + serverKeepAlive: 16, + maximumPacketSize: 95 + } + }) + }) + }).listen(ports.PORTAND116) + var opts = { + host: 'localhost', + port: ports.PORTAND116, + protocolVersion: 5, + properties: { + topicAliasMaximum: 10, + serverKeepAlive: 11, + maximumPacketSize: 100 + } + } + var client = mqtt.connect(opts) + client.on('connect', function () { + assert.strictEqual(client.options.keepalive, 16) + assert.strictEqual(client.options.properties.maximumPacketSize, 95) + server116.close() + client.end(true, done) + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { + this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server316 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + serverClient.on('subscribe', function () { + if (!tryReconnect) { + server316.close() + serverClient.end(done) + } + }) + }) + }).listen(ports.PORTAND316) + var opts = { + host: 'localhost', + port: ports.PORTAND316, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { + // this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server326 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + serverClient.on('subscribe', function (packet) { + if (!reconnectEvent) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } else { + if (!tryReconnect) { + assert.strictEqual(packet.properties.userProperties.test, 'test') + serverClient.end(done) + server326.close() + } + } + }) + }).listen(ports.PORTAND326) + + var opts = { + host: 'localhost', + port: ports.PORTAND326, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + var serverThatSendsErrors = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + packet.reasonCode = 142 + delete packet.cmd + serverClient.puback(packet) + break + case 2: + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubcomp(packet) + }) + }) + + it('Subscribe properties', function (done) { + this.timeout(15000) + var opts = { + host: 'localhost', + port: ports.PORTAND119, + protocolVersion: 5 + } + var subOptions = { properties: { subscriptionIdentifier: 1234 } } + var server119 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('subscribe', function (packet) { + assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) + server119.close() + serverClient.end() + done() + }) + }).listen(ports.PORTAND119) + + var client = mqtt.connect(opts) + client.on('connect', function () { + client.subscribe('a/b', subOptions) + }) + }) + + it('puback handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 1}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('pubrec handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND118) + var opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 2}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('puback handling custom reason code', function (done) { + // this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + + serverClient.on('puback', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + serverClient.end(done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('server side disconnect', function (done) { + this.timeout(15000) + var server327 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + serverClient.disconnect({reasonCode: 128}) + server327.close() + }) + }) + server327.listen(ports.PORTAND327) + var opts = { + host: 'localhost', + port: ports.PORTAND327, + protocolVersion: 5 + } + + var client = mqtt.connect(opts) + client.once('disconnect', function (disconnectPacket) { + assert.strictEqual(disconnectPacket.reasonCode, 128) + client.end(true, done) + }) + }) + + it('pubrec handling custom reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + + serverClient.on('pubrec', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + client.end(true, done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 124124 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for puback') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 34535 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for pubrec') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) +}) diff --git a/test/helpers/port_list.js b/test/helpers/port_list.js index d11b8df21..dc77ef07a 100644 --- a/test/helpers/port_list.js +++ b/test/helpers/port_list.js @@ -1,51 +1,51 @@ -var PORT = 9876 -var PORTAND40 = PORT + 40 -var PORTAND41 = PORT + 41 -var PORTAND42 = PORT + 42 -var PORTAND43 = PORT + 43 -var PORTAND44 = PORT + 44 -var PORTAND45 = PORT + 45 -var PORTAND46 = PORT + 46 -var PORTAND47 = PORT + 47 -var PORTAND48 = PORT + 48 -var PORTAND49 = PORT + 49 -var PORTAND50 = PORT + 50 -var PORTAND72 = PORT + 72 -var PORTAND103 = PORT + 103 -var PORTAND114 = PORT + 114 -var PORTAND115 = PORT + 115 -var PORTAND116 = PORT + 116 -var PORTAND117 = PORT + 117 -var PORTAND118 = PORT + 118 -var PORTAND119 = PORT + 119 -var PORTAND316 = PORT + 316 -var PORTAND326 = PORT + 326 -var PORTAND327 = PORT + 327 -var PORTAND400 = PORT + 400 - -module.exports = { - PORT, - PORTAND40, - PORTAND41, - PORTAND42, - PORTAND43, - PORTAND44, - PORTAND45, - PORTAND46, - PORTAND47, - PORTAND48, - PORTAND49, - PORTAND50, - PORTAND72, - PORTAND103, - PORTAND114, - PORTAND115, - PORTAND116, - PORTAND117, - PORTAND118, - PORTAND119, - PORTAND316, - PORTAND326, - PORTAND327, - PORTAND400 -} +var PORT = 9876 +var PORTAND40 = PORT + 40 +var PORTAND41 = PORT + 41 +var PORTAND42 = PORT + 42 +var PORTAND43 = PORT + 43 +var PORTAND44 = PORT + 44 +var PORTAND45 = PORT + 45 +var PORTAND46 = PORT + 46 +var PORTAND47 = PORT + 47 +var PORTAND48 = PORT + 48 +var PORTAND49 = PORT + 49 +var PORTAND50 = PORT + 50 +var PORTAND72 = PORT + 72 +var PORTAND103 = PORT + 103 +var PORTAND114 = PORT + 114 +var PORTAND115 = PORT + 115 +var PORTAND116 = PORT + 116 +var PORTAND117 = PORT + 117 +var PORTAND118 = PORT + 118 +var PORTAND119 = PORT + 119 +var PORTAND316 = PORT + 316 +var PORTAND326 = PORT + 326 +var PORTAND327 = PORT + 327 +var PORTAND400 = PORT + 400 + +module.exports = { + PORT, + PORTAND40, + PORTAND41, + PORTAND42, + PORTAND43, + PORTAND44, + PORTAND45, + PORTAND46, + PORTAND47, + PORTAND48, + PORTAND49, + PORTAND50, + PORTAND72, + PORTAND103, + PORTAND114, + PORTAND115, + PORTAND116, + PORTAND117, + PORTAND118, + PORTAND119, + PORTAND316, + PORTAND326, + PORTAND327, + PORTAND400 +} diff --git a/test/helpers/server.js b/test/helpers/server.js index d29042d3d..46bd79537 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -1,53 +1,53 @@ -'use strict' - -var MqttServer = require('../server').MqttServer -var MqttSecureServer = require('../server').MqttSecureServer -var fs = require('fs') - -module.exports.init_server = function (PORT) { - var server = new MqttServer(function (client) { - client.on('connect', function () { - client.connack(0) - }) - - client.on('publish', function (packet) { - switch (packet.qos) { - case 1: - client.puback({messageId: packet.messageId}) - break - case 2: - client.pubrec({messageId: packet.messageId}) - break - default: - break - } - }) - - client.on('pubrel', function (packet) { - client.pubcomp({messageId: packet.messageId}) - }) - - client.on('pingreq', function () { - client.pingresp() - }) - - client.on('disconnect', function () { - client.stream.end() - }) - }) - server.listen(PORT) - return server -} - -module.exports.init_secure_server = function (port, key, cert) { - var server = new MqttSecureServer({ - key: fs.readFileSync(key), - cert: fs.readFileSync(cert) - }, function (client) { - client.on('connect', function () { - client.connack({returnCode: 0}) - }) - }) - server.listen(port) - return server -} +'use strict' + +var MqttServer = require('../server').MqttServer +var MqttSecureServer = require('../server').MqttSecureServer +var fs = require('fs') + +module.exports.init_server = function (PORT) { + var server = new MqttServer(function (client) { + client.on('connect', function () { + client.connack(0) + }) + + client.on('publish', function (packet) { + switch (packet.qos) { + case 1: + client.puback({messageId: packet.messageId}) + break + case 2: + client.pubrec({messageId: packet.messageId}) + break + default: + break + } + }) + + client.on('pubrel', function (packet) { + client.pubcomp({messageId: packet.messageId}) + }) + + client.on('pingreq', function () { + client.pingresp() + }) + + client.on('disconnect', function () { + client.stream.end() + }) + }) + server.listen(PORT) + return server +} + +module.exports.init_secure_server = function (port, key, cert) { + var server = new MqttSecureServer({ + key: fs.readFileSync(key), + cert: fs.readFileSync(cert) + }, function (client) { + client.on('connect', function () { + client.connack({returnCode: 0}) + }) + }) + server.listen(port) + return server +} diff --git a/test/helpers/server_process.js b/test/helpers/server_process.js index d4c2681b4..1d1095cb3 100644 --- a/test/helpers/server_process.js +++ b/test/helpers/server_process.js @@ -1,9 +1,9 @@ -'use strict' - -var MqttServer = require('../server').MqttServer - -new MqttServer(function (client) { - client.on('connect', function () { - client.connack({ returnCode: 0 }) - }) -}).listen(3481, 'localhost') +'use strict' + +var MqttServer = require('../server').MqttServer + +new MqttServer(function (client) { + client.on('connect', function () { + client.connack({ returnCode: 0 }) + }) +}).listen(3481, 'localhost') diff --git a/test/message-id-provider.js b/test/message-id-provider.js index 667a8296f..2f84bdf35 100644 --- a/test/message-id-provider.js +++ b/test/message-id-provider.js @@ -1,91 +1,91 @@ -'use strict' -var assert = require('chai').assert -var DefaultMessageIdProvider = require('../lib/default-message-id-provider') -var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') - -describe('message id provider', function () { - describe('default', function () { - it('should return 1 once the internal counter reached limit', function () { - var provider = new DefaultMessageIdProvider() - provider.nextId = 65535 - - assert.equal(provider.allocate(), 65535) - assert.equal(provider.allocate(), 1) - }) - - it('should return 65535 for last message id once the internal counter reached limit', function () { - var provider = new DefaultMessageIdProvider() - provider.nextId = 65535 - - assert.equal(provider.allocate(), 65535) - assert.equal(provider.getLastAllocated(), 65535) - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - }) - it('should return true when register with non allocated messageId', function () { - var provider = new DefaultMessageIdProvider() - assert.equal(provider.register(10), true) - }) - }) - describe('unique', function () { - it('should return 1, 2, 3.., when allocate', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - }) - it('should skip registerd messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.register(2), true) - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 3) - }) - it('should return false register allocated messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.register(1), false) - assert.equal(provider.register(5), true) - assert.equal(provider.register(5), false) - }) - it('should retrun correct last messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.register(2), true) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.allocate(), 3) - assert.equal(provider.getLastAllocated(), 3) - }) - it('should be reusable deallocated messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - provider.deallocate(2) - assert.equal(provider.allocate(), 2) - }) - it('should allocate all messageId and then return null', function () { - var provider = new UniqueMessageIdProvider() - for (var i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.deallocate(10000) - assert.equal(provider.allocate(), 10000) - assert.equal(provider.allocate(), null) - }) - it('should all messageId reallocatable after clear', function () { - var provider = new UniqueMessageIdProvider() - var i - for (i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.clear() - for (i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - }) - }) -}) +'use strict' +var assert = require('chai').assert +var DefaultMessageIdProvider = require('../lib/default-message-id-provider') +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') + +describe('message id provider', function () { + describe('default', function () { + it('should return 1 once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.allocate(), 1) + }) + + it('should return 65535 for last message id once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.getLastAllocated(), 65535) + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + }) + it('should return true when register with non allocated messageId', function () { + var provider = new DefaultMessageIdProvider() + assert.equal(provider.register(10), true) + }) + }) + describe('unique', function () { + it('should return 1, 2, 3.., when allocate', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + }) + it('should skip registerd messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.register(2), true) + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 3) + }) + it('should return false register allocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.register(1), false) + assert.equal(provider.register(5), true) + assert.equal(provider.register(5), false) + }) + it('should retrun correct last messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.register(2), true) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.allocate(), 3) + assert.equal(provider.getLastAllocated(), 3) + }) + it('should be reusable deallocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + provider.deallocate(2) + assert.equal(provider.allocate(), 2) + }) + it('should allocate all messageId and then return null', function () { + var provider = new UniqueMessageIdProvider() + for (var i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.deallocate(10000) + assert.equal(provider.allocate(), 10000) + assert.equal(provider.allocate(), null) + }) + it('should all messageId reallocatable after clear', function () { + var provider = new UniqueMessageIdProvider() + var i + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.clear() + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + }) + }) +}) diff --git a/test/mqtt.js b/test/mqtt.js index d3315b69e..f55d04a33 100644 --- a/test/mqtt.js +++ b/test/mqtt.js @@ -1,230 +1,230 @@ -'use strict' - -var fs = require('fs') -var path = require('path') -var mqtt = require('../') - -describe('mqtt', function () { - describe('#connect', function () { - var sslOpts, sslOpts2 - it('should return an MqttClient when connect is called with mqtt:/ url', function () { - var c = mqtt.connect('mqtt://localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should throw an error when called with no protocol specified', function () { - (function () { - var c = mqtt.connect('foo.bar.com') - c.end() - }).should.throw('Missing protocol') - }) - - it('should throw an error when called with no protocol specified - with options', function () { - (function () { - var c = mqtt.connect('tcp://foo.bar.com', { protocol: null }) - c.end() - }).should.throw('Missing protocol') - }) - - it('should return an MqttClient with username option set', function () { - var c = mqtt.connect('mqtt://user:pass@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.options.should.have.property('password', 'pass') - c.end() - }) - - it('should return an MqttClient with username and password options set', function () { - var c = mqtt.connect('mqtt://user@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.end() - }) - - it('should return an MqttClient with the clientid with random value', function () { - var c = mqtt.connect('mqtt://user@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end() - }) - - it('should return an MqttClient with the clientid with empty string', function () { - var c = mqtt.connect('mqtt://user@localhost:1883?clientId=') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - c.end() - }) - - it('should return an MqttClient with the clientid option set', function () { - var c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end() - }) - - it('should return an MqttClient when connect is called with tcp:/ url', function () { - var c = mqtt.connect('tcp://localhost') - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient with correct host when called with a host and port', function () { - var c = mqtt.connect('tcp://user:pass@localhost:1883') - - c.options.should.have.property('hostname', 'localhost') - c.options.should.have.property('port', 1883) - c.end() - }) - - sslOpts = { - keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), - certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), - caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')] - } - - it('should return an MqttClient when connect is called with mqtts:/ url', function () { - var c = mqtt.connect('mqtts://localhost', sslOpts) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with ssl:/ url', function () { - var c = mqtt.connect('ssl://localhost', sslOpts) - - c.options.should.have.property('protocol', 'ssl') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with ws:/ url', function () { - var c = mqtt.connect('ws://localhost', sslOpts) - - c.options.should.have.property('protocol', 'ws') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with wss:/ url', function () { - var c = mqtt.connect('wss://localhost', sslOpts) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - sslOpts2 = { - key: fs.readFileSync(path.join(__dirname, 'helpers', 'private-key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem')), - ca: [fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem'))] - } - - it('should throw an error when it is called with cert and key set but no protocol specified', function () { - // to do rewrite wrap function - (function () { - var c = mqtt.connect(sslOpts2) - c.end() - }).should.throw('Missing secure protocol key') - }) - - it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', function () { - (function () { - sslOpts2.protocol = 'UNKNOWNPROTOCOL' - var c = mqtt.connect(sslOpts2) - c.end() - }).should.throw() - }) - - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function () { - sslOpts2.protocol = 'mqtt' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function () { - sslOpts2.protocol = 'mqtts' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function () { - sslOpts2.protocol = 'ws' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function () { - sslOpts2.protocol = 'wss' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return an MqttClient with the clientid with option of clientId as empty string', function () { - var c = mqtt.connect('mqtt://localhost:1883', { - clientId: '' - }) - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - }) - - it('should return an MqttClient with the clientid with option of clientId empty', function () { - var c = mqtt.connect('mqtt://localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end() - }) - - it('should return an MqttClient with the clientid with option of with specific clientId', function () { - var c = mqtt.connect('mqtt://localhost:1883', { - clientId: '123' - }) - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end() - }) - }) -}) +'use strict' + +var fs = require('fs') +var path = require('path') +var mqtt = require('../') + +describe('mqtt', function () { + describe('#connect', function () { + var sslOpts, sslOpts2 + it('should return an MqttClient when connect is called with mqtt:/ url', function () { + var c = mqtt.connect('mqtt://localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should throw an error when called with no protocol specified', function () { + (function () { + var c = mqtt.connect('foo.bar.com') + c.end() + }).should.throw('Missing protocol') + }) + + it('should throw an error when called with no protocol specified - with options', function () { + (function () { + var c = mqtt.connect('tcp://foo.bar.com', { protocol: null }) + c.end() + }).should.throw('Missing protocol') + }) + + it('should return an MqttClient with username option set', function () { + var c = mqtt.connect('mqtt://user:pass@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.options.should.have.property('password', 'pass') + c.end() + }) + + it('should return an MqttClient with username and password options set', function () { + var c = mqtt.connect('mqtt://user@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.end() + }) + + it('should return an MqttClient with the clientid with random value', function () { + var c = mqtt.connect('mqtt://user@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end() + }) + + it('should return an MqttClient with the clientid with empty string', function () { + var c = mqtt.connect('mqtt://user@localhost:1883?clientId=') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + c.end() + }) + + it('should return an MqttClient with the clientid option set', function () { + var c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end() + }) + + it('should return an MqttClient when connect is called with tcp:/ url', function () { + var c = mqtt.connect('tcp://localhost') + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient with correct host when called with a host and port', function () { + var c = mqtt.connect('tcp://user:pass@localhost:1883') + + c.options.should.have.property('hostname', 'localhost') + c.options.should.have.property('port', 1883) + c.end() + }) + + sslOpts = { + keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), + certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), + caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')] + } + + it('should return an MqttClient when connect is called with mqtts:/ url', function () { + var c = mqtt.connect('mqtts://localhost', sslOpts) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with ssl:/ url', function () { + var c = mqtt.connect('ssl://localhost', sslOpts) + + c.options.should.have.property('protocol', 'ssl') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with ws:/ url', function () { + var c = mqtt.connect('ws://localhost', sslOpts) + + c.options.should.have.property('protocol', 'ws') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with wss:/ url', function () { + var c = mqtt.connect('wss://localhost', sslOpts) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + sslOpts2 = { + key: fs.readFileSync(path.join(__dirname, 'helpers', 'private-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem')), + ca: [fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem'))] + } + + it('should throw an error when it is called with cert and key set but no protocol specified', function () { + // to do rewrite wrap function + (function () { + var c = mqtt.connect(sslOpts2) + c.end() + }).should.throw('Missing secure protocol key') + }) + + it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', function () { + (function () { + sslOpts2.protocol = 'UNKNOWNPROTOCOL' + var c = mqtt.connect(sslOpts2) + c.end() + }).should.throw() + }) + + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function () { + sslOpts2.protocol = 'mqtt' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function () { + sslOpts2.protocol = 'mqtts' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function () { + sslOpts2.protocol = 'ws' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function () { + sslOpts2.protocol = 'wss' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return an MqttClient with the clientid with option of clientId as empty string', function () { + var c = mqtt.connect('mqtt://localhost:1883', { + clientId: '' + }) + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + }) + + it('should return an MqttClient with the clientid with option of clientId empty', function () { + var c = mqtt.connect('mqtt://localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end() + }) + + it('should return an MqttClient with the clientid with option of with specific clientId', function () { + var c = mqtt.connect('mqtt://localhost:1883', { + clientId: '123' + }) + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end() + }) + }) +}) diff --git a/test/mqtt_store.js b/test/mqtt_store.js index 0eda04d8b..976a01aff 100644 --- a/test/mqtt_store.js +++ b/test/mqtt_store.js @@ -1,9 +1,9 @@ -'use strict' - -var mqtt = require('../lib/connect') - -describe('store in lib/connect/index.js (webpack entry point)', function () { - it('should create store', function (done) { - done(null, new mqtt.Store()) - }) -}) +'use strict' + +var mqtt = require('../lib/connect') + +describe('store in lib/connect/index.js (webpack entry point)', function () { + it('should create store', function (done) { + done(null, new mqtt.Store()) + }) +}) diff --git a/test/secure_client.js b/test/secure_client.js index 8c4904465..95b7a6197 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -1,188 +1,188 @@ -'use strict' - -var mqtt = require('..') -var path = require('path') -var abstractClientTests = require('./abstract_client') -var fs = require('fs') -var port = 9899 -var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') -var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') -var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') -var MqttSecureServer = require('./server').MqttSecureServer -var assert = require('chai').assert - -var serverListener = function (client) { - // this is the Server's MQTT Client - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) - } else { - server.emit('connect', client) - client.connack({returnCode: 0}) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - /* jshint -W027 */ - /* eslint default-case:0 */ - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - /* jshint +W027 */ - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -var server = new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) -}, serverListener).listen(port) - -describe('MqttSecureClient', function () { - var config = { protocol: 'mqtts', port: port, rejectUnauthorized: false } - abstractClientTests(server, config) - - describe('with secure parameters', function () { - it('should validate successfully the CA', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI', function (done) { - var client = mqtt.connect('mqtts://localhost:' + port, { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI with path', function (done) { - var client = mqtt.connect('mqtts://localhost:' + port + '/', { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate unsuccessfully the CA', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.once('error', function () { - done() - client.end() - client.on('error', function () {}) - }) - }) - - it('should emit close on TLS error', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.on('error', function () {}) - - // TODO node v0.8.x emits multiple close events - client.once('close', function () { - done() - }) - }) - - it('should support SNI on the TLS connection', function (done) { - var hostname, client - server.removeAllListeners('secureConnection') // clear eventHandler - server.once('secureConnection', function (tlsSocket) { // one time eventHandler - assert.equal(tlsSocket.servername, hostname) // validate SNI set - server.setupConnection(tlsSocket) - }) - - hostname = 'localhost' - client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true, - host: hostname - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - server.on('secureConnection', server.setupConnection) // reset eventHandler - done() - }) - }) - }) -}) +'use strict' + +var mqtt = require('..') +var path = require('path') +var abstractClientTests = require('./abstract_client') +var fs = require('fs') +var port = 9899 +var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') +var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') +var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') +var MqttSecureServer = require('./server').MqttSecureServer +var assert = require('chai').assert + +var serverListener = function (client) { + // this is the Server's MQTT Client + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({returnCode: 2}) + } else { + server.emit('connect', client) + client.connack({returnCode: 0}) + } + }) + + client.on('publish', function (packet) { + setImmediate(function () { + /* jshint -W027 */ + /* eslint default-case:0 */ + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + /* jshint +W027 */ + }) + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +var server = new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) +}, serverListener).listen(port) + +describe('MqttSecureClient', function () { + var config = { protocol: 'mqtts', port: port, rejectUnauthorized: false } + abstractClientTests(server, config) + + describe('with secure parameters', function () { + it('should validate successfully the CA', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate successfully the CA using URI', function (done) { + var client = mqtt.connect('mqtts://localhost:' + port, { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate successfully the CA using URI with path', function (done) { + var client = mqtt.connect('mqtts://localhost:' + port + '/', { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate unsuccessfully the CA', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true + }) + + client.once('error', function () { + done() + client.end() + client.on('error', function () {}) + }) + }) + + it('should emit close on TLS error', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true + }) + + client.on('error', function () {}) + + // TODO node v0.8.x emits multiple close events + client.once('close', function () { + done() + }) + }) + + it('should support SNI on the TLS connection', function (done) { + var hostname, client + server.removeAllListeners('secureConnection') // clear eventHandler + server.once('secureConnection', function (tlsSocket) { // one time eventHandler + assert.equal(tlsSocket.servername, hostname) // validate SNI set + server.setupConnection(tlsSocket) + }) + + hostname = 'localhost' + client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + host: hostname + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + server.on('secureConnection', server.setupConnection) // reset eventHandler + done() + }) + }) + }) +}) diff --git a/test/server.js b/test/server.js index 3b009d4fb..ccfe2f4d1 100644 --- a/test/server.js +++ b/test/server.js @@ -1,94 +1,94 @@ -'use strict' - -var net = require('net') -var tls = require('tls') -var Connection = require('mqtt-connection') - -/** - * MqttServer - * - * @param {Function} listener - fired on client connection - */ -class MqttServer extends net.Server { - constructor (listener) { - super() - this.connectionList = [] - - var that = this - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - var connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - }) - - if (listener) { - this.on('client', listener) - } - } -} - -/** - * MqttServerNoWait (w/o waiting for initialization) - * - * @param {Function} listener - fired on client connection - */ -class MqttServerNoWait extends net.Server { - constructor (listener) { - super() - this.connectionList = [] - - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - var connection = new Connection(duplex) - // do not wait for connection to return to send it to the client. - this.emit('client', connection) - }) - - if (listener) { - this.on('client', listener) - } - } -} - -/** - * MqttSecureServer - * - * @param {Object} opts - server options - * @param {Function} listener - */ -class MqttSecureServer extends tls.Server { - constructor (opts, listener) { - if (typeof opts === 'function') { - listener = opts - opts = {} - } - - // sets a listener for the 'connection' event - super(opts) - this.connectionList = [] - - this.on('secureConnection', function (socket) { - this.connectionList.push(socket) - var that = this - var connection = new Connection(socket, function () { - that.emit('client', connection) - }) - }) - - if (listener) { - this.on('client', listener) - } - } - - setupConnection (duplex) { - var that = this - var connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - } -} - -exports.MqttServer = MqttServer -exports.MqttServerNoWait = MqttServerNoWait -exports.MqttSecureServer = MqttSecureServer +'use strict' + +var net = require('net') +var tls = require('tls') +var Connection = require('mqtt-connection') + +/** + * MqttServer + * + * @param {Function} listener - fired on client connection + */ +class MqttServer extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + var that = this + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) + }) + + if (listener) { + this.on('client', listener) + } + } +} + +/** + * MqttServerNoWait (w/o waiting for initialization) + * + * @param {Function} listener - fired on client connection + */ +class MqttServerNoWait extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex) + // do not wait for connection to return to send it to the client. + this.emit('client', connection) + }) + + if (listener) { + this.on('client', listener) + } + } +} + +/** + * MqttSecureServer + * + * @param {Object} opts - server options + * @param {Function} listener + */ +class MqttSecureServer extends tls.Server { + constructor (opts, listener) { + if (typeof opts === 'function') { + listener = opts + opts = {} + } + + // sets a listener for the 'connection' event + super(opts) + this.connectionList = [] + + this.on('secureConnection', function (socket) { + this.connectionList.push(socket) + var that = this + var connection = new Connection(socket, function () { + that.emit('client', connection) + }) + }) + + if (listener) { + this.on('client', listener) + } + } + + setupConnection (duplex) { + var that = this + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) + } +} + +exports.MqttServer = MqttServer +exports.MqttServerNoWait = MqttServerNoWait +exports.MqttSecureServer = MqttSecureServer diff --git a/test/server_helpers_for_client_tests.js b/test/server_helpers_for_client_tests.js index e7ea345c4..9527d47e2 100644 --- a/test/server_helpers_for_client_tests.js +++ b/test/server_helpers_for_client_tests.js @@ -1,147 +1,147 @@ -'use strict' - -var MqttServer = require('./server').MqttServer -var MqttSecureServer = require('./server').MqttSecureServer -var debug = require('debug')('TEST:server_helpers') - -var path = require('path') -var fs = require('fs') -var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') -var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') - -var http = require('http') -var WebSocket = require('ws') -var MQTTConnection = require('mqtt-connection') - -/** - * This will build the client for the server to use during testing, and set up the - * server side client based on mqtt-connection for handling MQTT messages. - * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' - * @param {Function} handler - event handler - */ -function serverBuilder (protocol, handler) { - var defaultHandler = function (serverClient) { - serverClient.on('auth', function (packet) { - if (serverClient.writable) return false - var rc = 'reasonCode' - var connack = {} - connack[rc] = 0 - serverClient.connack(connack) - }) - serverClient.on('connect', function (packet) { - if (!serverClient.writable) return false - var rc = 'returnCode' - var connack = {} - if (serverClient.options && serverClient.options.protocolVersion === 5) { - rc = 'reasonCode' - if (packet.clientId === 'invalid') { - connack[rc] = 128 - } else { - connack[rc] = 0 - } - } else { - if (packet.clientId === 'invalid') { - connack[rc] = 2 - } else { - connack[rc] = 0 - } - } - if (packet.properties && packet.properties.authenticationMethod) { - return false - } else { - serverClient.connack(connack) - } - }) - - serverClient.on('publish', function (packet) { - if (!serverClient.writable) return false - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - serverClient.puback(packet) - break - case 2: - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - if (!serverClient.writable) return false - serverClient.pubcomp(packet) - }) - - serverClient.on('pubrec', function (packet) { - if (!serverClient.writable) return false - serverClient.pubrel(packet) - }) - - serverClient.on('pubcomp', function () { - // Nothing to be done - }) - - serverClient.on('subscribe', function (packet) { - if (!serverClient.writable) return false - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - serverClient.on('unsubscribe', function (packet) { - if (!serverClient.writable) return false - packet.granted = packet.unsubscriptions.map(function () { return 0 }) - serverClient.unsuback(packet) - }) - - serverClient.on('pingreq', function () { - if (!serverClient.writable) return false - serverClient.pingresp() - }) - - serverClient.on('end', function () { - debug('disconnected from server') - }) - } - - if (!handler) { - handler = defaultHandler - } - - switch (protocol) { - case 'mqtt': - return new MqttServer(handler) - case 'mqtts': - return new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) - }, - handler) - case 'ws': - var attachWebsocketServer = function (server) { - var webSocketServer = new WebSocket.Server({server: server, perMessageDeflate: false}) - - webSocketServer.on('connection', function (ws) { - var stream = WebSocket.createWebSocketStream(ws) - var connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - server.emit('client', connection) - stream.on('error', function () {}) - connection.on('error', function () {}) - connection.on('close', function () {}) - }) - } - - var httpServer = http.createServer() - attachWebsocketServer(httpServer) - httpServer.on('client', handler) - return httpServer - } -} - -exports.serverBuilder = serverBuilder +'use strict' + +var MqttServer = require('./server').MqttServer +var MqttSecureServer = require('./server').MqttSecureServer +var debug = require('debug')('TEST:server_helpers') + +var path = require('path') +var fs = require('fs') +var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') +var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') + +var http = require('http') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') + +/** + * This will build the client for the server to use during testing, and set up the + * server side client based on mqtt-connection for handling MQTT messages. + * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' + * @param {Function} handler - event handler + */ +function serverBuilder (protocol, handler) { + var defaultHandler = function (serverClient) { + serverClient.on('auth', function (packet) { + if (serverClient.writable) return false + var rc = 'reasonCode' + var connack = {} + connack[rc] = 0 + serverClient.connack(connack) + }) + serverClient.on('connect', function (packet) { + if (!serverClient.writable) return false + var rc = 'returnCode' + var connack = {} + if (serverClient.options && serverClient.options.protocolVersion === 5) { + rc = 'reasonCode' + if (packet.clientId === 'invalid') { + connack[rc] = 128 + } else { + connack[rc] = 0 + } + } else { + if (packet.clientId === 'invalid') { + connack[rc] = 2 + } else { + connack[rc] = 0 + } + } + if (packet.properties && packet.properties.authenticationMethod) { + return false + } else { + serverClient.connack(connack) + } + }) + + serverClient.on('publish', function (packet) { + if (!serverClient.writable) return false + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + serverClient.puback(packet) + break + case 2: + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + if (!serverClient.writable) return false + serverClient.pubcomp(packet) + }) + + serverClient.on('pubrec', function (packet) { + if (!serverClient.writable) return false + serverClient.pubrel(packet) + }) + + serverClient.on('pubcomp', function () { + // Nothing to be done + }) + + serverClient.on('subscribe', function (packet) { + if (!serverClient.writable) return false + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + serverClient.on('unsubscribe', function (packet) { + if (!serverClient.writable) return false + packet.granted = packet.unsubscriptions.map(function () { return 0 }) + serverClient.unsuback(packet) + }) + + serverClient.on('pingreq', function () { + if (!serverClient.writable) return false + serverClient.pingresp() + }) + + serverClient.on('end', function () { + debug('disconnected from server') + }) + } + + if (!handler) { + handler = defaultHandler + } + + switch (protocol) { + case 'mqtt': + return new MqttServer(handler) + case 'mqtts': + return new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) + }, + handler) + case 'ws': + var attachWebsocketServer = function (server) { + var webSocketServer = new WebSocket.Server({server: server, perMessageDeflate: false}) + + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + server.emit('client', connection) + stream.on('error', function () {}) + connection.on('error', function () {}) + connection.on('close', function () {}) + }) + } + + var httpServer = http.createServer() + attachWebsocketServer(httpServer) + httpServer.on('client', handler) + return httpServer + } +} + +exports.serverBuilder = serverBuilder diff --git a/test/store.js b/test/store.js index 5244cdf84..1489b2138 100644 --- a/test/store.js +++ b/test/store.js @@ -1,10 +1,10 @@ -'use strict' - -var Store = require('../lib/store') -var abstractTest = require('../test/abstract_store') - -describe('in-memory store', function () { - abstractTest(function (done) { - done(null, new Store()) - }) -}) +'use strict' + +var Store = require('../lib/store') +var abstractTest = require('../test/abstract_store') + +describe('in-memory store', function () { + abstractTest(function (done) { + done(null, new Store()) + }) +}) diff --git a/test/unique_message_id_provider_client.js b/test/unique_message_id_provider_client.js index a23625a85..933d85b82 100644 --- a/test/unique_message_id_provider_client.js +++ b/test/unique_message_id_provider_client.js @@ -1,21 +1,21 @@ -'use strict' - -var abstractClientTests = require('./abstract_client') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') -var ports = require('./helpers/port_list') - -describe('UniqueMessageIdProviderMqttClient', function () { - var server = serverBuilder('mqtt') - var config = {protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider()} - server.listen(ports.PORTAND400) - - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) - - abstractClientTests(server, config) -}) +'use strict' + +var abstractClientTests = require('./abstract_client') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') +var ports = require('./helpers/port_list') + +describe('UniqueMessageIdProviderMqttClient', function () { + var server = serverBuilder('mqtt') + var config = {protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider()} + server.listen(ports.PORTAND400) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) +}) diff --git a/test/util.js b/test/util.js index ab2661804..0dd559cb9 100644 --- a/test/util.js +++ b/test/util.js @@ -1,15 +1,15 @@ -'use strict' - -var Transform = require('readable-stream').Transform - -module.exports.testStream = function () { - return new Transform({ - transform (buf, enc, cb) { - var that = this - setImmediate(function () { - that.push(buf) - cb() - }) - } - }) -} +'use strict' + +var Transform = require('readable-stream').Transform + +module.exports.testStream = function () { + return new Transform({ + transform (buf, enc, cb) { + var that = this + setImmediate(function () { + that.push(buf) + cb() + }) + } + }) +} diff --git a/test/websocket_client.js b/test/websocket_client.js index 9eb7007c2..a7f59897a 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -1,191 +1,191 @@ -'use strict' - -var http = require('http') -var WebSocket = require('ws') -var MQTTConnection = require('mqtt-connection') -var abstractClientTests = require('./abstract_client') -var ports = require('./helpers/port_list') -var MqttServerNoWait = require('./server').MqttServerNoWait -var mqtt = require('../') -var xtend = require('xtend') -var assert = require('assert') -var port = 9999 -var httpServer = http.createServer() - -function attachWebsocketServer (httpServer) { - var webSocketServer = new WebSocket.Server({server: httpServer, perMessageDeflate: false}) - - webSocketServer.on('connection', function (ws) { - var stream = WebSocket.createWebSocketStream(ws) - var connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - httpServer.emit('client', connection) - stream.on('error', function () {}) - connection.on('error', function () {}) - }) - - return httpServer -} - -function attachClientEventHandlers (client) { - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({ returnCode: 2 }) - } else { - httpServer.emit('connect', client) - client.connack({returnCode: 0}) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -attachWebsocketServer(httpServer) - -httpServer.on('client', attachClientEventHandlers).listen(port) - -describe('Websocket Client', function () { - var baseConfig = { protocol: 'ws', port: port } - - function makeOptions (custom) { - // xtend returns a new object. Does not mutate arguments - return xtend(baseConfig, custom || {}) - } - - it('should use mqtt as the protocol by default', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqtt') - }) - mqtt.connect(makeOptions()).on('connect', function () { - this.end(true, done) - }) - }) - - it('should be able to transform the url (for e.g. to sign it)', function (done) { - var baseUrl = 'ws://localhost:9999/mqtt' - var sig = '?AUTH=token' - var expected = baseUrl + sig - var actual - var opts = makeOptions({ - path: '/mqtt', - transformWsUrl: function (url, opt, client) { - assert.equal(url, baseUrl) - assert.strictEqual(opt, opts) - assert.strictEqual(client.options, opts) - assert.strictEqual(typeof opt.transformWsUrl, 'function') - assert(client instanceof mqtt.MqttClient) - url += sig - actual = url - return url - }}) - mqtt.connect(opts) - .on('connect', function () { - assert.equal(this.stream.url, expected) - assert.equal(actual, expected) - this.end(true, done) - }) - }) - - it('should use mqttv3.1 as the protocol if using v3.1', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqttv3.1') - }) - - var opts = makeOptions({ - protocolId: 'MQIsdp', - protocolVersion: 3 - }) - - mqtt.connect(opts).on('connect', function () { - this.end(true, done) - }) - }) - - describe('reconnecting', () => { - it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { - var serverPort42Connected = false - var handler = function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - }) - } - this.timeout(15000) - var actualURL41 = 'wss://localhost:9917/' - var actualURL42 = 'ws://localhost:9918/' - var serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) - var serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) - - serverPort42.on('listening', function () { - let client = mqtt.connect({ - protocol: 'wss', - servers: [ - { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, - { port: ports.PORTAND41, host: 'localhost' } - ], - keepalive: 50 - }) - serverPort41.once('client', function (c) { - assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') - assert(serverPort42Connected) - c.stream.destroy() - client.end(true, done) - serverPort41.close() - }) - serverPort42.once('client', function (c) { - serverPort42Connected = true - assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') - c.stream.destroy() - serverPort42.close() - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - }) - - abstractClientTests(httpServer, makeOptions()) -}) +'use strict' + +var http = require('http') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') +var abstractClientTests = require('./abstract_client') +var ports = require('./helpers/port_list') +var MqttServerNoWait = require('./server').MqttServerNoWait +var mqtt = require('../') +var xtend = require('xtend') +var assert = require('assert') +var port = 9999 +var httpServer = http.createServer() + +function attachWebsocketServer (httpServer) { + var webSocketServer = new WebSocket.Server({server: httpServer, perMessageDeflate: false}) + + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + httpServer.emit('client', connection) + stream.on('error', function () {}) + connection.on('error', function () {}) + }) + + return httpServer +} + +function attachClientEventHandlers (client) { + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({ returnCode: 2 }) + } else { + httpServer.emit('connect', client) + client.connack({returnCode: 0}) + } + }) + + client.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + }) + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +attachWebsocketServer(httpServer) + +httpServer.on('client', attachClientEventHandlers).listen(port) + +describe('Websocket Client', function () { + var baseConfig = { protocol: 'ws', port: port } + + function makeOptions (custom) { + // xtend returns a new object. Does not mutate arguments + return xtend(baseConfig, custom || {}) + } + + it('should use mqtt as the protocol by default', function (done) { + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqtt') + }) + mqtt.connect(makeOptions()).on('connect', function () { + this.end(true, done) + }) + }) + + it('should be able to transform the url (for e.g. to sign it)', function (done) { + var baseUrl = 'ws://localhost:9999/mqtt' + var sig = '?AUTH=token' + var expected = baseUrl + sig + var actual + var opts = makeOptions({ + path: '/mqtt', + transformWsUrl: function (url, opt, client) { + assert.equal(url, baseUrl) + assert.strictEqual(opt, opts) + assert.strictEqual(client.options, opts) + assert.strictEqual(typeof opt.transformWsUrl, 'function') + assert(client instanceof mqtt.MqttClient) + url += sig + actual = url + return url + }}) + mqtt.connect(opts) + .on('connect', function () { + assert.equal(this.stream.url, expected) + assert.equal(actual, expected) + this.end(true, done) + }) + }) + + it('should use mqttv3.1 as the protocol if using v3.1', function (done) { + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqttv3.1') + }) + + var opts = makeOptions({ + protocolId: 'MQIsdp', + protocolVersion: 3 + }) + + mqtt.connect(opts).on('connect', function () { + this.end(true, done) + }) + }) + + describe('reconnecting', () => { + it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { + var serverPort42Connected = false + var handler = function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + }) + } + this.timeout(15000) + var actualURL41 = 'wss://localhost:9917/' + var actualURL42 = 'ws://localhost:9918/' + var serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) + var serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) + + serverPort42.on('listening', function () { + let client = mqtt.connect({ + protocol: 'wss', + servers: [ + { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, + { port: ports.PORTAND41, host: 'localhost' } + ], + keepalive: 50 + }) + serverPort41.once('client', function (c) { + assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') + assert(serverPort42Connected) + c.stream.destroy() + client.end(true, done) + serverPort41.close() + }) + serverPort42.once('client', function (c) { + serverPort42Connected = true + assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') + c.stream.destroy() + serverPort42.close() + }) + + client.once('connect', function () { + client.stream.destroy() + }) + }) + }) + }) + + abstractClientTests(httpServer, makeOptions()) +}) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index 0e76c4fd3..a8cf962d6 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -149,7 +149,7 @@ export interface IClientPublishOptions { * MQTT 5.0 properties object */ properties?: { - payloadFormatIndicator?: boolean, + payloadFormatIndicator?: number, messageExpiryInterval?: number, topicAlias?: string, responseTopic?: string, From c424426cd6345eba1f8016335839a667b3928e40 Mon Sep 17 00:00:00 2001 From: Caner Turkmen <31968083+canerturkmen0@users.noreply.github.com> Date: Thu, 18 Nov 2021 01:23:37 +0300 Subject: [PATCH 089/110] fix(README): typo Support (#1353) Co-authored-by: Caner Turkmen --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cebd1ca8a..b76ee36dc 100644 --- a/README.md +++ b/README.md @@ -671,7 +671,7 @@ const client = connect('wxs://test.mosquitto.org'); ``` ## Ali Mini Program -Surport [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). See [Doc](https://docs.alipay.com/mini/developer/getting-started). +Support [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). See [Doc](https://docs.alipay.com/mini/developer/getting-started). ## Example(js) From 2e845f3ce92e1bff53f2d2ef0f8e9149b5882d8e Mon Sep 17 00:00:00 2001 From: Clark Seanor Date: Wed, 17 Nov 2021 22:34:35 +0000 Subject: [PATCH 090/110] chore(README): rework examples to be a bit more specific (#1352) * Change examples to be more specific * Fix ToC * Fixed ToC order Co-authored-by: Yoseph Maguire --- README.md | 88 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b76ee36dc..5c0570c48 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,18 @@ MQTT.js is a client library for the [MQTT](http://mqtt.org/) protocol, written in JavaScript for node.js and the browser. +## Table of Contents * [__MQTT.js vNext__](#vnext) * [Upgrade notes](#notes) * [Installation](#install) * [Example](#example) +* [Import Styles](#example) * [Command Line Tools](#cli) * [API](#api) * [Browser](#browser) -* [Weapp](#weapp) * [About QoS](#qos) * [TypeScript](#typescript) +* [Weapp and Ali support](#weapp-alipay) * [Contributing](#contributing) * [License](#license) @@ -115,6 +117,25 @@ If you do not want to install a separate broker, you can try using the to use MQTT.js in the browser see the [browserify](#browserify) section + +## Import styles +### CommonJS (Require) +```js +var mqtt = require('mqtt') // require mqtt +var client = mqtt.connect('est.mosquitto.org') // create a client +``` +### ES6 Modules (Import) +#### Aliased wildcard import +```js +import * as mqtt from "mqtt" // import everything inside the mqtt module and give it the namespace "mqtt" +let client = mqtt.connect('mqtt://test.mosquitto.org') // create a client +``` +#### Importing individual components +```js +import { connect } from "mqtt" // import connect from mqtt +let client = connect('mqtt://test.mosquitto.org') // create a client +``` + ## Promise support @@ -275,7 +296,7 @@ Connects to the broker specified by the given url and options and returns a [Client](#client). The URL can be on the following protocols: 'mqtt', 'mqtts', 'tcp', -'tls', 'ws', 'wss'. The URL can also be an object as returned by +'tls', 'ws', 'wss', 'wxs', 'alis'. The URL can also be an object as returned by [`URL.parse()`](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost), in that case the two objects are merged, i.e. you can pass a single object with both the URL and the connect options. @@ -651,43 +672,6 @@ The MQTT.js bundle is available through http://unpkg.com, specifically at https://unpkg.com/mqtt/dist/mqtt.min.js. See http://unpkg.com for the full documentation on version ranges. - -## WeChat Mini Program -Support [WeChat Mini Program](https://mp.weixin.qq.com/). See [Doc](https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-socket.html). - - -## Example(js) - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('wxs://test.mosquitto.org') -``` - -## Example(ts) - -```ts -import { connect } from 'mqtt'; -const client = connect('wxs://test.mosquitto.org'); -``` - -## Ali Mini Program -Support [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). See [Doc](https://docs.alipay.com/mini/developer/getting-started). - - -## Example(js) - -```js -var mqtt = require('mqtt') -var client = mqtt.connect('alis://test.mosquitto.org') -``` - -## Example(ts) - -```ts -import { connect } from 'mqtt'; -const client = connect('alis://test.mosquitto.org'); -``` - ### Browserify @@ -806,6 +790,31 @@ Before you can begin using these TypeScript definitions with your project, you n * Set tsconfig.json: `{"compilerOptions" : {"moduleResolution" : "node"}, ...}` * Includes the TypeScript definitions for node. You can use npm to install this by typing the following into a terminal window: `npm install --save-dev @types/node` + +### Typescript example +``` +import * as mqtt from "mqtt" +let client : mqtt.MqttClient = mqtt.connect('mqtt://test.mosquitto.org') +``` + + +## WeChat and Ali Mini Program support +### WeChat Mini Program +Supports [WeChat Mini Program](https://mp.weixin.qq.com/). Use the `wxs` protocol. See [the WeChat docs](https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-socket.html). + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('wxs://test.mosquitto.org') +``` + +### Ali Mini Program +Supports [Ali Mini Program](https://open.alipay.com/channel/miniIndex.htm). Use the `alis` protocol. See [the Alipay docs](https://docs.alipay.com/mini/developer/getting-started). + + +```js +var mqtt = require('mqtt') +var client = mqtt.connect('alis://test.mosquitto.org') +``` ## Contributing @@ -827,6 +836,7 @@ MQTT.js is only possible due to the excellent work of the following contributors Siarhei BuntsevichGitHub/scarry1992 + ## License From cb6bdcb2c6c9e23f87bb24dbd1458eb0509cb02f Mon Sep 17 00:00:00 2001 From: oceanlvr <36698124+oceanlvr@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:14:38 +0800 Subject: [PATCH 091/110] fix(type): fix push properties types (#1359) --- types/lib/client-options.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index a8cf962d6..f3bcbb066 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -149,9 +149,9 @@ export interface IClientPublishOptions { * MQTT 5.0 properties object */ properties?: { - payloadFormatIndicator?: number, + payloadFormatIndicator?: boolean, messageExpiryInterval?: number, - topicAlias?: string, + topicAlias?: number, responseTopic?: string, correlationData?: Buffer, userProperties?: UserProperties, From 6581d3340602903d3434a0053eeabe7019595ea2 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Mon, 13 Dec 2021 17:12:55 +0200 Subject: [PATCH 092/110] fix(typescript): Use correct version of @types/ws (#1358) Version 8 is a stub, because in v8 types are integrated in the main package. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 712dc0350..c99fc372f 100644 --- a/package.json +++ b/package.json @@ -74,15 +74,15 @@ "number-allocator": "^1.0.7", "pump": "^3.0.0", "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", "reinterval": "^1.1.0", + "rfdc": "^1.3.0", "split2": "^3.1.0", - "ws": "^7.5.0", + "ws": "^7.5.5", "xtend": "^4.0.2" }, "devDependencies": { "@types/node": "^10.0.0", - "@types/ws": "^8.2.0", + "@types/ws": "^7.4.7", "aedes": "^0.42.5", "airtap": "^3.0.0", "browserify": "^16.5.0", From 2679952587a0e3e1b5fcbfd6b11fca72c65fba95 Mon Sep 17 00:00:00 2001 From: AThreeK Date: Tue, 14 Dec 2021 04:15:24 +1300 Subject: [PATCH 093/110] fix(tls): Skip TLS SNI if host is IP address (#1311) This avoids showing a warning when the broker address is just an IP [DEP0123] DeprecationWarning: Setting the TLS ServerName to an IP address is not permitted by RFC 6066. This will be ignored in a future version. Same fix as node-postgres has used https://github.com/brianc/node-postgres/pull/1890/commits/d3c8ebac78347ee3bd21f3734cd05ae9acf5762a --- lib/connect/tls.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/connect/tls.js b/lib/connect/tls.js index aac296666..ccf3731cd 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -1,12 +1,16 @@ 'use strict' var tls = require('tls') +var net = require('net') var debug = require('debug')('mqttjs:tls') function buildBuilder (mqttClient, opts) { var connection opts.port = opts.port || 8883 opts.host = opts.hostname || opts.host || 'localhost' - opts.servername = opts.host + + if(net.isIP(opts.host) === 0){ + opts.servername = opts.host + } opts.rejectUnauthorized = opts.rejectUnauthorized !== false From e1aa949a764f55b78667d3d5362cc7e92e1ff12f Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 20 Dec 2021 23:45:07 -0600 Subject: [PATCH 094/110] chore: var to let/const and audit dev dependencies (#1374) * fix: audit dev dependencies * add changes * timeout * add timeout * remove aftereach * fixes --- README.md | 20 +- benchmarks/bombing.js | 8 +- benchmarks/throughputCounter.js | 8 +- bin/mqtt.js | 6 +- bin/pub.js | 24 +- bin/sub.js | 12 +- example.js | 4 +- examples/client/secure-client.js | 16 +- examples/client/simple-both.js | 6 +- examples/client/simple-publish.js | 4 +- examples/client/simple-subscribe.js | 4 +- examples/tls client/mqttclient.js | 20 +- examples/ws/client.js | 10 +- examples/wss/client_with_proxy.js | 24 +- lib/client.js | 308 +++++---- lib/connect/ali.js | 28 +- lib/connect/index.js | 21 +- lib/connect/tcp.js | 9 +- lib/connect/tls.js | 11 +- lib/connect/ws.js | 28 +- lib/connect/wx.js | 24 +- lib/default-message-id-provider.js | 2 +- lib/store.js | 18 +- lib/topic-alias-send.js | 6 +- lib/unique-message-id-provider.js | 2 +- lib/validations.js | 6 +- mqtt.js | 10 +- package.json | 19 +- test/abstract_client.js | 807 +++++++++++----------- test/abstract_store.js | 19 +- test/browser/server.js | 43 +- test/browser/test.js | 24 +- test/client.js | 116 ++-- test/client_mqtt5.js | 244 +++---- test/helpers/port_list.js | 48 +- test/helpers/server.js | 18 +- test/helpers/server_process.js | 2 +- test/message-id-provider.js | 33 +- test/mqtt.js | 57 +- test/mqtt_store.js | 2 +- test/secure_client.js | 47 +- test/server.js | 20 +- test/server_helpers_for_client_tests.js | 86 +-- test/store.js | 4 +- test/unique_message_id_provider_client.js | 12 +- test/util.js | 4 +- test/websocket_client.js | 63 +- 47 files changed, 1173 insertions(+), 1134 deletions(-) diff --git a/README.md b/README.md index 5c0570c48..1ac0970c8 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ npm install mqtt --save For the sake of simplicity, let's put the subscriber and the publisher in the same file: ```js -var mqtt = require('mqtt') -var client = mqtt.connect('mqtt://test.mosquitto.org') +const mqtt = require('mqtt') +const client = mqtt.connect('mqtt://test.mosquitto.org') client.on('connect', function () { client.subscribe('presence', function (err) { @@ -121,8 +121,8 @@ to use MQTT.js in the browser see the [browserify](#browserify) section ## Import styles ### CommonJS (Require) ```js -var mqtt = require('mqtt') // require mqtt -var client = mqtt.connect('est.mosquitto.org') // create a client +const mqtt = require('mqtt') // require mqtt +const client = mqtt.connect('est.mosquitto.org') // create a client ``` ### ES6 Modules (Import) #### Aliased wildcard import @@ -695,7 +695,7 @@ gzip ### Webpack -Just like browserify, export MQTT.js as library. The exported module would be `var mqtt = xxx` and it will add an object in the global space. You could also export module in other [formats (AMD/CommonJS/others)](http://webpack.github.io/docs/configuration.html#output-librarytarget) by setting **output.libraryTarget** in webpack configuration. +Just like browserify, export MQTT.js as library. The exported module would be `const mqtt = xxx` and it will add an object in the global space. You could also export module in other [formats (AMD/CommonJS/others)](http://webpack.github.io/docs/configuration.html#output-librarytarget) by setting **output.libraryTarget** in webpack configuration. ```javascript npm install -g webpack // install webpack @@ -715,7 +715,7 @@ you can then use mqtt.js in the browser with the same api than node's one.