From 20f5a475bc5d689c96fb746620ce281c4f8fb07e Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 19 Aug 2021 13:20:48 -0700 Subject: [PATCH 1/9] initial redesign --- lib/client.js | 1714 +--------------------- lib/connect/ali.js | 128 -- lib/connect/index.js | 164 --- lib/connect/tcp.js | 21 - lib/connect/tls.js | 45 - lib/connect/ws.js | 256 ---- lib/connect/wx.js | 134 -- lib/default-message-id-provider.js | 100 +- lib/errors.js | 47 + lib/handlers/auth.js | 3 + lib/handlers/connack.js | 3 + lib/handlers/connect.js | 6 + lib/handlers/disconnect.js | 3 + lib/handlers/index.js | 73 + lib/handlers/ping.js | 3 + lib/handlers/pingreq.js | 3 + lib/handlers/pingresp.js | 3 + lib/handlers/pub.js | 3 + lib/handlers/puback.js | 3 + lib/handlers/pubcomp.js | 3 + lib/handlers/pubrec.js | 3 + lib/handlers/pubrel.js | 3 + lib/handlers/sub.js | 3 + lib/handlers/suback.js | 3 + lib/handlers/unsub.js | 3 + lib/handlers/unsuback.js | 3 + lib/index.js | 13 + lib/unique-message-id-provider.js | 102 +- lib/{validations.js => validateTopic.js} | 8 +- mqtt.js | 21 - types/lib/client.d.ts | 2 +- 31 files changed, 334 insertions(+), 2545 deletions(-) delete mode 100644 lib/connect/ali.js delete mode 100644 lib/connect/index.js delete mode 100644 lib/connect/tcp.js delete mode 100644 lib/connect/tls.js delete mode 100644 lib/connect/ws.js delete mode 100644 lib/connect/wx.js create mode 100644 lib/errors.js create mode 100644 lib/handlers/auth.js create mode 100644 lib/handlers/connack.js create mode 100644 lib/handlers/connect.js create mode 100644 lib/handlers/disconnect.js create mode 100644 lib/handlers/index.js create mode 100644 lib/handlers/ping.js create mode 100644 lib/handlers/pingreq.js create mode 100644 lib/handlers/pingresp.js create mode 100644 lib/handlers/pub.js create mode 100644 lib/handlers/puback.js create mode 100644 lib/handlers/pubcomp.js create mode 100644 lib/handlers/pubrec.js create mode 100644 lib/handlers/pubrel.js create mode 100644 lib/handlers/sub.js create mode 100644 lib/handlers/suback.js create mode 100644 lib/handlers/unsub.js create mode 100644 lib/handlers/unsuback.js create mode 100644 lib/index.js rename lib/{validations.js => validateTopic.js} (90%) delete mode 100644 mqtt.js diff --git a/lib/client.js b/lib/client.js index c481e7a04..2d07df752 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,1682 +1,72 @@ 'use strict' -/** - * Module dependencies - */ -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') -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 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) - client.outgoingStore.put(packet, 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) - - 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 = {} +const mqtt = require('mqtt-packet') +// const eventEmitter = require('events') +const handle = require('handlers') +// const mqttErrors = require('errors') - // map of a subscribe messageId and a topic - this.messageIdToTopic = {} +// const logger = require('pino')() - // 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 - - // 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) { - 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)) { - packet.messageeId = that.messageIdProvider.allocate() - if (packet.messageId === null) { - send = false - } - } - } - 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') - deliver() - }) - - this.on('close', function () { - debug('close :: connected set to `false`') +export class Client { + constructor (options) { + this.closed = false + this.connecting = 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 - } - - 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' - // 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 - 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) - 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) + this.errored = false + this.id = null + this.clean = true + this.version = null + + this._disconnected = false + this._authorized = false + this._parser = mqtt.parser() + this._defaultConnectOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true + } + + this._options = options || {...this._defaultConnectOptions} + this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` + this._parser.client = this + this._parser._queue = [] + this._parser.on('packet', this.enqueue) + this.once('connected', this.dequeue) + // TBD + } + + enqueue () { 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) - + dequeue () { 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 - - 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 - - // 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 (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) - - if (packet.properties) { - if (packet.properties.topicAliasMaximum) { - if (!options.properties) { options.properties = {} } - options.properties.topicAliasMaximum = 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] - 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 (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]] - resubscribeTopic.resubscribe = true - this.subscribe(resubscribeTopic, {properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties}) - } - } else { - this._resubscribeTopics.resubscribe = true - this.subscribe(this._resubscribeTopics) - } - } else { - this._resubscribeTopics = {} - } + static async connect (options) { + return new Client(options) } - this._firstConnection = false -} - -/** - * _onConnect - * - * @api private - */ -MqttClient.prototype._onConnect = function (packet) { - if (this.disconnected) { - this.emit('connect', packet) - return + async publish (topic, message, opts) { + const result = await handle.publish(this, message) + return result } - var that = this - - this.messageIdProvider.clear() - this._setupPingTimer() - this._resubscribe(packet) - - this.connected = true - - function startStreamProcess () { - var outStore = that.outgoingStore.createStream() - - function clearStoreProcessing () { - that._storeProcessing = false - that._packetIdsDuringStoreProcessing = {} + async subscribe (packet) { + if (!packet.subscriptions) { + packet = {subscriptions: Array.isArray(packet) ? packet : [packet]} } - - 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() + const result = await handle.subscribe(this, packet) + return result } - // 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 - } + async unsubscribe (packet) { + const result = await handle.unsubscribe(this, packet) + return result } - 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 deleted file mode 100644 index e7fe6a3c5..000000000 --- a/lib/connect/ali.js +++ /dev/null @@ -1,128 +0,0 @@ -'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 deleted file mode 100644 index 97e7b4c15..000000000 --- a/lib/connect/index.js +++ /dev/null @@ -1,164 +0,0 @@ -'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 deleted file mode 100644 index 9912102eb..000000000 --- a/lib/connect/tcp.js +++ /dev/null @@ -1,21 +0,0 @@ -'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 deleted file mode 100644 index aac296666..000000000 --- a/lib/connect/tls.js +++ /dev/null @@ -1,45 +0,0 @@ -'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 deleted file mode 100644 index 5c1d2c691..000000000 --- a/lib/connect/ws.js +++ /dev/null @@ -1,256 +0,0 @@ -'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 deleted file mode 100644 index b9c7a0705..000000000 --- a/lib/connect/wx.js +++ /dev/null @@ -1,134 +0,0 @@ -'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..bbe532ae8 100644 --- a/lib/default-message-id-provider.js +++ b/lib/default-message-id-provider.js @@ -4,66 +4,62 @@ * DefaultMessageAllocator constructor * @constructor */ -function DefaultMessageIdProvider () { - if (!(this instanceof DefaultMessageIdProvider)) { - return new DefaultMessageIdProvider() - } - +export class 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)) -} + constructor () { + 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 + /** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ + allocate () { + // 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 id -} -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -DefaultMessageIdProvider.prototype.getLastAllocated = function () { - return (this.nextId === 1) ? 65535 : (this.nextId - 1) -} + /** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ + getLastAllocated () { + 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 -} + /** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ + register (messageId) { + return true + } -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -DefaultMessageIdProvider.prototype.deallocate = function (messageId) { -} + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate (messageId) { + } -/** - * clear - * Deallocate all messageIds. - */ -DefaultMessageIdProvider.prototype.clear = function () { + /** + * clear + * Deallocate all messageIds. + */ + clear () { + } } - -module.exports = DefaultMessageIdProvider diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 000000000..7fb11ed00 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,47 @@ +'use strict' + +export 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' +} diff --git a/lib/handlers/auth.js b/lib/handlers/auth.js new file mode 100644 index 000000000..25176c9da --- /dev/null +++ b/lib/handlers/auth.js @@ -0,0 +1,3 @@ +export async function handleAuth () { + return true +} diff --git a/lib/handlers/connack.js b/lib/handlers/connack.js new file mode 100644 index 000000000..234f4b241 --- /dev/null +++ b/lib/handlers/connack.js @@ -0,0 +1,3 @@ +export async function handleConnAck () { + return true +} diff --git a/lib/handlers/connect.js b/lib/handlers/connect.js new file mode 100644 index 000000000..a89e5c072 --- /dev/null +++ b/lib/handlers/connect.js @@ -0,0 +1,6 @@ + +export async function handleConnect (client, packet) { + client.connecting = true + // Connection logic + return true +} diff --git a/lib/handlers/disconnect.js b/lib/handlers/disconnect.js new file mode 100644 index 000000000..2bed1ca5b --- /dev/null +++ b/lib/handlers/disconnect.js @@ -0,0 +1,3 @@ +export async function handleDisconnect () { + return true +} diff --git a/lib/handlers/index.js b/lib/handlers/index.js new file mode 100644 index 000000000..705b741b1 --- /dev/null +++ b/lib/handlers/index.js @@ -0,0 +1,73 @@ +import {handleConnect} from './connect' +import {handleConnAck} from './connack' +import {handleDisconnect} from './disconnect' +import {handlePing} from './ping' +import {handlePingReq} from './pingreq' +import {handlePingResp} from './pingresp' +import {handlePub} from './pub' +import {handlePubRec} from './pubrec' +import {handlePubRel} from './pubrel' +import {handlePubComp} from './pubcomp' +import {handlePubAck} from './puback' +import {handleSub} from './sub' +import {handleSubAck} from './suback' +import {handleUnsub} from './unsub' +import {handleUnsubAck} from './unsuback' +import { handleAuth } from './auth' + +export async function handle (client, packet) { + let result + switch (packet.cmd) { + case 'auth': + result = await handleAuth(client, packet) + break + case 'connect': + result = await handleConnect(client, packet) + break + case 'connack': + result = await handleConnAck(client, packet) + break + case 'publish': + result = await handlePub(client, packet) + break + case 'subscribe': + result = await handleSub(client, packet) + break + case 'suback': + result = await handleSubAck(client, packet) + break + case 'unsubscribe': + result = await handleUnsub(client, packet) + break + case 'unsuback': + result = await handleUnsubAck(client, packet) + break + case 'pubcomp': + result = await handlePubComp(client, packet) + break + case 'puback': + result = await handlePubAck(client, packet) + break + case 'pubrel': + result = await handlePubRel(client, packet) + break + case 'pubrec': + result = await handlePubRec(client, packet) + break + case 'ping': + result = await handlePing(client, packet) + break + case 'pingreq': + result = await handlePingReq(client, packet) + break + case 'pingresp': + result = await handlePingResp(client, packet) + break + case 'disconnect': + result = await handleDisconnect(client, packet) + client._disconnected = true + break + } + + return result +} diff --git a/lib/handlers/ping.js b/lib/handlers/ping.js new file mode 100644 index 000000000..cfede6bd8 --- /dev/null +++ b/lib/handlers/ping.js @@ -0,0 +1,3 @@ +export async function handlePing () { + return true +} diff --git a/lib/handlers/pingreq.js b/lib/handlers/pingreq.js new file mode 100644 index 000000000..98e97a870 --- /dev/null +++ b/lib/handlers/pingreq.js @@ -0,0 +1,3 @@ +export async function handlePingReq () { + return true +} diff --git a/lib/handlers/pingresp.js b/lib/handlers/pingresp.js new file mode 100644 index 000000000..db82e662f --- /dev/null +++ b/lib/handlers/pingresp.js @@ -0,0 +1,3 @@ +export async function handlePingResp () { + return true +} diff --git a/lib/handlers/pub.js b/lib/handlers/pub.js new file mode 100644 index 000000000..269f53d84 --- /dev/null +++ b/lib/handlers/pub.js @@ -0,0 +1,3 @@ +export async function handlePub () { + return true +} diff --git a/lib/handlers/puback.js b/lib/handlers/puback.js new file mode 100644 index 000000000..b3e83976c --- /dev/null +++ b/lib/handlers/puback.js @@ -0,0 +1,3 @@ +export async function handlePubAck () { + return true +} diff --git a/lib/handlers/pubcomp.js b/lib/handlers/pubcomp.js new file mode 100644 index 000000000..f88556a74 --- /dev/null +++ b/lib/handlers/pubcomp.js @@ -0,0 +1,3 @@ +export async function handlePubComp () { + return true +} diff --git a/lib/handlers/pubrec.js b/lib/handlers/pubrec.js new file mode 100644 index 000000000..b312f60e3 --- /dev/null +++ b/lib/handlers/pubrec.js @@ -0,0 +1,3 @@ +export async function handlePubRec () { + return true +} diff --git a/lib/handlers/pubrel.js b/lib/handlers/pubrel.js new file mode 100644 index 000000000..ae73babd8 --- /dev/null +++ b/lib/handlers/pubrel.js @@ -0,0 +1,3 @@ +export async function handlePubRel () { + return true +} diff --git a/lib/handlers/sub.js b/lib/handlers/sub.js new file mode 100644 index 000000000..c5af630ea --- /dev/null +++ b/lib/handlers/sub.js @@ -0,0 +1,3 @@ +export async function handleSub () { + return true +} diff --git a/lib/handlers/suback.js b/lib/handlers/suback.js new file mode 100644 index 000000000..0049e3f92 --- /dev/null +++ b/lib/handlers/suback.js @@ -0,0 +1,3 @@ +export async function handleSubAck () { + return true +} diff --git a/lib/handlers/unsub.js b/lib/handlers/unsub.js new file mode 100644 index 000000000..0ffb6af9f --- /dev/null +++ b/lib/handlers/unsub.js @@ -0,0 +1,3 @@ +export async function handleUnsub () { + return true +} diff --git a/lib/handlers/unsuback.js b/lib/handlers/unsuback.js new file mode 100644 index 000000000..a21275ede --- /dev/null +++ b/lib/handlers/unsuback.js @@ -0,0 +1,3 @@ +export async function handleUnsubAck () { + return true +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 000000000..15a0b3aac --- /dev/null +++ b/lib/index.js @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ + +import { connect } from './client' +import { Store } from './store' +import { DefaultMessageIdProvider } from './default-message-id-provider' +import { UniqueMessageIdProvider } from './unique-message-id-provider' + +export {connect, Store, DefaultMessageIdProvider, UniqueMessageIdProvider} diff --git a/lib/unique-message-id-provider.js b/lib/unique-message-id-provider.js index 6ffd4bde6..bf30c226e 100644 --- a/lib/unique-message-id-provider.js +++ b/lib/unique-message-id-provider.js @@ -1,65 +1,57 @@ 'use strict' -var NumberAllocator = require('number-allocator').NumberAllocator +import { NumberAllocator } from 'number-allocator' -/** - * UniqueMessageAllocator constructor - * @constructor - */ -function UniqueMessageIdProvider () { - if (!(this instanceof UniqueMessageIdProvider)) { - return new UniqueMessageIdProvider() +export class UniqueMessageIdProvider { + constructor () { + this.numberAllocator = new NumberAllocator(1, 65535) } - 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 -} + /** + * allocate + * + * Get the next messageId. + * @return if messageId is fully allocated then return null, + * otherwise return the smallest usable unsigned int messageId. + */ + allocate () { + this.lastId = this.numberAllocator.alloc() + return this.lastId + } -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -UniqueMessageIdProvider.prototype.getLastAllocated = function () { - return this.lastId -} + /** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ + getLastAllocated () { + 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) -} + /** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ + register (messageId) { + return this.numberAllocator.use(messageId) + } -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -UniqueMessageIdProvider.prototype.deallocate = function (messageId) { - this.numberAllocator.free(messageId) -} + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate (messageId) { + this.numberAllocator.free(messageId) + } -/** - * clear - * Deallocate all messageIds. - */ -UniqueMessageIdProvider.prototype.clear = function () { - this.numberAllocator.clear() + /** + * clear + * Deallocate all messageIds. + */ + clear () { + this.numberAllocator.clear() + } } - -module.exports = UniqueMessageIdProvider diff --git a/lib/validations.js b/lib/validateTopic.js similarity index 90% rename from lib/validations.js rename to lib/validateTopic.js index 1a3277901..e8c6d62dc 100644 --- a/lib/validations.js +++ b/lib/validateTopic.js @@ -9,7 +9,7 @@ * @param {String} topic - A topic * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. */ -function validateTopic (topic) { +export function validateTopic (topic) { var parts = topic.split('/') for (var i = 0; i < parts.length; i++) { @@ -35,7 +35,7 @@ function validateTopic (topic) { * @param {Array} topics - Array of topics * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one */ -function validateTopics (topics) { +export function validateTopics (topics) { if (topics.length === 0) { return 'empty_topic_list' } @@ -46,7 +46,3 @@ function validateTopics (topics) { } return null } - -module.exports = { - validateTopics: validateTopics -} diff --git a/mqtt.js b/mqtt.js deleted file mode 100644 index c8b94fda1..000000000 --- a/mqtt.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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/types/lib/client.d.ts b/types/lib/client.d.ts index 7821a96d7..94657b6a5 100644 --- a/types/lib/client.d.ts +++ b/types/lib/client.d.ts @@ -204,7 +204,7 @@ export declare class MqttClient extends events.EventEmitter { * @returns {MqttClient} this - for chaining * @api public * - * @example client.removeOutgoingMessage(client.getLastMessageId()); + * @example client.removeOutgoingMessage(client.getLastMessageId()) */ public removeOutgoingMessage (mid: number): this From 8e71d0136cb07fb79f8a63336f1bb8e81d76b9c2 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 26 Aug 2021 10:11:52 -0700 Subject: [PATCH 2/9] updates --- .eslintrc.json | 16 + lib/client.js | 653 ++++++++++++++++++++++++++++++++++++- lib/handlers/connack.js | 37 ++- lib/handlers/connect.js | 108 +++++- lib/handlers/disconnect.js | 4 +- lib/handlers/index.js | 32 +- lib/handlers/ping.js | 26 +- lib/handlers/pingresp.js | 4 + lib/handlers/pub.js | 92 +++++- lib/handlers/puback.js | 83 ++++- lib/handlers/pubcomp.js | 83 ++++- lib/handlers/pubrec.js | 82 ++++- lib/handlers/pubrel.js | 24 +- lib/handlers/sub.js | 181 +++++++++- lib/handlers/suback.js | 83 ++++- lib/handlers/unsub.js | 92 +++++- lib/handlers/unsuback.js | 83 ++++- lib/index.js | 61 ++++ package.json | 67 ++-- tslint.json | 3 - 20 files changed, 1718 insertions(+), 96 deletions(-) create mode 100644 .eslintrc.json delete mode 100644 tslint.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..ac3f85860 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "standard" + ], + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "rules": { + } +} diff --git a/lib/client.js b/lib/client.js index 2d07df752..ba7c91339 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,11 +1,14 @@ 'use strict' -const mqtt = require('mqtt-packet') +import { mqtt } from 'mqtt-packet' +import { handle } from 'handlers' +import net from 'net' +import WebSocket, { createWebSocketStream } from 'ws' + // const eventEmitter = require('events') -const handle = require('handlers') // const mqttErrors = require('errors') -// const logger = require('pino')() +const logger = require('pino')() export class Client { constructor (options) { @@ -16,6 +19,19 @@ export class Client { this.id = null this.clean = true this.version = null + // eslint-disable-next-line camelcase + // TODO: _isBrowser should be a global value and should be standardized.... + this._isBrowser = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' + + // Connect Information + this.protocol = null + this.port = null + this.hostname = null + this.rejectUnauthorized = null + + this.stream = this._streamBuilder() + + this._reconnectCount = 0 this._disconnected = false this._authorized = false @@ -31,25 +47,368 @@ export class Client { resubscribe: true } - this._options = options || {...this._defaultConnectOptions} + this._options = options || { ...this._defaultConnectOptions } this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` - this._parser.client = this this._parser._queue = [] this._parser.on('packet', this.enqueue) this.once('connected', this.dequeue) - // TBD + this.on('connect', this._sendQueuedPackets()) + this.on('close', this._closeClient()) + } + + _streamBuilder (opts) { + switch (this.protocol) { + case 'tcp': { + return net.createConnection(port, host) + } + case 'tls': { + // TODO: This needs to have options passed down to it. + // We should probably have the whole options object just + // passed down to tls.connect, right? + + function handleTLSErrors () { + // 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 = tls.connect(opts) + connection.on('secureConnect', function () { + if (opts.rejectUnauthorized && !connection.authorized) { + connection.emit('error', new Error('TLS not authorized')) + } else { + connection.removeListener('error', handleTLSErrors) + } + }) + + connection.on('error', handleTLSErrors) + return connection + } + case 'ws': { + if (this._isBrowser) { + this._buildWebSocketStreamBrowser(opts) + } else { + this._buildWebSocketStream(opts) + } + } + } + } + + // To consider : do we want to have this in the main code, + // or do we want to have a browser shim? + _buildWebSocketStreamBrowser (opts) { + const 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] + } + }) + } + + 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) + } + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + const url = buildUrl(opts, client) + /* global WebSocket */ + const socket = new WebSocket(url, [websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + logger('browserStreamBuilder') + let stream + // sets the maximum socket buffer size before throttling + const bufferSize = options.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + const 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) { + const 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 + } + + _buildWebSocketStream (opts) { + const 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] + } + }) + } + const url = buildUrl(options, client) + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) + const webSocketStream = createWebSocketStream(socket, options.wsOptions) + webSocketStream.url = url + socket.on('close', () => { webSocketStream.destroy() }) + return webSocketStream + } + + async enqueue (packet) { + this._parsingBatch++ + // already connected or it's the first packet + if (this.connackSent || this._parsingBatch === 1) { + await handle(this, packet, this._nextBatch) + } else { + if (this._parser._queue.length < this._queueLimit) { + this._parser._queue.push(packet) + } else { + this.emit('error', new Error('Client queue limit reached')) + } + } } - enqueue () { - return true + async dequeue () { + const q = this._parser._queue + if (q) { + for (let i = 0, len = q.length; i < len; i++) { + await handle(this, q[i], this._nextBatch) + } + + this._parser._queue = null + } } - dequeue () { - return true + _closeClient () { + logger('close :: connected set to `false`') + this.connected = false + + logger('close :: clearing connackTimer') + clearTimeout(this.connackTimer) + + logger('close :: clearing ping timer') + if (that.pingTimer !== null) { + that.pingTimer.clear() + that.pingTimer = null + } + + logger('close :: calling _setupReconnect') + this._setupReconnect() } - static async connect (options) { - return new Client(options) + _sendQueuedPackets () { + const queue = this.queue + + function deliver () { + const entry = queue.shift() + logger('deliver :: entry %o', entry) + let packet = null + + if (!entry) { + return + } + + packet = entry.packet + logger('deliver :: call _sendPacket for %o', packet) + let 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 + } + } + } + if (send) { + that._sendPacket( + packet, + function (err) { + if (entry.cb) { + entry.cb(err) + } + deliver() + } + ) + } else { + logger('messageId: %d has already used.', packet.messageId) + deliver() + } + } + + deliver() } async publish (topic, message, opts) { @@ -59,7 +418,7 @@ export class Client { async subscribe (packet) { if (!packet.subscriptions) { - packet = {subscriptions: Array.isArray(packet) ? packet : [packet]} + packet = { subscriptions: Array.isArray(packet) ? packet : [packet] } } const result = await handle.subscribe(this, packet) return result @@ -69,4 +428,272 @@ export class Client { const result = await handle.unsubscribe(this, packet) return result } + + async end (force, opts) { + const that = this + + logger('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 + } + + logger('end :: cb? %s', !!cb) + cb = cb || nop + + function closeStores () { + logger('end :: closeStores: closing incoming and outgoing stores') + that.disconnected = true + that.incomingStore.close(function (e1) { + that.outgoingStore.close(function (e2) { + logger('end :: closeStores: emitting end') + that.emit('end') + if (cb) { + const err = e1 || e2 + logger('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 + logger('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) + that._cleanUp(force, () => { + logger('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 + logger('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) + this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) + } else { + logger('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()); + */ + removeOutgoingMessage (messageId) { + const 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 (opts) { + logger('client reconnect') + const that = this + const 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 () { + logger('_reconnect: emitting reconnect to client') + this.emit('reconnect') + if (this.connected) { + this.end(() => { this._setupStream() }) + logger('client already connected. disconnecting first.') + } else { + logger('_reconnect: calling _setupStream') + this._setupStream() + } + } + + _setupReconnect () { + if (!this.disconnecting && !this.reconnectTimer && (this.options.reconnectPeriod > 0)) { + if (!this.reconnecting) { + logger('_setupReconnect :: emit `offline` state') + this.emit('offline') + logger('_setupReconnect :: set `reconnecting` to `true`') + this.reconnecting = true + } + logger('_setupReconnect :: setting reconnectTimer for %d ms', this.options.reconnectPeriod) + this.reconnectTimer = setInterval(() => { + logger('reconnectTimer :: reconnect triggered!') + this._reconnect() + }, this.options.reconnectPeriod) + } else { + logger('_setupReconnect :: doing nothing...') + } + } + + _clearReconnect () { + logger('_clearReconnect : clearing reconnect timer') + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer) + this.reconnectTimer = null + } + } + + async _cleanUp (forced) { + const opts = arguments[2] + if (done) { + logger('_cleanUp :: done callback provided for on stream close') + this.stream.on('close', done) + } + + logger('_cleanUp :: forced? %s', forced) + if (forced) { + if ((this.options.reconnectPeriod === 0) && this.options.clean) { + flush(this.outgoing) + } + logger('_cleanUp :: (%s) :: destroying stream', this.options.clientId) + this.stream.destroy() + } else { + const packet = xtend({ cmd: 'disconnect' }, opts) + logger('_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) { + logger('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') + this._clearReconnect() + this._setupReconnect() + } + + if (this.pingTimer !== null) { + logger('_cleanUp :: clearing pingTimer') + this.pingTimer.clear() + this.pingTimer = null + } + + if (done && !this.connected) { + logger('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) + this.stream.removeListener('close', done) + done() + } + } + + async _sendPacket (packet) { + logger('_sendPacket :: (%s) :: start', this.options.clientId) + cbStorePut = cbStorePut || nop + + if (!this.connected) { + logger('_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 + } + } + + _storePacket (packet) { + 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) { + cb = this.outgoing[packet.messageId] ? this.outgoing[packet.messageId].cb : null + this.outgoingStore.put(packet, function (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + }) + } else if (cb) { + cb(new Error('No connection to broker')) + } + } } diff --git a/lib/handlers/connack.js b/lib/handlers/connack.js index 234f4b241..c036953bf 100644 --- a/lib/handlers/connack.js +++ b/lib/handlers/connack.js @@ -1,3 +1,36 @@ -export async function handleConnAck () { - return true +export async function handleConnAck (packet) { + this.connackTimer = setTimeout(function () { + debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') + that._cleanUp(true) + }, this.options.connectTimeout) + + var options = this.options + var version = options.protocolVersion + var rc = version === 5 ? packet.reasonCode : packet.returnCode + + clearTimeout(this.connackTimer) + + if (packet.properties) { + if (packet.properties.topicAliasMaximum) { + if (!options.properties) { options.properties = {} } + options.properties.topicAliasMaximum = 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) + } } diff --git a/lib/handlers/connect.js b/lib/handlers/connect.js index a89e5c072..29b43b1ca 100644 --- a/lib/handlers/connect.js +++ b/lib/handlers/connect.js @@ -1,6 +1,108 @@ export async function handleConnect (client, packet) { - client.connecting = true - // Connection logic - return true + if (this.disconnected) { + this.emit('connect', packet) + return + } + + var that = this + + this.messageIdProvider.clear() + this._setupPingTimer() + this._resubscribe(packet) + + 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() } diff --git a/lib/handlers/disconnect.js b/lib/handlers/disconnect.js index 2bed1ca5b..cae2553c4 100644 --- a/lib/handlers/disconnect.js +++ b/lib/handlers/disconnect.js @@ -1,3 +1,3 @@ -export async function handleDisconnect () { - return true +export async function handleDisconnect (client) { + client.emit('disconnect', packet) } diff --git a/lib/handlers/index.js b/lib/handlers/index.js index 705b741b1..32138753e 100644 --- a/lib/handlers/index.js +++ b/lib/handlers/index.js @@ -1,18 +1,20 @@ -import {handleConnect} from './connect' -import {handleConnAck} from './connack' -import {handleDisconnect} from './disconnect' -import {handlePing} from './ping' -import {handlePingReq} from './pingreq' -import {handlePingResp} from './pingresp' -import {handlePub} from './pub' -import {handlePubRec} from './pubrec' -import {handlePubRel} from './pubrel' -import {handlePubComp} from './pubcomp' -import {handlePubAck} from './puback' -import {handleSub} from './sub' -import {handleSubAck} from './suback' -import {handleUnsub} from './unsub' -import {handleUnsubAck} from './unsuback' +'use strict' + +import { handleConnect } from './connect' +import { handleConnAck } from './connack' +import { handleDisconnect } from './disconnect' +import { handlePing } from './ping' +import { handlePingReq } from './pingreq' +import { handlePingResp } from './pingresp' +import { handlePub } from './pub' +import { handlePubRec } from './pubrec' +import { handlePubRel } from './pubrel' +import { handlePubComp } from './pubcomp' +import { handlePubAck } from './puback' +import { handleSub } from './sub' +import { handleSubAck } from './suback' +import { handleUnsub } from './unsub' +import { handleUnsubAck } from './unsuback' import { handleAuth } from './auth' export async function handle (client, packet) { diff --git a/lib/handlers/ping.js b/lib/handlers/ping.js index cfede6bd8..c80e22e68 100644 --- a/lib/handlers/ping.js +++ b/lib/handlers/ping.js @@ -1,3 +1,27 @@ + export async function handlePing () { - return true + debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) + + if (!this.pingTimer && this.options.keepalive) { + this.pingResp = true + this.pingTimer = reInterval(() => { + checkPing() + }, this.options.keepalive * 1000) + } } + +export async function shiftPingInterval () { + if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { + this.pingTimer.reschedule(this.options.keepalive * 1000) + } +} + +function checkPing () { + if (this.pingResp) { + this.pingResp = false + this._sendPacket({ cmd: 'pingreq' }) + } else { + // do a forced cleanup since socket will be in bad shape + this._cleanUp(true) + } +} \ No newline at end of file diff --git a/lib/handlers/pingresp.js b/lib/handlers/pingresp.js index db82e662f..3d555804e 100644 --- a/lib/handlers/pingresp.js +++ b/lib/handlers/pingresp.js @@ -1,3 +1,7 @@ export async function handlePingResp () { + // TODO: In old implementation, pingResp is a state in + // the client that is toggled to true when the pingResp + // is received. How does aedes do this? is there a + // better way than a boolean toggle? return true } diff --git a/lib/handlers/pub.js b/lib/handlers/pub.js index 269f53d84..e2c690850 100644 --- a/lib/handlers/pub.js +++ b/lib/handlers/pub.js @@ -1,3 +1,89 @@ -export async function handlePub () { - return true -} + +/** + * _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 +*/ +export async function handlePub (packet) { + 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] + 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 + } diff --git a/lib/handlers/puback.js b/lib/handlers/puback.js index b3e83976c..66a903ef6 100644 --- a/lib/handlers/puback.js +++ b/lib/handlers/puback.js @@ -1,3 +1,80 @@ -export async function handlePubAck () { - return true -} +export async function handlePubAck (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') + }} diff --git a/lib/handlers/pubcomp.js b/lib/handlers/pubcomp.js index f88556a74..350d65326 100644 --- a/lib/handlers/pubcomp.js +++ b/lib/handlers/pubcomp.js @@ -1,3 +1,80 @@ -export async function handlePubComp () { - return true -} +export async function handlePubComp (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') + }} diff --git a/lib/handlers/pubrec.js b/lib/handlers/pubrec.js index b312f60e3..8f9a1666b 100644 --- a/lib/handlers/pubrec.js +++ b/lib/handlers/pubrec.js @@ -1,3 +1,81 @@ -export async function handlePubRec () { - return true +export async function handlePubRec (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') + } } diff --git a/lib/handlers/pubrel.js b/lib/handlers/pubrel.js index ae73babd8..77bcfc7d2 100644 --- a/lib/handlers/pubrel.js +++ b/lib/handlers/pubrel.js @@ -1,3 +1,23 @@ -export async function handlePubRel () { - return true +export async function handlePubRel (packet) { + 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) + } + }) } diff --git a/lib/handlers/sub.js b/lib/handlers/sub.js index c5af630ea..c913e7098 100644 --- a/lib/handlers/sub.js +++ b/lib/handlers/sub.js @@ -1,3 +1,182 @@ + +/** + * 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); + */ export async function handleSub () { - return true + 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 } diff --git a/lib/handlers/suback.js b/lib/handlers/suback.js index 0049e3f92..2f9cee01f 100644 --- a/lib/handlers/suback.js +++ b/lib/handlers/suback.js @@ -1,3 +1,80 @@ -export async function handleSubAck () { - return true -} +export async function handleSubAck (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') + }} diff --git a/lib/handlers/unsub.js b/lib/handlers/unsub.js index 0ffb6af9f..db4691927 100644 --- a/lib/handlers/unsub.js +++ b/lib/handlers/unsub.js @@ -1,3 +1,93 @@ +/** + * 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); + */ + export async function handleUnsub () { - return true + 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 } diff --git a/lib/handlers/unsuback.js b/lib/handlers/unsuback.js index a21275ede..06fcf1f1e 100644 --- a/lib/handlers/unsuback.js +++ b/lib/handlers/unsuback.js @@ -1,3 +1,80 @@ -export async function handleUnsubAck () { - return true -} +export async function handleUnsubAck (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') + }} diff --git a/lib/index.js b/lib/index.js index 15a0b3aac..521677e73 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,4 +10,65 @@ import { Store } from './store' import { DefaultMessageIdProvider } from './default-message-id-provider' import { UniqueMessageIdProvider } from './unique-message-id-provider' + +/** + * connect() + * Connect will: + * 1) Validate the options provided by the user. + * 2) Instantiate a new client. + * 3) Return the client to the user. + */ +function connect (options) { + _validateProtocol(opts) +} + +function _validateProtocol(opts) { + 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 + // I don't like that we are manipulating the opts object here. + 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] + } +} + export {connect, Store, DefaultMessageIdProvider, UniqueMessageIdProvider} diff --git a/package.json b/package.json index 3be7ba77e..3f2ff2f24 100644 --- a/package.json +++ b/package.json @@ -24,20 +24,11 @@ "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" + "ci": "npm run test && codecov" }, - "pre-commit": [ - "pretest", - "tslint" - ], "bin": { "mqtt_pub": "./bin/pub.js", "mqtt_sub": "./bin/sub.js", @@ -62,44 +53,48 @@ "net": false }, "dependencies": { - "commist": "^1.0.0", + "commist": "^2.0.0", "concat-stream": "^2.0.0", - "debug": "^4.1.1", - "duplexify": "^4.1.1", + "debug": "^4.3.2", + "duplexify": "^4.1.2", "help-me": "^3.0.0", - "inherits": "^2.0.3", + "inherits": "^2.0.4", "minimist": "^1.2.5", - "mqtt-packet": "^6.8.0", + "mqtt-packet": "^7.0.0", "number-allocator": "^1.0.7", "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", - "split2": "^3.1.0", - "ws": "^7.5.0", + "split2": "^3.2.2", + "ws": "^8.2.0", "xtend": "^4.0.2" }, "devDependencies": { - "@types/node": "^10.0.0", - "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", - "nyc": "^15.0.1", - "pre-commit": "^1.2.2", + "@types/node": "^16.7.1", + "aedes": "^0.46.1", + "airtap": "^4.0.3", + "browserify": "^17.0.0", + "chai": "^4.3.4", + "codecov": "^3.8.3", + "end-of-stream": "^1.4.4", + "eslint": "^7.32.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.24.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "global": "^4.4.0", + "mkdirp": "^1.0.4", + "mocha": "^9.1.0", + "mqtt-connection": "^4.1.0", + "nyc": "^15.1.0", "rimraf": "^3.0.2", - "should": "^13.2.1", - "sinon": "^9.0.0", - "snazzy": "^8.0.0", - "standard": "^11.0.1", - "tslint": "^5.11.0", + "should": "^13.2.3", + "sinon": "^11.1.2", + "snazzy": "^9.0.0", + "standard": "^16.0.3", + "tslint": "^5.20.1", "tslint-config-standard": "^8.0.1", - "typescript": "^3.2.2", + "typescript": "^4.3.5", "uglify-es": "^3.3.9" }, "standard": { diff --git a/tslint.json b/tslint.json deleted file mode 100644 index 45052adc8..000000000 --- a/tslint.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "tslint-config-standard" -} From 196a4d1ee1ec1f1ba68c681fb0a3f07f1dc5a939 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 21 Sep 2021 09:03:41 -0700 Subject: [PATCH 3/9] updates --- .airtaprc.yml | 26 +- .editorconfig | 18 +- .eslintrc.json | 32 +- .github/workflows/nodejs.yml | 64 +- .github/workflows/syncToDevOps.yml | 46 +- .gitignore | 47 +- .npmrc | 1 - CHANGELOG.md | 56 +- CONTRIBUTING.md | 54 +- LICENSE.md | 30 +- README.md | 1586 ++++---- benchmarks/bombing.js | 26 - benchmarks/throughputCounter.js | 22 - bin/mqtt.js | 54 +- bin/pub.js | 292 +- bin/sub.js | 246 +- doc/help.txt | 16 +- doc/publish.txt | 52 +- doc/subscribe.txt | 52 +- 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/crt.ca.cg.pem | 68 +- examples/tls client/mqttclient.js | 96 +- examples/tls client/tls-cert.pem | 26 +- examples/tls client/tls-key.pem | 30 +- examples/ws/aedes_server.js | 84 +- examples/ws/client.js | 106 +- examples/wss/client_with_proxy.js | 116 +- lib/handlers/auth.js | 3 - lib/index.js | 74 - package.json | 216 +- review/mqtt.api.md | 77 + lib/client.js => src/client.ts | 1377 ++++--- src/connectionFactory/index.ts | 264 ++ .../default-message-id-provider.ts | 130 +- lib/errors.js => src/errors.ts | 94 +- src/handlers/auth.ts | 56 + .../connack.js => src/handlers/connack.ts | 72 +- .../connect.js => src/handlers/connect.ts | 216 +- .../handlers/disconnect.ts | 6 +- .../index.js => src/handlers/index.ts | 150 +- lib/handlers/ping.js => src/handlers/ping.ts | 52 +- .../pingreq.js => src/handlers/pingreq.ts | 6 +- .../pingresp.js => src/handlers/pingresp.ts | 14 +- lib/handlers/pub.js => src/handlers/pub.ts | 178 +- .../puback.js => src/handlers/puback.ts | 160 +- .../pubcomp.js => src/handlers/pubcomp.ts | 160 +- .../pubrec.js => src/handlers/pubrec.ts | 162 +- .../pubrel.js => src/handlers/pubrel.ts | 46 +- lib/handlers/sub.js => src/handlers/sub.ts | 364 +- .../suback.js => src/handlers/suback.ts | 160 +- .../unsub.js => src/handlers/unsub.ts | 186 +- .../unsuback.js => src/handlers/unsuback.ts | 160 +- src/index.ts | 186 + lib/store.js => src/store.ts | 256 +- .../unique-message-id-provider.ts | 114 +- lib/validateTopic.js => src/validateTopic.ts | 96 +- test/abstract_client.js | 3177 ----------------- test/abstract_store.js | 135 - test/browser/server.js | 132 - test/browser/test.js | 92 - test/client.js | 486 --- test/client_mqtt5.js | 536 --- test/helpers/port_list.js | 49 - test/helpers/private-csr.pem | 16 - test/helpers/private-key.pem | 27 - test/helpers/public-cert.pem | 19 - test/helpers/public-key.pem | 9 - test/helpers/server.js | 53 - test/helpers/server_process.js | 9 - test/helpers/tls-cert.pem | 22 - test/helpers/tls-key.pem | 28 - test/helpers/wrong-cert.pem | 13 - test/helpers/wrong-csr.pem | 11 - test/helpers/wrong-key.pem | 15 - test/message-id-provider.js | 91 - test/mocha.opts | 4 - test/mqtt.js | 230 -- test/mqtt_store.js | 9 - test/secure_client.js | 188 - test/server.js | 94 - test/server_helpers_for_client_tests.js | 147 - test/store.js | 10 - .../broker-connect-subscribe-and-publish.ts | 26 - test/typescript/tsconfig.json | 14 - test/unique_message_id_provider_client.js | 21 - test/util.js | 15 - test/websocket_client.js | 191 - tsconfig.json | 35 + types/index.d.ts | 30 - types/lib/client-options.d.ts | 193 - types/lib/client.d.ts | 240 -- types/lib/connect/index.d.ts | 10 - types/lib/default-message-id-provider.d.ts | 49 - types/lib/message-id-provider.d.ts | 40 - types/lib/store-options.d.ts | 6 - types/lib/store.d.ts | 46 - types/lib/unique-message-id-provider.d.ts | 48 - 101 files changed, 4463 insertions(+), 10512 deletions(-) delete mode 100755 benchmarks/bombing.js delete mode 100755 benchmarks/throughputCounter.js delete mode 100644 lib/handlers/auth.js delete mode 100644 lib/index.js create mode 100644 review/mqtt.api.md rename lib/client.js => src/client.ts (53%) create mode 100644 src/connectionFactory/index.ts rename lib/default-message-id-provider.js => src/default-message-id-provider.ts (95%) rename lib/errors.js => src/errors.ts (96%) create mode 100644 src/handlers/auth.ts rename lib/handlers/connack.js => src/handlers/connack.ts (97%) rename lib/handlers/connect.js => src/handlers/connect.ts (96%) rename lib/handlers/disconnect.js => src/handlers/disconnect.ts (96%) rename lib/handlers/index.js => src/handlers/index.ts (96%) rename lib/handlers/ping.js => src/handlers/ping.ts (96%) rename lib/handlers/pingreq.js => src/handlers/pingreq.ts (95%) rename lib/handlers/pingresp.js => src/handlers/pingresp.ts (97%) rename lib/handlers/pub.js => src/handlers/pub.ts (96%) rename lib/handlers/puback.js => src/handlers/puback.ts (96%) rename lib/handlers/pubcomp.js => src/handlers/pubcomp.ts (96%) rename lib/handlers/pubrec.js => src/handlers/pubrec.ts (96%) rename lib/handlers/pubrel.js => src/handlers/pubrel.ts (96%) rename lib/handlers/sub.js => src/handlers/sub.ts (96%) rename lib/handlers/suback.js => src/handlers/suback.ts (96%) rename lib/handlers/unsub.js => src/handlers/unsub.ts (95%) rename lib/handlers/unsuback.js => src/handlers/unsuback.ts (96%) create mode 100644 src/index.ts rename lib/store.js => src/store.ts (94%) rename lib/unique-message-id-provider.js => src/unique-message-id-provider.ts (95%) rename lib/validateTopic.js => src/validateTopic.ts (96%) delete mode 100644 test/abstract_client.js delete mode 100644 test/abstract_store.js delete mode 100644 test/browser/server.js delete mode 100644 test/browser/test.js delete mode 100644 test/client.js delete mode 100644 test/client_mqtt5.js delete mode 100644 test/helpers/port_list.js delete mode 100644 test/helpers/private-csr.pem delete mode 100644 test/helpers/private-key.pem delete mode 100644 test/helpers/public-cert.pem delete mode 100644 test/helpers/public-key.pem delete mode 100644 test/helpers/server.js delete mode 100644 test/helpers/server_process.js delete mode 100644 test/helpers/tls-cert.pem delete mode 100644 test/helpers/tls-key.pem delete mode 100644 test/helpers/wrong-cert.pem delete mode 100644 test/helpers/wrong-csr.pem delete mode 100644 test/helpers/wrong-key.pem delete mode 100644 test/message-id-provider.js delete mode 100644 test/mocha.opts delete mode 100644 test/mqtt.js delete mode 100644 test/mqtt_store.js delete mode 100644 test/secure_client.js delete mode 100644 test/server.js delete mode 100644 test/server_helpers_for_client_tests.js delete mode 100644 test/store.js delete mode 100644 test/typescript/broker-connect-subscribe-and-publish.ts delete mode 100644 test/typescript/tsconfig.json delete mode 100644 test/unique_message_id_provider_client.js delete mode 100644 test/util.js delete mode 100644 test/websocket_client.js create mode 100644 tsconfig.json delete mode 100644 types/index.d.ts delete mode 100644 types/lib/client-options.d.ts delete mode 100644 types/lib/client.d.ts delete mode 100644 types/lib/connect/index.d.ts delete mode 100644 types/lib/default-message-id-provider.d.ts delete mode 100644 types/lib/message-id-provider.d.ts delete mode 100644 types/lib/store-options.d.ts delete mode 100644 types/lib/store.d.ts delete mode 100644 types/lib/unique-message-id-provider.d.ts diff --git a/.airtaprc.yml b/.airtaprc.yml index 77d341d98..69178bfcc 100644 --- a/.airtaprc.yml +++ b/.airtaprc.yml @@ -1,13 +1,13 @@ -sauce_connect: true -ui: mocha-bdd -browsers: - - name: chrome - version: latest - - name: firefox - version: latest - - name: opera - version: latest - - name: internet explorer - version: latest - - name: microsoftedge - version: latest +sauce_connect: true +ui: mocha-bdd +browsers: + - name: chrome + version: latest + - name: firefox + version: latest + - name: opera + version: latest + - name: internet explorer + version: latest + - name: microsoftedge + version: latest diff --git a/.editorconfig b/.editorconfig index 2c0e19c8e..ed933f020 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,9 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true -charset = utf-8 -indent_style = space -indent_size = 2 -trim_trailing_whitespace = true +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.eslintrc.json b/.eslintrc.json index ac3f85860..8d184236c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,16 +1,16 @@ -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": [ - "standard" - ], - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "rules": { - } -} +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "standard" + ], + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "rules": { + } +} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 561fe9c8f..20fcb68bf 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,32 +1,32 @@ -name: MQTT.js CI - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [12.x, 14.x] - fail-fast: false - - 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 - - run: npm test - env: - CI: true - DEBUG: "mqttjs*" +name: MQTT.js CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [12.x, 14.x] + fail-fast: false + + 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 + - run: npm test + env: + CI: true + DEBUG: "mqttjs*" diff --git a/.github/workflows/syncToDevOps.yml b/.github/workflows/syncToDevOps.yml index b3a1e7ad5..6fc0ad573 100644 --- a/.github/workflows/syncToDevOps.yml +++ b/.github/workflows/syncToDevOps.yml @@ -1,23 +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 + +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 diff --git a/.gitignore b/.gitignore index 5c315db7f..c9445e115 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,26 @@ -node_modules -certs/* -[._]*.s[a-w][a-z] -[._]s[a-w][a-z] -*.un~ -Session.vim -.netrwhist -*~ -npm-debug.log -dist/ -yarn.lock -coverage -.nyc_output -.idea/* -test/typescript/.idea/* -test/typescript/*.js -test/typescript/*.map -package-lock.json -# VS Code stuff -**/typings/** -**/.vscode/** +node_modules +certs/* +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ +npm-debug.log +dist/ +yarn.lock +coverage +.nyc_output +.idea/* +test/typescript/.idea/* +test/typescript/*.js +test/typescript/*.map +package-lock.json +# VS Code stuff +**/typings/** +**/.vscode/** + +*.log +.DS_Store +node_modules +dist diff --git a/.npmrc b/.npmrc index c1ca392fe..e69de29bb 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ -package-lock = false diff --git a/CHANGELOG.md b/CHANGELOG.md index 57736f28f..b2c47632f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,29 @@ -# Release History - -## 4.2.8 - -### PR - -Fix ws vulnerability and typescript bug (#1292) - -## 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 - +# Release History + +## 4.2.8 + +### PR + +Fix ws vulnerability and typescript bug (#1292) + +## 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/CONTRIBUTING.md b/CONTRIBUTING.md index cdaf73aba..910107398 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,27 +1,27 @@ -# MQTT.js is an OPEN Open Source Project - ------------------------------------------ - -## What? - -Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. - -## Rules - -There are a few basic ground-rules for contributors: - -1. **No `--force` pushes** or modifying the Git history in any way. -1. **Non-master branches** ought to be used for ongoing work. -1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. -1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. -1. Contributors should attempt to adhere to the prevailing code-style. - -## Releases - -Declaring formal releases remains the prerogative of the project maintainer. - -## Changes to this arrangement - -This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. - ------------------------------------------ +# MQTT.js is an OPEN Open Source Project + +----------------------------------------- + +## What? + +Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. + +## Rules + +There are a few basic ground-rules for contributors: + +1. **No `--force` pushes** or modifying the Git history in any way. +1. **Non-master branches** ought to be used for ongoing work. +1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. +1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. +1. Contributors should attempt to adhere to the prevailing code-style. + +## Releases + +Declaring formal releases remains the prerogative of the project maintainer. + +## Changes to this arrangement + +This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. + +----------------------------------------- diff --git a/LICENSE.md b/LICENSE.md index d23ded902..ed1a95942 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,15 +1,15 @@ -The MIT License (MIT) -===================== - -Copyright (c) 2015-2016 MQTT.js contributors ---------------------------------------- - -*MQTT.js contributors listed at * - -Copyright 2011-2014 by Adam Rudd - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The MIT License (MIT) +===================== + +Copyright (c) 2015-2016 MQTT.js contributors +--------------------------------------- + +*MQTT.js contributors listed at * + +Copyright 2011-2014 by Adam Rudd + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index b4497dc97..922cd261b 100644 --- a/README.md +++ b/README.md @@ -1,793 +1,793 @@ -![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. - -* [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) - - - -## 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. - - - - -## 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)*/} - ``` - * `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 ( - <> - {lastMessages.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. + +* [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) + + + +## 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. + + + + +## 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)*/} + ``` + * `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 ( + <> + {lastMessages.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 deleted file mode 100755 index adef01445..000000000 --- a/benchmarks/bombing.js +++ /dev/null @@ -1,26 +0,0 @@ -#! /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 deleted file mode 100755 index 0b778ef2c..000000000 --- a/benchmarks/throughputCounter.js +++ /dev/null @@ -1,22 +0,0 @@ -#! /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/doc/help.txt b/doc/help.txt index 55473e3db..4db69ecb3 100644 --- a/doc/help.txt +++ b/doc/help.txt @@ -1,8 +1,8 @@ -MQTT.js command line interface, available commands are: - - * publish publish a message to the broker - * subscribe subscribe for updates from the broker - * version the current MQTT.js version - * help help about commands - -Launch 'mqtt help [command]' to know more about the commands. +MQTT.js command line interface, available commands are: + + * publish publish a message to the broker + * subscribe subscribe for updates from the broker + * version the current MQTT.js version + * help help about commands + +Launch 'mqtt help [command]' to know more about the commands. diff --git a/doc/publish.txt b/doc/publish.txt index 583989800..9c693a8d2 100644 --- a/doc/publish.txt +++ b/doc/publish.txt @@ -1,26 +1,26 @@ -Usage: mqtt publish [opts] topic [message] - -Available options: - - -h/--hostname HOST the broker host - -p/--port PORT the broker port - -i/--client-id ID the client id - -q/--qos 0/1/2 the QoS of the message - -t/--topic TOPIC the message topic - -m/--message MSG the message body - -r/--retain send a retained message - -s/--stdin read the message body from stdin - -M/--multiline read lines from stdin as multiple messages - -u/--username USER the username - -P/--password PASS the password - -C/--protocol PROTO the protocol to use, 'mqtt', - 'mqtts', 'ws' or 'wss' - --key PATH path to the key file - --cert PATH path to the cert file - --ca PATH path to the ca certificate - --insecure do not verify the server certificate - --will-topic TOPIC the will topic - --will-payload BODY the will message - --will-qos 0/1/2 the will qos - --will-retain send a will retained message - -H/--help show this +Usage: mqtt publish [opts] topic [message] + +Available options: + + -h/--hostname HOST the broker host + -p/--port PORT the broker port + -i/--client-id ID the client id + -q/--qos 0/1/2 the QoS of the message + -t/--topic TOPIC the message topic + -m/--message MSG the message body + -r/--retain send a retained message + -s/--stdin read the message body from stdin + -M/--multiline read lines from stdin as multiple messages + -u/--username USER the username + -P/--password PASS the password + -C/--protocol PROTO the protocol to use, 'mqtt', + 'mqtts', 'ws' or 'wss' + --key PATH path to the key file + --cert PATH path to the cert file + --ca PATH path to the ca certificate + --insecure do not verify the server certificate + --will-topic TOPIC the will topic + --will-payload BODY the will message + --will-qos 0/1/2 the will qos + --will-retain send a will retained message + -H/--help show this diff --git a/doc/subscribe.txt b/doc/subscribe.txt index 7933cc3ef..f78b77912 100644 --- a/doc/subscribe.txt +++ b/doc/subscribe.txt @@ -1,26 +1,26 @@ -Usage: mqtt subscribe [opts] [topic] - -Available options: - - -h/--hostname HOST the broker host - -p/--port PORT the broker port - -i/--client-id ID the client id - -q/--qos 0/1/2 the QoS of the message - --no-clean do not discard any pending message for - the given id - -t/--topic TOPIC the message topic - -k/--keepalive SEC send a ping every SEC seconds - -u/--username USER the username - -P/--password PASS the password - -l/--protocol PROTO the protocol to use, 'mqtt', - 'mqtts', 'ws' or 'wss' - --key PATH path to the key file - --cert PATH path to the cert file - --ca PATH path to the ca certificate - --insecure do not verify the server certificate - --will-topic TOPIC the will topic - --will-message BODY the will message - --will-qos 0/1/2 the will qos - --will-retain send a will retained message - -v/--verbose print the topic before the message - -H/--help show this +Usage: mqtt subscribe [opts] [topic] + +Available options: + + -h/--hostname HOST the broker host + -p/--port PORT the broker port + -i/--client-id ID the client id + -q/--qos 0/1/2 the QoS of the message + --no-clean do not discard any pending message for + the given id + -t/--topic TOPIC the message topic + -k/--keepalive SEC send a ping every SEC seconds + -u/--username USER the username + -P/--password PASS the password + -l/--protocol PROTO the protocol to use, 'mqtt', + 'mqtts', 'ws' or 'wss' + --key PATH path to the key file + --cert PATH path to the cert file + --ca PATH path to the ca certificate + --insecure do not verify the server certificate + --will-topic TOPIC the will topic + --will-message BODY the will message + --will-qos 0/1/2 the will qos + --will-retain send a will retained message + -v/--verbose print the topic before the message + -H/--help show this 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/crt.ca.cg.pem b/examples/tls client/crt.ca.cg.pem index b59208084..f8df6a2b0 100644 --- a/examples/tls client/crt.ca.cg.pem +++ b/examples/tls client/crt.ca.cg.pem @@ -1,34 +1,34 @@ ------BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIJAOeJR1p1PU3qMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD -VQQGEwJFUzERMA8GA1UECAwIWmFyYWdvemExETAPBgNVBAcMCFphcmFnb3phMRkw -FwYDVQQKDBBNUVRUIGZvciBub2RlLmpzMRAwDgYDVQQLDAdNUVRULmpzMQ0wCwYD -VQQDDARtcXR0MRwwGgYJKoZIhvcNAQkBFg1mYWtlQG1haWwuY29tMB4XDTEzMDgz -MDEzMDIwNVoXDTIzMDgyODEzMDIwNVowgY0xCzAJBgNVBAYTAkVTMREwDwYDVQQI -DAhaYXJhZ296YTERMA8GA1UEBwwIWmFyYWdvemExGTAXBgNVBAoMEE1RVFQgZm9y -IG5vZGUuanMxEDAOBgNVBAsMB01RVFQuanMxDTALBgNVBAMMBG1xdHQxHDAaBgkq -hkiG9w0BCQEWDWZha2VAbWFpbC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw -ggIKAoICAQC7Of6OppOE+xwPdPcsT0w3keCa5k4ufZCqUAHex7+mLlrpjfCjQ2z6 -Rm0XBiCu9vy+xvLtbGDh5e/gocjAkkEywjbtrRMiFq5i41BNT3dzEWb9cCXvMWYa -RxQgIqouJUz5r+TbaP1bm4gAWTHmp09ccoIs9Tykxhyc1nZxXVrEsHF4aBmuw5NJ -ZwxK1tJTgP4m5H38Ms7ahGpByPsnMg6GBRs/Yen0mGhOsG+MU4TFiQb4bwIxg8Eu -ntGP1gARvtmyTkkTDhfksRs+muEV17uPtdhGNS/0CGRWaZ2mjEYyD70Ijl2grLd4 -6Vz27uPaqUvbgntPNadKqFN+jEHTtptou3k6V9C8CeLHIq+5N6abfPVHBzaqyNqg -QelzpSgQQBJ1H0CYREjzAs9uLfeep5ejW99Ik4YwtL6UrTVUyGzGgAl9mevZN5a4 -7mEY7MNUFdwigq0ZpbZmzYiuOURGYnoiy5o64balG5XH6Zh6B1WWhK7CArPVosz8 -eoQacj1WEM5d2Ivg1OLlEdD8FZDABv5CMTmRvnoFQuuIDzWVfrhdcZQ2tQuNLWrz -YDKheCunPkAIFOlGi70Xv3DVrTCr6kixwL2p9MHTzF4xiWWtiOv41ZXHTMG0t2I3 -YmA45FEO5JawebPgUoGhoc2vgIw5Jo9dcGtwLCqBHSnCojPoTipVhQIDAQABo1Aw -TjAdBgNVHQ4EFgQU1yVv/ezoDLs+qjbx0O4KiHpC41swHwYDVR0jBBgwFoAU1yVv -/ezoDLs+qjbx0O4KiHpC41swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOC -AgEAScnfOewEk59DgfICJJ2vhcI33wqqn54zhJ1pi8SX3e7PLv26UEUXZaddIqeZ -JzA/IWF+GCBQFAL7Z+sI4djXx/UpZp5ptCQBFc0tinHk1CGlC0E+LI3JS/cnFf+2 -L8VKZHbSf4ua2f/VMJo7uoyrw/gQHgUToAlYYWpGcIKKm7d0JYQE60wlHk9TXgCc -s9XAwI+bP9VKNQkZCeooODG/5VcxdJafZSU3rW1WniFcD/R+ZNq7FZYbM+2u2mRt -Qm7Hh/FjrN4Hnmf3xdNUE0NLHznwk4CD6EeQukN12yP2ccubnG6Z7HFFdV0g9fEP -AVMsgY/9E9Te/BBoQKjhIg8c274ozIOsCHODx15Mn52848sq0LIQjyeOH4rtuWLL -1dFE1ysY2gzSMUtrP+on+r6F1GkndFszxfDrBcZMXs85VAy3eKfY/jzUMrdfn0YJ -36Wz7F40vnOUd2ni24kaOfnRodbu3lOEYD6l5fDGP79kfITyy+dtL6ExTLZQmEn+ -xKsWM9bBkV4STpFiTF61tJwzlcAL1ZDLqDaSwsM8UDZopnDgvklNoJK9XzdLwD1X -PofOtUe08G4tq5cBDVURLKif+7EfCyAqvUptQ3MJarhoXzhDy9CjtN8TmWexKC1q -kB5DBML0Y4NnqTEnfYCs/XFPosaS+0GximGySJcg08ay6ZA= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIJAOeJR1p1PU3qMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD +VQQGEwJFUzERMA8GA1UECAwIWmFyYWdvemExETAPBgNVBAcMCFphcmFnb3phMRkw +FwYDVQQKDBBNUVRUIGZvciBub2RlLmpzMRAwDgYDVQQLDAdNUVRULmpzMQ0wCwYD +VQQDDARtcXR0MRwwGgYJKoZIhvcNAQkBFg1mYWtlQG1haWwuY29tMB4XDTEzMDgz +MDEzMDIwNVoXDTIzMDgyODEzMDIwNVowgY0xCzAJBgNVBAYTAkVTMREwDwYDVQQI +DAhaYXJhZ296YTERMA8GA1UEBwwIWmFyYWdvemExGTAXBgNVBAoMEE1RVFQgZm9y +IG5vZGUuanMxEDAOBgNVBAsMB01RVFQuanMxDTALBgNVBAMMBG1xdHQxHDAaBgkq +hkiG9w0BCQEWDWZha2VAbWFpbC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC7Of6OppOE+xwPdPcsT0w3keCa5k4ufZCqUAHex7+mLlrpjfCjQ2z6 +Rm0XBiCu9vy+xvLtbGDh5e/gocjAkkEywjbtrRMiFq5i41BNT3dzEWb9cCXvMWYa +RxQgIqouJUz5r+TbaP1bm4gAWTHmp09ccoIs9Tykxhyc1nZxXVrEsHF4aBmuw5NJ +ZwxK1tJTgP4m5H38Ms7ahGpByPsnMg6GBRs/Yen0mGhOsG+MU4TFiQb4bwIxg8Eu +ntGP1gARvtmyTkkTDhfksRs+muEV17uPtdhGNS/0CGRWaZ2mjEYyD70Ijl2grLd4 +6Vz27uPaqUvbgntPNadKqFN+jEHTtptou3k6V9C8CeLHIq+5N6abfPVHBzaqyNqg +QelzpSgQQBJ1H0CYREjzAs9uLfeep5ejW99Ik4YwtL6UrTVUyGzGgAl9mevZN5a4 +7mEY7MNUFdwigq0ZpbZmzYiuOURGYnoiy5o64balG5XH6Zh6B1WWhK7CArPVosz8 +eoQacj1WEM5d2Ivg1OLlEdD8FZDABv5CMTmRvnoFQuuIDzWVfrhdcZQ2tQuNLWrz +YDKheCunPkAIFOlGi70Xv3DVrTCr6kixwL2p9MHTzF4xiWWtiOv41ZXHTMG0t2I3 +YmA45FEO5JawebPgUoGhoc2vgIw5Jo9dcGtwLCqBHSnCojPoTipVhQIDAQABo1Aw +TjAdBgNVHQ4EFgQU1yVv/ezoDLs+qjbx0O4KiHpC41swHwYDVR0jBBgwFoAU1yVv +/ezoDLs+qjbx0O4KiHpC41swDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOC +AgEAScnfOewEk59DgfICJJ2vhcI33wqqn54zhJ1pi8SX3e7PLv26UEUXZaddIqeZ +JzA/IWF+GCBQFAL7Z+sI4djXx/UpZp5ptCQBFc0tinHk1CGlC0E+LI3JS/cnFf+2 +L8VKZHbSf4ua2f/VMJo7uoyrw/gQHgUToAlYYWpGcIKKm7d0JYQE60wlHk9TXgCc +s9XAwI+bP9VKNQkZCeooODG/5VcxdJafZSU3rW1WniFcD/R+ZNq7FZYbM+2u2mRt +Qm7Hh/FjrN4Hnmf3xdNUE0NLHznwk4CD6EeQukN12yP2ccubnG6Z7HFFdV0g9fEP +AVMsgY/9E9Te/BBoQKjhIg8c274ozIOsCHODx15Mn52848sq0LIQjyeOH4rtuWLL +1dFE1ysY2gzSMUtrP+on+r6F1GkndFszxfDrBcZMXs85VAy3eKfY/jzUMrdfn0YJ +36Wz7F40vnOUd2ni24kaOfnRodbu3lOEYD6l5fDGP79kfITyy+dtL6ExTLZQmEn+ +xKsWM9bBkV4STpFiTF61tJwzlcAL1ZDLqDaSwsM8UDZopnDgvklNoJK9XzdLwD1X +PofOtUe08G4tq5cBDVURLKif+7EfCyAqvUptQ3MJarhoXzhDy9CjtN8TmWexKC1q +kB5DBML0Y4NnqTEnfYCs/XFPosaS+0GximGySJcg08ay6ZA= +-----END CERTIFICATE----- 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/tls client/tls-cert.pem b/examples/tls client/tls-cert.pem index ee7458d44..7b3d1df82 100644 --- a/examples/tls client/tls-cert.pem +++ b/examples/tls client/tls-cert.pem @@ -1,13 +1,13 @@ ------BEGIN CERTIFICATE----- -MIICATCCAWoCCQC2pNY4sfld/jANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTEzMDgyNzEyNTU0NVoXDTEzMDkyNjEyNTU0NVowRTELMAkG -A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzXGU -1mZUBqLwoP1fWkiZeypiKgWICUdNm+d2JHXnpQMEVBxSvsaRGOnzWVvgbMVxmD7n -5/p9qQGTj8FY/+t2NHpbt1I9lGV0+BlZxGJvyvHikEAXPD85EEFhqSbDwgkVuMqa -w08njqhJJ37fbd2ux6w4woRrDTN4r9CNMhFb9QECAwEAATANBgkqhkiG9w0BAQUF -AAOBgQBIlZYo1rf8GlISuV1haSBm8U/uiyjIX/pTE5Cs7Kb84SPzKB0tHnGGCa2t -Lu+TEwetF3NatuI1biqYuevQSfmEM75zsRSwt1P40sJ2y9B1XRTdamHOHCYCJG/b -rti7WJYjvO8JsCUeB6M+5jFodbmvjsGgAHLLUINXrxOqYe+PWg== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICATCCAWoCCQC2pNY4sfld/jANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTEzMDgyNzEyNTU0NVoXDTEzMDkyNjEyNTU0NVowRTELMAkG +A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzXGU +1mZUBqLwoP1fWkiZeypiKgWICUdNm+d2JHXnpQMEVBxSvsaRGOnzWVvgbMVxmD7n +5/p9qQGTj8FY/+t2NHpbt1I9lGV0+BlZxGJvyvHikEAXPD85EEFhqSbDwgkVuMqa +w08njqhJJ37fbd2ux6w4woRrDTN4r9CNMhFb9QECAwEAATANBgkqhkiG9w0BAQUF +AAOBgQBIlZYo1rf8GlISuV1haSBm8U/uiyjIX/pTE5Cs7Kb84SPzKB0tHnGGCa2t +Lu+TEwetF3NatuI1biqYuevQSfmEM75zsRSwt1P40sJ2y9B1XRTdamHOHCYCJG/b +rti7WJYjvO8JsCUeB6M+5jFodbmvjsGgAHLLUINXrxOqYe+PWg== +-----END CERTIFICATE----- diff --git a/examples/tls client/tls-key.pem b/examples/tls client/tls-key.pem index bd852d5db..39efcc5c1 100644 --- a/examples/tls client/tls-key.pem +++ b/examples/tls client/tls-key.pem @@ -1,15 +1,15 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDNcZTWZlQGovCg/V9aSJl7KmIqBYgJR02b53YkdeelAwRUHFK+ -xpEY6fNZW+BsxXGYPufn+n2pAZOPwVj/63Y0elu3Uj2UZXT4GVnEYm/K8eKQQBc8 -PzkQQWGpJsPCCRW4yprDTyeOqEknft9t3a7HrDjChGsNM3iv0I0yEVv1AQIDAQAB -AoGBALv9P+WEE0VTWf7mepdBsXfbi6HKF/Xtkh2kCh5I6WO8Q/y3Qhwh1OnIQg41 -nUHK1iwq+8fxFYVN1PoJQWhEzI6JdBCrn88oADo/aVm1mGN5CWr3pwn92SAVMhbw -442AWWG81RStrr2uPhLBNE6U/4P835qM8uG4rCP+5Z5SzX7VAkEA+TptuSc0TEkL -5B/Nml2fYNfbQvRGVzyCbdCXdgkeZt5xuSuwDgC4GvWgjL+SAN1fjTek/Iez5NnL -xHa5w93j2wJBANMGmRTaTxvpGdkUi/utTPtCp6GXL7hS9v41LClmQTYBOYscPn2b -Dny2fyZPp29sZ7+AvXHWZxw7QtH+jO2Xz1MCQCI7vlqSYgKgffulyq4LchrxS3LU -7tyIuTmwTz2tXvmuUFyo/ZPO0XsShi0PG1T3E2roW8c8NJ+Ysv6XeEjJL8UCQG0Z -/S0tzTa15no4SEM/jwxcosRFoRNgOXimTwW8azybl3+Xg6t27h+GTuikyAEwf9cf -nVJssfSDowFk5MG1+icCQQCqBOTXEukcJRXZixkpfEuuvS3RNzOYwG4ReKjpvWPy -EvsfHoCsO1Sz9qz8DXpwl3GEWUGGTfWwBfereX6HLXj+ ------END RSA PRIVATE KEY----- +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDNcZTWZlQGovCg/V9aSJl7KmIqBYgJR02b53YkdeelAwRUHFK+ +xpEY6fNZW+BsxXGYPufn+n2pAZOPwVj/63Y0elu3Uj2UZXT4GVnEYm/K8eKQQBc8 +PzkQQWGpJsPCCRW4yprDTyeOqEknft9t3a7HrDjChGsNM3iv0I0yEVv1AQIDAQAB +AoGBALv9P+WEE0VTWf7mepdBsXfbi6HKF/Xtkh2kCh5I6WO8Q/y3Qhwh1OnIQg41 +nUHK1iwq+8fxFYVN1PoJQWhEzI6JdBCrn88oADo/aVm1mGN5CWr3pwn92SAVMhbw +442AWWG81RStrr2uPhLBNE6U/4P835qM8uG4rCP+5Z5SzX7VAkEA+TptuSc0TEkL +5B/Nml2fYNfbQvRGVzyCbdCXdgkeZt5xuSuwDgC4GvWgjL+SAN1fjTek/Iez5NnL +xHa5w93j2wJBANMGmRTaTxvpGdkUi/utTPtCp6GXL7hS9v41LClmQTYBOYscPn2b +Dny2fyZPp29sZ7+AvXHWZxw7QtH+jO2Xz1MCQCI7vlqSYgKgffulyq4LchrxS3LU +7tyIuTmwTz2tXvmuUFyo/ZPO0XsShi0PG1T3E2roW8c8NJ+Ysv6XeEjJL8UCQG0Z +/S0tzTa15no4SEM/jwxcosRFoRNgOXimTwW8azybl3+Xg6t27h+GTuikyAEwf9cf +nVJssfSDowFk5MG1+icCQQCqBOTXEukcJRXZixkpfEuuvS3RNzOYwG4ReKjpvWPy +EvsfHoCsO1Sz9qz8DXpwl3GEWUGGTfWwBfereX6HLXj+ +-----END RSA PRIVATE KEY----- diff --git a/examples/ws/aedes_server.js b/examples/ws/aedes_server.js index e29032ff4..4b8196b17 100644 --- a/examples/ws/aedes_server.js +++ b/examples/ws/aedes_server.js @@ -1,42 +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) -}) +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/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/handlers/auth.js b/lib/handlers/auth.js deleted file mode 100644 index 25176c9da..000000000 --- a/lib/handlers/auth.js +++ /dev/null @@ -1,3 +0,0 @@ -export async function handleAuth () { - return true -} diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 521677e73..000000000 --- a/lib/index.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2015-2015 MQTT.js contributors. - * Copyright (c) 2011-2014 Adam Rudd. - * - * See LICENSE for more information - */ - -import { connect } from './client' -import { Store } from './store' -import { DefaultMessageIdProvider } from './default-message-id-provider' -import { UniqueMessageIdProvider } from './unique-message-id-provider' - - -/** - * connect() - * Connect will: - * 1) Validate the options provided by the user. - * 2) Instantiate a new client. - * 3) Return the client to the user. - */ -function connect (options) { - _validateProtocol(opts) -} - -function _validateProtocol(opts) { - 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 - // I don't like that we are manipulating the opts object here. - 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] - } -} - -export {connect, Store, DefaultMessageIdProvider, UniqueMessageIdProvider} diff --git a/package.json b/package.json index 3f2ff2f24..3a4e3ac79 100644 --- a/package.json +++ b/package.json @@ -1,105 +1,111 @@ -{ - "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", - "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", - "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 test && codecov" - }, - "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": { - "commist": "^2.0.0", - "concat-stream": "^2.0.0", - "debug": "^4.3.2", - "duplexify": "^4.1.2", - "help-me": "^3.0.0", - "inherits": "^2.0.4", - "minimist": "^1.2.5", - "mqtt-packet": "^7.0.0", - "number-allocator": "^1.0.7", - "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "reinterval": "^1.1.0", - "split2": "^3.2.2", - "ws": "^8.2.0", - "xtend": "^4.0.2" - }, - "devDependencies": { - "@types/node": "^16.7.1", - "aedes": "^0.46.1", - "airtap": "^4.0.3", - "browserify": "^17.0.0", - "chai": "^4.3.4", - "codecov": "^3.8.3", - "end-of-stream": "^1.4.4", - "eslint": "^7.32.0", - "eslint-config-standard": "^16.0.3", - "eslint-plugin-import": "^2.24.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.0", - "global": "^4.4.0", - "mkdirp": "^1.0.4", - "mocha": "^9.1.0", - "mqtt-connection": "^4.1.0", - "nyc": "^15.1.0", - "rimraf": "^3.0.2", - "should": "^13.2.3", - "sinon": "^11.1.2", - "snazzy": "^9.0.0", - "standard": "^16.0.3", - "tslint": "^5.20.1", - "tslint-config-standard": "^8.0.1", - "typescript": "^4.3.5", - "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": { + "start": "tsdx watch", + "build": "tsdx build", + "test": "tsdx test", + "lint": "tsdx lint", + "prepare": "tsdx build", + "size": "size-limit", + "analyze": "size-limit --why", + "test2": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", + "pretest": "standard | snazzy", + "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", + "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 test2 && codecov" + }, + "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": { + "commist": "^2.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.3.2", + "duplexify": "^4.1.2", + "help-me": "^3.0.0", + "inherits": "^2.0.4", + "minimist": "^1.2.5", + "mqtt-packet": "^7.0.0", + "number-allocator": "^1.0.7", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "split2": "^3.2.2", + "ws": "^8.2.0", + "xtend": "^4.0.2" + }, + "devDependencies": { + "@types/node": "^16.7.1", + "aedes": "^0.46.1", + "airtap": "^4.0.3", + "browserify": "^17.0.0", + "chai": "^4.3.4", + "codecov": "^3.8.3", + "end-of-stream": "^1.4.4", + "eslint": "^7.32.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.24.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "global": "^4.4.0", + "mkdirp": "^1.0.4", + "mocha": "^9.1.0", + "mqtt-connection": "^4.1.0", + "nyc": "^15.1.0", + "rimraf": "^3.0.2", + "should": "^13.2.3", + "sinon": "^11.1.2", + "snazzy": "^9.0.0", + "standard": "^16.0.3", + "tsdx": "^0.14.1", + "typescript": "^4.4.3", + "uglify-es": "^3.3.9" + }, + "standard": { + "env": [ + "mocha" + ] + } +} diff --git a/review/mqtt.api.md b/review/mqtt.api.md new file mode 100644 index 000000000..1e7b9ee08 --- /dev/null +++ b/review/mqtt.api.md @@ -0,0 +1,77 @@ +## API Report File for "mqtt" + +```ts + +import { OperationOptions } from '@azure/core-client'; + +export function connect(options: connectOptions): MqttClient + +export interface connectOptions { + wsOptions: BestGuessWebsocketOptions // Websockets + keepalive: number, + reschedulePings: boolean, + clientId: string + protocolId: string // Is this just MQTT or MQTTv3 + protocolVersion: number // There's only a limited number of protocols supported + reconnectPeriod: number, + connectTimeout: number, + username: string, + password: string, + incomingStore: Store, + outgoingStore: Store, + queueQoSZero: boolean, + customHandleAcks: (topic: string, message: unknown, packet: unknown): Promise<{}> + properties: { + sessionExpiryInterval: number, + receiveMaximum: number, + maximumPacketSize: number, + topicAliasMaximum: number, + requestResponseInformation: boolean, + requestProblemInformation: boolean, + userProperties: unknown, + authenticationMethod: string, + authenticationData: binary + } + authPacket: unknown, + will: { + topic: string, + payload: unknown, + qos: number, + retain: unknown, + properties: { + willDelayInterval: number, + payloadFormatIndicator: boolean, + messageExpiryInterval: number, + contentType: string, + responseTopic: string, + correlationData: binary, + userProperties: unknown + } + }, + transformWsUrl: (url, options, client) => string + resubscribe: boolean, + messageIdProvider: unknown +} + +// @public +export interface MqttClient { + connected: boolean; + reconnecting: boolean; + async publish(topic: string, message: buffer | string, options?: publishOptions): Promise<{err ?: Error}> + subscribe(topic: string | Array | TopicMap, options ?: subscribeOptions): Promise<{err: Error, granted: Array}> + unsubscribe(topic: string | Array, options?: unsubscribeOptions): Promise<{err: Error}> + end(force?: boolean, options?: disconnectOptions): Promise<{}> + removeOutgoingMessage(mId: number): void + reconnect(): void + handleMessage(): Promise<{}> + getLastMessageId(): number +} + +// @public +export interface MqttStore { + put(packet: MqttPacket): Promise<{void}> + createStream(): Stream + del(packet: MqttPacket): Promise<{void}> + close(): Promise<{void}> +} +``` diff --git a/lib/client.js b/src/client.ts similarity index 53% rename from lib/client.js rename to src/client.ts index ba7c91339..588de0694 100644 --- a/lib/client.js +++ b/src/client.ts @@ -1,699 +1,678 @@ -'use strict' - -import { mqtt } from 'mqtt-packet' -import { handle } from 'handlers' -import net from 'net' -import WebSocket, { createWebSocketStream } from 'ws' - -// const eventEmitter = require('events') -// const mqttErrors = require('errors') - -const logger = require('pino')() - -export class Client { - constructor (options) { - this.closed = false - this.connecting = false - this.connected = false - this.errored = false - this.id = null - this.clean = true - this.version = null - // eslint-disable-next-line camelcase - // TODO: _isBrowser should be a global value and should be standardized.... - this._isBrowser = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' - - // Connect Information - this.protocol = null - this.port = null - this.hostname = null - this.rejectUnauthorized = null - - this.stream = this._streamBuilder() - - this._reconnectCount = 0 - - this._disconnected = false - this._authorized = false - this._parser = mqtt.parser() - this._defaultConnectOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - resubscribe: true - } - - this._options = options || { ...this._defaultConnectOptions } - this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` - this._parser._queue = [] - this._parser.on('packet', this.enqueue) - this.once('connected', this.dequeue) - this.on('connect', this._sendQueuedPackets()) - this.on('close', this._closeClient()) - } - - _streamBuilder (opts) { - switch (this.protocol) { - case 'tcp': { - return net.createConnection(port, host) - } - case 'tls': { - // TODO: This needs to have options passed down to it. - // We should probably have the whole options object just - // passed down to tls.connect, right? - - function handleTLSErrors () { - // 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 = tls.connect(opts) - connection.on('secureConnect', function () { - if (opts.rejectUnauthorized && !connection.authorized) { - connection.emit('error', new Error('TLS not authorized')) - } else { - connection.removeListener('error', handleTLSErrors) - } - }) - - connection.on('error', handleTLSErrors) - return connection - } - case 'ws': { - if (this._isBrowser) { - this._buildWebSocketStreamBrowser(opts) - } else { - this._buildWebSocketStream(opts) - } - } - } - } - - // To consider : do we want to have this in the main code, - // or do we want to have a browser shim? - _buildWebSocketStreamBrowser (opts) { - const 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] - } - }) - } - - 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) - } - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - const url = buildUrl(opts, client) - /* global WebSocket */ - const socket = new WebSocket(url, [websocketSubProtocol]) - socket.binaryType = 'arraybuffer' - logger('browserStreamBuilder') - let stream - // sets the maximum socket buffer size before throttling - const bufferSize = options.browserBufferSize || 1024 * 512 - - const bufferTimeout = opts.browserBufferTimeout || 1000 - - const coerceToBuffer = !opts.objectMode - - const 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) { - const 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 - } - - _buildWebSocketStream (opts) { - const 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] - } - }) - } - const url = buildUrl(options, client) - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) - const webSocketStream = createWebSocketStream(socket, options.wsOptions) - webSocketStream.url = url - socket.on('close', () => { webSocketStream.destroy() }) - return webSocketStream - } - - async enqueue (packet) { - this._parsingBatch++ - // already connected or it's the first packet - if (this.connackSent || this._parsingBatch === 1) { - await handle(this, packet, this._nextBatch) - } else { - if (this._parser._queue.length < this._queueLimit) { - this._parser._queue.push(packet) - } else { - this.emit('error', new Error('Client queue limit reached')) - } - } - } - - async dequeue () { - const q = this._parser._queue - if (q) { - for (let i = 0, len = q.length; i < len; i++) { - await handle(this, q[i], this._nextBatch) - } - - this._parser._queue = null - } - } - - _closeClient () { - logger('close :: connected set to `false`') - this.connected = false - - logger('close :: clearing connackTimer') - clearTimeout(this.connackTimer) - - logger('close :: clearing ping timer') - if (that.pingTimer !== null) { - that.pingTimer.clear() - that.pingTimer = null - } - - logger('close :: calling _setupReconnect') - this._setupReconnect() - } - - _sendQueuedPackets () { - const queue = this.queue - - function deliver () { - const entry = queue.shift() - logger('deliver :: entry %o', entry) - let packet = null - - if (!entry) { - return - } - - packet = entry.packet - logger('deliver :: call _sendPacket for %o', packet) - let 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 - } - } - } - if (send) { - that._sendPacket( - packet, - function (err) { - if (entry.cb) { - entry.cb(err) - } - deliver() - } - ) - } else { - logger('messageId: %d has already used.', packet.messageId) - deliver() - } - } - - deliver() - } - - async publish (topic, message, opts) { - const result = await handle.publish(this, message) - return result - } - - async subscribe (packet) { - if (!packet.subscriptions) { - packet = { subscriptions: Array.isArray(packet) ? packet : [packet] } - } - const result = await handle.subscribe(this, packet) - return result - } - - async unsubscribe (packet) { - const result = await handle.unsubscribe(this, packet) - return result - } - - async end (force, opts) { - const that = this - - logger('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 - } - - logger('end :: cb? %s', !!cb) - cb = cb || nop - - function closeStores () { - logger('end :: closeStores: closing incoming and outgoing stores') - that.disconnected = true - that.incomingStore.close(function (e1) { - that.outgoingStore.close(function (e2) { - logger('end :: closeStores: emitting end') - that.emit('end') - if (cb) { - const err = e1 || e2 - logger('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 - logger('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) - that._cleanUp(force, () => { - logger('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 - logger('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) - this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) - } else { - logger('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()); - */ - removeOutgoingMessage (messageId) { - const 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 (opts) { - logger('client reconnect') - const that = this - const 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 () { - logger('_reconnect: emitting reconnect to client') - this.emit('reconnect') - if (this.connected) { - this.end(() => { this._setupStream() }) - logger('client already connected. disconnecting first.') - } else { - logger('_reconnect: calling _setupStream') - this._setupStream() - } - } - - _setupReconnect () { - if (!this.disconnecting && !this.reconnectTimer && (this.options.reconnectPeriod > 0)) { - if (!this.reconnecting) { - logger('_setupReconnect :: emit `offline` state') - this.emit('offline') - logger('_setupReconnect :: set `reconnecting` to `true`') - this.reconnecting = true - } - logger('_setupReconnect :: setting reconnectTimer for %d ms', this.options.reconnectPeriod) - this.reconnectTimer = setInterval(() => { - logger('reconnectTimer :: reconnect triggered!') - this._reconnect() - }, this.options.reconnectPeriod) - } else { - logger('_setupReconnect :: doing nothing...') - } - } - - _clearReconnect () { - logger('_clearReconnect : clearing reconnect timer') - if (this.reconnectTimer) { - clearInterval(this.reconnectTimer) - this.reconnectTimer = null - } - } - - async _cleanUp (forced) { - const opts = arguments[2] - if (done) { - logger('_cleanUp :: done callback provided for on stream close') - this.stream.on('close', done) - } - - logger('_cleanUp :: forced? %s', forced) - if (forced) { - if ((this.options.reconnectPeriod === 0) && this.options.clean) { - flush(this.outgoing) - } - logger('_cleanUp :: (%s) :: destroying stream', this.options.clientId) - this.stream.destroy() - } else { - const packet = xtend({ cmd: 'disconnect' }, opts) - logger('_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) { - logger('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') - this._clearReconnect() - this._setupReconnect() - } - - if (this.pingTimer !== null) { - logger('_cleanUp :: clearing pingTimer') - this.pingTimer.clear() - this.pingTimer = null - } - - if (done && !this.connected) { - logger('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) - this.stream.removeListener('close', done) - done() - } - } - - async _sendPacket (packet) { - logger('_sendPacket :: (%s) :: start', this.options.clientId) - cbStorePut = cbStorePut || nop - - if (!this.connected) { - logger('_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 - } - } - - _storePacket (packet) { - 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) { - cb = this.outgoing[packet.messageId] ? this.outgoing[packet.messageId].cb : null - this.outgoingStore.put(packet, function (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - }) - } else if (cb) { - cb(new Error('No connection to broker')) - } - } -} +'use strict' + +import * as mqtt from 'mqtt-packet' +import { handle } from './handlers' +import { ConnectOptions } from '.' +import { EventEmitter } from 'stream' +import { connectionFactory } from './connectionFactory' + +// const eventEmitter = require('events') +// const mqttErrors = require('errors') + +const logger = require('pino')() + +export class MqttClient extends EventEmitter { + static isBrowser: boolean // This can be the global check for browser compatibility. + closed: boolean + connecting: boolean + connected: boolean + errored: boolean + id: any + clean: boolean + version: any + _isBrowser: boolean + protocol: any + port: any + hostname: any + rejectUnauthorized: any + stream: any + _reconnectCount: number + _disconnected: boolean + _authorized: boolean + _parser: mqtt.Parser + _defaultConnectOptions: { keepalive: number; reschedulePings: boolean; protocolId: string; protocolVersion: number; reconnectPeriod: number; connectTimeout: number; clean: boolean; resubscribe: boolean } + _options: any + _parsingBatch: any + connackSent: boolean + _queueLimit: any + queue: any + options: any + disconnected: boolean + incomingStore: any + outgoingStore: any + _deferredReconnect: any + disconnecting: any + reconnectTimer: any + reconnecting: any + pingTimer: any + queueQoSZero: boolean + _port: any + _host: any + tlsOptions: any + wsOptions: any + brokerUrl: URL + keepalive: any + reschedulePings: any + clientId: any + protocolId: any + protocolVersion: any + reconnectPeriod: any + connectTimeout: any + username: any + password: any + customHandleAcks: any + properties: { sessionExpiryInterval: number; receiveMaximum: number; maximumPacketSize: number; topicAliasMaximum: number; requestResponseInformation: boolean; requestProblemInformation: boolean; userPropertis: any; authenticationMethod: string; authenticationData: BinaryData } + authPacket: any + will: { + topic: any; payload: any; qos: number; retain: boolean; properties: { + willDelayInterval: number; payloadFormatIndicator: boolean; messageExpiryInterval: number // eslint-disable-next-line camelcase + // eslint-disable-next-line camelcase + // TODO: _isBrowser should be a global value and should be standardized.... + // Connect Information + contentType: string; responseTopic: string; correlationData: BinaryData; userProperties: any + } + } + transformWsUrl: (opts: any) => URL + resubscribe: boolean + messageIdProvider: any + parserQueue: any[] + private _paused: any + private _eos: any + + constructor (options: ConnectOptions) { + super() + // assume that the options have been validated before instantiating the client. + this.closed = false + this.connecting = false + this.connected = false + this.errored = false + this.id = null + this.clean = true + this.version = null + this.parserQueue = [] + // eslint-disable-next-line camelcase + // TODO: _isBrowser should be a global value and should be standardized.... + + // Using this method to clean up the constructor to do options handling + this._injestOptions(options) + + // NOTE: STOP USING OPTIONS PAST THIS POINT + // buildStream shouldn't rely on the options object. Let's have the option object used up beforehand and then + // essentially discarded, so after this point it is never used again and only class fields are referenced. + this.stream = options.customStreamFactory? options.customStreamFactory(options) : connectionFactory(options) + + this._reconnectCount = 0 + + this._disconnected = false + this._authorized = false + this._parser = mqtt.parser() + this._defaultConnectOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true + } + + this._options = options || { ...this._defaultConnectOptions } + this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` + this._parser.on('packet', this.enqueue) + this.once('connected', this.dequeue) + this.on('close', this._closeClient) + + this.stream.on('readable', this.nextBatch) + + this.on('error', this.onError) + this.stream.on('error', this.emit.bind(this, 'error')) + this._parser.on('error', this.emit.bind(this, 'error')) + + this.stream.on('end', this.close.bind(this)) + this._eos = eos(this.stream, this.close.bind(this)) + } + + close (done) { + if (this.closed) { + if (typeof done === 'function') { + done() + } + return + } + + this.closed = true + + this._parser.removeAllListeners('packet') + conn.removeAllListeners('readable') + + this._parser._queue = null + + if (this._keepaliveTimer) { + this._keepaliveTimer.clear() + this._keepaliveInterval = -1 + this._keepaliveTimer = null + } + + if (this._connectTimer) { + clearTimeout(this._connectTimer) + this._connectTimer = null + } + + this._eos() + this._eos = noop + + handleUnsubscribe( + this, + { + unsubscriptions: Object.keys(this.subscriptions) + }, + finish) + + function finish () { + const will = that.will + // _disconnected is set only if client is disconnected with a valid disconnect packet + if (!that._disconnected && will) { + that.broker.authorizePublish(that, will, function (err) { + if (err) { return done() } + that.broker.publish(will, that, done) + + function done () { + that.broker.persistence.delWill({ + id: that.id, + brokerId: that.broker.id + }, noop) + } + }) + } + that.will = null // this function might be called twice + that._will = null + + that.connected = false + that.connecting = false + + conn.removeAllListeners('error') + conn.on('error', noop) + + if (that.broker.clients[that.id] && that._authorized) { + that.broker.unregisterClient(that) + } + + // clear up the drain event listeners + that.conn.emit('drain') + that.conn.removeAllListeners('drain') + + conn.destroy() + + if (typeof done === 'function') { + done() + } + } + } + + onError (err) { + if (!err) return + + this.errored = true + this.stream.removeAllListeners('error') + this.stream.on('error', noop) + // hack to clean up the write callbacks in case of error + const state = this.conn._writableState + const list = typeof state.getBuffer === 'function' ? state.getBuffer() : state.buffer + list.forEach(drainRequest) + this.broker.emit(this.id ? 'clientError' : 'connectionError', this, err) + this.close() + } + + _injestOptions(options: ConnectOptions) { + // Connect Information + this.brokerUrl = options.brokerUrl as URL + this.wsOptions = options.wsOptions + this.tlsOptions = options.tlsOptions + this.keepalive = options.keepalive + this.reschedulePings = options.reschedulePings + this.clientId = options.clientId + this.protocolId = options.protocolId + this.protocolVersion = options.protocolVersion + this.clean = options.clean + this.reconnectPeriod = options.reconnectPeriod + this.connectTimeout = options.connectTimeout + this.username = options.username + this.password = options.password + this.incomingStore = options.incomingStore + this.outgoingStore = options.outgoingStore + this.queueQoSZero = options.queueQoSZero + this.customHandleAcks = options.customHandleAcks + this.properties = options.properties + this.authPacket = options.authPacket + this.will = options.will + this.transformWsUrl = options.transformWsUrl + this.resubscribe = options.resubscribe + this.messageIdProvider = options.messageIdProvider + } + + async enqueue (packet) { + this._parsingBatch++ + // already connected or it's the first packet + if (this.connackSent || this._parsingBatch === 1) { + const result = await handle(this, packet) + this.nextBatch(result) + } else { + if (this.parserQueue.length < this._queueLimit) { + this.parserQueue.push(packet) + } else { + this.emit('error', new Error('Client queue limit reached')) + } + } + } + + async dequeue () { + const q = this.parserQueue + for (let i = 0, len = q.length; i < len; i++) { + const result = await handle(this, q[i]) + this.nextBatch(result) + } + } + + nextBatch (err) { + if (err) { + this.emit('error', err) + return + } + + if (this._paused) { + return + } + + this._parsingBatch-- + if (this._parsingBatch <= 0) { + this._parsingBatch = 0 + const buf = this.stream.read(null) + if (buf) { + this._parser.parse(buf) + } + } + } + + + + + deliver0 = function deliverQoS0 (_packet, cb) { + const toForward = dedupe(this, _packet) && + this.broker.authorizeForward(this, _packet) + if (toForward) { + // Give nodejs some time to clear stacks, or we will see + // "Maximum call stack size exceeded" in a very high load + setImmediate(() => { + const packet = new Packet(toForward, broker) + packet.qos = 0 + write(this, packet, function (err) { + this._onError(err) + cb() // don't pass the error here or it will be thrown by mqemitter + }) + }) + } else { + setImmediate(cb) + } + } + + _closeClient () { + logger('close :: connected set to `false`') + this.connected = false + + logger('close :: clearing connackTimer') + clearTimeout(this.connackTimer) + + logger('close :: clearing ping timer') + if (this.pingTimer !== null) { + this.pingTimer.clear() + this.pingTimer = null + } + + logger('close :: calling _setupReconnect') + this._setupReconnect() + } + connackTimer(connackTimer: any) { + throw new Error('Method not implemented.') + } + + _sendQueuedPackets () { + const queue = this.queue + + function deliver () { + const entry = queue.shift() + logger('deliver :: entry %o', entry) + let packet = null + + if (!entry) { + return + } + + packet = entry.packet + logger('deliver :: call _sendPacket for %o', packet) + let send = true + if (packet.messageId && packet.messageId !== 0) { + if (!this.messageIdProvider.register(packet.messageId)) { + packet.messageeId = this.messageIdProvider.allocate() + if (packet.messageId === null) { + send = false + } + } + } + if (send) { + this._sendPacket( + packet, + function (err) { + if (entry.cb) { + entry.cb(err) + } + deliver() + } + ) + } else { + logger('messageId: %d has already used.', packet.messageId) + deliver() + } + } + + deliver() + } + + async publish (topic, message, opts) { + const result = await handle.publish(this, message) + return result + } + + async subscribe (packet) { + if (!packet.subscriptions) { + packet = { subscriptions: Array.isArray(packet) ? packet : [packet] } + } + const result = await handle.subscribe(this, packet) + return result + } + + async unsubscribe (packet) { + const result = await handle.unsubscribe(this, packet) + return result + } + + async end (force, opts) { + + logger('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 + } + + logger('end :: cb? %s', !!cb) + cb = cb || nop + + function closeStores () { + logger('end :: closeStores: closing incoming and outgoing stores') + this.disconnected = true + this.incomingStore.close((e1) => { + this.outgoingStore.close((e2) => { + logger('end :: closeStores: emitting end') + this.emit('end') + if (cb) { + const err = e1 || e2 + logger('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 + logger('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) + that._cleanUp(force, () => { + logger('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 + logger('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) + this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) + } else { + logger('end :: (%s) :: immediately calling finish', that.options.clientId) + finish() + } + + return this + } + outgoing(outgoing: any) { + throw new Error('Method not implemented.') + } + + /** + * 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()); + */ + removeOutgoingMessage (messageId) { + const 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 (opts) { + logger('client reconnect') + const that = this + const 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 () { + logger('_reconnect: emitting reconnect to client') + this.emit('reconnect') + if (this.connected) { + this.end(() => { this._setupStream() }) + logger('client already connected. disconnecting first.') + } else { + logger('_reconnect: calling _setupStream') + this._setupStream() + } + } + _setupStream() { + throw new Error('Method not implemented.') + } + + _setupReconnect () { + if (!this.disconnecting && !this.reconnectTimer && (this.options.reconnectPeriod > 0)) { + if (!this.reconnecting) { + logger('_setupReconnect :: emit `offline` state') + this.emit('offline') + logger('_setupReconnect :: set `reconnecting` to `true`') + this.reconnecting = true + } + logger('_setupReconnect :: setting reconnectTimer for %d ms', this.options.reconnectPeriod) + this.reconnectTimer = setInterval(() => { + logger('reconnectTimer :: reconnect triggered!') + this._reconnect() + }, this.options.reconnectPeriod) + } else { + logger('_setupReconnect :: doing nothing...') + } + } + + _clearReconnect () { + logger('_clearReconnect : clearing reconnect timer') + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer) + this.reconnectTimer = null + } + } + + async _cleanUp (forced) { + const opts = arguments[2] + if (done) { + logger('_cleanUp :: done callback provided for on stream close') + this.stream.on('close', done) + } + + logger('_cleanUp :: forced? %s', forced) + if (forced) { + if ((this.options.reconnectPeriod === 0) && this.options.clean) { + flush(this.outgoing) + } + logger('_cleanUp :: (%s) :: destroying stream', this.options.clientId) + this.stream.destroy() + } else { + const packet = xtend({ cmd: 'disconnect' }, opts) + logger('_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) { + logger('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') + this._clearReconnect() + this._setupReconnect() + } + + if (this.pingTimer !== null) { + logger('_cleanUp :: clearing pingTimer') + this.pingTimer.clear() + this.pingTimer = null + } + + if (done && !this.connected) { + logger('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) + this.stream.removeListener('close', done) + done() + } + } + + async _sendPacket (packet) { + logger('_sendPacket :: (%s) :: start', this.options.clientId) + cbStorePut = cbStorePut || nop + + if (!this.connected) { + logger('_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 + } + } + _shiftPingInterval() { + throw new Error('Method not implemented.') + } + + _storePacket (packet) { + 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) { + cb = this.outgoing[packet.messageId] ? this.outgoing[packet.messageId].cb : null + this.outgoingStore.put(packet, function (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + }) + } else if (cb) { + cb(new Error('No connection to broker')) + } + } +} +function eos(conn: any, arg1: any): any { + throw new Error('Function not implemented.') +} + diff --git a/src/connectionFactory/index.ts b/src/connectionFactory/index.ts new file mode 100644 index 000000000..b7e4407e1 --- /dev/null +++ b/src/connectionFactory/index.ts @@ -0,0 +1,264 @@ +import net from 'net' +import { Duplex } from 'stream' +import tls from 'tls' +import { ConnectOptions, isBrowser } from '..' + +const logger = require('pino')() + +export function connectionFactory (options: ConnectOptions): Duplex { + const brokerUrl: URL = options.brokerUrl as URL + const tlsOptions = options.tlsOptions + switch (brokerUrl.protocol) { + case 'tcp': { + const port: number = parseInt(brokerUrl.port) || 1883 + const host: string = brokerUrl.hostname || brokerUrl.host || 'localhost' + + // logger('port %d and host %s', port, host) + return net.createConnection(port, host) + } + case 'tls': { + const port: number = parseInt(brokerUrl.port) || 8883 + const host: string = brokerUrl.hostname || brokerUrl.host || 'localhost' + const servername: string = brokerUrl.host + + logger(`port ${port} host ${host} servername ${servername}`) + + const connection: tls.TLSSocket = tls.connect({port: port, host: host, servername: servername, ...options.tlsOptions}) + /* eslint no-use-before-define: [2, "nofunc"] */ + connection.on('secureConnect', function () { + if (tlsOptions.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 (rejectUnauthorized) { + client.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 + } + case 'ws': { + const webSocketOptions: WebSocketOptions = { + url: client.transformWsUrl ? client.transformWsUrl(url, client) : url, + hostname: url.hostname || 'localhost', + port: url.port || url.protocol === 'wss' ? 443 : 80, + protocol: url.protocol, + path: url.pathname || '/', + wsOptions: client.wsOptions || {} + } + + // Add cert/key/ca etc options + const wssOptions = ['rejectUnauthorized', 'ca', 'cert', 'key', 'pfx', 'passphrase'] + wssOptions.forEach(function (prop) { + // TODO: Validate this works if the property is on the client + if (!client.wsOptions.hasOwnProperty(prop) && client.hasOwnProperty(prop)) { + webSocketOptions.wsOptions[prop] = client[prop] + } + }) + if (isBrowser) { + _buildWebSocketStreamBrowser(webSocketOptions) + } else { + this._buildWebSocketStream(webSocketOptions) + } + } + } +} + +interface WebSocketOptions { + url: URL, + hostname: string, + protocol: string, + port: number, + path: string, + wsOptions: { + rejectUnauthorized: any, + ca: any, + cert: any, + key: any, + pfx: any, + passphrase: any + } +} + +function _buildWebSocketStreamBrowser (opts: WebSocketOptions) { + 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 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) + } + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + const url = buildUrl(opts, client) + /* global WebSocket */ + const socket = new WebSocket(url, [websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + logger('browserStreamBuilder') + let stream + // sets the maximum socket buffer size before throttling + const bufferSize = options.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + const 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) { + const 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 +} + +function _buildWebSocketStream (opts: WebSocketOptions) { + if (!isBrowser && opts.protocol === 'wss') { + + } + const url = buildUrl(options, client) + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) + const webSocketStream = createWebSocketStream(socket, options.wsOptions) + webSocketStream.url = url + socket.on('close', () => { webSocketStream.destroy() }) + return webSocketStream +} diff --git a/lib/default-message-id-provider.js b/src/default-message-id-provider.ts similarity index 95% rename from lib/default-message-id-provider.js rename to src/default-message-id-provider.ts index bbe532ae8..943e4606a 100644 --- a/lib/default-message-id-provider.js +++ b/src/default-message-id-provider.ts @@ -1,65 +1,65 @@ -'use strict' - -/** - * DefaultMessageAllocator constructor - * @constructor - */ -export class DefaultMessageIdProvider { - /** - * MessageIDs starting with 1 - * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 - */ - constructor () { - this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) - } - - /** - * allocate - * - * Get the next messageId. - * @return unsigned int - */ - allocate () { - // 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 - */ - getLastAllocated () { - 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 - */ - register (messageId) { - return true - } - - /** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ - deallocate (messageId) { - } - - /** - * clear - * Deallocate all messageIds. - */ - clear () { - } -} +'use strict' + +/** + * DefaultMessageAllocator constructor + * @constructor + */ +export class DefaultMessageIdProvider { + /** + * MessageIDs starting with 1 + * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 + */ + constructor () { + this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) + } + + /** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ + allocate () { + // 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 + */ + getLastAllocated () { + 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 + */ + register (messageId) { + return true + } + + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate (messageId) { + } + + /** + * clear + * Deallocate all messageIds. + */ + clear () { + } +} diff --git a/lib/errors.js b/src/errors.ts similarity index 96% rename from lib/errors.js rename to src/errors.ts index 7fb11ed00..5ad54882e 100644 --- a/lib/errors.js +++ b/src/errors.ts @@ -1,47 +1,47 @@ -'use strict' - -export 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' -} +'use strict' + +export 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' +} diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts new file mode 100644 index 000000000..58102ed1b --- /dev/null +++ b/src/handlers/auth.ts @@ -0,0 +1,56 @@ +interface AuthProperties { + authenticationMethod: string, + authenticationData: Buffer, + reasonString: string, + userProperties: {[x: string]: unknown} +} + +export async function handleAuth (direction: 'toServer' | 'fromServer', reasonCode: number, props: AuthProperties) { + const packet = { + cmd: 'auth', + reasonCode: reasonCode, // MQTT 5.0 code + properties: { // properties MQTT 5.0 + authenticationMethod: props.authenticationMethod, + authenticationData: props.authenticationData, + reasonString: props.reasonString, + userProperties: props.userProperties + } + } +} + +/** + * Authentication in MQTT v5 + * + * AUTH messages can be used multiple times by the server and the client. + * Two message properties are at the heart of the authentication flow: + * - BYTE 21: Authentication Method + * - BYTE 22: Authentication Data + * + * These properties are set on every message that takes part in the enhanced authentication flow. + * + * Authentication Method + * The Authentication Method is used to choose and describe a way of authentication + * that the client and server have agreed upon. For example, SCRAM-SHA-1, GS2-KRB5, etc. + * The Authentication Method gives meaning to the data that is exchanged during the + * enhanced authentication and must not change. + * + * Authentication Data + * Authentication Data is binary information. This data is usually used to transfer + * multiple iterations of encryped secrets or protocol steps. The content is highly + * dependent on the specific mechanism that is used in the enhanced authentication + * and is application-specific. + * + * Format in MQTT-Packet for Auth: + * { + * cmd: 'auth', + * reasonCode: 0, // MQTT 5.0 code + * properties: { // properties MQTT 5.0 + * authenticationMethod: 'test', + * authenticationData: Buffer.from([0, 1, 2, 3]), + * reasonString: 'test', + * userProperties: { + * 'test': 'test' + * } + * } + * } + */ \ No newline at end of file diff --git a/lib/handlers/connack.js b/src/handlers/connack.ts similarity index 97% rename from lib/handlers/connack.js rename to src/handlers/connack.ts index c036953bf..f3a1711e4 100644 --- a/lib/handlers/connack.js +++ b/src/handlers/connack.ts @@ -1,36 +1,36 @@ -export async function handleConnAck (packet) { - this.connackTimer = setTimeout(function () { - debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') - that._cleanUp(true) - }, this.options.connectTimeout) - - var options = this.options - var version = options.protocolVersion - var rc = version === 5 ? packet.reasonCode : packet.returnCode - - clearTimeout(this.connackTimer) - - if (packet.properties) { - if (packet.properties.topicAliasMaximum) { - if (!options.properties) { options.properties = {} } - options.properties.topicAliasMaximum = 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) - } -} +export async function handleConnAck (packet) { + this.connackTimer = setTimeout(function () { + debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') + that._cleanUp(true) + }, this.options.connectTimeout) + + var options = this.options + var version = options.protocolVersion + var rc = version === 5 ? packet.reasonCode : packet.returnCode + + clearTimeout(this.connackTimer) + + if (packet.properties) { + if (packet.properties.topicAliasMaximum) { + if (!options.properties) { options.properties = {} } + options.properties.topicAliasMaximum = 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) + } +} diff --git a/lib/handlers/connect.js b/src/handlers/connect.ts similarity index 96% rename from lib/handlers/connect.js rename to src/handlers/connect.ts index 29b43b1ca..ce22d872e 100644 --- a/lib/handlers/connect.js +++ b/src/handlers/connect.ts @@ -1,108 +1,108 @@ - -export async function handleConnect (client, packet) { - if (this.disconnected) { - this.emit('connect', packet) - return - } - - var that = this - - this.messageIdProvider.clear() - this._setupPingTimer() - this._resubscribe(packet) - - 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() -} + +export async function handleConnect (client, packet) { + if (this.disconnected) { + this.emit('connect', packet) + return + } + + var that = this + + this.messageIdProvider.clear() + this._setupPingTimer() + this._resubscribe(packet) + + 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() +} diff --git a/lib/handlers/disconnect.js b/src/handlers/disconnect.ts similarity index 96% rename from lib/handlers/disconnect.js rename to src/handlers/disconnect.ts index cae2553c4..f176decec 100644 --- a/lib/handlers/disconnect.js +++ b/src/handlers/disconnect.ts @@ -1,3 +1,3 @@ -export async function handleDisconnect (client) { - client.emit('disconnect', packet) -} +export async function handleDisconnect (client) { + client.emit('disconnect', packet) +} diff --git a/lib/handlers/index.js b/src/handlers/index.ts similarity index 96% rename from lib/handlers/index.js rename to src/handlers/index.ts index 32138753e..3e136f9a2 100644 --- a/lib/handlers/index.js +++ b/src/handlers/index.ts @@ -1,75 +1,75 @@ -'use strict' - -import { handleConnect } from './connect' -import { handleConnAck } from './connack' -import { handleDisconnect } from './disconnect' -import { handlePing } from './ping' -import { handlePingReq } from './pingreq' -import { handlePingResp } from './pingresp' -import { handlePub } from './pub' -import { handlePubRec } from './pubrec' -import { handlePubRel } from './pubrel' -import { handlePubComp } from './pubcomp' -import { handlePubAck } from './puback' -import { handleSub } from './sub' -import { handleSubAck } from './suback' -import { handleUnsub } from './unsub' -import { handleUnsubAck } from './unsuback' -import { handleAuth } from './auth' - -export async function handle (client, packet) { - let result - switch (packet.cmd) { - case 'auth': - result = await handleAuth(client, packet) - break - case 'connect': - result = await handleConnect(client, packet) - break - case 'connack': - result = await handleConnAck(client, packet) - break - case 'publish': - result = await handlePub(client, packet) - break - case 'subscribe': - result = await handleSub(client, packet) - break - case 'suback': - result = await handleSubAck(client, packet) - break - case 'unsubscribe': - result = await handleUnsub(client, packet) - break - case 'unsuback': - result = await handleUnsubAck(client, packet) - break - case 'pubcomp': - result = await handlePubComp(client, packet) - break - case 'puback': - result = await handlePubAck(client, packet) - break - case 'pubrel': - result = await handlePubRel(client, packet) - break - case 'pubrec': - result = await handlePubRec(client, packet) - break - case 'ping': - result = await handlePing(client, packet) - break - case 'pingreq': - result = await handlePingReq(client, packet) - break - case 'pingresp': - result = await handlePingResp(client, packet) - break - case 'disconnect': - result = await handleDisconnect(client, packet) - client._disconnected = true - break - } - - return result -} +'use strict' + +import { handleConnect } from './connect' +import { handleConnAck } from './connack' +import { handleDisconnect } from './disconnect' +import { handlePing } from './ping' +import { handlePingReq } from './pingreq' +import { handlePingResp } from './pingresp' +import { handlePub } from './pub' +import { handlePubRec } from './pubrec' +import { handlePubRel } from './pubrel' +import { handlePubComp } from './pubcomp' +import { handlePubAck } from './puback' +import { handleSub } from './sub' +import { handleSubAck } from './suback' +import { handleUnsub } from './unsub' +import { handleUnsubAck } from './unsuback' +import { handleAuth } from './auth' + +export async function handle (client, packet) { + let result + switch (packet.cmd) { + case 'auth': + result = await handleAuth(client, packet) + break + case 'connect': + result = await handleConnect(client, packet) + break + case 'connack': + result = await handleConnAck(client, packet) + break + case 'publish': + result = await handlePub(client, packet) + break + case 'subscribe': + result = await handleSub(client, packet) + break + case 'suback': + result = await handleSubAck(client, packet) + break + case 'unsubscribe': + result = await handleUnsub(client, packet) + break + case 'unsuback': + result = await handleUnsubAck(client, packet) + break + case 'pubcomp': + result = await handlePubComp(client, packet) + break + case 'puback': + result = await handlePubAck(client, packet) + break + case 'pubrel': + result = await handlePubRel(client, packet) + break + case 'pubrec': + result = await handlePubRec(client, packet) + break + case 'ping': + result = await handlePing(client, packet) + break + case 'pingreq': + result = await handlePingReq(client, packet) + break + case 'pingresp': + result = await handlePingResp(client, packet) + break + case 'disconnect': + result = await handleDisconnect(client, packet) + client._disconnected = true + break + } + + return result +} diff --git a/lib/handlers/ping.js b/src/handlers/ping.ts similarity index 96% rename from lib/handlers/ping.js rename to src/handlers/ping.ts index c80e22e68..38e8a5753 100644 --- a/lib/handlers/ping.js +++ b/src/handlers/ping.ts @@ -1,27 +1,27 @@ - -export async function handlePing () { - debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) - - if (!this.pingTimer && this.options.keepalive) { - this.pingResp = true - this.pingTimer = reInterval(() => { - checkPing() - }, this.options.keepalive * 1000) - } -} - -export async function shiftPingInterval () { - if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { - this.pingTimer.reschedule(this.options.keepalive * 1000) - } -} - -function checkPing () { - if (this.pingResp) { - this.pingResp = false - this._sendPacket({ cmd: 'pingreq' }) - } else { - // do a forced cleanup since socket will be in bad shape - this._cleanUp(true) - } + +export async function handlePing () { + debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) + + if (!this.pingTimer && this.options.keepalive) { + this.pingResp = true + this.pingTimer = reInterval(() => { + checkPing() + }, this.options.keepalive * 1000) + } +} + +export async function shiftPingInterval () { + if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { + this.pingTimer.reschedule(this.options.keepalive * 1000) + } +} + +function checkPing () { + if (this.pingResp) { + this.pingResp = false + this._sendPacket({ cmd: 'pingreq' }) + } else { + // do a forced cleanup since socket will be in bad shape + this._cleanUp(true) + } } \ No newline at end of file diff --git a/lib/handlers/pingreq.js b/src/handlers/pingreq.ts similarity index 95% rename from lib/handlers/pingreq.js rename to src/handlers/pingreq.ts index 98e97a870..b57dd5816 100644 --- a/lib/handlers/pingreq.js +++ b/src/handlers/pingreq.ts @@ -1,3 +1,3 @@ -export async function handlePingReq () { - return true -} +export async function handlePingReq () { + return true +} diff --git a/lib/handlers/pingresp.js b/src/handlers/pingresp.ts similarity index 97% rename from lib/handlers/pingresp.js rename to src/handlers/pingresp.ts index 3d555804e..2c3c5fb7f 100644 --- a/lib/handlers/pingresp.js +++ b/src/handlers/pingresp.ts @@ -1,7 +1,7 @@ -export async function handlePingResp () { - // TODO: In old implementation, pingResp is a state in - // the client that is toggled to true when the pingResp - // is received. How does aedes do this? is there a - // better way than a boolean toggle? - return true -} +export async function handlePingResp () { + // TODO: In old implementation, pingResp is a state in + // the client that is toggled to true when the pingResp + // is received. How does aedes do this? is there a + // better way than a boolean toggle? + return true +} diff --git a/lib/handlers/pub.js b/src/handlers/pub.ts similarity index 96% rename from lib/handlers/pub.js rename to src/handlers/pub.ts index e2c690850..727ded6e7 100644 --- a/lib/handlers/pub.js +++ b/src/handlers/pub.ts @@ -1,89 +1,89 @@ - -/** - * _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 -*/ -export async function handlePub (packet) { - 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] - 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 - } + +/** + * _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 +*/ +export async function handlePub (packet) { + 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] + 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 + } diff --git a/lib/handlers/puback.js b/src/handlers/puback.ts similarity index 96% rename from lib/handlers/puback.js rename to src/handlers/puback.ts index 66a903ef6..96f46cbde 100644 --- a/lib/handlers/puback.js +++ b/src/handlers/puback.ts @@ -1,80 +1,80 @@ -export async function handlePubAck (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') - }} +export async function handlePubAck (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') + }} diff --git a/lib/handlers/pubcomp.js b/src/handlers/pubcomp.ts similarity index 96% rename from lib/handlers/pubcomp.js rename to src/handlers/pubcomp.ts index 350d65326..76655a91d 100644 --- a/lib/handlers/pubcomp.js +++ b/src/handlers/pubcomp.ts @@ -1,80 +1,80 @@ -export async function handlePubComp (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') - }} +export async function handlePubComp (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') + }} diff --git a/lib/handlers/pubrec.js b/src/handlers/pubrec.ts similarity index 96% rename from lib/handlers/pubrec.js rename to src/handlers/pubrec.ts index 8f9a1666b..0311b207e 100644 --- a/lib/handlers/pubrec.js +++ b/src/handlers/pubrec.ts @@ -1,81 +1,81 @@ -export async function handlePubRec (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') - } -} +export async function handlePubRec (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') + } +} diff --git a/lib/handlers/pubrel.js b/src/handlers/pubrel.ts similarity index 96% rename from lib/handlers/pubrel.js rename to src/handlers/pubrel.ts index 77bcfc7d2..04d64c59f 100644 --- a/lib/handlers/pubrel.js +++ b/src/handlers/pubrel.ts @@ -1,23 +1,23 @@ -export async function handlePubRel (packet) { - 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) - } - }) -} +export async function handlePubRel (packet) { + 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) + } + }) +} diff --git a/lib/handlers/sub.js b/src/handlers/sub.ts similarity index 96% rename from lib/handlers/sub.js rename to src/handlers/sub.ts index c913e7098..0b2558a50 100644 --- a/lib/handlers/sub.js +++ b/src/handlers/sub.ts @@ -1,182 +1,182 @@ - -/** - * 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); - */ -export async function handleSub () { - 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 -} + +/** + * 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); + */ +export async function handleSub () { + 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 +} diff --git a/lib/handlers/suback.js b/src/handlers/suback.ts similarity index 96% rename from lib/handlers/suback.js rename to src/handlers/suback.ts index 2f9cee01f..dd42d5baa 100644 --- a/lib/handlers/suback.js +++ b/src/handlers/suback.ts @@ -1,80 +1,80 @@ -export async function handleSubAck (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') - }} +export async function handleSubAck (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') + }} diff --git a/lib/handlers/unsub.js b/src/handlers/unsub.ts similarity index 95% rename from lib/handlers/unsub.js rename to src/handlers/unsub.ts index db4691927..ce20f7f10 100644 --- a/lib/handlers/unsub.js +++ b/src/handlers/unsub.ts @@ -1,93 +1,93 @@ -/** - * 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); - */ - -export async function handleUnsub () { - 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 -} +/** + * 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); + */ + +export async function handleUnsub () { + 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 +} diff --git a/lib/handlers/unsuback.js b/src/handlers/unsuback.ts similarity index 96% rename from lib/handlers/unsuback.js rename to src/handlers/unsuback.ts index 06fcf1f1e..9f02d8975 100644 --- a/lib/handlers/unsuback.js +++ b/src/handlers/unsuback.ts @@ -1,80 +1,80 @@ -export async function handleUnsubAck (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') - }} +export async function handleUnsubAck (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') + }} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..a610cf4e8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ + +import { MqttClient } from './client' +import { Store } from './store' +import { DefaultMessageIdProvider } from './default-message-id-provider' +import { UniqueMessageIdProvider } from './unique-message-id-provider' +import { urlToHttpOptions } from 'url' +import { Duplex } from 'stream' +import { TlsOptions } from 'tls' +import { Server } from 'http' +import {Server as HttpsServer} from 'https' + +type supportedProtocols = 'mqtt' | 'mqtts' | 'ws' | 'wss' + +const protocols = { + all : [ + 'mqtt', + 'mqtts', + 'ws', + 'wss' + ], + secure: [ + 'mqtts', + 'ws' + ], + insecure: [ + 'mqtt', + 'wss' + ] +} + +export const isBrowser = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' + +export type WsOptions = { + backlog: number, + clientTracking: boolean, + handleProtocols: () => unknown, + host: string, + maxPayload: number, + noServer: boolean, + path: string, + perMessageDeflate: boolean | {[x: string]: unknown}, + port: number, + server: Server | HttpsServer, + skipUTF8Validation: boolean, + verifyClient: () => unknown +} & { + [prop: string]: string +} + +export interface ConnectOptions { + brokerUrl: string | URL + wsOptions: {[key: string]: WsOptions | unknown}, + tlsOptions: {[key: string]: TlsOptions | unknown}, + keepalive: any, + reschedulePings: any, + clientId: any, + protocolId: 'MQIsdp' | 'MQTT', + protocolVersion: any, + clean: any, + reconnectPeriod: any, + connectTimeout: any, + username: any, + password: any, + incomingStore: any, + outgoingStore: any, + queueQoSZero: any, + customHandleAcks: any, + properties: { + sessionExpiryInterval: number, + receiveMaximum: number, + maximumPacketSize: number, + topicAliasMaximum: number, + requestResponseInformation: boolean, + requestProblemInformation: boolean, + userPropertis: any, + authenticationMethod: string, + authenticationData: BinaryData // TODO: Should this be something else? + } + authPacket: any, + will: { + topic: any, + payload: any, + qos: number, + retain: boolean, + properties: { + willDelayInterval: number, + payloadFormatIndicator: boolean, + messageExpiryInterval: number, + contentType: string, + responseTopic: string, + correlationData: BinaryData // TODO: is this the right type? + userProperties: any + } + } + transformWsUrl: () => URL, + resubscribe: boolean, + messageIdProvider: any + customStreamFactory: (options) => Duplex +} + + +/** + * connect() + * Connect will: + * 1) Validate the options provided by the user. + * 2) Instantiate a new client. + * 3) Return the client to the user. + */ +function connect (options: ConnectOptions) { + if (typeof(options.brokerUrl) === 'string') { + options.brokerUrl = new URL(options.brokerUrl) + } + + if (!options.brokerUrl.protocol) { + throw new Error( + `Missing protocol. \ + To provide a protocol, you have two options:\ + - Format the brokerUrl with a protocol, for example: 'mqtt://test.mosquitto.org'. + - Pass in the protocol via the protocol option.`) + } + + // If there is a colon at the end of the provided protocol, replace it with + options.brokerUrl.protocol = options.brokerUrl.protocol.replace(/:$/, '') + const validationErr: Error = _validateProtocol(options) + if (validationErr) { + throw validationErr + } + const client = new MqttClient(options) + return client +} + +function _validateProtocol(opts): Error | undefined { + if (opts.cert && opts.key) { + if (opts.protocol) { + if (protocols.secure.indexOf(opts.protocol) === -1) { + const protocolError: Error = formatSecureProtocolError(opts.protocol) + return protocolError + } + } else { + // A cert and key was provided, however no protocol was specified, so we will throw an error. + // TODO: Git Blame on this line. I don't understand the error message at all. + return new Error('Missing secure protocol key') + } + } + + // if the protocol provided in the options does not exist in the supported protocols... + _ensureBrowserUsesSecureProtocol(opts.protocol) + return +} + +function formatSecureProtocolError(protocol: string): Error { + let secureProtocol: string; + switch (protocol) { + case 'mqtt': + secureProtocol = 'mqtts' + break + case 'ws': + secureProtocol = 'wss' + break + default: + return new Error('Unknown protocol for secure connection: "' + protocol + '"!') + } + return new Error( + `user provided cert and key , but protocol ${protocol} is insecure. + Use ${secureProtocol} instead.`) +} + +function _ensureBrowserUsesSecureProtocol(protocol: supportedProtocols): string { + let browserCompatibleProtocol: string + if (Client.isBrowser) { + if (protocol === 'mqtt') { + browserCompatibleProtocol = 'ws' + } else if (protocol === 'mqtts') { + browserCompatibleProtocol = 'wss' + } + } + return browserCompatibleProtocol || protocol +} + +export {connect, Store, DefaultMessageIdProvider, UniqueMessageIdProvider} diff --git a/lib/store.js b/src/store.ts similarity index 94% rename from lib/store.js rename to src/store.ts index efbfabf09..37809750b 100644 --- a/lib/store.js +++ b/src/store.ts @@ -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/unique-message-id-provider.js b/src/unique-message-id-provider.ts similarity index 95% rename from lib/unique-message-id-provider.js rename to src/unique-message-id-provider.ts index bf30c226e..6362724e8 100644 --- a/lib/unique-message-id-provider.js +++ b/src/unique-message-id-provider.ts @@ -1,57 +1,57 @@ -'use strict' - -import { NumberAllocator } from 'number-allocator' - -export class UniqueMessageIdProvider { - constructor () { - 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. - */ - allocate () { - this.lastId = this.numberAllocator.alloc() - return this.lastId - } - - /** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ - getLastAllocated () { - return this.lastId - } - - /** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ - register (messageId) { - return this.numberAllocator.use(messageId) - } - - /** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ - deallocate (messageId) { - this.numberAllocator.free(messageId) - } - - /** - * clear - * Deallocate all messageIds. - */ - clear () { - this.numberAllocator.clear() - } -} +'use strict' + +import { NumberAllocator } from 'number-allocator' + +export class UniqueMessageIdProvider { + constructor () { + 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. + */ + allocate () { + this.lastId = this.numberAllocator.alloc() + return this.lastId + } + + /** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ + getLastAllocated () { + return this.lastId + } + + /** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ + register (messageId) { + return this.numberAllocator.use(messageId) + } + + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate (messageId) { + this.numberAllocator.free(messageId) + } + + /** + * clear + * Deallocate all messageIds. + */ + clear () { + this.numberAllocator.clear() + } +} diff --git a/lib/validateTopic.js b/src/validateTopic.ts similarity index 96% rename from lib/validateTopic.js rename to src/validateTopic.ts index e8c6d62dc..b88bb7b8f 100644 --- a/lib/validateTopic.js +++ b/src/validateTopic.ts @@ -1,48 +1,48 @@ -'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. - */ -export 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 - */ -export 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 -} +'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. + */ +export 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 + */ +export 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 +} diff --git a/test/abstract_client.js b/test/abstract_client.js deleted file mode 100644 index 4c8b0fa77..000000000 --- a/test/abstract_client.js +++ /dev/null @@ -1,3177 +0,0 @@ -'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 deleted file mode 100644 index 02b3ec849..000000000 --- a/test/abstract_store.js +++ /dev/null @@ -1,135 +0,0 @@ -'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 deleted file mode 100644 index 75a9a8994..000000000 --- a/test/browser/server.js +++ /dev/null @@ -1,132 +0,0 @@ -'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 deleted file mode 100644 index 8e9cd42e3..000000000 --- a/test/browser/test.js +++ /dev/null @@ -1,92 +0,0 @@ -'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 deleted file mode 100644 index 4ea052ab8..000000000 --- a/test/client.js +++ /dev/null @@ -1,486 +0,0 @@ -'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 deleted file mode 100644 index 48e1bcb6a..000000000 --- a/test/client_mqtt5.js +++ /dev/null @@ -1,536 +0,0 @@ -'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) - - // 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 deleted file mode 100644 index 89648b3c0..000000000 --- a/test/helpers/port_list.js +++ /dev/null @@ -1,49 +0,0 @@ -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 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, - PORTAND114, - PORTAND115, - PORTAND116, - PORTAND117, - PORTAND118, - PORTAND119, - PORTAND316, - PORTAND326, - PORTAND327, - PORTAND400 -} diff --git a/test/helpers/private-csr.pem b/test/helpers/private-csr.pem deleted file mode 100644 index 5c9f3924f..000000000 --- a/test/helpers/private-csr.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx -ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBANtzIZmqf7h3axO9mzo2VhiF/BF3Y4E/fDTkFy27 -PgssS/ipFOMe/IxyM/hA/o/bQb0BY+sH5s1020kNH79umtabWMaDmOi8bvmHWtVC -cYhn3mhbRFWcORdTnfQ8uRYXZGeoupjlhfrKkQCoSAFKh1OzU7aNx4CjMAjSa4py -trMAVNJ37RryhsfMuHAeG8+0Eo3qmYyaplpurtr8A3HWV65R2VFCwZ5hKG8I9X2F -3UrYKHr4xlxOgjD8j2OfYZxpGHI6YexJ28aR0xlsWfzS+TKKFVxy8ntgPGL0ZXL3 -vss80mAcBl9FfsJzufn4IHOYspX1OEM0M7plMmQw/yNT9B8CAwEAAaAAMA0GCSqG -SIb3DQEBBQUAA4IBAQBsONiE5HTjfR1pDrWPIhbLqMO3AqmuB5AwpQm8kAaM2Oz1 -DI/a8bHYyODMiyWUPTtwLMQWcJpAG2ZhE18gLqFwXZR1XSOxY1yF+uZ7Ls3hwzbq -9A6O254B5wXBnXkVbzZwFshV5HWiZwVivF5GDyLRsMoS2EtUHoDEP4YIRK0kPL9H -m3BB334KlWTc8NNXFFG62OL7q2fa8xRHlN8SYfeUjy79eEoBdHv5wL/ZN/YBCDNJ -2zrYUvbOmfoq1e+6AczZ6xAHHeneUQuaOF225aMwHHZTiP2TlIeFXwBvzV1BWIJv -dOaHX/f3NamKoGvwYyIR1FrI2FpXTJLRE/eu7TFD ------END CERTIFICATE REQUEST----- diff --git a/test/helpers/private-key.pem b/test/helpers/private-key.pem deleted file mode 100644 index fa0421293..000000000 --- a/test/helpers/private-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA23Mhmap/uHdrE72bOjZWGIX8EXdjgT98NOQXLbs+CyxL+KkU -4x78jHIz+ED+j9tBvQFj6wfmzXTbSQ0fv26a1ptYxoOY6Lxu+Yda1UJxiGfeaFtE -VZw5F1Od9Dy5FhdkZ6i6mOWF+sqRAKhIAUqHU7NTto3HgKMwCNJrinK2swBU0nft -GvKGx8y4cB4bz7QSjeqZjJqmWm6u2vwDcdZXrlHZUULBnmEobwj1fYXdStgoevjG -XE6CMPyPY59hnGkYcjph7EnbxpHTGWxZ/NL5MooVXHLye2A8YvRlcve+yzzSYBwG -X0V+wnO5+fggc5iylfU4QzQzumUyZDD/I1P0HwIDAQABAoIBAQDNgNdqS5wnZs1D -Qz/mF5QwiugugxsPoh/yd9as4LeNRwIt7ki9F/twmlHInTTGCpFZKcAkDNY6eMAR -fNTKNA2UAw3zeLDs4ekai4KoSvx+vKYuG6m2cgGUsp0sZuD8qxM/b2auX+JDpQZ9 -Exm6+8wWucwfHE5DTI5i9In4sMweeuiEUYnndTzElkvnP/44h1fGSU1iGUKn/ftc -P4X+3SU68KMT3kUsEBavtmSdyeG/lSFEjm73FwVIRZ+PfbQX2hDD+mmseAXGFKi1 -HudtQkEzTvYR+QAgvtjNgt/0qxFtPdj7Y+iRkCZQSJToAw8z6vwUn1qNCADauGMI -X6KIm8XBAoGBAPiwMLYpIqp1rksINbqpbVqjtqsoejQuPYeEF7OXHbH9il7pWrQF -wLbogo3YXX+a66RreVMhsUeq7+pIf/sK2lT73gDpFfvZnJG1ww94QkHBEPso0bN9 -pcGgceIK7KRRAiAl5Mjw6pZZNnIBxlIFaSbBqQau74NfdaalMBF2wi+3AoGBAOHm -3ttFtVjVlb2fHoiGNZCZDv3gnsQXZlCxS+rQ4XEmEWKHAH4T3+Kzmo8jWoX+DGGD -6UkxWHv7e+KrYIZDi7Dd2HFV0gHN6d1SNdPix3vN114bNOrbfqxuEVT5PdFHSuel -5d3ix+3U+tpHamwb88eyeq6Q3t5Lcl3gIRGLzo7ZAoGBAKVuLzk+K/1Qw1zOXU+K -nWAKP92j04caq3uWd13UTMC2dHGmsdvHZ+dEzHQnVisol1CM3exbIV8XavliuR/6 -nDqkQY5Bf4pFvE2Bp/yGdyzejblF8hmAn98qKBfCRKEZ8lwIWSUCfkr9laZJX+/4 -AXbypMn5XQL7YXw1rsAvTAYJAoGAV4ZL8kkf6jtWuRFdkyfsuQmUdWkCGpe2XK1U -7LXhoyVMtw/3cOHibMOJrsvT1vaHdYDWcjVcQy084qXj0CF7jhtmMQM/StOtOMMR -d/b1s1Idj6ia6CQDAGvk6zdmbB9jNj1gwoeLTuqmBsyEvz5VRZoxTlFzCE3TEew0 -48d3UIECgYBMxnLByVQA3pQWWIZZyqt+HgJAphYPdpnPalblQAbuCksKTZ/QKDkW -dzih1PQROVrYrX7VwJ3/I8gXIuvKVtN1NKOS3a0JtbJQhpH4YbRwyQskXWYP8oYa -MjBGPymNDhZh0zoGWzst5uR3NpdNV+7yNYPvyxzVNjlPjtAUqIxjBg== ------END RSA PRIVATE KEY----- diff --git a/test/helpers/public-cert.pem b/test/helpers/public-cert.pem deleted file mode 100644 index 881c8b022..000000000 --- a/test/helpers/public-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBjCCAe4CCQDkrq1PMPtmfzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTEzMDEyNTEwMzEyOVoXDTEzMDIyNDEwMzEyOVowRTELMAkG -A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ANtzIZmqf7h3axO9mzo2VhiF/BF3Y4E/fDTkFy27PgssS/ipFOMe/IxyM/hA/o/b -Qb0BY+sH5s1020kNH79umtabWMaDmOi8bvmHWtVCcYhn3mhbRFWcORdTnfQ8uRYX -ZGeoupjlhfrKkQCoSAFKh1OzU7aNx4CjMAjSa4pytrMAVNJ37RryhsfMuHAeG8+0 -Eo3qmYyaplpurtr8A3HWV65R2VFCwZ5hKG8I9X2F3UrYKHr4xlxOgjD8j2OfYZxp -GHI6YexJ28aR0xlsWfzS+TKKFVxy8ntgPGL0ZXL3vss80mAcBl9FfsJzufn4IHOY -spX1OEM0M7plMmQw/yNT9B8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHAwoKYl -6g9lUEwBDqm6ZxjgoYQi6V3loCjBcTr5OrMkLvvZrA55xsse0NRH40I/pvCaAZAZ -EEna0fr5GPYi+y+I8EoU2W/+ehSqRAU8Fkdm0eR5MjyLWYOwd3ClUND8EpUNNSKH -Xw9k9EQmyKsDxVsKWoJoO9rfFkUjooz07jGPCud18QCBs5i5ThbnQ9UP+26D8z5k -1Dii69LIcLXA3Vtm6R5fT57zNusfx8bqA9yy7UThYaXIazNMWNxiJRXfv0J4zFdD -RQ+SFdJ3p5jurPkc3oRWWPbn/Lpf0E5XlYTJImXT1WmWnQSaNtME4P+3kEL5x+v/ -u8zTLbobG4x0rQ== ------END CERTIFICATE----- diff --git a/test/helpers/public-key.pem b/test/helpers/public-key.pem deleted file mode 100644 index 23893ba28..000000000 --- a/test/helpers/public-key.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23Mhmap/uHdrE72bOjZW -GIX8EXdjgT98NOQXLbs+CyxL+KkU4x78jHIz+ED+j9tBvQFj6wfmzXTbSQ0fv26a -1ptYxoOY6Lxu+Yda1UJxiGfeaFtEVZw5F1Od9Dy5FhdkZ6i6mOWF+sqRAKhIAUqH -U7NTto3HgKMwCNJrinK2swBU0nftGvKGx8y4cB4bz7QSjeqZjJqmWm6u2vwDcdZX -rlHZUULBnmEobwj1fYXdStgoevjGXE6CMPyPY59hnGkYcjph7EnbxpHTGWxZ/NL5 -MooVXHLye2A8YvRlcve+yzzSYBwGX0V+wnO5+fggc5iylfU4QzQzumUyZDD/I1P0 -HwIDAQAB ------END PUBLIC KEY----- diff --git a/test/helpers/server.js b/test/helpers/server.js deleted file mode 100644 index 46bd79537..000000000 --- a/test/helpers/server.js +++ /dev/null @@ -1,53 +0,0 @@ -'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 deleted file mode 100644 index 1d1095cb3..000000000 --- a/test/helpers/server_process.js +++ /dev/null @@ -1,9 +0,0 @@ -'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/helpers/tls-cert.pem b/test/helpers/tls-cert.pem deleted file mode 100644 index 8b6be1c91..000000000 --- a/test/helpers/tls-cert.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -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-key.pem b/test/helpers/tls-key.pem deleted file mode 100644 index a6d427a79..000000000 --- a/test/helpers/tls-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------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----- diff --git a/test/helpers/wrong-cert.pem b/test/helpers/wrong-cert.pem deleted file mode 100644 index 3dfdb6f6b..000000000 --- a/test/helpers/wrong-cert.pem +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICATCCAWoCCQDEVSSDKkcTdjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTE0MDUxMTE2MzMxMVoXDTE0MDYxMDE2MzMxMVowRTELMAkG -A1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyDMI -VS2XSizZT8KeFYFQfKt7CcT5/Pkzw2BDJoMVLmrkfHdddjsTgswqHfzhO8Fmfg6B -MxgsEz2aKi24hJxQFuQ1DGhyfKHnjxM5PqSLiOkQDKllnAOgqOBDXpca0jXypCk1 -IVhMspM2ylrnBXps3nTBLJxFBkZSBov/JDkkL+cCAwEAATANBgkqhkiG9w0BAQUF -AAOBgQA8k93U0VDIpQ8lpScxrCtEu5jLZgB1fw0fdCUtDHaaM1v+LWr1xfCmFKyT -kUMcJl4e1pkcSNfXcI7LdNt8EJqMabOi2UpW1+VZJn206D0f3XmNSmZbk8oozGrl -qg2wSTZYlZClCTpWO2Y+iYzojY8kmLaQ2xbTxBz1XlshC8HvsA== ------END CERTIFICATE----- diff --git a/test/helpers/wrong-csr.pem b/test/helpers/wrong-csr.pem deleted file mode 100644 index 34ed36a49..000000000 --- a/test/helpers/wrong-csr.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEh -MB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB -AQUAA4GNADCBiQKBgQDIMwhVLZdKLNlPwp4VgVB8q3sJxPn8+TPDYEMmgxUuauR8 -d112OxOCzCod/OE7wWZ+DoEzGCwTPZoqLbiEnFAW5DUMaHJ8oeePEzk+pIuI6RAM -qWWcA6Co4ENelxrSNfKkKTUhWEyykzbKWucFemzedMEsnEUGRlIGi/8kOSQv5wID -AQABoAAwDQYJKoZIhvcNAQEFBQADgYEAFXqd8jhW+2hRvkRB1CCVBK5e6AQHq1rF -s3B36O64hRHIr1KC+dWr8vv1t9Rkud+7E3ELHtxWCORIYpqQ2Ddldt4PP+MTNj2C -qgwOpxM0VDxeeWml8fqx2uzfPhVduyHGm0yff2JS2KRVmnIPLTUuz/+udukIFDVO -Sc4/W3qY7f8= ------END CERTIFICATE REQUEST----- diff --git a/test/helpers/wrong-key.pem b/test/helpers/wrong-key.pem deleted file mode 100644 index af7434194..000000000 --- a/test/helpers/wrong-key.pem +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDIMwhVLZdKLNlPwp4VgVB8q3sJxPn8+TPDYEMmgxUuauR8d112 -OxOCzCod/OE7wWZ+DoEzGCwTPZoqLbiEnFAW5DUMaHJ8oeePEzk+pIuI6RAMqWWc -A6Co4ENelxrSNfKkKTUhWEyykzbKWucFemzedMEsnEUGRlIGi/8kOSQv5wIDAQAB -AoGBALOzszgaG2I2jb4dmJ7/G4s8tc2YJTlhS4iFgOEx6rJmur/KuXcmIiZXMzsF -wftMZ76hMHH3saB3vEk+DxHh6bR6cW/I82Vxts9suz2fRnd2mh5JHI+opXE53LVn -hJcQ4k6LJ9MNVxlHwCTrSuJvtikDOOrCARHRvYzFRL4wXvmpAkEA+DFzXGDg8PxX -RFp6RLLbqaUT6YXNi+E5ERuumru+rgRj+OF/dxNK3d1lcIJZjqVMDAgOsZ66/bkh -GfCzJPREUwJBAM5/HeHmTEM5K5B0X8b6XEHTgWFUNTu4K36Ee5ySd8RYI8zjQ9wS -NM1nXnx12npL7DSkShz9xgnTe0f8YmQnc50CQQCgdE/RXCxwf6LnZNsBCOSsIzXh -VgiRsxSSs+PI0zGuDNaY8yfV0ponH1fSSeMeLk0gxiDBwg2/tGzq+UrHzEdTAkB1 -/U5O0K+MzbLlxIkhgdaLSlYoDdyo9e/sR7j12v8SMqaqIMWajtCa+VCU3yZqMM2T -urgaXqr03GEZ3c0+mwhFAkAwWkczV1iwuedmWLKc36iQhoj+FRMUoxWe/fBixQls -g0lDvwWiZ3M6hjCsBRckmt8eU2mUh79Odrj5fRWIwXaX ------END RSA PRIVATE KEY----- diff --git a/test/message-id-provider.js b/test/message-id-provider.js deleted file mode 100644 index 2f84bdf35..000000000 --- a/test/message-id-provider.js +++ /dev/null @@ -1,91 +0,0 @@ -'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/mocha.opts b/test/mocha.opts deleted file mode 100644 index 4008c54c1..000000000 --- a/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ ---check-leaks ---timeout 10000 ---exit - diff --git a/test/mqtt.js b/test/mqtt.js deleted file mode 100644 index f55d04a33..000000000 --- a/test/mqtt.js +++ /dev/null @@ -1,230 +0,0 @@ -'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 deleted file mode 100644 index 976a01aff..000000000 --- a/test/mqtt_store.js +++ /dev/null @@ -1,9 +0,0 @@ -'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 deleted file mode 100644 index 95b7a6197..000000000 --- a/test/secure_client.js +++ /dev/null @@ -1,188 +0,0 @@ -'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 deleted file mode 100644 index ccfe2f4d1..000000000 --- a/test/server.js +++ /dev/null @@ -1,94 +0,0 @@ -'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 deleted file mode 100644 index 9527d47e2..000000000 --- a/test/server_helpers_for_client_tests.js +++ /dev/null @@ -1,147 +0,0 @@ -'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 deleted file mode 100644 index 1489b2138..000000000 --- a/test/store.js +++ /dev/null @@ -1,10 +0,0 @@ -'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/typescript/broker-connect-subscribe-and-publish.ts b/test/typescript/broker-connect-subscribe-and-publish.ts deleted file mode 100644 index 359e752a7..000000000 --- a/test/typescript/broker-connect-subscribe-and-publish.ts +++ /dev/null @@ -1,26 +0,0 @@ -// relative path uses package.json {"types":"types/index.d.ts", ...} -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}, - messageIdProvider: new UniqueMessageIdProvider()} - -console.log(`connect(${JSON.stringify(BROKER)})`) -const client:Client = connect(`mqtt://${BROKER}`, opts) - -client.subscribe({[TOPIC]: {qos: 2}}, (err, granted) => { - granted.forEach(({topic, qos}) => { - console.log(`subscribed to ${topic} with qos=${qos}`) - }) - 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() -}).on('connect', (packet: IConnackPacket) => { - console.log('connected!', JSON.stringify(packet)) -}) diff --git a/test/typescript/tsconfig.json b/test/typescript/tsconfig.json deleted file mode 100644 index 0b8d393ac..000000000 --- a/test/typescript/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "moduleResolution": "node", - "noEmitOnError": true, - "noImplicitAny": true, - "alwaysStrict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "sourceMap": true - } -} diff --git a/test/unique_message_id_provider_client.js b/test/unique_message_id_provider_client.js deleted file mode 100644 index 933d85b82..000000000 --- a/test/unique_message_id_provider_client.js +++ /dev/null @@ -1,21 +0,0 @@ -'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 deleted file mode 100644 index 0dd559cb9..000000000 --- a/test/util.js +++ /dev/null @@ -1,15 +0,0 @@ -'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 deleted file mode 100644 index a7f59897a..000000000 --- a/test/websocket_client.js +++ /dev/null @@ -1,191 +0,0 @@ -'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/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..2c85b2d99 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs + "include": ["src", "types"], + "compilerOptions": { + "module": "esnext", + "lib": ["dom", "esnext"], + "importHelpers": true, + // output .d.ts declaration files for consumers + "declaration": true, + // output .js.map sourcemap files for consumers + "sourceMap": true, + // match output dir to input dir. e.g. dist/index instead of dist/src/index + "rootDir": "./src", + // stricter type-checking for stronger correctness. Recommended by TS + "strict": true, + // linter checks for common issues + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative + "noUnusedLocals": true, + "noUnusedParameters": true, + // use Node's module resolution algorithm, instead of the legacy TS one + "moduleResolution": "node", + // transpile JSX to React.createElement + "jsx": "react", + // interop between ESM and CJS modules. Recommended by TS + "esModuleInterop": true, + // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS + "skipLibCheck": true, + // error out if import and file system have a casing mismatch. Recommended by TS + "forceConsistentCasingInFileNames": true, + // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` + "noEmit": true, + } +} diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index a3496b103..000000000 --- a/types/index.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -export * from './lib/client' -export * from './lib/connect' -export * from './lib/store' -export * from './lib/client-options' -import { MqttClient } from './lib/client' -export { MqttClient as Client } -export { - QoS, - PacketCmd, - IPacket, - IConnectPacket, - IPublishPacket, - IConnackPacket, - ISubscription, - ISubscribePacket, - ISubackPacket, - IUnsubscribePacket, - IUnsubackPacket, - IPubackPacket, - IPubcompPacket, - IPubrelPacket, - IPubrecPacket, - IPingreqPacket, - IPingrespPacket, - IDisconnectPacket, - 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 deleted file mode 100644 index a8cf962d6..000000000 --- a/types/lib/client-options.d.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { MqttClient } from './client' -import { Store } from './store' -import { ClientOptions } from 'ws' -import { ClientRequestArgs } from 'http' -import { QoS, UserProperties } from 'mqtt-packet' -import { IMessageIdProvider } from './message-id-provider' - -export declare type StorePutCallback = () => void - -export interface IClientOptions extends ISecureClientOptions { - port?: number // port is made into a number subsequently - host?: string // host does NOT include port - hostname?: string - path?: string - protocol?: 'wss' | 'ws' | 'mqtt' | 'mqtts' | 'tcp' | 'ssl' | 'wx' | 'wxs' - - wsOptions?: ClientOptions | ClientRequestArgs - /** - * 10 seconds, set to 0 to disable - */ - keepalive?: number - /** - * 'mqttjs_' + Math.random().toString(16).substr(2, 8) - */ - clientId?: string - /** - * 'MQTT' - */ - protocolId?: string - /** - * 4 - */ - protocolVersion?: number - /** - * true, set to false to receive QoS 1 and 2 messages while offline - */ - clean?: boolean - /** - * 1000 milliseconds, interval between two reconnections - */ - reconnectPeriod?: number - /** - * 30 * 1000 milliseconds, time to wait before a CONNACK is received - */ - connectTimeout?: number - /** - * the username required by your broker, if any - */ - username?: string - /** - * the password required by your broker, if any - */ - password?: string - /** - * a Store for the incoming packets - */ - incomingStore?: Store - /** - * a Store for the outgoing packets - */ - outgoingStore?: Store - queueQoSZero?: boolean - reschedulePings?: boolean - servers?: Array<{ - host: string - port: number - protocol?: 'wss' | 'ws' | 'mqtt' | 'mqtts' | 'tcp' | 'ssl' | 'wx' | 'wxs' - }> - /** - * true, set to false to disable re-subscribe functionality - */ - resubscribe?: boolean - /** - * a message that will sent by the broker automatically when the client disconnect badly. - */ - will?: { - /** - * the topic to publish - */ - topic: string - /** - * the message to publish - */ - payload: Buffer | string - /** - * the QoS - */ - qos: QoS - /** - * the retain flag - */ - retain: boolean, - /* - * properies object of will - * */ - properties?: { - willDelayInterval?: number, - payloadFormatIndicator?: boolean, - messageExpiryInterval?: number, - contentType?: string, - responseTopic?: string, - correlationData?: Buffer, - userProperties?: UserProperties - } - } - transformWsUrl?: (url: string, options: IClientOptions, client: MqttClient) => string, - properties?: { - sessionExpiryInterval?: number, - receiveMaximum?: number, - maximumPacketSize?: number, - topicAliasMaximum?: number, - requestResponseInformation?: boolean, - requestProblemInformation?: boolean, - userProperties?: UserProperties, - authenticationMethod?: string, - authenticationData?: Buffer - }, - messageIdProvider?: IMessageIdProvider -} -export interface ISecureClientOptions { - /** - * optional private keys in PEM format - */ - key?: string | string[] | Buffer | Buffer[] | Object[] - /** - * optional cert chains in PEM format - */ - cert?: string | string[] | Buffer | Buffer[] - /** - * Optionally override the trusted CA certificates in PEM format - */ - ca?: string | string[] | Buffer | Buffer[] - rejectUnauthorized?: boolean -} -export interface IClientPublishOptions { - /** - * the QoS - */ - qos?: QoS - /** - * the retain flag - */ - retain?: boolean - /** - * 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?: UserProperties, - subscriptionIdentifier?: number, - contentType?: string - } - /** - * callback called when message is put into `outgoingStore` - */ - cbStorePut?: StorePutCallback -} -export interface IClientSubscribeOptions { - /** - * the QoS - */ - qos: QoS, - /* - * no local flag - * */ - nl?: boolean, - /* - * Retain As Published flag - * */ - rap?: boolean, - /* - * Retain Handling option - * */ - rh?: number -} -export interface IClientReconnectOptions { - /** - * a Store for the incoming packets - */ - incomingStore?: Store - /** - * a Store for the outgoing packets - */ - outgoingStore?: Store -} diff --git a/types/lib/client.d.ts b/types/lib/client.d.ts deleted file mode 100644 index 94657b6a5..000000000 --- a/types/lib/client.d.ts +++ /dev/null @@ -1,240 +0,0 @@ -/// - -import * as events from 'events' -import { - IClientOptions, - IClientPublishOptions, - IClientSubscribeOptions, - IClientReconnectOptions -} from './client-options' -import { Store } from './store' -import { Packet, IConnectPacket, IPublishPacket, IDisconnectPacket, QoS } from 'mqtt-packet' - -export interface ISubscriptionGrant { - /** - * is a subscribed to topic - */ - topic: string - /** - * is the granted qos level on it, may return 128 on error - */ - qos: QoS | number - /* - * no local flag - * */ - nl?: boolean, - /* - * Retain As Published flag - * */ - rap?: boolean, - /* - * Retain Handling option - * */ - rh?: number -} -export interface ISubscriptionRequest { - /** - * is a subscribed to topic - */ - topic: string - /** - * is the granted qos level on it - */ - qos: QoS - /* - * no local flag - * */ - nl?: boolean, - /* - * Retain As Published flag - * */ - rap?: boolean, - /* - * Retain Handling option - * */ - rh?: number -} -export interface ISubscriptionMap { - /** - * object which has topic names as object keys and as value the options, like {'test1': {qos: 0}, 'test2': {qos: 2}}. - */ - [topic: string]: { - qos: QoS, - nl?: boolean, - rap?: boolean, - rh?: number - } -} - -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: 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 = (error?: Error) => void - -export interface IStream extends events.EventEmitter { - pipe (to: any): any - destroy (): any - end (): any -} -/** - * MqttClient constructor - * - * @param {Stream} stream - stream - * @param {Object} [options] - connection options - * (see Connection#connect) - */ -export declare class MqttClient extends events.EventEmitter { - public connected: boolean - public disconnecting: boolean - public disconnected: boolean - public reconnecting: boolean - public incomingStore: Store - public outgoingStore: Store - public options: IClientOptions - public queueQoSZero: boolean - - 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: '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 - - /** - * publish - publish to - * - * @param {String} topic - topic to publish to - * @param {(String|Buffer)} message - message to publish - * - * @param {Object} [opts] - publish options, includes: - * @param {Number} [opts.qos] - qos level to publish on - * @param {Boolean} [opts.retain] - whether or not to retain the message - * @param {Function}[opts.cbStorePut] - function(){} - * called when message is put into `outgoingStore` - * - * @param {Function} [callback] - function(err){} - * called when publish succeeds or fails - * - * @returns {Client} this - for chaining - * @api public - * - * @example client.publish('topic', 'message') - * @example - * client.publish('topic', 'message', {qos: 1, retain: true}) - * @example client.publish('topic', 'message', console.log) - */ - public publish (topic: string, message: string | Buffer, - opts: IClientPublishOptions, callback?: PacketCallback): this - public publish (topic: string, message: string | Buffer, - callback?: PacketCallback): 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: - * @param {Number} [opts.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': 0, 'topic2': 1}, console.log) - * @example client.subscribe('topic', console.log) - */ - public subscribe (topic: - string - | string[], opts: IClientSubscribeOptions, callback?: ClientSubscribeCallback): this - public subscribe (topic: - string - | string[] - | ISubscriptionMap, callback?: ClientSubscribeCallback): this - - /** - * unsubscribe - unsubscribe from topic(s) - * - * @param {String, Array} topic - topics to unsubscribe from - * @param {Object} opts - opts of unsubscribe - * @param {Function} [callback] - callback fired on unsuback - * @returns {MqttClient} this - for chaining - * @api public - * @example client.unsubscribe('topic') - * @example client.unsubscribe('topic', console.log) - * @example client.unsubscribe('topic', opts, console.log) - */ - public unsubscribe (topic: string | string[], opts?: Object, callback?: PacketCallback): 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 - opts disconnect - * @param {Function} cb - called when the client has been closed - * - * @api public - */ - public end (force?: boolean, opts?: Object, cb?: CloseCallback): 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} mid - messageId to remove message - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.removeOutgoingMessage(client.getLastMessageId()) - */ - public removeOutgoingMessage (mid: number): 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 - */ - public reconnect (opts?: IClientReconnectOptions): this - - /** - * Handle messages with backpressure support, one at a time. - * Override at will. - * - * @param packet packet the packet - * @param callback callback call when finished - * @api public - */ - public handleMessage (packet: Packet, callback: PacketCallback): void - - /** - * getLastMessageId - */ - public getLastMessageId (): number -} -export { IClientOptions } diff --git a/types/lib/connect/index.d.ts b/types/lib/connect/index.d.ts deleted file mode 100644 index d058373f2..000000000 --- a/types/lib/connect/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IClientOptions, MqttClient } from '../client' -/** - * connect - connect to an MQTT broker. - * - * @param {String} [brokerUrl] - url of the broker, optional - * @param {Object} opts - see MqttClient#constructor - */ -declare function connect (brokerUrl?: string | any, opts?: IClientOptions): MqttClient -export { connect } -export { MqttClient } diff --git a/types/lib/default-message-id-provider.d.ts b/types/lib/default-message-id-provider.d.ts deleted file mode 100644 index fafaa4c9b..000000000 --- a/types/lib/default-message-id-provider.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 9468cf3e2..000000000 --- a/types/lib/message-id-provider.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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/store-options.d.ts b/types/lib/store-options.d.ts deleted file mode 100644 index 03a175e7f..000000000 --- a/types/lib/store-options.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IStoreOptions { - /** - * true, clear _inflights at close - */ - clean?: boolean -} diff --git a/types/lib/store.d.ts b/types/lib/store.d.ts deleted file mode 100644 index dd4c17f20..000000000 --- a/types/lib/store.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - IStoreOptions -} from './store-options' - -/** - * In-memory implementation of the message store - * This can actually be saved into files. - * - */ -declare class Store { - /** - * Store constructor - * - * @param {Object} [options] - store options - */ - constructor (options: IStoreOptions) - - /** - * Adds a packet to the store, a packet is - * anything that has a messageId property. - * - */ - public put (packet: any, cb?: Function): this - - /** - * Creates a stream with all the packets in the store - * - */ - public createStream (): any - - /** - * deletes a packet from the store. - */ - public del (packet: any, cb: Function): this - - /** - * get a packet from the store. - */ - public get (packet: any, cb: Function): this - - /** - * Close the store - */ - public close (cb: Function): void -} -export { Store } diff --git a/types/lib/unique-message-id-provider.d.ts b/types/lib/unique-message-id-provider.d.ts deleted file mode 100644 index 0941b2865..000000000 --- a/types/lib/unique-message-id-provider.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 80381c874c07e83be8ca1b7141488a9499373c9c Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Mon, 27 Sep 2021 08:58:30 -0700 Subject: [PATCH 4/9] updates --- package.json | 1 + src/client.ts | 560 ++++-------------- src/defaultClientId.ts | 3 + src/defaultConnectOptions.ts | 10 + ...rovider.ts => defaultMessageIdProvider.ts} | 0 src/handlers/connect.ts | 219 +++---- src/handlers/index.ts | 41 +- src/index.ts | 106 ++-- src/isBrowser.ts | 11 + src/store.ts | 128 ---- src/utils.ts | 27 + src/write.ts | 26 + 12 files changed, 370 insertions(+), 762 deletions(-) create mode 100644 src/defaultClientId.ts create mode 100644 src/defaultConnectOptions.ts rename src/{default-message-id-provider.ts => defaultMessageIdProvider.ts} (100%) create mode 100644 src/isBrowser.ts delete mode 100644 src/store.ts create mode 100644 src/utils.ts create mode 100644 src/write.ts diff --git a/package.json b/package.json index 3a4e3ac79..5ae2ec54f 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "xtend": "^4.0.2" }, "devDependencies": { + "@types/end-of-stream": "^1.4.1", "@types/node": "^16.7.1", "aedes": "^0.46.1", "airtap": "^4.0.3", diff --git a/src/client.ts b/src/client.ts index 588de0694..74ca4ce52 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,15 +1,17 @@ 'use strict' -import * as mqtt from 'mqtt-packet' +import { IConnectPacket, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' import { handle } from './handlers' import { ConnectOptions } from '.' -import { EventEmitter } from 'stream' +import { Duplex, EventEmitter } from 'stream' import { connectionFactory } from './connectionFactory' +import eos from 'end-of-stream' +import { defaultConnectOptions } from './defaultConnectOptions' // const eventEmitter = require('events') // const mqttErrors = require('errors') -const logger = require('pino')() +// const logger = require('pino')() export class MqttClient extends EventEmitter { static isBrowser: boolean // This can be the global check for browser compatibility. @@ -18,26 +20,24 @@ export class MqttClient extends EventEmitter { connected: boolean errored: boolean id: any - clean: boolean + clean?: boolean version: any - _isBrowser: boolean protocol: any port: any hostname: any rejectUnauthorized: any - stream: any + conn: Duplex _reconnectCount: number _disconnected: boolean _authorized: boolean - _parser: mqtt.Parser - _defaultConnectOptions: { keepalive: number; reschedulePings: boolean; protocolId: string; protocolVersion: number; reconnectPeriod: number; connectTimeout: number; clean: boolean; resubscribe: boolean } + _parser: MqttParser _options: any _parsingBatch: any - connackSent: boolean + connackSent: boolean = false _queueLimit: any queue: any options: any - disconnected: boolean + disconnected: boolean = true incomingStore: any outgoingStore: any _deferredReconnect: any @@ -45,7 +45,7 @@ export class MqttClient extends EventEmitter { reconnectTimer: any reconnecting: any pingTimer: any - queueQoSZero: boolean + queueQoSZero: boolean = false _port: any _host: any tlsOptions: any @@ -61,9 +61,9 @@ export class MqttClient extends EventEmitter { username: any password: any customHandleAcks: any - properties: { sessionExpiryInterval: number; receiveMaximum: number; maximumPacketSize: number; topicAliasMaximum: number; requestResponseInformation: boolean; requestProblemInformation: boolean; userPropertis: any; authenticationMethod: string; authenticationData: BinaryData } + properties?: { sessionExpiryInterval: number; receiveMaximum: number; maximumPacketSize: number; topicAliasMaximum: number; requestResponseInformation: boolean; requestProblemInformation: boolean; userPropertis: any; authenticationMethod: string; authenticationData: BinaryData } authPacket: any - will: { + will?: { topic: any; payload: any; qos: number; retain: boolean; properties: { willDelayInterval: number; payloadFormatIndicator: boolean; messageExpiryInterval: number // eslint-disable-next-line camelcase // eslint-disable-next-line camelcase @@ -72,8 +72,8 @@ export class MqttClient extends EventEmitter { contentType: string; responseTopic: string; correlationData: BinaryData; userProperties: any } } - transformWsUrl: (opts: any) => URL - resubscribe: boolean + transformWsUrl?: (opts: any) => URL + resubscribe: boolean = false messageIdProvider: any parserQueue: any[] private _paused: any @@ -99,129 +99,95 @@ export class MqttClient extends EventEmitter { // NOTE: STOP USING OPTIONS PAST THIS POINT // buildStream shouldn't rely on the options object. Let's have the option object used up beforehand and then // essentially discarded, so after this point it is never used again and only class fields are referenced. - this.stream = options.customStreamFactory? options.customStreamFactory(options) : connectionFactory(options) + this.conn = options.customStreamFactory? options.customStreamFactory(options) : connectionFactory(options) this._reconnectCount = 0 this._disconnected = false this._authorized = false - this._parser = mqtt.parser() - this._defaultConnectOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - resubscribe: true - } + this._parser = mqttParser() - this._options = options || { ...this._defaultConnectOptions } + this._options = options || defaultConnectOptions + // Loop through the defaultConnectOptions. If there is an option + // that is a default that has not been provided through the options + // object passed to the constructor, then update that value with the default Option. + for (const [key, value] of Object.entries(defaultConnectOptions)) { + this._options[key] = this._options[key] ?? value + } this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` this._parser.on('packet', this.enqueue) this.once('connected', this.dequeue) this.on('close', this._closeClient) - this.stream.on('readable', this.nextBatch) + this.conn.on('readable', this.nextBatch) this.on('error', this.onError) - this.stream.on('error', this.emit.bind(this, 'error')) + this.conn.on('error', this.emit.bind(this, 'error')) this._parser.on('error', this.emit.bind(this, 'error')) - this.stream.on('end', this.close.bind(this)) - this._eos = eos(this.stream, this.close.bind(this)) + this.conn.on('end', this.close.bind(this)) + this._eos = eos(this.conn, this.close.bind(this)) + } - close (done) { - if (this.closed) { - if (typeof done === 'function') { - done() - } - return - } - - this.closed = true - - this._parser.removeAllListeners('packet') - conn.removeAllListeners('readable') - - this._parser._queue = null - - if (this._keepaliveTimer) { - this._keepaliveTimer.clear() - this._keepaliveInterval = -1 - this._keepaliveTimer = null - } - - if (this._connectTimer) { - clearTimeout(this._connectTimer) - this._connectTimer = null - } - - this._eos() - this._eos = noop - - handleUnsubscribe( - this, - { - unsubscriptions: Object.keys(this.subscriptions) - }, - finish) - - function finish () { - const will = that.will - // _disconnected is set only if client is disconnected with a valid disconnect packet - if (!that._disconnected && will) { - that.broker.authorizePublish(that, will, function (err) { - if (err) { return done() } - that.broker.publish(will, that, done) - - function done () { - that.broker.persistence.delWill({ - id: that.id, - brokerId: that.broker.id - }, noop) - } + public static async connect(options: ConnectOptions) { + const client = new MqttClient(options) + await client._sendConnect() + await client._sendAuth() + return client + } + + private _sendAuth(): Promise { + return new Promise((resolve, reject) => { + if ( + this._options.properties && + this._options.properties.authenticationMethod && + this._options.authPacket && + typeof this._options.authPacket === 'object') { + var authPacket = {cmd: 'auth', reasonCode: 0, ...this._options.authPacket} + try { + // TODO: Should we worry about the 'drain' event?? See old code. + // If a call to stream.write(chunk) returns false, the 'drain' event will + // be emitted when it is appropriate to resume writing data to the stream. + writeToStream(authPacket, this.conn, this._options) + resolve() + } catch (e) { + reject (e) + }} + }) + } + + private _sendConnect(): Promise { + const packet: IConnectPacket = { + cmd: 'connect', + clientId: this._options.clientId, + protocolVersion: this._options.protocolVersion, + protocolId: this._options.protocolId, + clean: this._options.clean, + keepalive: this._options.keepalive, + username: this._options.username, + password: this._options.password, + will: this._options.will, + properties: this._options.properties + } + + this.emit('packetsend', packet) + return new Promise((resolve, reject) => { + try { + setImmediate(() => { + writeToStream(packet, this._options.conn, this._options._options) + resolve() }) + } catch (e) { + reject(e) } - that.will = null // this function might be called twice - that._will = null - - that.connected = false - that.connecting = false - - conn.removeAllListeners('error') - conn.on('error', noop) - - if (that.broker.clients[that.id] && that._authorized) { - that.broker.unregisterClient(that) - } - - // clear up the drain event listeners - that.conn.emit('drain') - that.conn.removeAllListeners('drain') - - conn.destroy() - - if (typeof done === 'function') { - done() - } - } + }); } - onError (err) { - if (!err) return - - this.errored = true - this.stream.removeAllListeners('error') - this.stream.on('error', noop) - // hack to clean up the write callbacks in case of error - const state = this.conn._writableState - const list = typeof state.getBuffer === 'function' ? state.getBuffer() : state.buffer - list.forEach(drainRequest) - this.broker.emit(this.id ? 'clientError' : 'connectionError', this, err) - this.close() + close (_done: any) { + } + + onError (_err: any) { } _injestOptions(options: ConnectOptions) { @@ -251,7 +217,7 @@ export class MqttClient extends EventEmitter { this.messageIdProvider = options.messageIdProvider } - async enqueue (packet) { + async enqueue (packet: string) { this._parsingBatch++ // already connected or it's the first packet if (this.connackSent || this._parsingBatch === 1) { @@ -268,13 +234,16 @@ export class MqttClient extends EventEmitter { async dequeue () { const q = this.parserQueue - for (let i = 0, len = q.length; i < len; i++) { - const result = await handle(this, q[i]) - this.nextBatch(result) + if (q) { + for (let i = 0, len = q.length; i < len; i++) { + const result = await handle(this, q[i]) + this.nextBatch(result) + } } + this.parserQueue = null } - nextBatch (err) { + nextBatch (err: void) { if (err) { this.emit('error', err) return @@ -287,7 +256,7 @@ export class MqttClient extends EventEmitter { this._parsingBatch-- if (this._parsingBatch <= 0) { this._parsingBatch = 0 - const buf = this.stream.read(null) + const buf = this.conn.read(null) if (buf) { this._parser.parse(buf) } @@ -297,183 +266,34 @@ export class MqttClient extends EventEmitter { - deliver0 = function deliverQoS0 (_packet, cb) { - const toForward = dedupe(this, _packet) && - this.broker.authorizeForward(this, _packet) - if (toForward) { - // Give nodejs some time to clear stacks, or we will see - // "Maximum call stack size exceeded" in a very high load - setImmediate(() => { - const packet = new Packet(toForward, broker) - packet.qos = 0 - write(this, packet, function (err) { - this._onError(err) - cb() // don't pass the error here or it will be thrown by mqemitter - }) - }) - } else { - setImmediate(cb) - } + deliver0 = function deliverQoS0 (_packet: any, _cb: any) { } _closeClient () { - logger('close :: connected set to `false`') - this.connected = false - - logger('close :: clearing connackTimer') - clearTimeout(this.connackTimer) - - logger('close :: clearing ping timer') - if (this.pingTimer !== null) { - this.pingTimer.clear() - this.pingTimer = null - } - - logger('close :: calling _setupReconnect') - this._setupReconnect() } - connackTimer(connackTimer: any) { + + connackTimer(_connackTimer: any) { throw new Error('Method not implemented.') } _sendQueuedPackets () { - const queue = this.queue - - function deliver () { - const entry = queue.shift() - logger('deliver :: entry %o', entry) - let packet = null - - if (!entry) { - return - } - - packet = entry.packet - logger('deliver :: call _sendPacket for %o', packet) - let send = true - if (packet.messageId && packet.messageId !== 0) { - if (!this.messageIdProvider.register(packet.messageId)) { - packet.messageeId = this.messageIdProvider.allocate() - if (packet.messageId === null) { - send = false - } - } - } - if (send) { - this._sendPacket( - packet, - function (err) { - if (entry.cb) { - entry.cb(err) - } - deliver() - } - ) - } else { - logger('messageId: %d has already used.', packet.messageId) - deliver() - } - } - - deliver() } - async publish (topic, message, opts) { - const result = await handle.publish(this, message) + async publish (_topic: any, message: string, _opts: any) { + const result = await handle(this, message) return result } - async subscribe (packet) { - if (!packet.subscriptions) { - packet = { subscriptions: Array.isArray(packet) ? packet : [packet] } - } - const result = await handle.subscribe(this, packet) - return result + async subscribe (_packet: any) { } - async unsubscribe (packet) { - const result = await handle.unsubscribe(this, packet) - return result + async unsubscribe (_packet: any) { } - async end (force, opts) { - - logger('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 - } - - logger('end :: cb? %s', !!cb) - cb = cb || nop - - function closeStores () { - logger('end :: closeStores: closing incoming and outgoing stores') - this.disconnected = true - this.incomingStore.close((e1) => { - this.outgoingStore.close((e2) => { - logger('end :: closeStores: emitting end') - this.emit('end') - if (cb) { - const err = e1 || e2 - logger('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 - logger('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) - that._cleanUp(force, () => { - logger('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 - logger('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) - this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) - } else { - logger('end :: (%s) :: immediately calling finish', that.options.clientId) - finish() - } - - return this + async end (_force: any, _opts: any) { } - outgoing(outgoing: any) { - throw new Error('Method not implemented.') + + outgoing(_outgoing: any) { } /** @@ -486,7 +306,7 @@ export class MqttClient extends EventEmitter { * * @example client.removeOutgoingMessage(client.getLastAllocated()); */ - removeOutgoingMessage (messageId) { + removeOutgoingMessage (messageId: string | number) { const cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null delete this.outgoing[messageId] this.outgoingStore.del({ messageId: messageId }, function () { @@ -495,184 +315,18 @@ export class MqttClient extends EventEmitter { return this } - reconnect (opts) { - logger('client reconnect') - const that = this - const 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 () { - logger('_reconnect: emitting reconnect to client') - this.emit('reconnect') - if (this.connected) { - this.end(() => { this._setupStream() }) - logger('client already connected. disconnecting first.') - } else { - logger('_reconnect: calling _setupStream') - this._setupStream() - } - } - _setupStream() { - throw new Error('Method not implemented.') + reconnect (_opts: any) { } _setupReconnect () { - if (!this.disconnecting && !this.reconnectTimer && (this.options.reconnectPeriod > 0)) { - if (!this.reconnecting) { - logger('_setupReconnect :: emit `offline` state') - this.emit('offline') - logger('_setupReconnect :: set `reconnecting` to `true`') - this.reconnecting = true - } - logger('_setupReconnect :: setting reconnectTimer for %d ms', this.options.reconnectPeriod) - this.reconnectTimer = setInterval(() => { - logger('reconnectTimer :: reconnect triggered!') - this._reconnect() - }, this.options.reconnectPeriod) - } else { - logger('_setupReconnect :: doing nothing...') - } } _clearReconnect () { - logger('_clearReconnect : clearing reconnect timer') - if (this.reconnectTimer) { - clearInterval(this.reconnectTimer) - this.reconnectTimer = null - } - } - - async _cleanUp (forced) { - const opts = arguments[2] - if (done) { - logger('_cleanUp :: done callback provided for on stream close') - this.stream.on('close', done) - } - - logger('_cleanUp :: forced? %s', forced) - if (forced) { - if ((this.options.reconnectPeriod === 0) && this.options.clean) { - flush(this.outgoing) - } - logger('_cleanUp :: (%s) :: destroying stream', this.options.clientId) - this.stream.destroy() - } else { - const packet = xtend({ cmd: 'disconnect' }, opts) - logger('_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) { - logger('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') - this._clearReconnect() - this._setupReconnect() - } - - if (this.pingTimer !== null) { - logger('_cleanUp :: clearing pingTimer') - this.pingTimer.clear() - this.pingTimer = null - } - - if (done && !this.connected) { - logger('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) - this.stream.removeListener('close', done) - done() - } } - async _sendPacket (packet) { - logger('_sendPacket :: (%s) :: start', this.options.clientId) - cbStorePut = cbStorePut || nop - - if (!this.connected) { - logger('_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 - } - } - _shiftPingInterval() { - throw new Error('Method not implemented.') + async _cleanUp (_forced: any) { } - _storePacket (packet) { - 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) { - cb = this.outgoing[packet.messageId] ? this.outgoing[packet.messageId].cb : null - this.outgoingStore.put(packet, function (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - }) - } else if (cb) { - cb(new Error('No connection to broker')) - } + _storePacket (_packet: any) { } -} -function eos(conn: any, arg1: any): any { - throw new Error('Function not implemented.') -} - +} \ No newline at end of file diff --git a/src/defaultClientId.ts b/src/defaultClientId.ts new file mode 100644 index 000000000..b86a7e15c --- /dev/null +++ b/src/defaultClientId.ts @@ -0,0 +1,3 @@ +export function defaultId () { + return 'mqttjs_' + Math.random().toString(16).substr(2, 8) +} \ No newline at end of file diff --git a/src/defaultConnectOptions.ts b/src/defaultConnectOptions.ts new file mode 100644 index 000000000..19a1689fe --- /dev/null +++ b/src/defaultConnectOptions.ts @@ -0,0 +1,10 @@ +export const defaultConnectOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true +} \ No newline at end of file diff --git a/src/default-message-id-provider.ts b/src/defaultMessageIdProvider.ts similarity index 100% rename from src/default-message-id-provider.ts rename to src/defaultMessageIdProvider.ts diff --git a/src/handlers/connect.ts b/src/handlers/connect.ts index ce22d872e..899ee7909 100644 --- a/src/handlers/connect.ts +++ b/src/handlers/connect.ts @@ -1,108 +1,113 @@ - -export async function handleConnect (client, packet) { - if (this.disconnected) { - this.emit('connect', packet) - return - } - - var that = this - - this.messageIdProvider.clear() - this._setupPingTimer() - this._resubscribe(packet) - - 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() +import { MqttClient } from "../client" +import { IConnectPacket } from "mqtt-packet" +import { ConnectOptions } from ".." + +export async function handleConnect (client: MqttClient, opts: ConnectOptions) { + + client.conn + // if (this.disconnected) { + // this.emit('connect', packet) + // return + // } + + // var that = this + + // this.messageIdProvider.clear() + // this._setupPingTimer() + // this._resubscribe(packet) + + // 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() } diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 3e136f9a2..69336860e 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -16,58 +16,57 @@ import { handleSubAck } from './suback' import { handleUnsub } from './unsub' import { handleUnsubAck } from './unsuback' import { handleAuth } from './auth' +import { MqttClient } from '../client' +import { PacketCmd, Packet } from 'mqtt-packet' -export async function handle (client, packet) { +export async function handle (client: MqttClient, cmd: PacketCmd, options: unknown) { let result - switch (packet.cmd) { + switch (cmd) { case 'auth': - result = await handleAuth(client, packet) + // result = await handleAuth(client, options) break case 'connect': - result = await handleConnect(client, packet) + result = await handleConnect(client, options) break case 'connack': - result = await handleConnAck(client, packet) + result = await handleConnAck(client, options) break case 'publish': - result = await handlePub(client, packet) + // result = await handlePub(client, options) break case 'subscribe': - result = await handleSub(client, packet) + // result = await handleSub(client, options) break case 'suback': - result = await handleSubAck(client, packet) + // result = await handleSubAck(client, options) break case 'unsubscribe': - result = await handleUnsub(client, packet) + // result = await handleUnsub(client, options) break case 'unsuback': - result = await handleUnsubAck(client, packet) + // result = await handleUnsubAck(client, options) break case 'pubcomp': - result = await handlePubComp(client, packet) + // result = await handlePubComp(client, options) break case 'puback': - result = await handlePubAck(client, packet) + // result = await handlePubAck(client, options) break case 'pubrel': - result = await handlePubRel(client, packet) + // result = await handlePubRel(client, options) break case 'pubrec': - result = await handlePubRec(client, packet) - break - case 'ping': - result = await handlePing(client, packet) + // result = await handlePubRec(client, options) break case 'pingreq': - result = await handlePingReq(client, packet) + // result = await handlePingReq(client, options) break case 'pingresp': - result = await handlePingResp(client, packet) + // result = await handlePingResp(client, options) break case 'disconnect': - result = await handleDisconnect(client, packet) - client._disconnected = true + // result = await handleDisconnect(client, options) + // client._disconnected = true break } diff --git a/src/index.ts b/src/index.ts index a610cf4e8..5558e3c70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,16 +6,14 @@ */ import { MqttClient } from './client' -import { Store } from './store' -import { DefaultMessageIdProvider } from './default-message-id-provider' +import { DefaultMessageIdProvider } from './defaultMessageIdProvider' import { UniqueMessageIdProvider } from './unique-message-id-provider' -import { urlToHttpOptions } from 'url' import { Duplex } from 'stream' import { TlsOptions } from 'tls' import { Server } from 'http' import {Server as HttpsServer} from 'https' - -type supportedProtocols = 'mqtt' | 'mqtts' | 'ws' | 'wss' +import { isBrowser } from './isBrowser' +import { QoS, UserProperties } from 'mqtt-packet' const protocols = { all : [ @@ -34,7 +32,7 @@ const protocols = { ] } -export const isBrowser = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' +// TODO: export type WsOptions = { backlog: number, @@ -53,55 +51,56 @@ export type WsOptions = { [prop: string]: string } -export interface ConnectOptions { +export interface ConnectOptions { + cmd: 'connect' + clientId: string + protocolVersion?: 4 | 5 | 3 + protocolId?: 'MQTT' | 'MQIsdp' + clean?: boolean + keepalive?: number + username?: string + password?: Buffer + will?: { + topic: string + payload: Buffer + qos?: QoS + retain?: boolean + properties?: { + willDelayInterval?: number, + payloadFormatIndicator?: number, + messageExpiryInterval?: number, + contentType?: string, + responseTopic?: string, + correlationData?: Buffer, + userProperties?: UserProperties + } + } + properties?: { + sessionExpiryInterval?: number, + receiveMaximum?: number, + maximumPacketSize?: number, + topicAliasMaximum?: number, + requestResponseInformation?: boolean, + requestProblemInformation?: boolean, + userProperties?: UserProperties, + authenticationMethod?: string, + authenticationData?: Buffer + } brokerUrl: string | URL wsOptions: {[key: string]: WsOptions | unknown}, tlsOptions: {[key: string]: TlsOptions | unknown}, - keepalive: any, reschedulePings: any, - clientId: any, - protocolId: 'MQIsdp' | 'MQTT', - protocolVersion: any, - clean: any, reconnectPeriod: any, connectTimeout: any, - username: any, - password: any, incomingStore: any, outgoingStore: any, queueQoSZero: any, customHandleAcks: any, - properties: { - sessionExpiryInterval: number, - receiveMaximum: number, - maximumPacketSize: number, - topicAliasMaximum: number, - requestResponseInformation: boolean, - requestProblemInformation: boolean, - userPropertis: any, - authenticationMethod: string, - authenticationData: BinaryData // TODO: Should this be something else? - } authPacket: any, - will: { - topic: any, - payload: any, - qos: number, - retain: boolean, - properties: { - willDelayInterval: number, - payloadFormatIndicator: boolean, - messageExpiryInterval: number, - contentType: string, - responseTopic: string, - correlationData: BinaryData // TODO: is this the right type? - userProperties: any - } - } transformWsUrl: () => URL, resubscribe: boolean, messageIdProvider: any - customStreamFactory: (options) => Duplex + customStreamFactory: (options: ConnectOptions) => Duplex } @@ -127,19 +126,20 @@ function connect (options: ConnectOptions) { // If there is a colon at the end of the provided protocol, replace it with options.brokerUrl.protocol = options.brokerUrl.protocol.replace(/:$/, '') - const validationErr: Error = _validateProtocol(options) + const validationErr: Error | undefined = _validateProtocol(options) if (validationErr) { throw validationErr } - const client = new MqttClient(options) + const client = MqttClient.connect(options) return client } -function _validateProtocol(opts): Error | undefined { - if (opts.cert && opts.key) { - if (opts.protocol) { - if (protocols.secure.indexOf(opts.protocol) === -1) { - const protocolError: Error = formatSecureProtocolError(opts.protocol) +function _validateProtocol(opts: ConnectOptions): Error | undefined { + if (opts.tlsOptions.cert && opts.tlsOptions.key) { + const urlProtocol = (opts.brokerUrl as URL).protocol + if (urlProtocol) { + if (protocols.secure.indexOf(urlProtocol) === -1) { + const protocolError: Error = formatSecureProtocolError(urlProtocol) return protocolError } } else { @@ -150,7 +150,7 @@ function _validateProtocol(opts): Error | undefined { } // if the protocol provided in the options does not exist in the supported protocols... - _ensureBrowserUsesSecureProtocol(opts.protocol) + _ensureBrowserUsesSecureProtocol((opts.brokerUrl as URL).protocol) return } @@ -171,9 +171,9 @@ function formatSecureProtocolError(protocol: string): Error { Use ${secureProtocol} instead.`) } -function _ensureBrowserUsesSecureProtocol(protocol: supportedProtocols): string { - let browserCompatibleProtocol: string - if (Client.isBrowser) { +function _ensureBrowserUsesSecureProtocol(protocol: string): string { + let browserCompatibleProtocol: string = '' + if (isBrowser()) { if (protocol === 'mqtt') { browserCompatibleProtocol = 'ws' } else if (protocol === 'mqtts') { @@ -183,4 +183,4 @@ function _ensureBrowserUsesSecureProtocol(protocol: supportedProtocols): string return browserCompatibleProtocol || protocol } -export {connect, Store, DefaultMessageIdProvider, UniqueMessageIdProvider} +export {connect, DefaultMessageIdProvider, UniqueMessageIdProvider} diff --git a/src/isBrowser.ts b/src/isBrowser.ts new file mode 100644 index 000000000..5b6180743 --- /dev/null +++ b/src/isBrowser.ts @@ -0,0 +1,11 @@ +/** + * isBrowser + * + * Determines if program is running in browser environment. + * @returns boolean + */ + +export function isBrowser (): boolean { + const inBrowser = typeof process !== 'undefined' && process.title === 'browser' + return inBrowser +} \ No newline at end of file diff --git a/src/store.ts b/src/store.ts deleted file mode 100644 index 37809750b..000000000 --- a/src/store.ts +++ /dev/null @@ -1,128 +0,0 @@ -'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/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..e7def5eff --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,27 @@ +export function validateTopic (topic, message) { + const end = topic.length - 1 + const endMinus = end - 1 + const slashInPreEnd = endMinus > 0 && topic.charCodeAt(endMinus) !== 47 + if (topic.length === 0) { // [MQTT-3.8.3-3] + return new Error('impossible to ' + message + ' to an empty topic') + } + for (let i = 0; i < topic.length; i++) { + switch (topic.charCodeAt(i)) { + case 35: { // # + const notAtTheEnd = i !== end + if (notAtTheEnd || slashInPreEnd) { + return new Error('# is only allowed in ' + message + ' in the last position') + } + break + } + case 43: { // + + const pastChar = i < end - 1 && topic.charCodeAt(i + 1) !== 47 + const preChar = i > 1 && topic.charCodeAt(i - 1) !== 47 + if (pastChar || preChar) { + return new Error('+ is only allowed in ' + message + ' between /') + } + break + } + } + } +} diff --git a/src/write.ts b/src/write.ts new file mode 100644 index 000000000..508fb6ec6 --- /dev/null +++ b/src/write.ts @@ -0,0 +1,26 @@ +import mqtt from 'mqtt-packet' +import { MqttClient } from './client' + +export async function write (client: MqttClient, packet: mqtt.Packet) { + let error = null + return new Promise((resolve, reject) => { + + if (client.connecting || client.connected) { + try { + mqtt.writeToStream(packet, client.conn) + if (!client.errored) { + client.conn.once('drain', resolve) + return + } + } catch (e) { + error = new Error('packet received not valid') + } + } else { + error = new Error('connection closed') + } + + if (error) { + reject(error) + } + }) +} \ No newline at end of file From 0d62951f57ca08c305153f3b5797b4b11c39f024 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Thu, 30 Sep 2021 10:02:00 -0700 Subject: [PATCH 5/9] more stuff --- src/client.ts | 224 ++++++++++---------- src/connectionFactory/index.ts | 377 ++++++++++++++++----------------- src/errors.ts | 2 +- src/handlers/auth.ts | 27 +-- src/handlers/connack.ts | 159 ++++++++++++-- src/handlers/connect.ts | 124 ++--------- src/handlers/index.ts | 98 ++++++--- src/handlers/pingreq.ts | 9 +- src/index.ts | 5 +- src/write.ts | 4 +- 10 files changed, 538 insertions(+), 491 deletions(-) diff --git a/src/client.ts b/src/client.ts index 74ca4ce52..8ba1b9c69 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,7 +1,7 @@ 'use strict' -import { IConnectPacket, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' -import { handle } from './handlers' +import { IConnectPacket, IPingreqPacket, Packet, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' +import { handleInboundPackets, handleOutgoingPackets } from './handlers' import { ConnectOptions } from '.' import { Duplex, EventEmitter } from 'stream' import { connectionFactory } from './connectionFactory' @@ -11,46 +11,22 @@ import { defaultConnectOptions } from './defaultConnectOptions' // const eventEmitter = require('events') // const mqttErrors = require('errors') -// const logger = require('pino')() +const logger = require('pino')() export class MqttClient extends EventEmitter { static isBrowser: boolean // This can be the global check for browser compatibility. - closed: boolean - connecting: boolean - connected: boolean - errored: boolean - id: any - clean?: boolean - version: any - protocol: any - port: any - hostname: any - rejectUnauthorized: any - conn: Duplex - _reconnectCount: number - _disconnected: boolean - _authorized: boolean _parser: MqttParser - _options: any - _parsingBatch: any + _options: ConnectOptions connackSent: boolean = false - _queueLimit: any - queue: any - options: any disconnected: boolean = true incomingStore: any outgoingStore: any - _deferredReconnect: any disconnecting: any reconnectTimer: any reconnecting: any pingTimer: any queueQoSZero: boolean = false - _port: any - _host: any - tlsOptions: any - wsOptions: any - brokerUrl: URL + connackTimer: any | undefined keepalive: any reschedulePings: any clientId: any @@ -61,23 +37,25 @@ export class MqttClient extends EventEmitter { username: any password: any customHandleAcks: any - properties?: { sessionExpiryInterval: number; receiveMaximum: number; maximumPacketSize: number; topicAliasMaximum: number; requestResponseInformation: boolean; requestProblemInformation: boolean; userPropertis: any; authenticationMethod: string; authenticationData: BinaryData } authPacket: any - will?: { - topic: any; payload: any; qos: number; retain: boolean; properties: { - willDelayInterval: number; payloadFormatIndicator: boolean; messageExpiryInterval: number // eslint-disable-next-line camelcase - // eslint-disable-next-line camelcase - // TODO: _isBrowser should be a global value and should be standardized.... - // Connect Information - contentType: string; responseTopic: string; correlationData: BinaryData; userProperties: any - } - } - transformWsUrl?: (opts: any) => URL resubscribe: boolean = false messageIdProvider: any - parserQueue: any[] + parserQueue: Packet[] | null private _paused: any - private _eos: any + closed: boolean + connecting: boolean + connected: boolean + errored: boolean + id: null + clean: boolean + version: null + conn: Duplex + _reconnectCount: number + _disconnected: boolean + _authorized: boolean + _eos: () => void + _parsingBatch: any + pingResp: boolean | null constructor (options: ConnectOptions) { super() @@ -90,16 +68,16 @@ export class MqttClient extends EventEmitter { this.clean = true this.version = null this.parserQueue = [] + this.pingResp = null // eslint-disable-next-line camelcase // TODO: _isBrowser should be a global value and should be standardized.... // Using this method to clean up the constructor to do options handling - this._injestOptions(options) + this._options = options || defaultConnectOptions - // NOTE: STOP USING OPTIONS PAST THIS POINT - // buildStream shouldn't rely on the options object. Let's have the option object used up beforehand and then - // essentially discarded, so after this point it is never used again and only class fields are referenced. - this.conn = options.customStreamFactory? options.customStreamFactory(options) : connectionFactory(options) + this.conn = this._options.customStreamFactory? this._options.customStreamFactory(this._options) : connectionFactory(this._options) + // many drain listeners are needed for qos 1 callbacks if the connection is intermittent + this.conn.setMaxListeners(1000) this._reconnectCount = 0 @@ -107,15 +85,18 @@ export class MqttClient extends EventEmitter { this._authorized = false this._parser = mqttParser() - this._options = options || defaultConnectOptions // Loop through the defaultConnectOptions. If there is an option // that is a default that has not been provided through the options // object passed to the constructor, then update that value with the default Option. for (const [key, value] of Object.entries(defaultConnectOptions)) { - this._options[key] = this._options[key] ?? value + // TODO: This type coersion is bad. How can I make it better? + (this._options as any)[key] = this._options[key as keyof ConnectOptions] ?? value } this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` this._parser.on('packet', this.enqueue) + // Echo connection errors + this._parser.on('error', this.emit.bind(this, 'error')) + this.once('connected', this.dequeue) this.on('close', this._closeClient) @@ -137,6 +118,47 @@ export class MqttClient extends EventEmitter { return client } + /** + * _shiftPingInterval - reschedule the ping interval + * + * @api private + */ + _shiftPingInterval () { + 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 + */ + _checkPing () { + logger('_checkPing :: checking ping...') + if (this.pingResp) { + logger('_checkPing :: ping response received. Clearing flag and sending `pingreq`') + this.pingResp = false + const pingPacket: IPingreqPacket = { cmd: 'pingreq' } + handle(this, pingPacket) + } else { + // do a forced cleanup since socket will be in bad shape + logger('_checkPing :: calling _cleanUp with force true') + this._cleanUp(true) + } + } + + + /** + * _handlePingresp - handle a pingresp + * + * @api private + */ + MqttClient.prototype._handlePingresp = function () { + this.pingResp = true + } + + private _sendAuth(): Promise { return new Promise((resolve, reject) => { if ( @@ -157,8 +179,8 @@ export class MqttClient extends EventEmitter { }) } - private _sendConnect(): Promise { - const packet: IConnectPacket = { + private async _sendConnect(): Promise { + const connectPacket: IConnectPacket = { cmd: 'connect', clientId: this._options.clientId, protocolVersion: this._options.protocolVersion, @@ -170,18 +192,21 @@ export class MqttClient extends EventEmitter { will: this._options.will, properties: this._options.properties } - - this.emit('packetsend', packet) - return new Promise((resolve, reject) => { - try { - setImmediate(() => { - writeToStream(packet, this._options.conn, this._options._options) - resolve() - }) - } catch (e) { - reject(e) + + const connectResult = await handleOutgoingPackets(this, connectPacket) + // auth + if (this._options.properties) { + if (!this._options.properties.authenticationMethod && this._options.properties.authenticationData) { + this.end(() => + this.emit('error', new Error('Packet has no Authentication Method') + )) + return + } + if (this._options.properties.authenticationMethod && this._options.authPacket && typeof this.options.authPacket === 'object') { + var authPacket = {cmd: 'auth', reasonCode: 0, ...this._options.authPacket} + writeToStream(authPacket, this.conn, this._options) } - }); + } } close (_done: any) { @@ -190,41 +215,14 @@ export class MqttClient extends EventEmitter { onError (_err: any) { } - _injestOptions(options: ConnectOptions) { - // Connect Information - this.brokerUrl = options.brokerUrl as URL - this.wsOptions = options.wsOptions - this.tlsOptions = options.tlsOptions - this.keepalive = options.keepalive - this.reschedulePings = options.reschedulePings - this.clientId = options.clientId - this.protocolId = options.protocolId - this.protocolVersion = options.protocolVersion - this.clean = options.clean - this.reconnectPeriod = options.reconnectPeriod - this.connectTimeout = options.connectTimeout - this.username = options.username - this.password = options.password - this.incomingStore = options.incomingStore - this.outgoingStore = options.outgoingStore - this.queueQoSZero = options.queueQoSZero - this.customHandleAcks = options.customHandleAcks - this.properties = options.properties - this.authPacket = options.authPacket - this.will = options.will - this.transformWsUrl = options.transformWsUrl - this.resubscribe = options.resubscribe - this.messageIdProvider = options.messageIdProvider - } - - async enqueue (packet: string) { + async enqueue (packet: Packet) { this._parsingBatch++ // already connected or it's the first packet if (this.connackSent || this._parsingBatch === 1) { - const result = await handle(this, packet) + const result = await handleOutgoingPackets(this, packet) this.nextBatch(result) } else { - if (this.parserQueue.length < this._queueLimit) { + if (this.parserQueue.length < this._options.queueLimit) { this.parserQueue.push(packet) } else { this.emit('error', new Error('Client queue limit reached')) @@ -235,20 +233,25 @@ export class MqttClient extends EventEmitter { async dequeue () { const q = this.parserQueue if (q) { + // This will loop through all of the packets stored in the ParserQueue + // If there are errors while sending any of the packets an error will be + // emitted but it will continue through the queue. for (let i = 0, len = q.length; i < len; i++) { - const result = await handle(this, q[i]) - this.nextBatch(result) + let err: Error | undefined + try { + await handleOutgoingPackets(this, q[i]) + } catch (e) { + this.emit('error', err) + } + this.nextBatch() } } this.parserQueue = null } - nextBatch (err: void) { - if (err) { - this.emit('error', err) - return - } - + nextBatch () { + // NOTE: removed error checking for nextbatch. Should be + // handled before this function is called from now on. if (this._paused) { return } @@ -256,7 +259,9 @@ export class MqttClient extends EventEmitter { this._parsingBatch-- if (this._parsingBatch <= 0) { this._parsingBatch = 0 - const buf = this.conn.read(null) + // The readable.read() method pulls some data out of the internal buffer + // and returns it. If no data available to be read, null is returned. + const buf = this.conn.read() if (buf) { this._parser.parse(buf) } @@ -272,25 +277,26 @@ export class MqttClient extends EventEmitter { _closeClient () { } - connackTimer(_connackTimer: any) { - throw new Error('Method not implemented.') - } - _sendQueuedPackets () { } - async publish (_topic: any, message: string, _opts: any) { + async publish (topic: any, message: string, opts: any) { + const defaultPublishOpts = {qos: 0, retain: false, dup: false} + const publishOpts = {...defaultPublishOpts, ...opts} const result = await handle(this, message) return result } - async subscribe (_packet: any) { + async subscribe (packet: any) { + return new Error('subscribe is not implemented.') } - async unsubscribe (_packet: any) { + async unsubscribe (packet: any) { + return new Error('unsubscribe is not implemented.') } - async end (_force: any, _opts: any) { + async end (force?: any, opts?: any) { + return new Error('end is not implemented.') } outgoing(_outgoing: any) { diff --git a/src/connectionFactory/index.ts b/src/connectionFactory/index.ts index b7e4407e1..3f292e1b8 100644 --- a/src/connectionFactory/index.ts +++ b/src/connectionFactory/index.ts @@ -1,7 +1,8 @@ import net from 'net' import { Duplex } from 'stream' import tls from 'tls' -import { ConnectOptions, isBrowser } from '..' +import { ConnectOptions } from '..' +import { isBrowser } from '../isBrowser' const logger = require('pino')() @@ -33,10 +34,12 @@ export function connectionFactory (options: ConnectOptions): Duplex { } }) - function handleTLSerrors (err) { + const handleTLSerrors = (err: Error) => { // How can I get verify this error is a tls error? - if (rejectUnauthorized) { - client.emit('error', err) + // TODO: In the old version this was emitted via the client. + // We need to make this better. + if (options.tlsOptions.rejectUnauthorized) { + connection.emit('error', err) } // close this connection to match the behaviour of net @@ -51,29 +54,22 @@ export function connectionFactory (options: ConnectOptions): Duplex { return connection } case 'ws': { + const url = options.transformWsUrl ? options.transformWsUrl(options.brokerUrl) : options.brokerUrl as URL const webSocketOptions: WebSocketOptions = { - url: client.transformWsUrl ? client.transformWsUrl(url, client) : url, + url: url, hostname: url.hostname || 'localhost', port: url.port || url.protocol === 'wss' ? 443 : 80, protocol: url.protocol, path: url.pathname || '/', - wsOptions: client.wsOptions || {} + wsOptions: options.wsOptions || {} } - - // Add cert/key/ca etc options - const wssOptions = ['rejectUnauthorized', 'ca', 'cert', 'key', 'pfx', 'passphrase'] - wssOptions.forEach(function (prop) { - // TODO: Validate this works if the property is on the client - if (!client.wsOptions.hasOwnProperty(prop) && client.hasOwnProperty(prop)) { - webSocketOptions.wsOptions[prop] = client[prop] - } - }) - if (isBrowser) { - _buildWebSocketStreamBrowser(webSocketOptions) + if (isBrowser()) { + return _buildWebSocketStreamBrowser(webSocketOptions) } else { - this._buildWebSocketStream(webSocketOptions) + return _buildWebSocketStream(webSocketOptions) } - } + } default: + throw new Error('Unrecognized protocol') } } @@ -83,182 +79,177 @@ interface WebSocketOptions { protocol: string, port: number, path: string, - wsOptions: { - rejectUnauthorized: any, - ca: any, - cert: any, - key: any, - pfx: any, - passphrase: any - } + wsOptions: any } -function _buildWebSocketStreamBrowser (opts: WebSocketOptions) { - 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 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) - } - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - const url = buildUrl(opts, client) - /* global WebSocket */ - const socket = new WebSocket(url, [websocketSubProtocol]) - socket.binaryType = 'arraybuffer' - logger('browserStreamBuilder') - let stream - // sets the maximum socket buffer size before throttling - const bufferSize = options.browserBufferSize || 1024 * 512 - - const bufferTimeout = opts.browserBufferTimeout || 1000 - - const coerceToBuffer = !opts.objectMode - - const 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) { - const 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 +function _buildWebSocketStreamBrowser (opts: WebSocketOptions): Duplex { + throw new Error('_buildWebSocketStreamBrowser is not implemented.') + // if (!opts.url.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) + // opts.url.hostname = parsed.hostname + + // if (!opts.url.port) { + // opts.url.port = parsed.port + // } + // } + + // // objectMode should be defined for logic + // if (opts.wsOptions.objectMode === undefined) { + // opts.wsOptions.objectMode = !(opts.binary === true || opts.binary === undefined) + // } + // const websocketSubProtocol = + // (opts.url.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + // ? 'mqttv3.1' + // : 'mqtt' + + // const url = buildUrl(opts, client) + // /* global WebSocket */ + // const socket = new WebSocket(url, [websocketSubProtocol]) + // socket.binaryType = 'arraybuffer' + // logger('browserStreamBuilder') + // let stream: Duplex + // // sets the maximum socket buffer size before throttling + // const bufferSize = options.browserBufferSize || 1024 * 512 + + // const bufferTimeout = opts.browserBufferTimeout || 1000 + + // const coerceToBuffer = !opts.objectMode + + // const 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: ConnectOptions, socketWrite: (chunk: any, enc: any, next: any) => any, socketEnd: (done: any) => void) { + // const 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: any) { + // stream.destroy(err) + // } + + // function onmessage (event: { data: any }) { + // 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: string | any[], cb: any) { + // 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: string | ArrayBuffer | { valueOf(): string } | { [Symbol.toPrimitive](hint: "string"): string } | Blob | ArrayBufferView, enc: any, next: (arg0: unknown) => void) { + // 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: () => void) { + // socket.close() + // done() + // } + + // // end methods for browserStreamBuilder + + // return stream } -function _buildWebSocketStream (opts: WebSocketOptions) { - if (!isBrowser && opts.protocol === 'wss') { - - } - const url = buildUrl(options, client) - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) - const webSocketStream = createWebSocketStream(socket, options.wsOptions) - webSocketStream.url = url - socket.on('close', () => { webSocketStream.destroy() }) - return webSocketStream +function _buildWebSocketStream (opts: WebSocketOptions): Duplex { + throw new Error('_buildWebSocketStreamBrowser is not implemented.') + // if (!isBrowser && opts.protocol === 'wss') { + + // } + // const url = buildUrl(options, client) + // const websocketSubProtocol = + // (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + // ? 'mqttv3.1' + // : 'mqtt' + + // const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) + // const webSocketStream = createWebSocketStream(socket, options.wsOptions) + // webSocketStream.url = url + // socket.on('close', () => { webSocketStream.destroy() }) + // return webSocketStream } diff --git a/src/errors.ts b/src/errors.ts index 5ad54882e..83ce0b084 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,6 +1,6 @@ 'use strict' -export var errors = { +export const ReasonCodeErrors = { 0: '', 1: 'Unacceptable protocol version', 2: 'Identifier rejected', diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index 58102ed1b..04774bc64 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -1,22 +1,5 @@ -interface AuthProperties { - authenticationMethod: string, - authenticationData: Buffer, - reasonString: string, - userProperties: {[x: string]: unknown} -} - -export async function handleAuth (direction: 'toServer' | 'fromServer', reasonCode: number, props: AuthProperties) { - const packet = { - cmd: 'auth', - reasonCode: reasonCode, // MQTT 5.0 code - properties: { // properties MQTT 5.0 - authenticationMethod: props.authenticationMethod, - authenticationData: props.authenticationData, - reasonString: props.reasonString, - userProperties: props.userProperties - } - } -} +import { IAuthPacket, writeToStream } from "mqtt-packet" +import { MqttClient } from "../client" /** * Authentication in MQTT v5 @@ -53,4 +36,8 @@ export async function handleAuth (direction: 'toServer' | 'fromServer', reasonCo * } * } * } - */ \ No newline at end of file + */ +export async function handleAuth (client: MqttClient, packet: IAuthPacket) { + client.emit('packetsend', packet) + writeToStream(packet, client.conn, client._options) +} diff --git a/src/handlers/connack.ts b/src/handlers/connack.ts index f3a1711e4..363e03585 100644 --- a/src/handlers/connack.ts +++ b/src/handlers/connack.ts @@ -1,36 +1,153 @@ -export async function handleConnAck (packet) { - this.connackTimer = setTimeout(function () { - debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') - that._cleanUp(true) - }, this.options.connectTimeout) +import { debug } from "console" +import { IConnackPacket } from "mqtt-packet" +import { MqttClient } from "../client" +import { ReasonCodeErrors } from "../errors" - var options = this.options - var version = options.protocolVersion - var rc = version === 5 ? packet.reasonCode : packet.returnCode - - clearTimeout(this.connackTimer) +export async function handleConnAck (this: any, client: MqttClient, packet: IConnackPacket) { + // TODO: What number should this be if there is there is no reason code provided by the broker??? + const rc: number = (client._options.protocolVersion === 5 ? packet.reasonCode : packet.returnCode) || -1 + // Once the CONNACK Packet is received, we can clear the connackTimer on the client, + // so as not to trigger an error. + clearTimeout(client.connackTimer) + // if the Packet has properties, we want to update the options in the client. if (packet.properties) { if (packet.properties.topicAliasMaximum) { - if (!options.properties) { options.properties = {} } - options.properties.topicAliasMaximum = packet.properties.topicAliasMaximum + if (!client._options.properties) { client._options.properties = {} } + client._options.properties.topicAliasMaximum = packet.properties.topicAliasMaximum } - if (packet.properties.serverKeepAlive && options.keepalive) { - options.keepalive = packet.properties.serverKeepAlive - this._shiftPingInterval() + if (packet.properties.serverKeepAlive && client._options.keepalive) { + client._options.keepalive = packet.properties.serverKeepAlive + // When sending a packet, reschedule the ping timer + client._shiftPingInterval() } if (packet.properties.maximumPacketSize) { - if (!options.properties) { options.properties = {} } - options.properties.maximumPacketSize = packet.properties.maximumPacketSize + if (!client._options.properties) { client._options.properties = {} } + client._options.properties.maximumPacketSize = packet.properties.maximumPacketSize } } + // The response code indicates whether the connection is successful. + // If there is a response code of 0, then the client is connected to the + // broker successfully and it can continue functioning. + // Otherwise it should emit an error indicating the connection to the + // specified broker was refused, providing the translation for the specific + // error code. + // Error cod + if (rc === 0) { - this.reconnecting = false - this._onConnect(packet) - } else if (rc > 0) { - var err = new Error('Connection refused: ' + errors[rc]) + client.reconnecting = false + _onConnect(client, packet) + } else if (rc > 0 && rc in Object.keys(ReasonCodeErrors)) { + var err = new ConnectionRefusedError('Connection refused: ' + ReasonCodeErrors[rc as keyof ReasonCodeErrors]) err.code = rc this.emit('error', err) } } + +function _onConnect (client: MqttClient, packet: IConnackPacket) { + if (client.disconnected) { + client.emit('connect', packet) + return + } + + + client.messageIdProvider.clear() + client._setupPingTimer() + client._resubscribe(packet) + + client.connected = true + + const startStreamProcess = () => { + var outStore = client.outgoingStore.createStream() + + function clearStoreProcessing () { + client._storeProcessing = false + client._packetIdsDuringStoreProcessing = {} + } + + client.once('close', remove) + outStore.on('error', function (err: any) { + clearStoreProcessing() + client._flushStoreProcessingQueue() + client.removeListener('close', remove) + client.emit('error', err) + }) + + function remove () { + outStore.destroy() + outStore = null + client._flushStoreProcessingQueue() + clearStoreProcessing() + } + + function storeDeliver () { + // edge case, we wrapped this twice + if (!outStore) { + return + } + client._storeProcessing = true + + var packet = outStore.read(1) + + var cb: (arg0: any, arg1: any) => void + + if (!packet) { + // read when data is available in the future + outStore.once('readable', storeDeliver) + return + } + + // Skip already processed store packets + if (client._packetIdsDuringStoreProcessing[packet.messageId]) { + storeDeliver() + return + } + + // Avoid unnecessary stream read operations when disconnected + if (!client.disconnecting && !client.reconnectTimer) { + cb = client.outgoing[packet.messageId] ? client.outgoing[packet.messageId].cb : null + client.outgoing[packet.messageId] = { + volatile: false, + cb: function (err: any, status: any) { + // Ensure that the original callback passed in to publish gets invoked + if (cb) { + cb(err, status) + } + + storeDeliver() + } + } + client._packetIdsDuringStoreProcessing[packet.messageId] = true + if (client.messageIdProvider.register(packet.messageId)) { + client._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 client._packetIdsDuringStoreProcessing) { + if (!client._packetIdsDuringStoreProcessing[id]) { + allProcessed = false + break + } + } + if (allProcessed) { + clearStoreProcessing() + client.removeListener('close', remove) + client._invokeAllStoreProcessingQueue() + client.emit('connect', packet) + } else { + startStreamProcess() + } + }) + storeDeliver() + } + // start flowing + startStreamProcess() +} \ No newline at end of file diff --git a/src/handlers/connect.ts b/src/handlers/connect.ts index 899ee7909..7d9d5708d 100644 --- a/src/handlers/connect.ts +++ b/src/handlers/connect.ts @@ -1,113 +1,15 @@ import { MqttClient } from "../client" -import { IConnectPacket } from "mqtt-packet" -import { ConnectOptions } from ".." - -export async function handleConnect (client: MqttClient, opts: ConnectOptions) { - - client.conn - // if (this.disconnected) { - // this.emit('connect', packet) - // return - // } - - // var that = this - - // this.messageIdProvider.clear() - // this._setupPingTimer() - // this._resubscribe(packet) - - // 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() +import { Packet } from "mqtt-packet" +import { write } from "../write" + +export async function handleConnect (client: MqttClient, packet: Packet): Promise { + clearTimeout(client.connackTimer) + client.connackTimer = null + client.connecting = true + client.connackTimer = setTimeout(function () { + client._cleanUp(true) + }, client._options.connectTimeout) + + await write(client, packet) + return } diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 69336860e..2dfaa1b3c 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,35 +1,79 @@ -'use strict' - import { handleConnect } from './connect' import { handleConnAck } from './connack' -import { handleDisconnect } from './disconnect' -import { handlePing } from './ping' +// import { handleDisconnect } from './disconnect' +// import { handlePing } from './ping' import { handlePingReq } from './pingreq' -import { handlePingResp } from './pingresp' -import { handlePub } from './pub' -import { handlePubRec } from './pubrec' -import { handlePubRel } from './pubrel' -import { handlePubComp } from './pubcomp' -import { handlePubAck } from './puback' -import { handleSub } from './sub' -import { handleSubAck } from './suback' -import { handleUnsub } from './unsub' -import { handleUnsubAck } from './unsuback' -import { handleAuth } from './auth' +// import { handlePingResp } from './pingresp' +// import { handlePub } from './pub' +// import { handlePubRec } from './pubrec' +// import { handlePubRel } from './pubrel' +// import { handlePubComp } from './pubcomp' +// import { handlePubAck } from './puback' +// import { handleSub } from './sub' +// import { handleSubAck } from './suback' +// import { handleUnsub } from './unsub' +// import { handleUnsubAck } from './unsuback' +// import { handleAuth } from './auth' import { MqttClient } from '../client' -import { PacketCmd, Packet } from 'mqtt-packet' +import { Packet, writeToStream } from 'mqtt-packet' +import { handleAuth } from './auth' +import { ReasonCodeErrors } from '../errors' + +export class ConnectionRefusedError extends Error { + code?: number +} + +export async function handleInboundPackets(client: MqttClient, packet: Packet): Promise { + /* 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) { + // Server sent an ack in error, ignore it. + return + } -export async function handle (client: MqttClient, cmd: PacketCmd, options: unknown) { + switch (packet.cmd) { + case 'connack': + await handleConnAck(client, packet) + break + 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 ConnectionRefusedError('Publish error: ' + ReasonCodeErrors[`${pubackRC}`]) + err.code = pubackRC + cb(err, packet) + } + delete client.outgoing[messageId] + client.outgoingStore.del(packet, cb) + client.messageIdProvider.deallocate(messageId) + client._invokeStoreProcessingQueue() + case 'unsuback': + // result = await handleUnsubAck(client, options) + break + case 'suback': + // result = await handleSubAck(client, options) + break + } + +} + +export async function handleOutgoingPackets(client: MqttClient, packet: Packet): Promise { let result - switch (cmd) { + switch (packet.cmd) { case 'auth': - // result = await handleAuth(client, options) + // TODO: Does this follow the spec correctly? Shouldn't auth be able to be sent at any time??? + result = await handleAuth(client, packet) break case 'connect': - result = await handleConnect(client, options) - break - case 'connack': - result = await handleConnAck(client, options) + await handleConnect(client, packet) break case 'publish': // result = await handlePub(client, options) @@ -37,15 +81,9 @@ export async function handle (client: MqttClient, cmd: PacketCmd, options: unkno case 'subscribe': // result = await handleSub(client, options) break - case 'suback': - // result = await handleSubAck(client, options) - break case 'unsubscribe': // result = await handleUnsub(client, options) break - case 'unsuback': - // result = await handleUnsubAck(client, options) - break case 'pubcomp': // result = await handlePubComp(client, options) break @@ -59,7 +97,7 @@ export async function handle (client: MqttClient, cmd: PacketCmd, options: unkno // result = await handlePubRec(client, options) break case 'pingreq': - // result = await handlePingReq(client, options) + result = await handlePingReq(client, packet) break case 'pingresp': // result = await handlePingResp(client, options) diff --git a/src/handlers/pingreq.ts b/src/handlers/pingreq.ts index b57dd5816..ceb86aeb2 100644 --- a/src/handlers/pingreq.ts +++ b/src/handlers/pingreq.ts @@ -1,3 +1,8 @@ -export async function handlePingReq () { - return true +import { IPingreqPacket } from "mqtt-packet"; +import { MqttClient } from "../client"; +import { write } from "../write"; + +export async function handlePingReq (client: MqttClient, packet: IPingreqPacket) { + write(client, packet) + // client._sendPacket({ cmd: 'pingreq' }) } diff --git a/src/index.ts b/src/index.ts index 5558e3c70..15d106f05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,7 @@ export type WsOptions = { } export interface ConnectOptions { + queueLimit: number cmd: 'connect' clientId: string protocolVersion?: 4 | 5 | 3 @@ -86,7 +87,7 @@ export interface ConnectOptions { authenticationMethod?: string, authenticationData?: Buffer } - brokerUrl: string | URL + brokerUrl: URL wsOptions: {[key: string]: WsOptions | unknown}, tlsOptions: {[key: string]: TlsOptions | unknown}, reschedulePings: any, @@ -97,7 +98,7 @@ export interface ConnectOptions { queueQoSZero: any, customHandleAcks: any, authPacket: any, - transformWsUrl: () => URL, + transformWsUrl: (options: any) => URL, resubscribe: boolean, messageIdProvider: any customStreamFactory: (options: ConnectOptions) => Duplex diff --git a/src/write.ts b/src/write.ts index 508fb6ec6..0881d3e5a 100644 --- a/src/write.ts +++ b/src/write.ts @@ -1,8 +1,8 @@ import mqtt from 'mqtt-packet' import { MqttClient } from './client' -export async function write (client: MqttClient, packet: mqtt.Packet) { - let error = null +export function write (client: MqttClient, packet: mqtt.Packet): Promise { + let error: Error | null = null return new Promise((resolve, reject) => { if (client.connecting || client.connected) { From bcf5780bab108b368ba1245b9299fc2be7381e57 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Wed, 10 Nov 2021 14:13:43 -0800 Subject: [PATCH 6/9] updates to code --- examples/client/secure-client.js | 42 +- package.json | 9 +- src/client.ts | 542 +++++++++++------- src/handlers/index.ts | 61 +- src/index.ts | 15 +- src/isBrowser.ts | 11 - src/store.ts | 111 ++++ src/topicAliasRecv.ts | 55 ++ src/topicAliasSend.ts | 134 +++++ ...provider.ts => uniqueMessageIdProvider.ts} | 116 ++-- src/write.ts | 62 +- 11 files changed, 836 insertions(+), 322 deletions(-) delete mode 100644 src/isBrowser.ts create mode 100644 src/store.ts create mode 100644 src/topicAliasRecv.ts create mode 100644 src/topicAliasSend.ts rename src/{unique-message-id-provider.ts => uniqueMessageIdProvider.ts} (88%) diff --git a/examples/client/secure-client.js b/examples/client/secure-client.js index fefe65d73..5138ab91a 100644 --- a/examples/client/secure-client.js +++ b/examples/client/secure-client.js @@ -1,24 +1,36 @@ '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')) +const mqtt = require('../..') +const path = require('path') +const fs = require('fs') +const KEY = fs.readFileSync(path.join(__dirname, '../../certtest/ca.key')) +const CERT = fs.readFileSync(path.join(__dirname, '../../certtest/ca.crt')) -var PORT = 8443 - -var options = { +const PORT = 8883 +const brokerUrl = 'mqtts://yosephhub.azure-devices.net' +const options = { + protocolId: 'MQTT', + protocolVersion: 4, + clean: false, + clientId: 'myiothubdevice', + username: 'yosephhub.azure-devices.net/myiothubdevice/?api-version=2021-04-12', + reconnectPeriod: 20, + connectTimeout: 60 * 1000, + keepalive: 180, + reschedulePings: false, 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) -}) +const client = mqtt.connect(brokerUrl, options) +let number = 0 +setInterval(() => { + number++ + // client.subscribe('messages') + client.publish('devices/myiothubdevice/messages/events/', `{ "message": "${number}"}`, { qos: 1 }) + client.on('message', function (topic, message) { + console.log(message) + }) +}, 3000) diff --git a/package.json b/package.json index 5ae2ec54f..6924d836c 100644 --- a/package.json +++ b/package.json @@ -72,18 +72,23 @@ "pump": "^3.0.0", "readable-stream": "^3.6.0", "reinterval": "^1.1.0", + "rfdc": "^1.3.0", "split2": "^3.2.2", "ws": "^8.2.0", "xtend": "^4.0.2" }, "devDependencies": { + "@types/collections": "^5.1.2", "@types/end-of-stream": "^1.4.1", "@types/node": "^16.7.1", + "@types/readable-stream": "^2.3.11", + "@types/rfdc": "^1.2.0", "aedes": "^0.46.1", "airtap": "^4.0.3", "browserify": "^17.0.0", "chai": "^4.3.4", "codecov": "^3.8.3", + "collections": "^5.1.12", "end-of-stream": "^1.4.4", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", @@ -92,14 +97,14 @@ "eslint-plugin-promise": "^5.1.0", "global": "^4.4.0", "mkdirp": "^1.0.4", - "mocha": "^9.1.0", + "mocha": "^9.1.3", "mqtt-connection": "^4.1.0", "nyc": "^15.1.0", "rimraf": "^3.0.2", "should": "^13.2.3", "sinon": "^11.1.2", "snazzy": "^9.0.0", - "standard": "^16.0.3", + "standard": "^16.0.4", "tsdx": "^0.14.1", "typescript": "^4.4.3", "uglify-es": "^3.3.9" diff --git a/src/client.ts b/src/client.ts index 8ba1b9c69..24044a55d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,12 +1,23 @@ 'use strict' -import { IConnectPacket, IPingreqPacket, Packet, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' -import { handleInboundPackets, handleOutgoingPackets } from './handlers' +import { IAuthPacket, IConnackPacket, IConnectPacket, IDisconnectPacket, IPingreqPacket, IPingrespPacket, IPubackPacket, IPubcompPacket, IPublishPacket, IPubrecPacket, IPubrelPacket, ISubackPacket, ISubscribePacket, IUnsubackPacket, IUnsubscribePacket, Packet, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' +import { ConnectionRefusedError, handle } from './handlers' import { ConnectOptions } from '.' -import { Duplex, EventEmitter } from 'stream' +import { Duplex, EventEmitter, Readable } from 'stream' import { connectionFactory } from './connectionFactory' import eos from 'end-of-stream' import { defaultConnectOptions } from './defaultConnectOptions' +import { applyTopicAlias, write } from './write' +import { ReasonCodeErrors } from './errors' +import {TopicAliasSend} from './topicAliasSend' +import {TopicAliasRecv} from './topicAliasRecv' +import rfdc from 'rfdc' +import { debug } from 'console' +import { Store } from './store' +import { rejects } from 'assert' +import { nextTick } from 'process' + +const clone = rfdc() // const eventEmitter = require('events') // const mqttErrors = require('errors') @@ -14,19 +25,17 @@ import { defaultConnectOptions } from './defaultConnectOptions' const logger = require('pino')() export class MqttClient extends EventEmitter { - static isBrowser: boolean // This can be the global check for browser compatibility. - _parser: MqttParser + _incomingPacketParser: MqttParser _options: ConnectOptions - connackSent: boolean = false + connacked: boolean = false disconnected: boolean = true - incomingStore: any - outgoingStore: any + incomingStore: Store + outgoingStore: Store disconnecting: any reconnectTimer: any reconnecting: any pingTimer: any queueQoSZero: boolean = false - connackTimer: any | undefined keepalive: any reschedulePings: any clientId: any @@ -41,7 +50,6 @@ export class MqttClient extends EventEmitter { resubscribe: boolean = false messageIdProvider: any parserQueue: Packet[] | null - private _paused: any closed: boolean connecting: boolean connected: boolean @@ -54,28 +62,75 @@ export class MqttClient extends EventEmitter { _disconnected: boolean _authorized: boolean _eos: () => void - _parsingBatch: any + _parsingBatch: number = 0 pingResp: boolean | null + topicAliasSend?: TopicAliasSend + inflightMessagesThatNeedToBeCleanedUpIfTheConnCloses: {[x: string]: any} + storeProcessingQueue: any[] + topicAliasRecv: TopicAliasRecv + messageIdToTopic: {[x: string]: string[]} + resubscribeTopics: {[x: string]: any} + connectedPromise: () => Promise + _storeProcessingQueue: any[] + outgoing: {[x:string]: any} + + constructor (options: ConnectOptions) { super() - // assume that the options have been validated before instantiating the client. + // assume this the options have been validated before instantiating the client. this.closed = false this.connecting = false this.connected = false + this.connectedPromise = () => { + const promise = new Promise((res, rej) => { + if (this.connected) { + res() + } else { + this.once('connected', res) + } + }); + return promise + } this.errored = false this.id = null this.clean = true this.version = null this.parserQueue = [] this.pingResp = null + this.inflightMessagesThatNeedToBeCleanedUpIfTheConnCloses = {} + this.storeProcessingQueue = [] + this.messageIdToTopic = {} + this.resubscribeTopics = {} + this._storeProcessingQueue = [] + this.outgoing = {} + + + + // eslint-disable-next-line camelcase // TODO: _isBrowser should be a global value and should be standardized.... // Using this method to clean up the constructor to do options handling this._options = options || defaultConnectOptions + + // Loop through the defaultConnectOptions. If there is an option + // this is a default this has not been provided through the options + // object passed to the constructor, then update this value with the default Option. + for (const [key, value] of Object.entries(defaultConnectOptions)) { + // TODO: This type coersion is bad. How can I make it better? + (this._options as any)[key] = this._options[key as keyof ConnectOptions] ?? value + } + this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` + this._options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } + this.conn = this._options.customStreamFactory? this._options.customStreamFactory(this._options) : connectionFactory(this._options) + this.topicAliasRecv = new TopicAliasRecv(this._options.topicAliasMaximum) + + this.outgoingStore = options.outgoingStore || new Store() + this.incomingStore = options.incomingStore || new Store() + // many drain listeners are needed for qos 1 callbacks if the connection is intermittent this.conn.setMaxListeners(1000) @@ -83,256 +138,347 @@ export class MqttClient extends EventEmitter { this._disconnected = false this._authorized = false - this._parser = mqttParser() + this._incomingPacketParser = mqttParser(this._options) + + // Handle incoming packets this are parsed + // NOTE: THis is only handling incoming packets from the + // readable stream of the conn stream. + this._incomingPacketParser.on('packet', this.handleIncomingPacket) - // Loop through the defaultConnectOptions. If there is an option - // that is a default that has not been provided through the options - // object passed to the constructor, then update that value with the default Option. - for (const [key, value] of Object.entries(defaultConnectOptions)) { - // TODO: This type coersion is bad. How can I make it better? - (this._options as any)[key] = this._options[key as keyof ConnectOptions] ?? value - } - this._options.clientId = options.clientId || `mqttjs_ ${Math.random().toString(16).substr(2, 8)}` - this._parser.on('packet', this.enqueue) // Echo connection errors - this._parser.on('error', this.emit.bind(this, 'error')) + this._incomingPacketParser.on('error', this.emit.bind(this, 'error')) - this.once('connected', this.dequeue) + this.once('connected', () => {}) this.on('close', this._closeClient) - this.conn.on('readable', this.nextBatch) + this.conn.on('readable', () => { + let data + + while (data = this.conn.read()) { + // process the data + this._incomingPacketParser.parse(data) + } + }) this.on('error', this.onError) this.conn.on('error', this.emit.bind(this, 'error')) - this._parser.on('error', this.emit.bind(this, 'error')) this.conn.on('end', this.close.bind(this)) this._eos = eos(this.conn, this.close.bind(this)) } - public static async connect(options: ConnectOptions) { - const client = new MqttClient(options) - await client._sendConnect() - await client._sendAuth() - return client + async handleIncomingPacket (packet: Packet): Promise { + switch (packet.cmd) { + case 'connack': + this.emit('connack', packet) + break; + } } /** - * _shiftPingInterval - reschedule the ping interval - * - * @api private + * connect + * @param options + * @returns */ - _shiftPingInterval () { - if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { - this.pingTimer.reschedule(this.options.keepalive * 1000) - } + public static async connect(options: ConnectOptions): Promise { + const client = new MqttClient(options) + await client._sendConnect() + const connackPromise = client.waitForConnack() // client.createAPromiseTimeoutThatResolvesOnConnack() + if (client._options.properties && client._options.properties.authenticationMethod + && client._options.authPacket && typeof client._options.authPacket === 'object') { + await client._sendAuth() + } + const connack: IConnackPacket = await connackPromise + await client._onConnected(connack) + + return client } - /** - * _checkPing - check if a pingresp has come back, and ping the server again - * - * @api private - */ - _checkPing () { - logger('_checkPing :: checking ping...') - if (this.pingResp) { - logger('_checkPing :: ping response received. Clearing flag and sending `pingreq`') - this.pingResp = false - const pingPacket: IPingreqPacket = { cmd: 'pingreq' } - handle(this, pingPacket) + async _cleanUp(forced?: boolean, opts?: any) { + if (forced) { + if ((this._options.reconnectPeriod === 0) && this._options.clean) { + Object.keys(this.inflightMessagesThatNeedToBeCleanedUpIfTheConnCloses).forEach((messageId) => { + if (typeof this.inflightMessagesThatNeedToBeCleanedUpIfTheConnCloses[messageId].cb === 'function') { + this.inflightMessagesThatNeedToBeCleanedUpIfTheConnCloses[messageId].cb(new Error('Connection closed')) + delete this.inflightMessagesThatNeedToBeCleanedUpIfTheConnCloses[messageId] + } + }) } + this.conn.destroy() } else { - // do a forced cleanup since socket will be in bad shape - logger('_checkPing :: calling _cleanUp with force true') - this._cleanUp(true) + let packet = { cmd: 'disconnect' , ...opts} + applyTopicAlias(this, packet) + + if (!this.connected) { + const deferred: any = { + promise: null, + resolve: null, + reject: null + } + deferred.promise = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + setImmediate.bind(null, this.conn.end.bind(this.conn)) + return + } + this.emit('packetsend', packet) + writeToStream(packet, this.conn, this._options) + setImmediate.bind(null, this.conn.end.bind(this.conn)) } - } - - /** - * _handlePingresp - handle a pingresp - * - * @api private - */ - MqttClient.prototype._handlePingresp = function () { - this.pingResp = true - } - - - private _sendAuth(): Promise { - return new Promise((resolve, reject) => { - if ( - this._options.properties && - this._options.properties.authenticationMethod && - this._options.authPacket && - typeof this._options.authPacket === 'object') { - var authPacket = {cmd: 'auth', reasonCode: 0, ...this._options.authPacket} - try { - // TODO: Should we worry about the 'drain' event?? See old code. - // If a call to stream.write(chunk) returns false, the 'drain' event will - // be emitted when it is appropriate to resume writing data to the stream. - writeToStream(authPacket, this.conn, this._options) - resolve() - } catch (e) { - reject (e) - }} - }) - } - - private async _sendConnect(): Promise { - const connectPacket: IConnectPacket = { - cmd: 'connect', - clientId: this._options.clientId, - protocolVersion: this._options.protocolVersion, - protocolId: this._options.protocolId, - clean: this._options.clean, - keepalive: this._options.keepalive, - username: this._options.username, - password: this._options.password, - will: this._options.will, - properties: this._options.properties + if (!this.disconnecting) { + this._clearReconnect() + this._setupReconnect() } - - const connectResult = await handleOutgoingPackets(this, connectPacket) - // auth - if (this._options.properties) { - if (!this._options.properties.authenticationMethod && this._options.properties.authenticationData) { - this.end(() => - this.emit('error', new Error('Packet has no Authentication Method') - )) - return - } - if (this._options.properties.authenticationMethod && this._options.authPacket && typeof this.options.authPacket === 'object') { - var authPacket = {cmd: 'auth', reasonCode: 0, ...this._options.authPacket} - writeToStream(authPacket, this.conn, this._options) - } + + if (this.pingTimer !== null) { + this.pingTimer.clear() + this.pingTimer = null } - } + + if (!this.connected) { + logger('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this._options.clientId) + this.conn.removeListener('close', this.remove) + return - close (_done: any) { + } } - onError (_err: any) { + private async _sendAuth(): Promise { + if ( + this._options.properties && + !this._options.properties.authenticationMethod && + this._options.properties.authenticationData) { + const authPacket = {cmd: 'auth', reasonCode: 0, ...this._options.authPacket} + // TODO: Should we worry about the 'drain' event?? See old code. + // If a call to stream.write(chunk) returns false, the 'drain' event will + // be emitted when it is appropriate to resume writing data to the stream. + writeToStream(authPacket, this.conn, this._options) } +} - async enqueue (packet: Packet) { - this._parsingBatch++ - // already connected or it's the first packet - if (this.connackSent || this._parsingBatch === 1) { - const result = await handleOutgoingPackets(this, packet) - this.nextBatch(result) - } else { - if (this.parserQueue.length < this._options.queueLimit) { - this.parserQueue.push(packet) - } else { - this.emit('error', new Error('Client queue limit reached')) + private waitForConnack(): Promise { + return new Promise((res, rej) => { + const connectionTimeout = () => { + rej('CONNECTION TIMEOUT') } - } + const connackTimer = setTimeout(connectionTimeout, this._options.connectTimeout) + this.on('connack', (connackPacket) => { + this.removeListener('connack', res) + clearTimeout(connackTimer) + res(connackPacket) + }) + }) } - async dequeue () { - const q = this.parserQueue - if (q) { - // This will loop through all of the packets stored in the ParserQueue - // If there are errors while sending any of the packets an error will be - // emitted but it will continue through the queue. - for (let i = 0, len = q.length; i < len; i++) { - let err: Error | undefined - try { - await handleOutgoingPackets(this, q[i]) - } catch (e) { + private async _onConnected(connackPacket: IConnackPacket): Promise { + delete this.topicAliasSend + + if (connackPacket.properties) { + if (connackPacket.properties.topicAliasMaximum) { + if (connackPacket.properties.topicAliasMaximum > 0xffff) { + const err = new Error('topicAliasMaximum from broker is out of range') this.emit('error', err) + throw err + } + if (connackPacket.properties.topicAliasMaximum > 0) { + this.topicAliasSend = new TopicAliasSend(connackPacket.properties.topicAliasMaximum) } - this.nextBatch() } - } - this.parserQueue = null - } - nextBatch () { - // NOTE: removed error checking for nextbatch. Should be - // handled before this function is called from now on. - if (this._paused) { - return - } - - this._parsingBatch-- - if (this._parsingBatch <= 0) { - this._parsingBatch = 0 - // The readable.read() method pulls some data out of the internal buffer - // and returns it. If no data available to be read, null is returned. - const buf = this.conn.read() - if (buf) { - this._parser.parse(buf) + if (connackPacket.properties.maximumPacketSize) { + if (!this._options.properties) { this._options.properties = {} } + this._options.properties.maximumPacketSize = connackPacket.properties.maximumPacketSize } } - } - + const rc: number = (this._options.protocolVersion === 5 ? connackPacket.reasonCode : connackPacket.returnCode) as number + if (rc === 0) { + this.reconnecting = false + return + } else if (rc > 0) { + const err = new ConnectionRefusedError('Connection refused: ' + ReasonCodeErrors[rc as keyof typeof ReasonCodeErrors]) + err.code = rc + this.emit('error', err) + throw err + } + let outStore: Readable | null = await this.outgoingStore.createStream() + const clearStoreProcessing = () => { + } - deliver0 = function deliverQoS0 (_packet: any, _cb: any) { - } + this.once('close', () => { + if (outStore) { + outStore.destroy() + clearStoreProcessing() + } + }) + outStore.on('error', (err) => { + clearStoreProcessing() + this.removeListener('close', remove) + this.emit('error', err) + }) - _closeClient () { + const remove = () => { + if (outStore) { + outStore.destroy() + outStore = null + clearStoreProcessing() + } + } } - _sendQueuedPackets () { + private async _sendConnect(): Promise { + const connectPacket: IConnectPacket = { + cmd: 'connect', + clientId: this._options.clientId, + protocolVersion: this._options.protocolVersion, + protocolId: this._options.protocolId, + clean: this._options.clean, + keepalive: this._options.keepalive, + username: this._options.username, + password: this._options.password, + will: this._options.will, + properties: this._options.properties + } + + await write(this, connectPacket) } - async publish (topic: any, message: string, opts: any) { - const defaultPublishOpts = {qos: 0, retain: false, dup: false} - const publishOpts = {...defaultPublishOpts, ...opts} - const result = await handle(this, message) - return result + close (_done: any) { } - async subscribe (packet: any) { - return new Error('subscribe is not implemented.') + onError (_err: any) { } - async unsubscribe (packet: any) { - return new Error('unsubscribe is not implemented.') + sendPacket(packet: Packet) { + this.emit('packetsend', packet) + writeToStream(packet, this.conn, this._options) } - async end (force?: any, opts?: any) { - return new Error('end is not implemented.') + _clearReconnect (): boolean { + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer) + this.reconnectTimer = null + return true + } + return false } - outgoing(_outgoing: any) { + _setupReconnect (): void { + if (this.disconnecting && this.reconnectTimer && (this._options.reconnectPeriod > 0)) { + if (!this.reconnecting) { + this.emit('offline') + this.reconnecting = true + } + this.reconnectTimer = setInterval(() => { + this._reconnect(), this._options.reconnectPeriod + }) + } } /** - * 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()); + * This is necessary as a method call even from the user. If the client is ever disconnected, they can manually call this, because + * there is no exposed 'connect' method that doesn't create a new client. */ - removeOutgoingMessage (messageId: string | number) { - const 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 (_opts: any) { - } + async _reconnect (): void { + this.emit('reconnect') + this._clearReconnect() + if (this.connected) { + await this.end() + } + + this.conn = this._options.customStreamFactory? this._options.customStreamFactory(this._options) : connectionFactory(this._options) + this.conn.setMaxListeners(1000) + this.conn.on('readable', () => { + let data - _setupReconnect () { - } + while (data = this.conn.read()) { + // process the data + this._incomingPacketParser.parse(data) + } + }) - _clearReconnect () { + this.on('error', this.onError) + this.conn.on('error', this.emit.bind(this, 'error')) + + this.conn.on('end', this.close.bind(this)) + this._eos = eos(this.conn, this.close.bind(this)) + await this._sendConnect() + const connackPromise = this.waitForConnack() + if (this._options.properties && this._options.properties.authenticationMethod + && this._options.authPacket && typeof this._options.authPacket === 'object') { + await this._sendAuth() + } + const connack: IConnackPacket = await connackPromise + await this._onConnected(connack) } - async _cleanUp (_forced: any) { + removeTopicAliasAndRecoverTopicName (packet: IPublishPacket): void { + let alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + + let topic = packet.topic.toString() + if (topic.length === 0) { + // restore topic from alias + if (typeof alias === 'undefined') { + throw new Error('Unregistered Topic Alias') + } else { + topic = this.topicAliasSend?.getTopicByAlias(alias) + if (typeof topic === 'undefined') { + throw new Error('Unregistered Topic Alias') + } else { + packet.topic = topic + } + } + } + if (alias) { + delete packet.properties?.topicAlias + } } - _storePacket (_packet: any) { + async end (force?: boolean, opts?: any) { + const closeStores = async () => { + logger('end :: closeStores: closing incoming and outgoing stores') + this.disconnected = true + try { + await this.incomingStore.close(); + await this.outgoingStore.close(); + } catch (e) { + logger(`error closing stores: ${e}`) + } + this.emit('end') + } + + const finish = async () => { + // defer closesStores of an I/O cycle, + // just to make sure things are + // ok for websockets + logger('end :: (%s) :: finish :: calling _cleanUp with force %s', this._options.clientId, force) + await this._cleanUp(force, opts); + nextTick(closeStores.bind(this)) + } + + if (this.disconnecting) { + 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 + logger('end :: (%s) :: calling finish in 10ms once outgoing is empty', this._options.clientId) + this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) + } else { + debug('end :: (%s) :: immediately calling finish', this._options.clientId) + finish() + } + + return this } } \ No newline at end of file diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 2dfaa1b3c..06438517e 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -38,34 +38,12 @@ export async function handleInboundPackets(client: MqttClient, packet: Packet): } switch (packet.cmd) { - case 'connack': - await handleConnAck(client, packet) - break - 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 ConnectionRefusedError('Publish error: ' + ReasonCodeErrors[`${pubackRC}`]) - err.code = pubackRC - cb(err, packet) - } - delete client.outgoing[messageId] - client.outgoingStore.del(packet, cb) - client.messageIdProvider.deallocate(messageId) - client._invokeStoreProcessingQueue() - case 'unsuback': - // result = await handleUnsubAck(client, options) - break - case 'suback': - // result = await handleSubAck(client, options) - break + } } -export async function handleOutgoingPackets(client: MqttClient, packet: Packet): Promise { +export async function handle(client: MqttClient, packet: Packet): Promise { let result switch (packet.cmd) { case 'auth': @@ -84,21 +62,25 @@ export async function handleOutgoingPackets(client: MqttClient, packet: Packet): case 'unsubscribe': // result = await handleUnsub(client, options) break - case 'pubcomp': - // result = await handlePubComp(client, options) - break - case 'puback': - // result = await handlePubAck(client, options) - break case 'pubrel': - // result = await handlePubRel(client, options) - break case 'pubrec': - // result = await handlePubRec(client, options) - break case 'pingreq': result = await handlePingReq(client, packet) break + case 'pubcomp': + // fallthrough + case 'puback': + var pubackRC = packet.reasonCode + // Callback - we're done + if (pubackRC && pubackRC > 0 && pubackRC !== 16) { + const err = new ConnectionRefusedError('Publish error: ' + ReasonCodeErrors[pubackRC as keyof typeof ReasonCodeErrors]) + err.code = pubackRC + throw err + } + delete client.outgoing[messageId] + client.outgoingStore.del(packet, cb) + client.messageIdProvider.deallocate(messageId) + client._invokeStoreProcessingQueue() case 'pingresp': // result = await handlePingResp(client, options) break @@ -106,6 +88,17 @@ export async function handleOutgoingPackets(client: MqttClient, packet: Packet): // result = await handleDisconnect(client, options) // client._disconnected = true break + case 'connack': + await handleConnAck(client, packet) + break + case 'pubcomp': + // same thing as puback for QoS 2 + case 'unsuback': + // result = await handleUnsubAck(client, options) + break + case 'suback': + // result = await handleSubAck(client, options) + break } return result diff --git a/src/index.ts b/src/index.ts index 15d106f05..76f78e779 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,12 +7,11 @@ import { MqttClient } from './client' import { DefaultMessageIdProvider } from './defaultMessageIdProvider' -import { UniqueMessageIdProvider } from './unique-message-id-provider' +import { UniqueMessageIdProvider } from './uniqueMessageIdProvider' import { Duplex } from 'stream' import { TlsOptions } from 'tls' import { Server } from 'http' import {Server as HttpsServer} from 'https' -import { isBrowser } from './isBrowser' import { QoS, UserProperties } from 'mqtt-packet' const protocols = { @@ -52,6 +51,9 @@ export type WsOptions = { } export interface ConnectOptions { + autoUseTopicAlias: any + autoAssignTopicAlias: any + topicAliasMaximum: number queueLimit: number cmd: 'connect' clientId: string @@ -131,6 +133,11 @@ function connect (options: ConnectOptions) { if (validationErr) { throw validationErr } + + if (!options.messageIdProvider) { + options.messageIdProvider = new DefaultMessageIdProvider() + } + const client = MqttClient.connect(options) return client } @@ -174,7 +181,9 @@ function formatSecureProtocolError(protocol: string): Error { function _ensureBrowserUsesSecureProtocol(protocol: string): string { let browserCompatibleProtocol: string = '' - if (isBrowser()) { + // TODO: This used to be if (isBrowser) but I'm removing isBrowser. We should + // just shim this. + if (false) { if (protocol === 'mqtt') { browserCompatibleProtocol = 'ws' } else if (protocol === 'mqtts') { diff --git a/src/isBrowser.ts b/src/isBrowser.ts deleted file mode 100644 index 5b6180743..000000000 --- a/src/isBrowser.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * isBrowser - * - * Determines if program is running in browser environment. - * @returns boolean - */ - -export function isBrowser (): boolean { - const inBrowser = typeof process !== 'undefined' && process.title === 'browser' - return inBrowser -} \ No newline at end of file diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 000000000..16362dddd --- /dev/null +++ b/src/store.ts @@ -0,0 +1,111 @@ +'use strict' + +import { Packet } from "mqtt-packet" +import {Readable} from 'stream' + +const streamsOpts = { objectMode: true } +const defaultStoreOptions = { + clean: true +} + +/** + * In-memory implementation of the message store + * This can actually be saved into files. + * + * @param {Object} [options] - store options + */ +export class Store { + private _inflights: any + options: any + + constructor(options: any = {}) { + this.options = {...options, defaultStoreOptions} + this._inflights = new Map() + } + + /** + * Adds a packet to the store, a packet is + * anything that has a messageId property. + * + */ + async put (packet: Packet): Promise { + this._inflights.set(packet.messageId, packet) + return this + } + + /** + * Creates a stream with all the packets in the store + * + */ + async createStream () { + const stream = new Readable(streamsOpts) + let destroyed = false + const values: any[] = [] + let i = 0 + + this._inflights.forEach((value: any) => { + 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 + } + + const self = this + + destroyed = true + + setTimeout(function () { + self.emit('close') + }, 0) + } + + return stream + } + + /** + * deletes a packet from the store. + */ + async del (packet: Packet): Promise { + packet = this._inflights.get(packet.messageId) + if (packet) { + this._inflights.delete(packet.messageId) + return packet + } else { + throw new Error('missing packet') + } + } + + + + /** + * get a packet from the store. + */ + async get (packet: Packet): Promise { + packet = this._inflights.get(packet.messageId) + if (packet) { + return packet + } else { + throw new Error('missing packet') + } + } + + /** + * Close the store + */ + async close () { + if (this.options.clean) { + this._inflights = null + } + return + } +} diff --git a/src/topicAliasRecv.ts b/src/topicAliasRecv.ts new file mode 100644 index 000000000..e86b570d9 --- /dev/null +++ b/src/topicAliasRecv.ts @@ -0,0 +1,55 @@ +const logger = require('pino')() + +export class TopicAliasRecv { + max: number + aliasToTopic: {[alias: string]: string} + length?: number + + /** + * Topic Alias receiving manager + * This holds alias to topic map + * @param {Number} [max] - topic alias maximum entries + */ + constructor(max: number) { + if (max > 0) { + if (max > 0xffff) { + throw new Error('MqttClient :: options.topicAliasMaximum is out of range') + } + } + this.max = max + this.aliasToTopic = {} + } + + /** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ + put(topic: string, alias: number): boolean { + logger(`put topic ${topic} in alias ${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 + */ + getTopicByAlias(alias: string | number): string { + return this.aliasToTopic[alias] + + } + + /** + * Clear all entries + */ + clear(): void { + this.aliasToTopic = {} + } +} \ No newline at end of file diff --git a/src/topicAliasSend.ts b/src/topicAliasSend.ts new file mode 100644 index 000000000..e70ef5d7c --- /dev/null +++ b/src/topicAliasSend.ts @@ -0,0 +1,134 @@ +'use strict' + +/** + * Module dependencies + */ +import {LruMap} from 'collections/lru-map' +import {NumberAllocator} from 'number-allocator' + +export class TopicAliasSend { + max: number + aliasToTopic: LruMap + topicToAlias: {[topic: string]: string} + numberAllocator: NumberAllocator + length: number + + /** + * Topic Alias sending manager + * This holds both topic to alias and alias to topic map + * @param {Number} [max] - topic alias maximum entries + */ + constructor(max: number) { + this.max = max + + this.aliasToTopic = new LruMap() + this.topicToAlias = {} + this.numberAllocator = new NumberAllocator(1, 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 + */ + put (topic: string | number, alias: number) { + 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 + */ + getTopicByAlias(alias: any) { + 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 + */ + getAliasByTopic(topic: string | number) { + const alias = this.topicToAlias[topic] + if (typeof alias !== 'undefined') { + this.aliasToTopic.get(alias) // LRU update + } + return alias + } + + /** + * Clear all entries + */ + clear() { + 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 + */ + getLruAlias() { + const alias = this.numberAllocator.firstVacant() + if (alias) return alias + return this.aliasToTopic.min().alias + } +} + + + + + + +/** + * 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/src/unique-message-id-provider.ts b/src/uniqueMessageIdProvider.ts similarity index 88% rename from src/unique-message-id-provider.ts rename to src/uniqueMessageIdProvider.ts index 6362724e8..fdae57193 100644 --- a/src/unique-message-id-provider.ts +++ b/src/uniqueMessageIdProvider.ts @@ -1,57 +1,59 @@ -'use strict' - -import { NumberAllocator } from 'number-allocator' - -export class UniqueMessageIdProvider { - constructor () { - 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. - */ - allocate () { - this.lastId = this.numberAllocator.alloc() - return this.lastId - } - - /** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ - getLastAllocated () { - return this.lastId - } - - /** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ - register (messageId) { - return this.numberAllocator.use(messageId) - } - - /** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ - deallocate (messageId) { - this.numberAllocator.free(messageId) - } - - /** - * clear - * Deallocate all messageIds. - */ - clear () { - this.numberAllocator.clear() - } -} +'use strict' + +import { NumberAllocator } from 'number-allocator' + +export class UniqueMessageIdProvider { + numberAllocator: NumberAllocator + lastId: Number | null | undefined + constructor () { + 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. + */ + allocate () { + this.lastId = this.numberAllocator.alloc() + return this.lastId + } + + /** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ + getLastAllocated () { + return this.lastId + } + + /** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ + register (messageId: Number) { + return this.numberAllocator.use(messageId) + } + + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate (messageId: Number) { + this.numberAllocator.free(messageId) + } + + /** + * clear + * Deallocate all messageIds. + */ + clear () { + this.numberAllocator.clear() + } +} diff --git a/src/write.ts b/src/write.ts index 0881d3e5a..e3372f00f 100644 --- a/src/write.ts +++ b/src/write.ts @@ -1,9 +1,14 @@ -import mqtt from 'mqtt-packet' +import mqtt, { IPacket, Packet } from 'mqtt-packet' import { MqttClient } from './client' +import rfdc from 'rfdc' + +const logger = require('pino')() +const clone = rfdc() export function write (client: MqttClient, packet: mqtt.Packet): Promise { let error: Error | null = null return new Promise((resolve, reject) => { + const topicAliasErr = applyTopicAlias(client, packet) if (client.connecting || client.connected) { try { @@ -23,4 +28,57 @@ export function write (client: MqttClient, packet: mqtt.Packet): Promise { reject(error) } }) -} \ No newline at end of file +} + + +export function applyTopicAlias (client: MqttClient, pkt: Packet): Packet { + let packet = clone(pkt) + 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 + logger('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) + if (!client.topicAliasSend.put(topic, alias)) { + logger('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + throw 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} + logger('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} + logger('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} + logger('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) + } + } + } + } + } else if (alias) { + logger('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + throw new Error('Sending Topic Alias out of range') + } + } + } + return packet +} From 8c4fd3ebe33031f811bd60cfaae006e3ba0c0894 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Wed, 10 Nov 2021 14:25:09 -0800 Subject: [PATCH 7/9] updates --- src/client.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index 24044a55d..d33f6f5a9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,7 +14,6 @@ import {TopicAliasRecv} from './topicAliasRecv' import rfdc from 'rfdc' import { debug } from 'console' import { Store } from './store' -import { rejects } from 'assert' import { nextTick } from 'process' const clone = rfdc() @@ -149,7 +148,17 @@ export class MqttClient extends EventEmitter { this._incomingPacketParser.on('error', this.emit.bind(this, 'error')) this.once('connected', () => {}) - this.on('close', this._closeClient) + this.on('close', () => { + debug('close :: connected set to `false`') + this.connected = false + + if (this.topicAliasRecv) { + this.topicAliasRecv.clear() + } + + debug('close :: calling _setupReconnect') + this._setupReconnect() + }) this.conn.on('readable', () => { let data @@ -239,7 +248,7 @@ export class MqttClient extends EventEmitter { if (!this.connected) { logger('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this._options.clientId) - this.conn.removeListener('close', this.remove) + this.conn.removeListener('close', () => {}) return } @@ -382,7 +391,7 @@ export class MqttClient extends EventEmitter { * This is necessary as a method call even from the user. If the client is ever disconnected, they can manually call this, because * there is no exposed 'connect' method that doesn't create a new client. */ - async _reconnect (): void { + async _reconnect (): Promise { this.emit('reconnect') this._clearReconnect() if (this.connected) { From d42152fbe3ac4aa656774dc43e47f74136c92532 Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Wed, 10 Nov 2021 14:43:58 -0800 Subject: [PATCH 8/9] updates --- package.json | 39 +++-- src/client.ts | 5 +- src/connectionFactory/index.ts | 181 +------------------- src/connectionFactory/webSocketOptions.ts | 8 + src/connectionFactory/websocket.browser.ts | 157 ++++++++++++++++++ src/connectionFactory/websocket.ts | 20 +++ src/handlers/auth.ts | 43 ----- src/handlers/connack.ts | 153 ----------------- src/handlers/connect.ts | 15 -- src/handlers/disconnect.ts | 3 - src/handlers/index.ts | 105 ------------ src/handlers/ping.ts | 27 --- src/handlers/pingreq.ts | 8 - src/handlers/pingresp.ts | 7 - src/handlers/pub.ts | 89 ---------- src/handlers/puback.ts | 80 --------- src/handlers/pubcomp.ts | 80 --------- src/handlers/pubrec.ts | 81 --------- src/handlers/pubrel.ts | 23 --- src/handlers/sub.ts | 182 --------------------- src/handlers/suback.ts | 80 --------- src/handlers/unsub.ts | 93 ----------- src/handlers/unsuback.ts | 80 --------- src/utils/isBrowser.ts | 4 + 24 files changed, 219 insertions(+), 1344 deletions(-) create mode 100644 src/connectionFactory/webSocketOptions.ts create mode 100644 src/connectionFactory/websocket.browser.ts create mode 100644 src/connectionFactory/websocket.ts delete mode 100644 src/handlers/auth.ts delete mode 100644 src/handlers/connack.ts delete mode 100644 src/handlers/connect.ts delete mode 100644 src/handlers/disconnect.ts delete mode 100644 src/handlers/index.ts delete mode 100644 src/handlers/ping.ts delete mode 100644 src/handlers/pingreq.ts delete mode 100644 src/handlers/pingresp.ts delete mode 100644 src/handlers/pub.ts delete mode 100644 src/handlers/puback.ts delete mode 100644 src/handlers/pubcomp.ts delete mode 100644 src/handlers/pubrec.ts delete mode 100644 src/handlers/pubrel.ts delete mode 100644 src/handlers/sub.ts delete mode 100644 src/handlers/suback.ts delete mode 100644 src/handlers/unsub.ts delete mode 100644 src/handlers/unsuback.ts create mode 100644 src/utils/isBrowser.ts diff --git a/package.json b/package.json index 6924d836c..397e46127 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mqtt", "description": "A library for the MQTT protocol", - "version": "4.2.8", + "version": "5.0.0", "contributors": [ "Adam Rudd ", "Matteo Collina (https://github.com/mcollina)", @@ -22,19 +22,30 @@ "main": "mqtt.js", "types": "types/index.d.ts", "scripts": { - "start": "tsdx watch", - "build": "tsdx build", - "test": "tsdx test", - "lint": "tsdx lint", - "prepare": "tsdx build", - "size": "size-limit", - "analyze": "size-limit --why", - "test2": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", - "pretest": "standard | snazzy", - "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", - "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 test2 && codecov" + "build:browser": "tsc -p . && cross-env ONLY_BROWSER=true rollup -c 2>&1", + "build:node": "tsc -p . && cross-env ONLY_NODE=true rollup -c 2>&1", + "build:samples": "echo Obsolete.", + "build:test": "tsc -p . && rollup -c 2>&1", + "build": "tsc -p . && rollup -c 2>&1 && api-extractor run --local", + "check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-* test-dist temp types *.tgz *.log", + "execute:samples": "dev-tool samples run samples-dev", + "extract-api": "tsc -p . && api-extractor run --local", + "format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "integration-test:browser": "karma start --single-run", + "integration-test:node": "nyc mocha -r esm --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 5000000 --full-trace \"dist-esm/test/{,!(browser)/**/}/*.spec.js\"", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json api-extractor.json src test --ext .ts", + "pack": "npm pack 2>&1", + "prebuild": "npm run clean", + "test:browser": "npm run build:test && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run build:test && npm run unit-test:node && npm run integration-test:node", + "test": "npm run build:test && npm run unit-test && npm run integration-test", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "mocha --require esm --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 1200000 --full-trace \"dist-esm/test/{,!(browser)/**/}/*.spec.js\"", + "unit-test": "npm run unit-test:node && npm run unit-test:browser", + "temp-unit-test": "mocha -r ts-node/register --timeout 1200000 test/node/*.spec.ts" }, "bin": { "mqtt_pub": "./bin/pub.js", diff --git a/src/client.ts b/src/client.ts index d33f6f5a9..fa99f5331 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,7 +1,6 @@ 'use strict' -import { IAuthPacket, IConnackPacket, IConnectPacket, IDisconnectPacket, IPingreqPacket, IPingrespPacket, IPubackPacket, IPubcompPacket, IPublishPacket, IPubrecPacket, IPubrelPacket, ISubackPacket, ISubscribePacket, IUnsubackPacket, IUnsubscribePacket, Packet, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' -import { ConnectionRefusedError, handle } from './handlers' +import { IConnackPacket, IConnectPacket, IPublishPacket, Packet, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' import { ConnectOptions } from '.' import { Duplex, EventEmitter, Readable } from 'stream' import { connectionFactory } from './connectionFactory' @@ -307,7 +306,7 @@ export class MqttClient extends EventEmitter { this.reconnecting = false return } else if (rc > 0) { - const err = new ConnectionRefusedError('Connection refused: ' + ReasonCodeErrors[rc as keyof typeof ReasonCodeErrors]) + const err:any = new Error('Connection refused: ' + ReasonCodeErrors[rc as keyof typeof ReasonCodeErrors]) err.code = rc this.emit('error', err) throw err diff --git a/src/connectionFactory/index.ts b/src/connectionFactory/index.ts index 3f292e1b8..05bc2e474 100644 --- a/src/connectionFactory/index.ts +++ b/src/connectionFactory/index.ts @@ -2,7 +2,7 @@ import net from 'net' import { Duplex } from 'stream' import tls from 'tls' import { ConnectOptions } from '..' -import { isBrowser } from '../isBrowser' +import { _buildWebSocketStream } from './websocket' const logger = require('pino')() @@ -63,11 +63,8 @@ export function connectionFactory (options: ConnectOptions): Duplex { path: url.pathname || '/', wsOptions: options.wsOptions || {} } - if (isBrowser()) { - return _buildWebSocketStreamBrowser(webSocketOptions) - } else { - return _buildWebSocketStream(webSocketOptions) - } + const wsStream = _buildWebSocketStream(webSocketOptions) + return wsStream } default: throw new Error('Unrecognized protocol') } @@ -81,175 +78,3 @@ interface WebSocketOptions { path: string, wsOptions: any } - -function _buildWebSocketStreamBrowser (opts: WebSocketOptions): Duplex { - throw new Error('_buildWebSocketStreamBrowser is not implemented.') - // if (!opts.url.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) - // opts.url.hostname = parsed.hostname - - // if (!opts.url.port) { - // opts.url.port = parsed.port - // } - // } - - // // objectMode should be defined for logic - // if (opts.wsOptions.objectMode === undefined) { - // opts.wsOptions.objectMode = !(opts.binary === true || opts.binary === undefined) - // } - // const websocketSubProtocol = - // (opts.url.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - // ? 'mqttv3.1' - // : 'mqtt' - - // const url = buildUrl(opts, client) - // /* global WebSocket */ - // const socket = new WebSocket(url, [websocketSubProtocol]) - // socket.binaryType = 'arraybuffer' - // logger('browserStreamBuilder') - // let stream: Duplex - // // sets the maximum socket buffer size before throttling - // const bufferSize = options.browserBufferSize || 1024 * 512 - - // const bufferTimeout = opts.browserBufferTimeout || 1000 - - // const coerceToBuffer = !opts.objectMode - - // const 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: ConnectOptions, socketWrite: (chunk: any, enc: any, next: any) => any, socketEnd: (done: any) => void) { - // const 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: any) { - // stream.destroy(err) - // } - - // function onmessage (event: { data: any }) { - // 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: string | any[], cb: any) { - // 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: string | ArrayBuffer | { valueOf(): string } | { [Symbol.toPrimitive](hint: "string"): string } | Blob | ArrayBufferView, enc: any, next: (arg0: unknown) => void) { - // 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: () => void) { - // socket.close() - // done() - // } - - // // end methods for browserStreamBuilder - - // return stream -} - -function _buildWebSocketStream (opts: WebSocketOptions): Duplex { - throw new Error('_buildWebSocketStreamBrowser is not implemented.') - // if (!isBrowser && opts.protocol === 'wss') { - - // } - // const url = buildUrl(options, client) - // const websocketSubProtocol = - // (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - // ? 'mqttv3.1' - // : 'mqtt' - - // const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) - // const webSocketStream = createWebSocketStream(socket, options.wsOptions) - // webSocketStream.url = url - // socket.on('close', () => { webSocketStream.destroy() }) - // return webSocketStream -} diff --git a/src/connectionFactory/webSocketOptions.ts b/src/connectionFactory/webSocketOptions.ts new file mode 100644 index 000000000..afe3d4d49 --- /dev/null +++ b/src/connectionFactory/webSocketOptions.ts @@ -0,0 +1,8 @@ +export interface WebSocketOptions { + url: URL, + hostname: string, + protocol: string, + port: number, + path: string, + wsOptions: any +} diff --git a/src/connectionFactory/websocket.browser.ts b/src/connectionFactory/websocket.browser.ts new file mode 100644 index 000000000..7fdad3232 --- /dev/null +++ b/src/connectionFactory/websocket.browser.ts @@ -0,0 +1,157 @@ +import { Duplex } from "stream"; +import { WebSocketOptions } from "./webSocketOptions"; + + +export function _buildWebSocketStream (opts: WebSocketOptions): Duplex { + throw new Error('_buildWebSocketStreamBrowser is not implemented.') + // if (!opts.url.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) + // opts.url.hostname = parsed.hostname + + // if (!opts.url.port) { + // opts.url.port = parsed.port + // } + // } + + // // objectMode should be defined for logic + // if (opts.wsOptions.objectMode === undefined) { + // opts.wsOptions.objectMode = !(opts.binary === true || opts.binary === undefined) + // } + // const websocketSubProtocol = + // (opts.url.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + // ? 'mqttv3.1' + // : 'mqtt' + + // const url = buildUrl(opts, client) + // /* global WebSocket */ + // const socket = new WebSocket(url, [websocketSubProtocol]) + // socket.binaryType = 'arraybuffer' + // logger('browserStreamBuilder') + // let stream: Duplex + // // sets the maximum socket buffer size before throttling + // const bufferSize = options.browserBufferSize || 1024 * 512 + + // const bufferTimeout = opts.browserBufferTimeout || 1000 + + // const coerceToBuffer = !opts.objectMode + + // const 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: ConnectOptions, socketWrite: (chunk: any, enc: any, next: any) => any, socketEnd: (done: any) => void) { + // const 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: any) { + // stream.destroy(err) + // } + + // function onmessage (event: { data: any }) { + // 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: string | any[], cb: any) { + // 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: string | ArrayBuffer | { valueOf(): string } | { [Symbol.toPrimitive](hint: "string"): string } | Blob | ArrayBufferView, enc: any, next: (arg0: unknown) => void) { + // 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: () => void) { + // socket.close() + // done() + // } + + // // end methods for browserStreamBuilder + + // return stream +} \ No newline at end of file diff --git a/src/connectionFactory/websocket.ts b/src/connectionFactory/websocket.ts new file mode 100644 index 000000000..08d597267 --- /dev/null +++ b/src/connectionFactory/websocket.ts @@ -0,0 +1,20 @@ +import { Duplex } from "stream"; +import { WebSocketOptions } from "./webSocketOptions"; + +export function _buildWebSocketStream (opts: WebSocketOptions): Duplex { + throw new Error('_buildWebSocketStreamBrowser is not implemented.') + // if (!isBrowser && opts.protocol === 'wss') { + + // } + // const url = buildUrl(options, client) + // const websocketSubProtocol = + // (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + // ? 'mqttv3.1' + // : 'mqtt' + + // const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) + // const webSocketStream = createWebSocketStream(socket, options.wsOptions) + // webSocketStream.url = url + // socket.on('close', () => { webSocketStream.destroy() }) + // return webSocketStream +} diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts deleted file mode 100644 index 04774bc64..000000000 --- a/src/handlers/auth.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IAuthPacket, writeToStream } from "mqtt-packet" -import { MqttClient } from "../client" - -/** - * Authentication in MQTT v5 - * - * AUTH messages can be used multiple times by the server and the client. - * Two message properties are at the heart of the authentication flow: - * - BYTE 21: Authentication Method - * - BYTE 22: Authentication Data - * - * These properties are set on every message that takes part in the enhanced authentication flow. - * - * Authentication Method - * The Authentication Method is used to choose and describe a way of authentication - * that the client and server have agreed upon. For example, SCRAM-SHA-1, GS2-KRB5, etc. - * The Authentication Method gives meaning to the data that is exchanged during the - * enhanced authentication and must not change. - * - * Authentication Data - * Authentication Data is binary information. This data is usually used to transfer - * multiple iterations of encryped secrets or protocol steps. The content is highly - * dependent on the specific mechanism that is used in the enhanced authentication - * and is application-specific. - * - * Format in MQTT-Packet for Auth: - * { - * cmd: 'auth', - * reasonCode: 0, // MQTT 5.0 code - * properties: { // properties MQTT 5.0 - * authenticationMethod: 'test', - * authenticationData: Buffer.from([0, 1, 2, 3]), - * reasonString: 'test', - * userProperties: { - * 'test': 'test' - * } - * } - * } - */ -export async function handleAuth (client: MqttClient, packet: IAuthPacket) { - client.emit('packetsend', packet) - writeToStream(packet, client.conn, client._options) -} diff --git a/src/handlers/connack.ts b/src/handlers/connack.ts deleted file mode 100644 index 363e03585..000000000 --- a/src/handlers/connack.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { debug } from "console" -import { IConnackPacket } from "mqtt-packet" -import { MqttClient } from "../client" -import { ReasonCodeErrors } from "../errors" - -export async function handleConnAck (this: any, client: MqttClient, packet: IConnackPacket) { - // TODO: What number should this be if there is there is no reason code provided by the broker??? - const rc: number = (client._options.protocolVersion === 5 ? packet.reasonCode : packet.returnCode) || -1 - // Once the CONNACK Packet is received, we can clear the connackTimer on the client, - // so as not to trigger an error. - clearTimeout(client.connackTimer) - - // if the Packet has properties, we want to update the options in the client. - if (packet.properties) { - if (packet.properties.topicAliasMaximum) { - if (!client._options.properties) { client._options.properties = {} } - client._options.properties.topicAliasMaximum = packet.properties.topicAliasMaximum - } - if (packet.properties.serverKeepAlive && client._options.keepalive) { - client._options.keepalive = packet.properties.serverKeepAlive - // When sending a packet, reschedule the ping timer - client._shiftPingInterval() - } - if (packet.properties.maximumPacketSize) { - if (!client._options.properties) { client._options.properties = {} } - client._options.properties.maximumPacketSize = packet.properties.maximumPacketSize - } - } - - // The response code indicates whether the connection is successful. - // If there is a response code of 0, then the client is connected to the - // broker successfully and it can continue functioning. - // Otherwise it should emit an error indicating the connection to the - // specified broker was refused, providing the translation for the specific - // error code. - // Error cod - - if (rc === 0) { - client.reconnecting = false - _onConnect(client, packet) - } else if (rc > 0 && rc in Object.keys(ReasonCodeErrors)) { - var err = new ConnectionRefusedError('Connection refused: ' + ReasonCodeErrors[rc as keyof ReasonCodeErrors]) - err.code = rc - this.emit('error', err) - } -} - -function _onConnect (client: MqttClient, packet: IConnackPacket) { - if (client.disconnected) { - client.emit('connect', packet) - return - } - - - client.messageIdProvider.clear() - client._setupPingTimer() - client._resubscribe(packet) - - client.connected = true - - const startStreamProcess = () => { - var outStore = client.outgoingStore.createStream() - - function clearStoreProcessing () { - client._storeProcessing = false - client._packetIdsDuringStoreProcessing = {} - } - - client.once('close', remove) - outStore.on('error', function (err: any) { - clearStoreProcessing() - client._flushStoreProcessingQueue() - client.removeListener('close', remove) - client.emit('error', err) - }) - - function remove () { - outStore.destroy() - outStore = null - client._flushStoreProcessingQueue() - clearStoreProcessing() - } - - function storeDeliver () { - // edge case, we wrapped this twice - if (!outStore) { - return - } - client._storeProcessing = true - - var packet = outStore.read(1) - - var cb: (arg0: any, arg1: any) => void - - if (!packet) { - // read when data is available in the future - outStore.once('readable', storeDeliver) - return - } - - // Skip already processed store packets - if (client._packetIdsDuringStoreProcessing[packet.messageId]) { - storeDeliver() - return - } - - // Avoid unnecessary stream read operations when disconnected - if (!client.disconnecting && !client.reconnectTimer) { - cb = client.outgoing[packet.messageId] ? client.outgoing[packet.messageId].cb : null - client.outgoing[packet.messageId] = { - volatile: false, - cb: function (err: any, status: any) { - // Ensure that the original callback passed in to publish gets invoked - if (cb) { - cb(err, status) - } - - storeDeliver() - } - } - client._packetIdsDuringStoreProcessing[packet.messageId] = true - if (client.messageIdProvider.register(packet.messageId)) { - client._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 client._packetIdsDuringStoreProcessing) { - if (!client._packetIdsDuringStoreProcessing[id]) { - allProcessed = false - break - } - } - if (allProcessed) { - clearStoreProcessing() - client.removeListener('close', remove) - client._invokeAllStoreProcessingQueue() - client.emit('connect', packet) - } else { - startStreamProcess() - } - }) - storeDeliver() - } - // start flowing - startStreamProcess() -} \ No newline at end of file diff --git a/src/handlers/connect.ts b/src/handlers/connect.ts deleted file mode 100644 index 7d9d5708d..000000000 --- a/src/handlers/connect.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MqttClient } from "../client" -import { Packet } from "mqtt-packet" -import { write } from "../write" - -export async function handleConnect (client: MqttClient, packet: Packet): Promise { - clearTimeout(client.connackTimer) - client.connackTimer = null - client.connecting = true - client.connackTimer = setTimeout(function () { - client._cleanUp(true) - }, client._options.connectTimeout) - - await write(client, packet) - return -} diff --git a/src/handlers/disconnect.ts b/src/handlers/disconnect.ts deleted file mode 100644 index f176decec..000000000 --- a/src/handlers/disconnect.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function handleDisconnect (client) { - client.emit('disconnect', packet) -} diff --git a/src/handlers/index.ts b/src/handlers/index.ts deleted file mode 100644 index 06438517e..000000000 --- a/src/handlers/index.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { handleConnect } from './connect' -import { handleConnAck } from './connack' -// import { handleDisconnect } from './disconnect' -// import { handlePing } from './ping' -import { handlePingReq } from './pingreq' -// import { handlePingResp } from './pingresp' -// import { handlePub } from './pub' -// import { handlePubRec } from './pubrec' -// import { handlePubRel } from './pubrel' -// import { handlePubComp } from './pubcomp' -// import { handlePubAck } from './puback' -// import { handleSub } from './sub' -// import { handleSubAck } from './suback' -// import { handleUnsub } from './unsub' -// import { handleUnsubAck } from './unsuback' -// import { handleAuth } from './auth' -import { MqttClient } from '../client' -import { Packet, writeToStream } from 'mqtt-packet' -import { handleAuth } from './auth' -import { ReasonCodeErrors } from '../errors' - -export class ConnectionRefusedError extends Error { - code?: number -} - -export async function handleInboundPackets(client: MqttClient, packet: Packet): Promise { - /* 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) { - // Server sent an ack in error, ignore it. - return - } - - switch (packet.cmd) { - - } - -} - -export async function handle(client: MqttClient, packet: Packet): Promise { - let result - switch (packet.cmd) { - case 'auth': - // TODO: Does this follow the spec correctly? Shouldn't auth be able to be sent at any time??? - result = await handleAuth(client, packet) - break - case 'connect': - await handleConnect(client, packet) - break - case 'publish': - // result = await handlePub(client, options) - break - case 'subscribe': - // result = await handleSub(client, options) - break - case 'unsubscribe': - // result = await handleUnsub(client, options) - break - case 'pubrel': - case 'pubrec': - case 'pingreq': - result = await handlePingReq(client, packet) - break - case 'pubcomp': - // fallthrough - case 'puback': - var pubackRC = packet.reasonCode - // Callback - we're done - if (pubackRC && pubackRC > 0 && pubackRC !== 16) { - const err = new ConnectionRefusedError('Publish error: ' + ReasonCodeErrors[pubackRC as keyof typeof ReasonCodeErrors]) - err.code = pubackRC - throw err - } - delete client.outgoing[messageId] - client.outgoingStore.del(packet, cb) - client.messageIdProvider.deallocate(messageId) - client._invokeStoreProcessingQueue() - case 'pingresp': - // result = await handlePingResp(client, options) - break - case 'disconnect': - // result = await handleDisconnect(client, options) - // client._disconnected = true - break - case 'connack': - await handleConnAck(client, packet) - break - case 'pubcomp': - // same thing as puback for QoS 2 - case 'unsuback': - // result = await handleUnsubAck(client, options) - break - case 'suback': - // result = await handleSubAck(client, options) - break - } - - return result -} diff --git a/src/handlers/ping.ts b/src/handlers/ping.ts deleted file mode 100644 index 38e8a5753..000000000 --- a/src/handlers/ping.ts +++ /dev/null @@ -1,27 +0,0 @@ - -export async function handlePing () { - debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) - - if (!this.pingTimer && this.options.keepalive) { - this.pingResp = true - this.pingTimer = reInterval(() => { - checkPing() - }, this.options.keepalive * 1000) - } -} - -export async function shiftPingInterval () { - if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { - this.pingTimer.reschedule(this.options.keepalive * 1000) - } -} - -function checkPing () { - if (this.pingResp) { - this.pingResp = false - this._sendPacket({ cmd: 'pingreq' }) - } else { - // do a forced cleanup since socket will be in bad shape - this._cleanUp(true) - } -} \ No newline at end of file diff --git a/src/handlers/pingreq.ts b/src/handlers/pingreq.ts deleted file mode 100644 index ceb86aeb2..000000000 --- a/src/handlers/pingreq.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IPingreqPacket } from "mqtt-packet"; -import { MqttClient } from "../client"; -import { write } from "../write"; - -export async function handlePingReq (client: MqttClient, packet: IPingreqPacket) { - write(client, packet) - // client._sendPacket({ cmd: 'pingreq' }) -} diff --git a/src/handlers/pingresp.ts b/src/handlers/pingresp.ts deleted file mode 100644 index 2c3c5fb7f..000000000 --- a/src/handlers/pingresp.ts +++ /dev/null @@ -1,7 +0,0 @@ -export async function handlePingResp () { - // TODO: In old implementation, pingResp is a state in - // the client that is toggled to true when the pingResp - // is received. How does aedes do this? is there a - // better way than a boolean toggle? - return true -} diff --git a/src/handlers/pub.ts b/src/handlers/pub.ts deleted file mode 100644 index 727ded6e7..000000000 --- a/src/handlers/pub.ts +++ /dev/null @@ -1,89 +0,0 @@ - -/** - * _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 -*/ -export async function handlePub (packet) { - 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] - 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 - } diff --git a/src/handlers/puback.ts b/src/handlers/puback.ts deleted file mode 100644 index 96f46cbde..000000000 --- a/src/handlers/puback.ts +++ /dev/null @@ -1,80 +0,0 @@ -export async function handlePubAck (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') - }} diff --git a/src/handlers/pubcomp.ts b/src/handlers/pubcomp.ts deleted file mode 100644 index 76655a91d..000000000 --- a/src/handlers/pubcomp.ts +++ /dev/null @@ -1,80 +0,0 @@ -export async function handlePubComp (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') - }} diff --git a/src/handlers/pubrec.ts b/src/handlers/pubrec.ts deleted file mode 100644 index 0311b207e..000000000 --- a/src/handlers/pubrec.ts +++ /dev/null @@ -1,81 +0,0 @@ -export async function handlePubRec (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') - } -} diff --git a/src/handlers/pubrel.ts b/src/handlers/pubrel.ts deleted file mode 100644 index 04d64c59f..000000000 --- a/src/handlers/pubrel.ts +++ /dev/null @@ -1,23 +0,0 @@ -export async function handlePubRel (packet) { - 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) - } - }) -} diff --git a/src/handlers/sub.ts b/src/handlers/sub.ts deleted file mode 100644 index 0b2558a50..000000000 --- a/src/handlers/sub.ts +++ /dev/null @@ -1,182 +0,0 @@ - -/** - * 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); - */ -export async function handleSub () { - 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 -} diff --git a/src/handlers/suback.ts b/src/handlers/suback.ts deleted file mode 100644 index dd42d5baa..000000000 --- a/src/handlers/suback.ts +++ /dev/null @@ -1,80 +0,0 @@ -export async function handleSubAck (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') - }} diff --git a/src/handlers/unsub.ts b/src/handlers/unsub.ts deleted file mode 100644 index ce20f7f10..000000000 --- a/src/handlers/unsub.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * 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); - */ - -export async function handleUnsub () { - 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 -} diff --git a/src/handlers/unsuback.ts b/src/handlers/unsuback.ts deleted file mode 100644 index 9f02d8975..000000000 --- a/src/handlers/unsuback.ts +++ /dev/null @@ -1,80 +0,0 @@ -export async function handleUnsubAck (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') - }} diff --git a/src/utils/isBrowser.ts b/src/utils/isBrowser.ts new file mode 100644 index 000000000..05c8188b0 --- /dev/null +++ b/src/utils/isBrowser.ts @@ -0,0 +1,4 @@ + +export function isBrowser() { + return false +} \ No newline at end of file From ee8c31dd538ccde82319052bd8a46db9c39a443e Mon Sep 17 00:00:00 2001 From: Yoseph Maguire Date: Tue, 23 Nov 2021 14:14:52 -0800 Subject: [PATCH 9/9] updates --- .gitignore | 3 +- VNEXT.md | 9 + package.json | 46 +++-- rollup.config.js | 34 ++++ src/client.ts | 10 +- .../buildWebSocketStream.browser.ts | 151 +++++++++++++++++ src/connectionFactory/buildWebSocketStream.ts | 11 ++ src/connectionFactory/index.ts | 23 +-- .../interfaces/webSocketOptions.js | 2 + .../interfaces/webSocketOptions.ts | 17 ++ .../interfaces/webSocketStream.js | 2 + .../interfaces/webSocketStream.ts | 12 ++ src/connectionFactory/webSocketOptions.ts | 8 - src/connectionFactory/websocket.browser.ts | 157 ------------------ src/connectionFactory/websocket.ts | 20 --- src/defaultConnectOptions.ts | 10 -- src/index.ts | 107 +----------- src/interfaces/connectOptions.ts | 61 +++++++ src/interfaces/wsOptions.ts | 19 +++ src/topicAliasRecv.ts | 13 +- src/topicAliasSend.ts | 10 +- src/utils.ts | 17 +- src/utils/constants.ts | 29 ++++ src/utils/defaultClientId.ts | 7 + src/{ => utils}/defaultMessageIdProvider.ts | 131 +++++++-------- src/validateTopic.ts | 4 +- src/write.ts | 6 +- tsconfig.json | 37 +++-- 28 files changed, 520 insertions(+), 436 deletions(-) create mode 100644 VNEXT.md create mode 100644 rollup.config.js create mode 100644 src/connectionFactory/buildWebSocketStream.browser.ts create mode 100644 src/connectionFactory/buildWebSocketStream.ts create mode 100644 src/connectionFactory/interfaces/webSocketOptions.js create mode 100644 src/connectionFactory/interfaces/webSocketOptions.ts create mode 100644 src/connectionFactory/interfaces/webSocketStream.js create mode 100644 src/connectionFactory/interfaces/webSocketStream.ts delete mode 100644 src/connectionFactory/webSocketOptions.ts delete mode 100644 src/connectionFactory/websocket.browser.ts delete mode 100644 src/connectionFactory/websocket.ts delete mode 100644 src/defaultConnectOptions.ts create mode 100644 src/interfaces/connectOptions.ts create mode 100644 src/interfaces/wsOptions.ts create mode 100644 src/utils/constants.ts create mode 100644 src/utils/defaultClientId.ts rename src/{ => utils}/defaultMessageIdProvider.ts (90%) diff --git a/.gitignore b/.gitignore index c9445e115..c96465b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,8 @@ Session.vim .netrwhist *~ npm-debug.log -dist/ +dist* +types yarn.lock coverage .nyc_output diff --git a/VNEXT.md b/VNEXT.md new file mode 100644 index 000000000..3a603135b --- /dev/null +++ b/VNEXT.md @@ -0,0 +1,9 @@ +# Project Goals + +## Rudimentary Preview Release Goals +- Target Node v14 +- Typescript +- Promise-based API +- Connect +- Disconnect +- Reconnect \ No newline at end of file diff --git a/package.json b/package.json index 397e46127..b1527950c 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,13 @@ "type": "git", "url": "git://github.com/mqttjs/MQTT.js.git" }, - "main": "mqtt.js", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", "types": "types/index.d.ts", "scripts": { "build:browser": "tsc -p . && cross-env ONLY_BROWSER=true rollup -c 2>&1", "build:node": "tsc -p . && cross-env ONLY_NODE=true rollup -c 2>&1", - "build:samples": "echo Obsolete.", - "build:test": "tsc -p . && rollup -c 2>&1", - "build": "tsc -p . && rollup -c 2>&1 && api-extractor run --local", + "build": "tsc -p . && rollup -c 2>&1", "check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", "clean": "rimraf dist dist-* test-dist temp types *.tgz *.log", "execute:samples": "dev-tool samples run samples-dev", @@ -54,52 +53,47 @@ }, "files": [ "dist/", - "CONTRIBUTING.md", - "doc", - "lib", - "bin", + "dist-esm/src/", "types", - "mqtt.js" + "bin", + "doc", + "LICENSE.md", + "CONTRIBUTING.md" ], "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" }, "browser": { "./mqtt.js": "./lib/connect/index.js", "fs": false, "tls": false, - "net": false + "net": false, + "./dist-esm/src/connectionFactory/buildWebSocketStream.js": "./dist-esm/src/connectionFactory/buildWebSocketStream.browser.js" }, "dependencies": { - "commist": "^2.0.0", - "concat-stream": "^2.0.0", - "debug": "^4.3.2", - "duplexify": "^4.1.2", - "help-me": "^3.0.0", - "inherits": "^2.0.4", - "minimist": "^1.2.5", "mqtt-packet": "^7.0.0", "number-allocator": "^1.0.7", - "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "reinterval": "^1.1.0", "rfdc": "^1.3.0", - "split2": "^3.2.2", - "ws": "^8.2.0", - "xtend": "^4.0.2" + "ws": "^8.2.0" }, "devDependencies": { + "@types/chai": "^4.1.6", "@types/collections": "^5.1.2", + "@types/duplexify": "^3.6.0", "@types/end-of-stream": "^1.4.1", - "@types/node": "^16.7.1", + "@types/mocha": "^7.0.2", + "@types/node": "^12.0.0", "@types/readable-stream": "^2.3.11", "@types/rfdc": "^1.2.0", + "@types/sinon": "^9.0.4", + "@types/ws": "^8.2.0", "aedes": "^0.46.1", "airtap": "^4.0.3", "browserify": "^17.0.0", "chai": "^4.3.4", "codecov": "^3.8.3", "collections": "^5.1.12", + "duplexify": "^4.1.2", "end-of-stream": "^1.4.4", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", @@ -112,10 +106,12 @@ "mqtt-connection": "^4.1.0", "nyc": "^15.1.0", "rimraf": "^3.0.2", + "rollup": "^1.16.3", "should": "^13.2.3", "sinon": "^11.1.2", "snazzy": "^9.0.0", "standard": "^16.0.4", + "ts-node": "^9.0.0", "tsdx": "^0.14.1", "typescript": "^4.4.3", "uglify-es": "^3.3.9" diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 000000000..e4119e0b1 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,34 @@ +import resolve from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' +import pkg from './package.json' + +export default [ + // browser-friendly UMD build + { + input: 'src/main.js', + output: { + name: 'howLongUntilLunch', + file: pkg.browser, + format: 'umd' + }, + plugins: [ + resolve(), // so Rollup can find `ms` + commonjs() // so Rollup can convert `ms` to an ES module + ] + }, + + // CommonJS (for Node) and ES module (for bundlers) build. + // (We could have three entries in the configuration array + // instead of two, but it's quicker to generate multiple + // builds from a single configuration where possible, using + // an array for the `output` option, where we can specify + // `file` and `format` for each target) + { + input: 'src/main.js', + external: ['ms'], + output: [ + { file: pkg.main, format: 'cjs' }, + { file: pkg.module, format: 'es' } + ] + } +] diff --git a/src/client.ts b/src/client.ts index fa99f5331..2db4da778 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,21 +1,21 @@ 'use strict' import { IConnackPacket, IConnectPacket, IPublishPacket, Packet, parser as mqttParser, Parser as MqttParser, writeToStream } from 'mqtt-packet' -import { ConnectOptions } from '.' +import { ConnectOptions } from './interfaces/connectOptions' import { Duplex, EventEmitter, Readable } from 'stream' import { connectionFactory } from './connectionFactory' import eos from 'end-of-stream' -import { defaultConnectOptions } from './defaultConnectOptions' +import { defaultConnectOptions } from './utils/constants' import { applyTopicAlias, write } from './write' import { ReasonCodeErrors } from './errors' import {TopicAliasSend} from './topicAliasSend' import {TopicAliasRecv} from './topicAliasRecv' -import rfdc from 'rfdc' +// import rfdc from 'rfdc' import { debug } from 'console' import { Store } from './store' import { nextTick } from 'process' -const clone = rfdc() +// const clone = rfdc() // const eventEmitter = require('events') // const mqttErrors = require('errors') @@ -81,7 +81,7 @@ export class MqttClient extends EventEmitter { this.connecting = false this.connected = false this.connectedPromise = () => { - const promise = new Promise((res, rej) => { + const promise = new Promise((res) => { if (this.connected) { res() } else { diff --git a/src/connectionFactory/buildWebSocketStream.browser.ts b/src/connectionFactory/buildWebSocketStream.browser.ts new file mode 100644 index 000000000..6a60c4554 --- /dev/null +++ b/src/connectionFactory/buildWebSocketStream.browser.ts @@ -0,0 +1,151 @@ +import { Transform } from "stream"; +import { WebSocketOptions } from "./interfaces/webSocketOptions"; +import duplexify from 'duplexify'; +import { WebSocketStream } from "./interfaces/webSocketStream"; + +const logger = require('pino')() + +export function buildWebSocketStream (opts: WebSocketOptions): WebSocketStream { + // objectMode should be defined for logic + if (opts.wsOptions.ObjectMode === undefined) { + opts.wsOptions.objectMode = !(opts.binary === true || opts.binary === undefined) + } + + /* global WebSocket */ + const socket = new WebSocket(opts.url, [opts.websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + logger('browserStreamBuilder') + let stream: WebSocketStream + // sets the maximum socket buffer size before throttling + const bufferSize = opts.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + const proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) + + if (!opts.objectMode) { + proxy._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] as any, 'utf8') + } else { + buffers[i] = chunks[i].chunk + } + } + proxy._write(Buffer.concat(buffers), 'binary', cb) + } + } + 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 = (chunks: string | any[], cb: any) => { + 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 + } + } + + stream._write(Buffer.concat(buffers), 'binary', cb) + } + } + + 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 + } + + interface BuildProxyOptions extends WebSocketOptions { + + } + // methods for browserStreamBuilder + + function buildProxy (options: BuildProxyOptions, socketWrite: (chunk: any, enc: any, next: any) => any, socketEnd: (done: any) => void) { + const proxy = new Transform({ + objectMode: 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: any) { + stream.destroy(err) + } + + function onmessage (event: { data: any }) { + 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 socketWriteBrowser (chunk: string | ArrayBufferLike | Blob | ArrayBufferView, enc: any, next: (err?: unknown) => unknown) { + 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: () => void) { + socket.close() + done() + } + + // end methods for browserStreamBuilder + + return stream +} diff --git a/src/connectionFactory/buildWebSocketStream.ts b/src/connectionFactory/buildWebSocketStream.ts new file mode 100644 index 000000000..5f59d6193 --- /dev/null +++ b/src/connectionFactory/buildWebSocketStream.ts @@ -0,0 +1,11 @@ +import { WebSocketOptions } from "./interfaces/webSocketOptions"; +import WS from 'ws' +import { WebSocketStream } from "./interfaces/webSocketStream"; + +export function buildWebSocketStream (opts: WebSocketOptions): WebSocketStream { + const socket = new WS.WebSocket(opts.url, [opts.websocketSubProtocol]) + const webSocketStream: WebSocketStream = WS.createWebSocketStream(socket, opts.wsOptions) + webSocketStream.url = opts.url + socket.on('close', () => { webSocketStream.destroy() }) + return webSocketStream +} \ No newline at end of file diff --git a/src/connectionFactory/index.ts b/src/connectionFactory/index.ts index 05bc2e474..9515effd7 100644 --- a/src/connectionFactory/index.ts +++ b/src/connectionFactory/index.ts @@ -1,8 +1,11 @@ import net from 'net' import { Duplex } from 'stream' import tls from 'tls' -import { ConnectOptions } from '..' -import { _buildWebSocketStream } from './websocket' +import { ConnectOptions } from '../interfaces/connectOptions' +import { buildWebSocketStream } from './buildWebSocketStream' +import { WebSocketOptions } from './interfaces/webSocketOptions' +import { URL } from "url"; + const logger = require('pino')() @@ -55,26 +58,24 @@ export function connectionFactory (options: ConnectOptions): Duplex { } case 'ws': { const url = options.transformWsUrl ? options.transformWsUrl(options.brokerUrl) : options.brokerUrl as URL + const websocketSubProtocol = + (options.protocolId === 'MQIsdp') && (options.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' const webSocketOptions: WebSocketOptions = { url: url, hostname: url.hostname || 'localhost', port: url.port || url.protocol === 'wss' ? 443 : 80, protocol: url.protocol, + protocolId: options.protocolId, + websocketSubProtocol: websocketSubProtocol, path: url.pathname || '/', wsOptions: options.wsOptions || {} } - const wsStream = _buildWebSocketStream(webSocketOptions) + const wsStream = buildWebSocketStream(webSocketOptions) return wsStream } default: throw new Error('Unrecognized protocol') } } -interface WebSocketOptions { - url: URL, - hostname: string, - protocol: string, - port: number, - path: string, - wsOptions: any -} diff --git a/src/connectionFactory/interfaces/webSocketOptions.js b/src/connectionFactory/interfaces/webSocketOptions.js new file mode 100644 index 000000000..0e345787d --- /dev/null +++ b/src/connectionFactory/interfaces/webSocketOptions.js @@ -0,0 +1,2 @@ +"use strict"; +exports.__esModule = true; diff --git a/src/connectionFactory/interfaces/webSocketOptions.ts b/src/connectionFactory/interfaces/webSocketOptions.ts new file mode 100644 index 000000000..6b41d15fb --- /dev/null +++ b/src/connectionFactory/interfaces/webSocketOptions.ts @@ -0,0 +1,17 @@ +import { URL } from "url"; + +export interface WebSocketOptions { + protocolVersion?: number; + browserBufferTimeout?: number; + browserBufferSize?: number; + objectMode?: any; + url: URL, + hostname: string, + protocol: string, + protocolId?: string, + websocketSubProtocol: 'mqttv3.1' | 'mqtt', + port: number, + path: string, + binary?: boolean + wsOptions: any +} \ No newline at end of file diff --git a/src/connectionFactory/interfaces/webSocketStream.js b/src/connectionFactory/interfaces/webSocketStream.js new file mode 100644 index 000000000..0e345787d --- /dev/null +++ b/src/connectionFactory/interfaces/webSocketStream.js @@ -0,0 +1,2 @@ +"use strict"; +exports.__esModule = true; diff --git a/src/connectionFactory/interfaces/webSocketStream.ts b/src/connectionFactory/interfaces/webSocketStream.ts new file mode 100644 index 000000000..4535c10ec --- /dev/null +++ b/src/connectionFactory/interfaces/webSocketStream.ts @@ -0,0 +1,12 @@ +import { Duplex } from "stream"; +import { WebSocketOptions } from "./webSocketOptions"; +import { URL } from 'url' + +export interface WebSocketStream extends Duplex { + setReadable?: any; + setWritable?: any; + socket?: WebSocket; + opts?: WebSocketOptions + websocketSubProtocol?: any + url?: URL; +} \ No newline at end of file diff --git a/src/connectionFactory/webSocketOptions.ts b/src/connectionFactory/webSocketOptions.ts deleted file mode 100644 index afe3d4d49..000000000 --- a/src/connectionFactory/webSocketOptions.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface WebSocketOptions { - url: URL, - hostname: string, - protocol: string, - port: number, - path: string, - wsOptions: any -} diff --git a/src/connectionFactory/websocket.browser.ts b/src/connectionFactory/websocket.browser.ts deleted file mode 100644 index 7fdad3232..000000000 --- a/src/connectionFactory/websocket.browser.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Duplex } from "stream"; -import { WebSocketOptions } from "./webSocketOptions"; - - -export function _buildWebSocketStream (opts: WebSocketOptions): Duplex { - throw new Error('_buildWebSocketStreamBrowser is not implemented.') - // if (!opts.url.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) - // opts.url.hostname = parsed.hostname - - // if (!opts.url.port) { - // opts.url.port = parsed.port - // } - // } - - // // objectMode should be defined for logic - // if (opts.wsOptions.objectMode === undefined) { - // opts.wsOptions.objectMode = !(opts.binary === true || opts.binary === undefined) - // } - // const websocketSubProtocol = - // (opts.url.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - // ? 'mqttv3.1' - // : 'mqtt' - - // const url = buildUrl(opts, client) - // /* global WebSocket */ - // const socket = new WebSocket(url, [websocketSubProtocol]) - // socket.binaryType = 'arraybuffer' - // logger('browserStreamBuilder') - // let stream: Duplex - // // sets the maximum socket buffer size before throttling - // const bufferSize = options.browserBufferSize || 1024 * 512 - - // const bufferTimeout = opts.browserBufferTimeout || 1000 - - // const coerceToBuffer = !opts.objectMode - - // const 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: ConnectOptions, socketWrite: (chunk: any, enc: any, next: any) => any, socketEnd: (done: any) => void) { - // const 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: any) { - // stream.destroy(err) - // } - - // function onmessage (event: { data: any }) { - // 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: string | any[], cb: any) { - // 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: string | ArrayBuffer | { valueOf(): string } | { [Symbol.toPrimitive](hint: "string"): string } | Blob | ArrayBufferView, enc: any, next: (arg0: unknown) => void) { - // 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: () => void) { - // socket.close() - // done() - // } - - // // end methods for browserStreamBuilder - - // return stream -} \ No newline at end of file diff --git a/src/connectionFactory/websocket.ts b/src/connectionFactory/websocket.ts deleted file mode 100644 index 08d597267..000000000 --- a/src/connectionFactory/websocket.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Duplex } from "stream"; -import { WebSocketOptions } from "./webSocketOptions"; - -export function _buildWebSocketStream (opts: WebSocketOptions): Duplex { - throw new Error('_buildWebSocketStreamBrowser is not implemented.') - // if (!isBrowser && opts.protocol === 'wss') { - - // } - // const url = buildUrl(options, client) - // const websocketSubProtocol = - // (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - // ? 'mqttv3.1' - // : 'mqtt' - - // const socket = new WebSocket(url, [websocketSubProtocol], opts.wsOptions) - // const webSocketStream = createWebSocketStream(socket, options.wsOptions) - // webSocketStream.url = url - // socket.on('close', () => { webSocketStream.destroy() }) - // return webSocketStream -} diff --git a/src/defaultConnectOptions.ts b/src/defaultConnectOptions.ts deleted file mode 100644 index 19a1689fe..000000000 --- a/src/defaultConnectOptions.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const defaultConnectOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - resubscribe: true -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 76f78e779..6dfff018b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,105 +6,16 @@ */ import { MqttClient } from './client' -import { DefaultMessageIdProvider } from './defaultMessageIdProvider' +import { DefaultMessageIdProvider } from './utils/defaultMessageIdProvider' import { UniqueMessageIdProvider } from './uniqueMessageIdProvider' -import { Duplex } from 'stream' -import { TlsOptions } from 'tls' -import { Server } from 'http' -import {Server as HttpsServer} from 'https' -import { QoS, UserProperties } from 'mqtt-packet' - -const protocols = { - all : [ - 'mqtt', - 'mqtts', - 'ws', - 'wss' - ], - secure: [ - 'mqtts', - 'ws' - ], - insecure: [ - 'mqtt', - 'wss' - ] -} -// TODO: - -export type WsOptions = { - backlog: number, - clientTracking: boolean, - handleProtocols: () => unknown, - host: string, - maxPayload: number, - noServer: boolean, - path: string, - perMessageDeflate: boolean | {[x: string]: unknown}, - port: number, - server: Server | HttpsServer, - skipUTF8Validation: boolean, - verifyClient: () => unknown -} & { - [prop: string]: string -} +import { URL } from 'url' +import { ConnectOptions } from './interfaces/connectOptions' +import { protocols } from './utils/constants' + + + -export interface ConnectOptions { - autoUseTopicAlias: any - autoAssignTopicAlias: any - topicAliasMaximum: number - queueLimit: number - cmd: 'connect' - clientId: string - protocolVersion?: 4 | 5 | 3 - protocolId?: 'MQTT' | 'MQIsdp' - clean?: boolean - keepalive?: number - username?: string - password?: Buffer - will?: { - topic: string - payload: Buffer - qos?: QoS - retain?: boolean - properties?: { - willDelayInterval?: number, - payloadFormatIndicator?: number, - messageExpiryInterval?: number, - contentType?: string, - responseTopic?: string, - correlationData?: Buffer, - userProperties?: UserProperties - } - } - properties?: { - sessionExpiryInterval?: number, - receiveMaximum?: number, - maximumPacketSize?: number, - topicAliasMaximum?: number, - requestResponseInformation?: boolean, - requestProblemInformation?: boolean, - userProperties?: UserProperties, - authenticationMethod?: string, - authenticationData?: Buffer - } - brokerUrl: URL - wsOptions: {[key: string]: WsOptions | unknown}, - tlsOptions: {[key: string]: TlsOptions | unknown}, - reschedulePings: any, - reconnectPeriod: any, - connectTimeout: any, - incomingStore: any, - outgoingStore: any, - queueQoSZero: any, - customHandleAcks: any, - authPacket: any, - transformWsUrl: (options: any) => URL, - resubscribe: boolean, - messageIdProvider: any - customStreamFactory: (options: ConnectOptions) => Duplex -} /** @@ -119,7 +30,7 @@ function connect (options: ConnectOptions) { options.brokerUrl = new URL(options.brokerUrl) } - if (!options.brokerUrl.protocol) { + if (!options?.brokerUrl?.protocol) { throw new Error( `Missing protocol. \ To provide a protocol, you have two options:\ @@ -143,7 +54,7 @@ function connect (options: ConnectOptions) { } function _validateProtocol(opts: ConnectOptions): Error | undefined { - if (opts.tlsOptions.cert && opts.tlsOptions.key) { + if (opts.tlsOptions && 'cert' in opts.tlsOptions && 'key' in opts.tlsOptions) { const urlProtocol = (opts.brokerUrl as URL).protocol if (urlProtocol) { if (protocols.secure.indexOf(urlProtocol) === -1) { diff --git a/src/interfaces/connectOptions.ts b/src/interfaces/connectOptions.ts new file mode 100644 index 000000000..cbe769839 --- /dev/null +++ b/src/interfaces/connectOptions.ts @@ -0,0 +1,61 @@ +import { QoS, UserProperties } from 'mqtt-packet' +import { Duplex } from 'stream'; +import { TlsOptions } from 'tls'; +import { WsOptions } from './wsOptions'; + +export interface ConnectOptions { + objectMode?: any + autoUseTopicAlias?: any + autoAssignTopicAlias?: any + topicAliasMaximum?: number + queueLimit?: number + cmd?: 'connect' + clientId?: string + protocolVersion?: 4 | 5 | 3 + protocolId?: 'MQTT' | 'MQIsdp' + clean?: boolean + keepalive?: number + username?: string + password?: Buffer + will?: { + topic: string + payload: Buffer + qos?: QoS + retain?: boolean + properties?: { + willDelayInterval?: number, + payloadFormatIndicator?: number, + messageExpiryInterval?: number, + contentType?: string, + responseTopic?: string, + correlationData?: Buffer, + userProperties?: UserProperties + } + } + properties?: { + sessionExpiryInterval?: number, + receiveMaximum?: number, + maximumPacketSize?: number, + topicAliasMaximum?: number, + requestResponseInformation?: boolean, + requestProblemInformation?: boolean, + userProperties?: UserProperties, + authenticationMethod?: string, + authenticationData?: Buffer + } + brokerUrl?: URL + wsOptions?: {[key: string]: WsOptions | unknown}, + tlsOptions?: {[key: string]: TlsOptions | unknown}, + reschedulePings?: any, + reconnectPeriod?: any, + connectTimeout?: any, + incomingStore?: any, + outgoingStore?: any, + queueQoSZero?: any, + customHandleAcks?: any, + authPacket?: any, + transformWsUrl?: (options: any) => URL, + resubscribe?: boolean, + messageIdProvider?: any + customStreamFactory?: (options: ConnectOptions) => Duplex +} diff --git a/src/interfaces/wsOptions.ts b/src/interfaces/wsOptions.ts new file mode 100644 index 000000000..934dcaca6 --- /dev/null +++ b/src/interfaces/wsOptions.ts @@ -0,0 +1,19 @@ +import { Server } from 'http' +import {Server as HttpsServer} from 'https' + +export type WsOptions = { + backlog: number, + clientTracking: boolean, + handleProtocols: () => unknown, + host: string, + maxPayload: number, + noServer: boolean, + path: string, + perMessageDeflate: boolean | {[x: string]: unknown}, + port: number, + server: Server | HttpsServer, + skipUTF8Validation: boolean, + verifyClient: () => unknown +} & { + [prop: string]: string +} \ No newline at end of file diff --git a/src/topicAliasRecv.ts b/src/topicAliasRecv.ts index e86b570d9..fe4f44acb 100644 --- a/src/topicAliasRecv.ts +++ b/src/topicAliasRecv.ts @@ -1,5 +1,7 @@ const logger = require('pino')() +const maxDefault = 10 + export class TopicAliasRecv { max: number aliasToTopic: {[alias: string]: string} @@ -10,13 +12,14 @@ export class TopicAliasRecv { * This holds alias to topic map * @param {Number} [max] - topic alias maximum entries */ - constructor(max: number) { - if (max > 0) { - if (max > 0xffff) { - throw new Error('MqttClient :: options.topicAliasMaximum is out of range') + constructor(max?: number) { + if (max) { + if (max > 0 && max > 0xffff) { + throw new Error('topicAliasMaximum is out of range') } } - this.max = max + + this.max = max || maxDefault this.aliasToTopic = {} } diff --git a/src/topicAliasSend.ts b/src/topicAliasSend.ts index e70ef5d7c..a0d11ebc2 100644 --- a/src/topicAliasSend.ts +++ b/src/topicAliasSend.ts @@ -3,13 +3,13 @@ /** * Module dependencies */ -import {LruMap} from 'collections/lru-map' import {NumberAllocator} from 'number-allocator' +var LruMap = require("collections/lru-map"); export class TopicAliasSend { max: number - aliasToTopic: LruMap - topicToAlias: {[topic: string]: string} + aliasToTopic: typeof LruMap + topicToAlias: {[topic: string]: number} numberAllocator: NumberAllocator length: number @@ -64,12 +64,12 @@ export class TopicAliasSend { * @param {String} [topic] - topic * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined */ - getAliasByTopic(topic: string | number) { + getAliasByTopic(topic: string | number): number { const alias = this.topicToAlias[topic] if (typeof alias !== 'undefined') { this.aliasToTopic.get(alias) // LRU update } - return alias + return Number(alias) } /** diff --git a/src/utils.ts b/src/utils.ts index e7def5eff..dcb1a2c7a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,16 +1,22 @@ -export function validateTopic (topic, message) { +const logger = require('pino')() + +export function validateTopic (topic: string, message: string): boolean { const end = topic.length - 1 const endMinus = end - 1 const slashInPreEnd = endMinus > 0 && topic.charCodeAt(endMinus) !== 47 if (topic.length === 0) { // [MQTT-3.8.3-3] - return new Error('impossible to ' + message + ' to an empty topic') + const err = new Error('impossible to ' + message + ' to an empty topic') + logger(err) + return false } for (let i = 0; i < topic.length; i++) { switch (topic.charCodeAt(i)) { case 35: { // # const notAtTheEnd = i !== end if (notAtTheEnd || slashInPreEnd) { - return new Error('# is only allowed in ' + message + ' in the last position') + const err = new Error('# is only allowed in ' + message + ' in the last position') + logger(err) + return false } break } @@ -18,10 +24,13 @@ export function validateTopic (topic, message) { const pastChar = i < end - 1 && topic.charCodeAt(i + 1) !== 47 const preChar = i > 1 && topic.charCodeAt(i - 1) !== 47 if (pastChar || preChar) { - return new Error('+ is only allowed in ' + message + ' between /') + const err = new Error('+ is only allowed in ' + message + ' between /') + logger(err) + return false } break } } } + return true } diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 000000000..bcbdde07a --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,29 @@ +import { ConnectOptions } from ".."; + +export const defaultConnectOptions: ConnectOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true +} + +export const protocols = { + all : [ + 'mqtt', + 'mqtts', + 'ws', + 'wss' + ], + secure: [ + 'mqtts', + 'ws' + ], + insecure: [ + 'mqtt', + 'wss' + ] +} diff --git a/src/utils/defaultClientId.ts b/src/utils/defaultClientId.ts new file mode 100644 index 000000000..943aa00e5 --- /dev/null +++ b/src/utils/defaultClientId.ts @@ -0,0 +1,7 @@ +/** + * Generates client id with random 8 digit long base 16 value + * @returns clientId + */ +export function defaultClientId (): string { + return 'mqttjs_' + Math.random().toString(16).substr(2, 8) +} \ No newline at end of file diff --git a/src/defaultMessageIdProvider.ts b/src/utils/defaultMessageIdProvider.ts similarity index 90% rename from src/defaultMessageIdProvider.ts rename to src/utils/defaultMessageIdProvider.ts index 943e4606a..15e334b66 100644 --- a/src/defaultMessageIdProvider.ts +++ b/src/utils/defaultMessageIdProvider.ts @@ -1,65 +1,66 @@ -'use strict' - -/** - * DefaultMessageAllocator constructor - * @constructor - */ -export class DefaultMessageIdProvider { - /** - * MessageIDs starting with 1 - * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 - */ - constructor () { - this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) - } - - /** - * allocate - * - * Get the next messageId. - * @return unsigned int - */ - allocate () { - // 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 - */ - getLastAllocated () { - 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 - */ - register (messageId) { - return true - } - - /** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ - deallocate (messageId) { - } - - /** - * clear - * Deallocate all messageIds. - */ - clear () { - } -} +'use strict' + +/** + * DefaultMessageAllocator constructor + * @constructor + */ +export class DefaultMessageIdProvider { + nextId: number + + /** + * MessageIDs starting with 1 + * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 + */ + constructor () { + this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) + } + + /** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ + allocate () { + // 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 + */ + getLastAllocated () { + 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 + */ + register (_messageId: number) { + } + + /** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ + deallocate (_messageId: number) { + } + + /** + * clear + * Deallocate all messageIds. + */ + clear () { + } +} diff --git a/src/validateTopic.ts b/src/validateTopic.ts index b88bb7b8f..4cf37080c 100644 --- a/src/validateTopic.ts +++ b/src/validateTopic.ts @@ -9,7 +9,7 @@ * @param {String} topic - A topic * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. */ -export function validateTopic (topic) { +export function validateTopic (topic: string) { var parts = topic.split('/') for (var i = 0; i < parts.length; i++) { @@ -35,7 +35,7 @@ export function validateTopic (topic) { * @param {Array} topics - Array of topics * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one */ -export function validateTopics (topics) { +export function validateTopics (topics: Array) { if (topics.length === 0) { return 'empty_topic_list' } diff --git a/src/write.ts b/src/write.ts index e3372f00f..226971036 100644 --- a/src/write.ts +++ b/src/write.ts @@ -1,4 +1,4 @@ -import mqtt, { IPacket, Packet } from 'mqtt-packet' +import mqtt, { Packet } from 'mqtt-packet' import { MqttClient } from './client' import rfdc from 'rfdc' @@ -8,11 +8,11 @@ const clone = rfdc() export function write (client: MqttClient, packet: mqtt.Packet): Promise { let error: Error | null = null return new Promise((resolve, reject) => { - const topicAliasErr = applyTopicAlias(client, packet) + const aliasedPacket = applyTopicAlias(client, packet) if (client.connecting || client.connected) { try { - mqtt.writeToStream(packet, client.conn) + mqtt.writeToStream(aliasedPacket, client.conn) if (!client.errored) { client.conn.once('drain', resolve) return diff --git a/tsconfig.json b/tsconfig.json index 2c85b2d99..4c03a35c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs - "include": ["src", "types"], "compilerOptions": { + // "target": "es2018", "module": "esnext", "lib": ["dom", "esnext"], "importHelpers": true, @@ -10,26 +10,29 @@ // output .js.map sourcemap files for consumers "sourceMap": true, // match output dir to input dir. e.g. dist/index instead of dist/src/index - "rootDir": "./src", + "rootDir": "src", + "outDir": "dist-esm", + "declarationDir": "types", // stricter type-checking for stronger correctness. Recommended by TS "strict": true, - // linter checks for common issues - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative - "noUnusedLocals": true, - "noUnusedParameters": true, - // use Node's module resolution algorithm, instead of the legacy TS one + // // linter checks for common issues + // "noImplicitReturns": true, + // "noFallthroughCasesInSwitch": true, + // // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // // use Node's module resolution algorithm, instead of the legacy TS one "moduleResolution": "node", - // transpile JSX to React.createElement + // // transpile JSX to React.createElement "jsx": "react", - // interop between ESM and CJS modules. Recommended by TS + // // interop between ESM and CJS modules. Recommended by TS "esModuleInterop": true, - // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS + // // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS "skipLibCheck": true, - // error out if import and file system have a casing mismatch. Recommended by TS - "forceConsistentCasingInFileNames": true, - // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` - "noEmit": true, - } + // // error out if import and file system have a casing mismatch. Recommended by TS + // "forceConsistentCasingInFileNames": true, + // // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` + // "noEmit": true, + }, + "include": ["src/**/*"], }